1. 这不是又一个 Mock 工具,而是一次对 AI Agent 测试范式的重写
我做 traceplay 的起因特别朴素:上周五下午三点,我盯着 CI 流水线里第 7 次失败的test_agent_routing_logic用例发呆。它在本地跑得飞快、结果完美,一上 GitLab CI 就随机超时——不是模型响应慢,是 OpenAI API 的 rate limit 突然抖动,是 Anthropic 的 streaming response 偶尔多吐一个换行,是本地 mock server 没覆盖到某个 header 的大小写敏感逻辑。更讽刺的是,我们花三周写的那个“智能路由 Agent”,核心逻辑其实只用了 200 行 TypeScript,但测试代码写了 1200 行,其中 800 行在跟网络抖动、token 计费、服务端非确定性行为死磕。
traceplay 就是在这个崩溃时刻诞生的。它不拦截请求,不伪造响应,不做任何 runtime patch;它只做一件事:把真实世界里 AI Agent 和外部服务(LLM API、RAG 向量库、工具调用 Webhook)之间那条 HTTP 通信链路,像胶片一样完整录下来——包括每一个 request headers 的空格、每一个 streaming chunk 的 timing、每一个 retry 重试的间隔、甚至 LLM 返回的content字段里那个被前端 trim 掉的末尾空格。录完之后,你得到的不是一个 JSON mock 文件,而是一个带时间戳、带二进制 payload、带完整 TCP 层语义的 trace 文件。回放时,它不走网络,不消耗 token,不触发任何计费,连 DNS 查询都跳过——它直接把当年那个“真实世界”的快照,在你的本地内存里原样复现。
关键词traceplay、API测试、HTTP回放、CI、TypeScript不是堆砌的标签,而是它的 DNA:它用 TypeScript 写成,天然兼容 Vite/React/Vue 项目;它的 trace 文件是纯 JSON + base64,Git 友好,可 diff,可 review;它设计之初就为gitlab ci和github ci而生——CI 里跑回归?直接npx traceplay replay --trace=prod-trace-20240520.json,0 token,0 网络,300ms 跑完 12 个复杂对话链路。这不是“模拟”,是“时光机”。你测的不是代码在理想环境下的表现,而是它在上周三凌晨 2:17 那个真实生产流量下的行为。这才是 AI Agent 测试该有的样子。
2. 为什么传统方案在 AI Agent 场景下集体失效?
2.1 Mock 的幻觉:当“假装”比“真实”更难维护
绝大多数团队第一反应是 mock。用jest.mock('axios')或msw拦截请求,返回预设 JSON。这在 CRUD 应用里很稳,但在 AI Agent 世界里,它迅速崩塌:
响应结构非确定性:同一个 prompt,GPT-4 Turbo 可能返回
{choices:[{message:{content:"A"}}]},也可能返回{choices:[{message:{content:"A\n"}}]}(注意末尾换行)。mock 数据写死content: "A",Agent 里一句response.trim()就让测试通过;但线上真实流量里那个\n会触发下游 parser 的边界错误。你 mock 的不是 API,是运气。Streaming 的时间维度丢失:AI Agent 很少等完整响应才处理。它监听
text/event-stream,逐 chunk 解析data: {"delta":{"content":"h"}}。mock 工具只能返回完整 JSON,或者用setTimeout模拟延迟——但真实 streaming 的 chunk 大小、间隔、buffering 行为,mock 根本无法建模。我们曾为修复一个 streaming 下游解析 bug,写了 17 个不同 delay 组合的 mock 场景,最后发现真正问题是 Cloudflare 在特定 region 对 small chunk 的 TCP ACK 合并策略。Header 和 Metadata 的隐形战场:OpenAI 的
x-ratelimit-limit-tokens、Anthropic 的x-amzn-bedrock-invocation-id、自研 RAG 服务的x-trace-id……这些 header 不参与业务逻辑,但决定重试策略、日志追踪、甚至 billing 分账。mock 通常只 mock body,header 全部忽略。结果就是本地测试绿灯,CI 里因为 missingx-api-key被网关 401,排查两小时才发现 mock config 里漏了一行。
提示:Mock 不是错,错在把它当成“替代品”。在 AI Agent 测试里,mock 的唯一合理角色,是隔离那些你明确不想测的依赖(比如第三方支付回调),而不是替代核心 LLM 交互链路。
2.2 录制回放(Record & Replay)的老路为何走不通?
你可能立刻想到nock或pollyjs——它们确实是 HTTP 录制回放的先驱。但 traceplay 必须和它们划清界限,因为 AI Agent 带来了三个新维度:
| 维度 | 传统录制工具(如 nock) | traceplay 的应对 |
|---|---|---|
| Payload 类型 | 默认 text/plain, application/json;binary(如 multipart/form-data)支持弱,base64 编码混乱 | 原始二进制录制:Buffer.from(payload)直接序列化为 base64,保留所有字节,包括\0、BOM、UTF-8 surrogate pairs。回放时new Uint8Array(atob(...))精确还原。 |
| Timing 语义 | 只记录 request/response 时间戳,不保存中间状态 | 记录每个 TCP segment 的到达时间(基于 Node.jsnet.Socket的'data'事件 timestamp),回放时用setImmediate+performance.now()精确控制 chunk 发送节奏,模拟真实网络 jitter。 |
| Stateful 协议 | 假设每次 request 是独立原子操作 | 显式建模 connection lifecycle:connect→request sent→first chunk→last chunk→close。回放时重建 socket state,让 Agent 里的socket.on('close', ...)逻辑真实触发。 |
举个真实例子:我们有个 Agent 需要调用 AWS Bedrock 的invokeModelWithResponseStream,它返回的 stream 包含event: content-block-delta和event: message-stop两种 event type。nock 只能 mock 一个静态 JSON,而 traceplay 录下的 trace 文件里,你会看到:
{ "events": [ {"type": "chunk", "data": "event: content-block-delta\ndata: {\"delta\":{\"text\":\"Hello\"}}\n\n", "timestamp": 1716234567890}, {"type": "chunk", "data": "event: content-block-delta\ndata: {\"delta\":{\"text\":\" world\"}}\n\n", "timestamp": 1716234567923}, {"type": "chunk", "data": "event: message-stop\ndata: {\"stopReason\":\"end_turn\"}\n\n", "timestamp": 1716234567956} ] }回放时,fetch()的 ReadableStream 会按毫秒级精度 emit 这三个 chunk——Agent 里那个const reader = stream.getReader(); while (true) { const {done, value} = await reader.read(); }循环,行为和生产环境 100% 一致。
2.3 CI 友好性:为什么 “录一次,跑百次” 是硬需求?
GitLab CI 和 GitHub CI 的本质是“无状态容器”。每次 job 启动,都是一个干净的 Ubuntu 镜像。这意味着:
- 不能依赖本地 mock server:
msw需要启动一个 Express server,CI 容器里没端口权限,且 server 生命周期难管理。 - 不能动态生成 mock 数据:
jest.fn()在 CI 里无法访问生产环境的实时数据,mock 数据陈旧。 - token 计费是真金白银:在 CI 里每跑一次 test,就调用一次 OpenAI API,按 $0.01/1k tokens 算,一个中等规模的 Agent 测试套件,每天 CI 成本轻松破百美元。
traceplay 的解法极其暴力:录制阶段(Record)必须在真实生产或预发环境进行。我们约定每周四晚 8 点,运维同学执行npx traceplay record --target=https://api.openai.com --output=weekly-prod-trace.json,抓取当晚 1 小时的真实流量(带 sampling rate=0.1 防爆内存)。这个 JSON 文件提交到 Git 仓库的/traces/目录下,受 code review 约束——就像 review 任何一行业务代码。CI job 里,npm test脚本第一行就是npx traceplay replay --trace=traces/weekly-prod-trace.json,整个测试过程离线、零网络、零 token。你不是在测“代码能不能跑”,而是在测“代码在上周四晚 8:15 那个真实流量下,会不会出错”。
3. traceplay 的核心实现:从录制到回放的全链路拆解
3.1 录制引擎:如何在不侵入业务代码的前提下捕获原始流量?
traceplay 不要求你改一行业务代码。它的录制基于 Node.js 的http/https模块底层 hook,原理如下:
模块加载劫持(Module Load Hook):
traceplay 提供一个register()函数,它在 Node.js 启动早期,通过require.extensions['.js']或process.env.NODE_OPTIONS=--require ./traceplay-hook.js注入。这个 hook 会监测后续所有require('http')、require('https')、require('node-fetch')、require('axios')的加载,并动态 patch 它们的request()方法。原始 socket 层捕获:
关键不是 patchrequest(),而是 patchhttp.ClientRequest.prototype.write和net.Socket.prototype.on('data')。当 Agent 调用fetch('https://api.openai.com/v1/chat/completions', {...})时:ClientRequest.write()被拦截,将原始 request headers + body(作为 Buffer)存入内存 trace buffer;Socket.on('data')被拦截,每次收到 TCP packet,都记录Buffer+performance.now()时间戳;Socket.on('close')触发,将本次 connection 的所有 events(request + chunks + close)打包为一个TraceSpan。
智能采样与过滤:
生产环境流量巨大,全量录制不现实。traceplay 支持:--sampling-rate=0.01:1% 请求概率录制;--include-path="/v1/chat/completions":只录匹配路径的请求;--include-header="x-agent-id: my-cool-agent":只录带特定 header 的请求;--max-body-size=100kb:body 超过则截断并标记"body_truncated": true。
注意:录制时 traceplay 会自动添加
x-traceplay-id: tp_abc123到每个 request header,用于在 production logs 中关联 trace ID,方便事后审计。这个 header 在回放时会被自动 strip,避免污染测试环境。
3.2 Trace 文件格式:为什么是 JSON + base64,而不是 Protocol Buffer?
我们反复讨论过是否用 Protobuf 或 MsgPack 来压缩 trace 文件。最终选择纯 JSON + base64,理由非常务实:
Git 友好:JSON 是文本,可 diff。当你
git diff traces/prod-trace-20240520.json,你能清晰看到:- "data": "event: content-block-delta\ndata: {\"delta\":{\"text\":\"Hello\"}}\n\n" + "data": "event: content-block-delta\ndata: {\"delta\":{\"text\":\"Hello!\"}}\n\n"而 Protobuf binary diff 是一团乱码,Code Review 无法进行。
人类可读可编辑:测试失败时,开发者可以直接打开
.json文件,用 VS Code 搜索"error",定位到那个失败的 response chunk,手动修改status: 500为status: 200,再回放验证修复逻辑——这是 Protobuf 永远做不到的。VS Code 插件生态:已有成熟的 JSON Schema 插件、Prettify 插件、JSONPath 查询插件。我们为 traceplay 定义了 官方 JSON Schema ,VS Code 安装 Schema 插件后,
.json文件自动获得字段提示、格式校验、错误高亮。
一个典型的 trace 文件结构(精简版):
{ "version": "1.2", "recorded_at": "2024-05-20T20:15:33.123Z", "environment": "production", "spans": [ { "id": "span_001", "protocol": "https", "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "request": { "headers": { "authorization": "Bearer sk-...", "content-type": "application/json" }, "body": "ewogICAicG9tIjogImFzc2lzdGFudCIsCiAgInVzZXIiOiAiYm9iIgp9" // base64 of '{"model":"assistant","user":"bob"}' }, "response": { "status": 200, "headers": { "content-type": "text/event-stream", "x-ratelimit-remaining-tokens": "9999" }, "events": [ { "type": "chunk", "data": "ZGF0YTogeyJkZWx0YSI6eyJ0ZXh0OiJIZWxsbyJ9fQoK", "timestamp": 1716234567890 }, { "type": "chunk", "data": "ZGF0YTogeyJkZWx0YSI6eyJ0ZXh0OiIgd29ybGQifX0K", "timestamp": 1716234567923 } ] } } ] }3.3 回放引擎:如何让 fetch() “相信”它正在连接真实服务器?
回放的核心挑战是:如何让业务代码里的fetch()、axios.post()等调用,不经过真实网络,却得到和录制时完全一致的响应流?traceplay 采用“双层拦截”策略:
Layer 1: Global Fetch/Request Override
在回放模式下,traceplay 动态 patchglobalThis.fetch和require('http').request。当fetch('https://api.openai.com/...')被调用时,它不发起网络请求,而是:- 解析 URL,匹配 trace 文件中
spans[].url; - 找到对应 span,检查
spans[].request.body是否与当前 request body 一致(base64 decode 后 deepEqual); - 如果匹配,进入 Layer 2。
- 解析 URL,匹配 trace 文件中
Layer 2: Stream 仿真
这是最精妙的部分。traceplay 不返回一个静态 Response,而是返回一个ReadableStream实例,其内部逻辑是:const stream = new ReadableStream({ start(controller) { const events = span.response.events; let index = 0; const sendNextChunk = () => { if (index >= events.length) { controller.close(); return; } const event = events[index]; // 计算从上一个 chunk 到现在的 delay(毫秒) const delay = index === 0 ? 0 : event.timestamp - events[index-1].timestamp; setTimeout(() => { controller.enqueue(new TextEncoder().encode(event.data)); index++; sendNextChunk(); }, delay); }; sendNextChunk(); } });这个 stream 的
getReader()返回的 reader,其read()方法的行为,和真实 streaming response 的 reader 完全一致——包括done: false、value是Uint8Array、abort()可中断等。Agent 代码无需任何修改,const reader = response.body.getReader();照常工作。
3.4 TypeScript 类型系统深度集成:不只是语法糖,而是安全网
traceplay 的 TypeScript 支持不是“有就行”,而是贯穿开发、测试、CI 全流程的安全增强:
录制时类型推导:
当你在src/agent.ts里写:const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4', messages: [...] }) });traceplay 的录制 hook 会自动提取这个 call site 的
RequestInit类型,并在生成的 trace 文件 schema 中,为spans[].request添加@ts-type注释:{ "request": { "@ts-type": "RequestInit", "headers": { "...": "..." } } }回放时编译期校验:
在test/agent.test.ts中,你写:it('should handle streaming response', async () => { await traceplay.replay('traces/prod-trace.json'); const result = await runAgent(); // calls fetch(...) expect(result).toBe('Hello world'); });traceplay 的
replay()函数签名是:declare function replay(tracePath: string): Promise<void>;但更重要的是,它提供了一个
@traceplay/types包,其中定义了:// node_modules/@traceplay/types/index.d.ts declare module 'traceplay' { export interface TraceSpan { id: string; url: string; request: { headers: Record<string, string>; body: string; // base64 }; response: { status: number; headers: Record<string, string>; events: Array<{ data: string; timestamp: number }>; }; } }这意味着,如果你在 test 里试图访问
span.response.nonexistentField,TypeScript 编译器会直接报错,而不是等到 CI 运行时才发现。VS Code 智能提示:
安装@traceplay/types后,当你在.jsontrace 文件里输入"respo,VS Code 会自动提示"response",并显示其完整接口定义。你甚至可以Ctrl+Click跳转到类型定义——这比任何文档都直观。
4. 实操指南:从零开始,5 分钟接入你的第一个 AI Agent 测试
4.1 环境准备:TypeScript 项目一键集成
假设你有一个基于 Vite + React + TypeScript 的 AI Agent 项目,目录结构如下:
my-agent/ ├── src/ │ ├── agent.ts # 核心 Agent 逻辑,含 fetch 调用 │ └── main.ts ├── tests/ │ └── agent.test.ts # 未来存放 traceplay 测试 ├── package.json └── tsconfig.jsonStep 1: 安装 traceplay
# 开发依赖,只在测试时需要 npm install --save-dev traceplay @traceplay/types # 或 yarn yarn add -D traceplay @traceplay/typesStep 2: 配置 TypeScript(关键!)
在tsconfig.json的compilerOptions中,确保包含:
{ "compilerOptions": { "types": ["node", "traceplay"], // 👈 加入这一行!让 TS 知道 @traceplay/types "lib": ["ES2020", "DOM", "DOM.Iterable", "ScriptHost"], "moduleResolution": "node", "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "resolveJsonModule": true, // 👈 必须开启,因为 trace 文件是 .json "isolatedModules": true, "jsx": "react-jsx" } }Step 3: 创建第一个 trace 文件(录制)
在生产环境服务器上(或你的本地 dev 环境,确保能访问真实 API):
# 录制 10 个请求,保存到 traces/demo.json npx traceplay record \ --target=https://api.openai.com \ --output=traces/demo.json \ --count=10 \ --timeout=30000实操心得:第一次录制,务必用
--count=1和--verbose参数。它会打印出每个 captured span 的摘要,确认你录到了想要的请求。我们曾因.env里OPENAI_BASE_URL指向了 mock server,录了一堆无效 trace,浪费 2 小时。
4.2 编写第一个回放测试:告别 token,拥抱确定性
在tests/agent.test.ts中:
// 引入 traceplay 的全局类型和函数 import { replay } from 'traceplay'; import { runAgent } from '../src/agent'; describe('Agent Streaming Logic', () => { // beforeAll 在所有 test 之前运行,一次性加载 trace beforeAll(async () => { // 注意:路径是相对于 process.cwd(),即项目根目录 await replay('traces/demo.json'); }); it('should parse streaming chunks correctly', async () => { const result = await runAgent(); // runAgent() 内部会调用 fetch(...),但此时已被 traceplay 拦截 expect(result).toBe('Hello world'); // 这个字符串来自 trace 文件里的 events }); it('should handle error status gracefully', async () => { // 我们可以在 trace 文件里手动修改一个 span 的 status 为 500 // 然后测试 Agent 的错误 fallback 逻辑 const result = await runAgent(); expect(result).toBe('Fallback response'); }); });Step 4: 运行测试
# 使用 Vitest(推荐,对 streaming 支持最好) npm install -D vitest # 在 package.json scripts 中添加 "scripts": { "test": "vitest" } # 运行!全程离线,0 token,< 1s npm test4.3 CI 集成:GitLab CI / GitHub CI 的标准配置
GitHub CI (.github/workflows/test.yml):
name: Test AI Agent on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run traceplay tests run: npm test # 👇 关键:确保 trace 文件在 git 中,且 CI 能读取 # traceplay replay 会自动查找 traces/ 目录下的最新文件GitLab CI (.gitlab-ci.yml):
test:ai-agent: image: node:20 before_script: - npm ci script: - npm test artifacts: - coverage/**/* only: - main - develop注意事项:trace 文件必须 commit 到 Git。我们禁止在 CI 中动态录制(
npx traceplay record),因为 CI 环境没有生产 API 密钥,且录制行为本身不可控。所有 trace 文件都由 SRE 团队在受控环境下生成,经 QA review 后 merge。这是 traceplay 的铁律:trace 是代码,不是日志。
4.4 高级技巧:如何用 traceplay 做真正的“回归测试”
录制一次,只是开始。真正的价值在于“对比”和“变异”。
Diff 模式:发现 API 行为漂移
当 OpenAI 更新了/v1/chat/completions的 response schema(比如新增usage字段),你的 Agent 可能因response.usage.total_tokens报错。用 traceplay 的 diff 工具:npx traceplay diff \ --old=traces/openai-v1.0.json \ --new=traces/openai-v1.1.json \ --output=diff-report.md它会生成 Markdown 报告,高亮所有
response.headers、response.events[].data的变化,甚至指出哪个 chunk 的data字段多了 3 个字节。Mutation 模式:注入故障,测试韧性
你怀疑 Agent 在 LLM 返回status: 429时重试逻辑有 bug?不用等真实限流,用 traceplay mutation:npx traceplay mutate \ --input=traces/prod-trace.json \ --output=traces/prod-trace-429.json \ --path='spans[0].response.status' \ --value=429然后在 test 里
await replay('traces/prod-trace-429.json'),直接验证重试逻辑。Coverage 模式:量化测试完整性
traceplay 可以分析你的测试用例覆盖了 trace 文件中多少个 spans:npx traceplay coverage \ --trace=traces/prod-trace.json \ --test-dir=tests/ \ --output=coverage.json输出 JSON 包含
total_spans: 120,covered_spans: 87,coverage_percent: 72.5。这比任何 Istanbul 覆盖率都真实——因为它覆盖的是你真正面对的生产流量。
5. 常见问题与避坑指南:那些只有踩过才懂的细节
5.1 “为什么我的 fetch 调用没被 traceplay 拦截?”
这是新手最高频问题。根本原因只有一个:traceplay 的 hook 没在 fetch 调用之前生效。
典型场景和解决方案:
Scenario A:Vite 开发服务器热更新导致 hook 失效
Vite HMR 会 reload 模块,但require.extensionshook 是全局的,不会被 HMR 清除。问题在于,你可能在main.ts里import { runAgent } from './agent',而agent.ts里import 'traceplay/register'—— 但 Vite 的 import order 不保证traceplay/register一定在fetch之前执行。
✅ 正确做法:在vite.config.ts的optimizeDeps.include中加入['traceplay/register'],强制它最先加载;或在index.html的<script>中提前import 'traceplay/register'。Scenario B:使用了非标准 fetch 实现(如 undici)
traceplay 默认只 patchnode:http和node:https。如果你用undici或cross-fetch,需要显式启用:npx traceplay record --target=https://api.openai.com --use-undiciScenario C:fetch 调用在 Worker 或 iframe 中
traceplay 的 hook 只作用于主 Node.js 进程。Web Worker 有自己的 global scope,需要在 worker script 开头import 'traceplay/register'。
实操心得:永远先运行
npx traceplay debug --list-hooks。它会列出当前进程所有被 patch 的模块(http,https,node-fetch,axios),如果列表为空,说明 hook 根本没加载。
5.2 “回放时 fetch 报错:TypeError: Cannot read properties of undefined (reading 'getReader')”**
这是 streaming response 的经典陷阱。错误原因是:你的 Agent 代码里写了response.body.getReader(),但 traceplay 回放返回的Response对象,其body是一个ReadableStream,而某些旧版node-fetch或whatwg-fetchpolyfill 会把body强制转成null或undefined。
✅ 解决方案分三步:
- 升级 fetch 实现:确保用
node-fetch@3.x或原生globalThis.fetch(Node.js 18+)。node-fetch@2.x不支持 streaming body。 - 在 test 中显式设置全局 fetch:
// tests/setup.ts import { fetch as nodeFetch } from 'node-fetch'; globalThis.fetch = nodeFetch as any; - 检查 Agent 代码的健壮性:
// ❌ 危险写法 const reader = response.body.getReader(); // ✅ 安全写法(traceplay 也推荐) if (!response.body) { throw new Error('No response body'); } const reader = response.body.getReader();
5.3 “trace 文件太大,Git 提交失败”**
一个 1 小时的全量 trace 可能达 500MB。这不是设计缺陷,而是你需要主动管理的数据资产。
✅ 三个层次的瘦身策略:
- Level 1: 录制时过滤
--include-path="/v1/chat/completions" --exclude-header="x-debug",去掉无关请求和 debug header。 - Level 2: 录制后压缩
traceplay 提供npx traceplay compress --input=big.json --output=small.json --max-chunk-size=8192,它会把大 chunk 拆分成 8KB 小块,base64 编码后更利于 Git delta compression。 - Level 3: Git LFS
对于 >10MB 的 trace 文件,用 Git LFS:git lfs track "traces/*.json" git add .gitattributes git commit -m "track traces with lfs"
注意:不要用
gzip压缩 trace 文件!因为.json.gz不可 diff,违背了 traceplay 的 Git 友好原则。压缩必须在 JSON 结构内完成(如 chunk 拆分、base64 优化)。
5.4 “CI 里 replay 报错:Error: ENOENT: no such file or directory, open 'traces/prod-trace.json'”**
这通常不是文件不存在,而是路径问题。
✅ 排查清单:
ls -la traces/确认文件存在,且权限为644(CI 容器默认无写权限);pwd确认当前工作目录是项目根目录(不是tests/子目录);cat traces/prod-trace.json | head -n 5确认文件内容可读;- 最关键:检查
.gitignore—— 是否误加了traces/?trace 文件必须被 Git track!
实操心得:我们在 CI 的 first step 总是加一行
echo "Current dir: $(pwd) && Files in traces/: $(ls -la traces/)",日志里一眼看清路径真相。
5.5 “如何测试多个 Agent 并行调用同一个 LLM API?”**
traceplay 天然支持并发。录制时,它为每个 TCP connection 分配唯一span.id;回放时,replay()会并发处理所有 spans。
✅ 但要注意:你的 Agent 代码必须是“纯函数式”的,不能依赖全局 mutable state。例如:
// ❌ 危险:共享 counter let requestCount = 0; async function runAgent() { requestCount++; // 这个值在回放时会错乱! return fetch(...); } // ✅ 安全:stateless async function runAgent() { return fetch(...); }traceplay 的回放是 deterministic 的,但前提是你的业务逻辑也是 deterministic 的。这是 AI Agent 测试的终极哲学:你无法控制 LLM,但你能控制自己的代码。
6. 从 traceplay 到 AI Engineering:一个工具引发的工程范式迁移
我最初写 traceplay,只是想让 CI 不再因网络抖动失败。但上线三个月后,它意外地重塑了我们整个 AI Engineering 流程。
首先是测试文化的转变。以前 PR review 的焦点是“这段代码逻辑对不对”,现在变成了“这个 trace 文件覆盖了哪些真实流量场景?有没有 edge case 漏掉?”。SRE 同学开始主动给每个新上线的 Agent 提供trace-sample.json,里面包含 5 个典型对话流(成功、streaming timeout、429 rate limit、503 service unavailable、malformed response)。这比任何 swagger doc 都直观。
其次是故障复盘的效率革命。上周生产环境出现一个偶发 bug:Agent 在处理长文本时,RAG 服务返回的 chunk 顺序错乱。过去,我们要翻查 3 小时的 CloudWatch logs,grep 出 200 行相关日志,再拼凑出时间线。现在,运维直接git checkout出那个时间点的 trace 文件,npx traceplay replay --trace=traces/20240519-2215.json,在本地 10 秒内复现,然后git bisect找出引入 bug 的 commit。整个过程 12 分钟,而不是过去的 3 小时。
最深远的影响,是对“确定性”的重新定义。在传统软件工程里,“确定性”意味着相同输入必