ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Mastra @internal/llm-recorder 深度解析:LLM 响应录制回放与二进制工件(Binary Artifact)支持

Mastra @internal/llm-recorder 深度解析:LLM 响应录制回放与二进制工件(Binary Artifact)支持 Mastra internal/llm-recorder 深度解析LLM 响应录制回放与二进制工件Binary Artifact支持【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra本文基于 Mastra 仓库中 packages/_llm-recorder/CHANGELOG.md 的核心变更记录展开深入剖析internal/llm-recorder这一内部测试基础设施的录制/回放机制重点讲解其针对音频等非 JSON 载荷新增的二进制工件binary artifact支持哈希命名的 sidecar 文件如何存储、如何通过元数据与 JSON 录制文件关联、回放时如何还原原始字节与 Content-Type 头。读完本文你将掌握该包的四种接入方式、五种测试模式、请求匹配与请求变换策略以及二进制载荷从录制到回放的全链路原理。注意该包当前是 Mastra 仓库的内部包private: true包名为internal/llm-recorderREADME 中说明未来会对外公开。一、变更记录背后的核心能力二进制工件支持CHANGELOG 中从 0.0.8 到 0.0.63 反复出现同一条 Minor Changes 记录多版本重复是该仓库变更记录生成方式的产物其描述的正是这个包最值得关注的能力演进Added binary artifact support for non-JSON request/response payloads (for example audio) in the LLM recorder. Binary bytes are now written as hash-based sidecar files in__recordings__/and referenced from JSON recordings with metadata (contentType,size, and artifactpath). Replay restores the original binary payload and content-type headers from artifacts, while keeping JSON fixtures small and readable.这条记录概括了三层含义问题LLM API 的请求/响应并不总是 JSON。音频输入语音转写、音频输出TTS、图像等二进制载荷无法直接嵌入 JSON 录制文件否则会产生巨大且不可读的 fixture。方案二进制字节被写成基于哈希命名的 sidecar 文件存放在__recordings__/目录JSON 录制文件中只保留轻量元数据contentType、size、工件path通过引用关系指向 sidecar 文件。效果回放时从工件文件还原原始二进制载荷与 Content-Type 头同时保持 JSON fixture 体积小、可读性高。二、录制文件存储与二进制工件的实际形态2.1 目录布局录制文件默认存放在process.cwd()下的__recordings__/目录可通过recordingsDir选项覆盖。当请求或响应包含二进制载荷时sidecar 文件直接存放在该目录中your-package/ ├── __recordings__/ │ ├── my-agent-tests.json │ └── a1b2c3d4-response.wav └── src/ └── tests/从 llm-recorder.ts 的writeBinaryArtifact实现可以看到 sidecar 文件的命名规则const payloadDigest crypto.createHash(md5).update(params.bytes).digest(hex).slice(0, 12); const fileName ${params.hash}-${params.kind}-${payloadDigest}.${ext};即{请求哈希}-{request|response}-{载荷内容MD5前12位}.{扩展名}其中扩展名根据 Content-Type 映射为mp3、wav、ogg、webm无法识别时回退为bin。这种请求哈希 内容哈希的双重命名既能保证同一次交互的关联性又能利用内容哈希天然去重——完全相同的字节只落盘一次。2.2 JSON 中的二进制引用结构二进制元数据在 JSON 录制文件中以__binary标记配合binaryArtifact字段描述 sidecar 文件{ response: { body: { __binary: true, contentType: audio/wav, size: 8192 }, binaryArtifact: { path: a1b2c3d4-response.wav, contentType: audio/wav, size: 8192 } } }请求侧同样支持二进制工件parseRequestBodyllm-recorder.ts会依据 Content-Type 分流——application/json/json走 JSON 解析text/走纯文本其余内容读取原始字节并生成{ __binary: true, contentType, size, digest }占位值同时把字节交给writeBinaryArtifact落盘。这样即使请求是音频上传如语音转写也能被完整录制。2.3 录制文件的自描述格式录制文件采用带版本信息的{ meta, recordings }顶层结构RecordingFile见 llm-recorder.tsmeta包含name录制名称与文件名同名testFile生成该录制的测试文件相对路径testName测试名称尽力而为provider提供商 ID如openai、anthropicmodel模型 ID如gpt-4ocreatedAt/updatedAt创建与更新时间戳model会在录制时从请求体自动推断provider/model也可通过metaContext显式提供。旧版纯数组格式的录制文件在读取时会被自动迁移loadRecordingFile保证向后兼容。三、二进制回放字节级还原回放时readBinaryArtifactllm-recorder.ts会做一次路径穿越防护——校验解析后的绝对路径必须位于 recordingsDir 内然后读取原始字节并通过HttpResponse携带录制时保存的 headers含 Content-Type原样返回给测试代码if (recording.response.binaryArtifact) { return new HttpResponse(readBinaryArtifact(recordingsDir, recording.response.binaryArtifact), { status: recording.response.status, statusText: recording.response.statusText, headers: recording.response.headers, }); }也就是说测试端拿到的响应与真实 API 返回的字节序列、状态码和响应头完全一致。这一点在 llm-recorder.test.ts 的binary-response-artifact测试中有端到端验证录制阶段 mock 一个audio/wav的字节响应断言 JSON 中binaryArtifact存在、sidecar 文件落盘切换到 replay 模式后断言content-type仍是audio/wav且replayBytes与原始payload逐字节相等。四、二进制请求的匹配策略录制/回放的核心是请求匹配。普通 JSON 请求按URL body 的 MD5 哈希匹配body 会先做对象键深度排序与 ISO 日期归一化以保证确定性见stableSortKeys/canonicalizeISODateString。但二进制请求序列化后的字符串主要是随机的 multipart boundary 和二进制摘要字符串相似度匹配完全失效。因此findRecordingllm-recorder.ts对__binary请求走载荷大小近似匹配在 URL 相同的前提下计算候选录制与当前请求的字节数差值取差值最小者若相对差值超过 10% 则拒绝同一 TTS 输出跨运行通常只差几个字节10% 的容差已足够宽松并通过usedHashes集合避免多个相近的二进制请求如多次音频转写全部命中同一条录制。五、四种接入方式从全自动到最手动CHANGELOG 之外包的 README.md 给出了从自动化到手动四种启用方式适用不同粒度5.1 Suite 级Vite 插件推荐在vitest.config.ts中注册所有匹配的测试文件自动注入录制逻辑无需改动测试代码import { defineConfig } from vitest/config; import { llmRecorderPlugin } from internal/llm-recorder/vite-plugin; export default defineConfig({ plugins: [llmRecorderPlugin()], test: { /* ... */ }, });插件选项llmRecorderPlugin({ include: [src/**/*.test.ts], // 包含的 glob默认 **/*.test.{ts,tsx,js,jsx} exclude: [src/**/*.unit.test.ts], // 排除的 glob默认 node_modules、dist nameGenerator: filepath custom, // 自定义录制名推导 recordingsDir: ./__recordings__, // 覆盖录制目录 transformRequest: { importPath: ./test/my-transform, // 变换模块路径 exportName: normalizeRequest, // 导出名默认 transformRequest }, });插件在构建期改写测试文件注入__autoUseLLMRecording(...)调用已经手动调用useLLMRecording或enableAutoRecording的文件会被跳过避免重复注入。由于插件在构建期生成代码transformRequest不能直接传函数只能以模块路径 导出名的方式引用相对路径会自动换算为从测试文件到变换模块的路径。录制名由文件路径自动推导defaultNameGenerator见 vite-plugin.tspackages/memory/src/index.test.ts→memory-src-indexstores/pg/src/storage.test.ts→pg-src-storage5.2 文件级enableAutoRecording()在测试文件顶部直接调用通过调用栈自动定位当前测试文件并推导录制名import { enableAutoRecording } from internal/llm-recorder; enableAutoRecording(); // 也可传 { nameOverride: my-custom-name } 指定名称 describe(My Tests, () { it(works, async () { const result await agent.generate(Hello); expect(result.text).toBeDefined(); }); });5.3 describe 级useLLMRecording()在describe块内使用自动挂载beforeAll/beforeEach/afterAll钩子完成 server 启停与保存import { useLLMRecording } from internal/llm-recorder; describe(My Agent Tests, () { useLLMRecording(my-agent-tests); it(generates text, async () { const response await agent.generate(Hello); expect(response.text).toBeDefined(); }); });5.4 单测级withLLMRecording()把单个测试包进录制作用域回调的返回值原样透传若外层已有 suite 级录制会自动暂停外层 server 避免冲突结束后恢复import { withLLMRecording } from internal/llm-recorder; it(generates a response, () withLLMRecording(my-single-test, async () { const response await agent.generate(Hello); expect(response.text).toBeDefined(); }));六、五种测试模式类快照的录制/回放工作流与 Vitest 快照的体验一致——首次运行自动录制之后确定性回放。模式由 CLI 标志或环境变量控制# Auto 模式默认—— 有录制则回放无录制则录制 pnpm test # 强制全部重录等价于快照的 vitest -u pnpm test -- --update-recordings # 或 UPDATE_RECORDINGStrue pnpm test # 完全跳过录制用真实 API 调试 LLM_TEST_MODElive pnpm test # 严格回放 —— 无录制则失败 LLM_TEST_MODEreplay pnpm test模式选择优先级getLLMTestMode见 llm-recorder.ts--update-recordings/-U标志或UPDATE_RECORDINGStrue→ update强制重录LLM_TEST_MODElive→ live不录制LLM_TEST_MODErecord→ record旧别名等价 updateLLM_TEST_MODEreplay→ replay严格回放缺失即失败RECORD_LLMtrue→ record旧环境变量默认 →auto有录制回放、无录制录制注意 Auto 模式在启动时一次性判定存在录制文件则整个 suite 走 replay不存在则整场录制并落盘。严格 replay 模式下即使录制文件缺失也不会立即报错文件缺失可能只是该测试没发 LLM 请求真正发起请求且找不到匹配时才会抛错并提示Run with UPDATE_RECORDINGStrue to re-record。update/record 模式还会在录制前删除旧文件并从零开始损坏的录制文件不会阻塞重录。七、请求变换让动态字段不影响匹配transformRequest回调接收{ url, body }并返回归一化后的{ url, body }在录制和回放两侧都会执行因此哈希总是基于归一化值计算。适用于请求中带时间戳、UUID、会话 ID 等跨运行变化的动态字段、但不影响要回放响应的场景。测试代码内写法四种录制方法都支持useLLMRecording(my-tests, { transformRequest: ({ url, body }) ({ url, body: { ...(body as any), timestamp: STABLE, sessionId: STABLE }, }), });回放侧还会把归一化前的哈希与归一化后的哈希同时作为lookupHashes参与精确匹配prepareReplayRecordings进一步提升兼容性。八、测试粒度内的 Live 模式suite 已启用录制时可用useLiveMode()让特定describe内的测试走真实 API——它在作用域内每个测试前关闭 MSW server、测试后重启互不干扰import { useLLMRecording, useLiveMode } from internal/llm-recorder; describe(My Agent Tests, () { useLLMRecording(my-suite); it(replays from recording, async () { const response await agent.generate(Hello); // 走录制 expect(response.text).toBeDefined(); }); describe(real API validation, () { useLiveMode(); it(hits the real API, async () { const response await agent.generate(Hello); // 走真实 API expect(response.text).toBeDefined(); }); }); });若当前没有活动的 recorder例如全局已是 live 模式useLiveMode()是空操作。九、匹配细节精确匹配、模糊回退与流式重放9.1 精确匹配与多响应消费同一哈希可能对应多条录制同一请求因重试产生不同响应、或归一化后哈希碰撞的不同测试变体。findRecording会按录制顺序依次消费这些精确匹配全部消费完后持续返回最后一条保证请求-响应链按真实发生顺序逐条还原exactReplayCounts在测试文件生命周期内刻意不重置。9.2 模糊匹配回退没有精确命中时对序列化后的请求内容计算 Dice 相似度string-similarity库阈值SIMILARITY_THRESHOLD 0.6且优先选择 URL 相同的候选避免跨 API如/v1/chat/completions与/v1/responses错配。模糊命中会打印警告及 JSON diff基于diff库供排查并提示可重录开启exactMatch选项后模糊匹配会被拒绝并直接抛错。注意非二进制录制刻意不消耗可跨测试复用例如 v1/v2 模型变体共享一条录制只有二进制模糊匹配受usedHashes约束。9.3 SSE 流式捕获与重放流式响应text/event-stream/text/plain在录制时逐 chunk 读取并记录每块的时间间隔captureStreamingResponse回放时通过ReadableStream按原顺序推送 chunkreplayWithTiming: true时模拟原始间隔受maxChunkDelay上限钳制默认 10ms默认关闭以保证测试速度。十、契约校验捕捉上游 API 漂移包内还提供响应结构契约校验用于夜间测试发现 API schema 漂移llm-contract.tsextractSchema(value)从值生成 schema 树对象/数组/基础类型validateLLMContract(actual, expected, options?)比较结构而非具体值——类型、字段有无、数组项结构validateStreamingContract(actualChunks, expectedChunks)解析 SSE 事件序列校验response.created、response.completed等关键事件是否存在及其数据结构formatContractResult(result)格式化差异输出默认忽略id、created、model、usage.*、x-request-id、x-ratelimit-*、cf-*等动态/敏感路径DEFAULT_IGNORE_PATHS可通过ignorePaths扩展默认allowExtraFields: true、allowMissingFields: false、treatNullAsOptional: true。十一、支持的提供商与性能预期MSW 拦截的默认主机LLM_API_HOSTS可用hosts选项收窄api.openai.comapi.anthropic.comgenerativelanguage.googleapis.comopenrouter.ai录制时会跳过authorization、x-api-key、api-key、content-encoding、set-cookie、openai-organization等敏感/压缩头SKIP_HEADERS避免凭据与账户元数据进入提交的 fixture。模式典型耗时适用场景Auto回放 100ms / 首录 5-30s默认——开箱即用Update每个测试 5-30s重录 fixtureLive每个测试 5-30s真实 API 调试Replay每个测试 100msCI、严格回放十二、API 一览导出说明useLLMRecording(name, options?)Vitest 助手——自动挂载beforeAll/afterAll钩子useLiveMode()在已录制的 suite 中让指定测试走真实 APIwithLLMRecording(name, fn, options?)单测试录制回调包装setupLLMRecording(options)底层手动设置 APIenableAutoRecording(options?)文件级自动录制getActiveRecorder()返回当前活动 recorder 实例getLLMTestMode()返回当前模式auto \| update \| replay \| live \| recordhasLLMRecording(name, dir?)检查录制文件是否存在deleteLLMRecording(name, dir?)删除录制文件listLLMRecordings(dir?)列出所有录制getLLMRecordingsDir(dir?)获取录制目录绝对路径validateLLMContract(actual, expected, options?)比较响应结构validateStreamingContract(actual, expected)比较流式 chunk 结构extractSchema(value)从值生成 schemaformatContractResult(result)格式化校验结果llmRecorderPlugin(options?)Vite 自动注入插件defaultNameGenerator(filepath)默认录制名推导十三、本地开发与重录实践# 构建 pnpm build # 跑测试无录制自动录制有录制则回放 pnpm test # 强制重录全部 fixture UPDATE_RECORDINGStrue OPENAI_API_KEYsk-xxx pnpm test包内测试本身即最佳示例pnpm test会先录制再回放覆盖了二进制工件落盘/还原、旧格式迁移、哈希匹配等关键路径见 llm-recorder.test.ts。依赖方面该包基于mswHTTP 拦截、diff差异输出、string-similarity模糊匹配实现入口统一从 index.ts 导出。结语internal/llm-recorder用类快照的录制/回放模型解决了 LLM 测试中最棘手的确定性问题MSW 拦截真实 HTTP 流量、MD5 内容匹配保证并行与乱序安全、transformRequest归一化动态字段、模糊回退容忍轻微请求漂移而二进制工件支持则把音频等非 JSON 载荷安全地收纳进 sidecar 文件既保字节级回放 fidelity又让 JSON fixture 保持小巧可读。结合契约校验能力它构成了 Mastra 仓库 LLM 相关测试从录制、回放到防漂移的完整闭环。【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表