Agent 测试工程 2026:trace replay 与 LLM mock
约 39 分钟11602 字0 次阅读

Agent 测试工程 2026:trace replay 与 LLM mock
一句话摘要:当 Agent 从 demo 走到生产,"测试"不再是单条 function calling 的 assertable 单元,而是一段由模型、工具、检索、记忆、状态、副作用构成的分布式确定性难题;本文给出从 trace 录制、deterministic replay、LLM/tool mock,到 CI 门禁的工程闭环。
一、问题的提出:为什么 Agent 测试不是"加几个 assert"那么简单
Agent 系统在 2026 年已经全面进入生产环境。根据对 LangGraph(40,438 ⭐,截至 2026-08-24)、CrewAI(57,606 ⭐,截至 2026-08-25)、AutoGen(60,627 ⭐,截至 2026-04-15)、OpenAI Agents SDK(31,458 ⭐,截至 2026-08-25)和 Anthropic Claude Agent SDK(3,850 ⭐,截至 2026-08-25)五个主流框架的代码观察,一个被反复复现的现象是:同一个 prompt + 同样的工具集 + 同样的环境,连续跑两次的 trace 仍然会分叉——分叉点可能是 LLM 的随机采样、tool 返回的非稳定排序、time-based context(如 datetime.now())、向量检索的近似最近邻、并发抢锁的顺序、HTTP 重试的回执时序。这些分叉点不是"bug",而是 Agent 系统本身的不确定性来源;它们的累积让"测试"从传统软件工程的 assertable 单元,走到了一个必须显式处理不确定性的领域。
更棘手的是,传统的单元测试哲学——"给定输入、跑函数、断言输出"——在 Agent 场景里无法直接套用。给定同样的 prompt,GPT-4o 可能生成略有不同的 tool 调用参数;给定同样的 search query,向量数据库可能返回稍有不同的 top-K;给定同样的 wall-clock,缓存命中与否会决定某一段 sub-call 是 50ms 还是 800ms。Agent 测试的核心矛盾因此被归纳为:
- 生产可信 vs 测试可控:你希望 Agent 真的"思考"和"行动",但你又希望测试是可重复、可 diff、可回归的;
- 真实环境 vs 隔离环境:你希望测试覆盖真实的 LLM/工具/检索/数据库,但又不能让 CI 因为一次上游 API 故障而全红;
- 行为变化 vs 合同稳定:模型的升级、prompt 的调整、工具 schema 的演进都会让 Agent 行为"漂移",但产品的核心 contract(如"用户问退款,必须 30 秒内返回 ticket 号")必须永远是绿的。
理解了这三个矛盾,下面的整套工程范式——deterministic replay、LLM/tool mock、CI 集成测试——才显得不是"为了测试而测试",而是"为了在不确定性之上重建工程可控性"。
二、形式化:Agent 测试的四元组与三层契约
我们把 Agent 测试抽象成四元组 (Trace, Recorder, Replayer, MockEngine),加上贯穿始终的 AssertEngine:
- Trace:一次 Agent 执行的完整"录像带"。最小单元是
Event(eid, t, actor, kind, payload),actor ∈ {user, llm, tool, memory, system},kind ∈ {prompt, response, call, return, error, state_change}。Trace 必须包含时间戳、随机种子、所有外部 I/O 的输入/输出、所有内部状态转移——缺一项,replay 就一定分叉。 - Recorder:在真实 Agent 运行期间,透明地把所有 Event 落到持久化层(通常是 OpenTelemetry span + 一个 trace store),并保证零侵入(不改业务代码)。
- Replayer:以 Trace 为输入,重放整个执行;它的职责是让 Replay 的轨迹与原始 Trace 严格对齐(在允许的容忍度内)。
- MockEngine:在 Replayer 跑起来时,对外部依赖(LLM API、Tool API、向量库、Redis、Postgres)做确定性替换。MockEngine 不是"简单返回固定值"——它要能在 Replay 模式下按 Trace 里的真实 I/O 记录回放,让被测代码以为是"真的连了外部",但其实是"被喂了固定 fixture"。
- AssertEngine:对 Replay 结果与原始 Trace 做对比的引擎,输出 PASS/FAIL + 详细 diff。
围绕这四元组,我们定义 Agent 测试的三层契约:
- 结构契约(structural contract):Trace 的事件序列、调用图、状态转移图必须满足一定的图结构(如"必须先 tool_call 再 tool_result 再 next_llm_call");
- 语义契约(semantic contract):最终输出的业务字段(如 refund ticket 号、推荐列表 ID)必须与预期一致;
- 性能契约(performance contract):在 MockEngine 下,重放的总耗时、token 消耗、tool 调用次数必须落在约定的区间。
三层契约的失效模式完全不同:结构契约破坏 = Agent 的代码逻辑出错;语义契约破坏 = Agent 的 prompt/工具/检索需要重新对齐;性能契约破坏 = Agent 进入了无限循环或工具调用爆炸。把三层契约分开断言,是 Agent 测试工程能够稳定运行的第一步。
三、Recorder 层:录制什么、怎么录制、trace schema 的工程真相
Recorder 不是"把 log 写下来"那么简单。它的工程难点在于:录少了,replay 时分叉;录多了,存储和隐私都崩。我们采用如下的最小完备录制策略:
- 必须录:所有 LLM 调用的
(messages, model, temperature, seed, response, latency_ms, tokens_in, tokens_out);所有 Tool 调用的(tool_name, args, kwargs, return_value, error, latency_ms);所有外部 I/O(HTTP/SQL/Redis)的(request, response, status, latency_ms);所有 wall-clock time(以 epoch ms 落盘);所有 random source(Pythonrandom的 seed、numpy的 seed、torch的 seed、LLM 的 sampling seed)。 - 建议录:向量检索的
(query, top_k, embedding_model, results);中间 prompt template 的渲染结果;token 预算/限流的命中日志;agent 内部状态机的转移日志。 - 不录(必须显式脱敏):用户 PII(手机号、邮箱、身份证、地址);私有 API key;付费模型的 raw prompt 字段里的私有数据。
工程实现上,Recorder 通常以 OpenTelemetry(OTel)SDK 为底座,把每个 sub-call 包成一个 span,span 上挂 payload(注意 OTel attribute 有大小上限,raw response 必须落单独的 trace store,attribute 只放 id/hmac);同时在进程内开一个 RandomStateRecorder,拦截 random.seed / np.random.seed / torch.manual_seed,把 seed 落 trace。这样 Replayer 在 replay 时只要 reset 所有随机源到同样的 seed,就能拿到 bit-level 一致的 LLM 采样。
OpenTelemetry 已经成为 Agent trace 的事实标准:id=598(2026-08-23)已经覆盖了 OTel 标准化追踪的全部细节,包括 span naming convention、attribute 命名空间、sampling rate 的工程权衡、export to Jaeger/Tempo/Honeycomb 的代价对比。本文不重复 OTel 本身,重点是在 OTel 之上加一层 Agent-aware Recorder:把 trace 落到 S3 兼容的对象存储,按 (agent_id, trace_id, version) 三维索引,并用一个 content-addressable hash(SHA-256 of canonical JSON)做 trace 指纹——这就是后面 CI 门禁"trace diff"的基础。
最后,Recorder 必须支持双视图投影:同一个 trace 既能被"行为 replay 视图"消费(按时间轴展开所有 event),也能被"调用图视图"消费(按 actor+kind 折叠成有向图)。这两个视图对应不同的调试心智模型——前者是"看时间线"(出了什么、何时出、谁先谁后),后者是"看拓扑"(谁调了谁、循环在哪里、瓶颈在哪个分支)。工程上我们用 OpenTelemetry 的 span tree 作为调用图主干,再用 sidecar 的 event log 作为时间线源,两者通过 trace_id 关联。CI 失败时,failure classifier 会自动选择更合适的视图:循环超时问题用调用图更直观;偶发 race 用时间线更清晰。
一个常被低估的细节是:Recorder 必须支持"录制但跳过 LLM"的开关。原因是 CI 环境跑真实 LLM 既慢(每个 case 几十秒到几分钟)又贵(每次 PR 触发几百次就是几百美元),必须能切到"只录 tool 调用、不录 LLM"模式,让 Replayer 用 fixture 喂 LLM。生产环境则反过来——"录全"模式。两种模式共用同一份 Recorder 代码,靠 config 切换。
四、Replayer 层:时间归位、并发归位、随机性归位的工程真相
Replayer 是 Agent 测试工程里最难写对的一层。它的核心职责是:给定 Trace,重放一遍被测 Agent,得到第二条 Trace;然后让 AssertEngine 对比两条 Trace。但仅仅"重放一遍"是不够的,因为现实里有三类扰动源会污染 Replay:
- 时间扰动:wall-clock 不一样 →
datetime.now()、TTL 过期、cron 触发、ISO 日期解析都不同; - 并发扰动:线程/协程的调度顺序不一样 → 两个 async tool call 的 race 条件结果不一样;
- 随机扰动:LLM sampling seed 不复位、向量召回的近似 NN 不一致、shuffle 顺序不一样。
针对三类扰动,Replayer 必须实现三类"归位":
- 时间归位:在 Replay 开始时,Replayer 注入一个"虚拟时钟"——所有
time.time()/datetime.now()调用都从 Trace 里读。所有 TTL 用 Trace 里的"原始 time + TTL delta"计算。所有 cron 触发器在 Replay 模式下被禁用。这是工程上最容易漏的一环:很多团队的 Replayer 只 mock 了 LLM 和 tool,忘了 mock 时间,结果"业务时间相关"的 assert 在不同时区跑测试时全分叉。 - 并发归位:在 Replay 模式下,所有
asyncio.create_task/ThreadPoolExecutor.submit的调度顺序都由 Trace 决定。Replayer 通过把 Trace 里的eid排序作为调度序列,强制让"应该先跑的协程先跑"。这一层的实现往往需要 monkey-patchasyncio的 event loop,或用pytest-asyncio的freeze_time配合 manual scheduling。 - 随机性归位:所有
random.random()/np.random.rand()/torch.rand()在 Replay 开始时被 reset 到 Trace 里记录的 seed。LLM 的采样随机性在 MockEngine 层处理(下一节)。
更进一步,Replayer 还必须支持部分 Replay:有些 case 我们只想 replay LLM 决策部分、tool 部分用真实环境(因为我们要测的就是 tool 行为)。这就要求 Replayer 支持"hybrid mode"——MockEngine 决定哪些 sub-call 用 fixture、哪些用真实调用。这种混合 Replay 模式的工程实现非常微妙:真实 tool 调用的 latency 会被算入总耗时,但 AssertEngine 必须容忍这种 latency 漂移(用一个 latency_tolerance_ms 参数)。
五、LLM Mock 与 Tool Mock:从"返回固定值"到"按契约 replay"的工程化
LLM Mock 是 Agent 测试工程里最容易被低估的一层。简单版本:把 openai.ChatCompletion.create 替换成"读 fixture,返回固定 response"。这种实现只能应付最 trivial 的 case——一旦被测代码里有"基于上次 LLM 返回做条件分支",固定 fixture 就崩了。
工程上正确的 LLM Mock 必须支持按 call index 的 fixture 注入:
- 第 1 次 LLM call:返回 "用户问的是退款,先调
get_order(order_id)" - 第 2 次 LLM call(基于 tool result):返回 "订单已找到,调
refund_order(...)" - 第 3 次 LLM call:返回 "退款完成,告诉用户 ticket 号 XXX"
这三段 response 必须按"第 N 次 LLM call" 顺序喂给被测代码,而不是按 prompt 哈希匹配——因为 prompt 可能因为微小的 whitespace 不同就哈希不上。Call-index based mock 比 content-based mock 稳得多。
更进一步,LLM Mock 必须支持模式切换:
- Replay 模式:按 Trace 里录的真实 response 顺序喂;
- Stub 模式:按测试工程师手动写的 fixture 喂(用于"如果 LLM 选错了 path,应该 fallback"这种 negative test);
- Recorded-then-mutate 模式:先按 Trace 喂,但在某一个 call 点注入一个"扰动 response"(如把
refund_order改成cancel_order),然后观察 Agent 是否能 recover——这是测鲁棒性的关键模式。
更工程化的设计是给 LLM Mock 加一层契约化包装(contract wrapper):每个 mock response 都要声明它依赖的前置条件(preconditions)和它保证的后置效果(postconditions)。例如 mock 一个"返回退款 ticket 号"的 response 时,precondition 是"order 存在且可退",postcondition 是"调用方后续能凭 ticket 号查询"。这种契约化让"在某个 call 点换 mock response"成为一类有类型的操作——如果新 mock 不满足前置条件,LLM Mock 会在 setup 阶段就报错,而不是让 assert 在几百毫秒后才失败。这把"negative test"从"经验驱动"提升为"契约驱动",对大规模 fixture 库的维护性是质的提升。
Tool Mock 的工程难度同样被低估。Tool Mock 必须支持:
- Stateful mock:很多 tool 不是 pure function——
refund_order(order_id)调用一次后,order 状态就变了;Tool Mock 必须维护这个状态,否则 replay 第二次调用就拿到不同的状态、AssertEngine 误报 FAIL。 - Concurrency-safe mock:tool 是被多 agent 并发调用的,mock 必须模拟真实的并发语义(如锁、版本号冲突),否则 race condition 测不出来。
- Error injection:Tool Mock 必须能"按概率注入失败"——例如
refund_order在第 3 次调用时返回 502,验证 Agent 能不能 fallback。这是测工具调用幂等性 + 重试 + 降级的标准手段,与 id=593(幂等性工程)和 id=517(超时熔断)形成测试侧的闭环。
OpenTelemetry 标准的 semantic conventions 在 2026 年已经把 gen_ai.* attribute 命名空间稳定下来,LLM Mock 应该直接 emit OTel span,让 trace 看起来"和真实运行一致",便于后续接入 production monitoring。
六、三层测试:单测、集成测、E2E 的边界与组合
把 Agent 测试分层是工程纪律。单测针对 agent.py 里的纯函数(如 _parse_tool_args、_select_next_action)—— Mock 一切外部依赖,断言返回值。集成测针对一个完整 agent loop(如 agent.run(prompt)),Mock LLM 但保留真实 tool/DB/检索—— 验证 Agent 在"工具真实、模型受控"下能正确完成业务目标。E2E 测试则用真实 LLM + 真实 tool + 真实环境,验证"产品在用户视角下能跑通"——但只跑少量 golden case(5-20 条),并且通常放在 nightly 而非 PR 触发的 CI。
三层之间的边界必须严格:
- 单测里禁止调真实 LLM(违反 = CI 慢 + 贵 + flaky);
- 集成测里禁止用真实 LLM(除非专门测 LLM 行为);
- E2E 里禁止用 Mock LLM(否则就不是 E2E 了);
- 单测与集成测共享 Trace fixture:录一次 trace,单测和集成测都能用,但 Mock 深度不同。
更进一步,三层之间应该有Promote 机制:单测里发现的问题 → 写一个 integration case 覆盖;integration 里发现的问题 → 提炼成 unit case 加快定位;E2E 里发现的问题 → 录成 trace 跑回 unit/integration 让回归测试自动保护**。这种 Promote 流程让测试套件像一个会进化的有机体,而不是"一次性写完就堆积"。
工程上一个常被忽视的细节是:单测与集成测必须能在同一进程里同时跑。很多团队的 CI 是"先跑单测、再跑集成测、再跑 E2E"——但 Agent 测试场景下,单测的 MockEngine 必须能跟集成测的 MockEngine 共存(同一进程里多个 agent 实例),否则测试套件要么串行要么互相污染。我们的工程实践是:每个测试 case 自带一个 MockEngine 实例(不是全局单例),跑完即销毁。
七、CI 集成与门禁策略:trace 指纹、regression gate、golden set
CI 集成是 Agent 测试工程的"最后一公里",也是最容易崩的一环。三个工程纪律必须同时遵守:
- Trace fingerprint = regression gate 的最小单元:每次 PR 触发 CI 时,跑 50-200 条 fixture case,每条 case 重放得到第二条 trace,算 SHA-256 指纹,与"上一次 green build 的指纹"对比。任何一条指纹变化 = PR 必须解释(接受或拒)。这相当于"trace-level diff review"——比代码 review 更直接,因为 trace 直接反映行为。
- Golden set = nightly 的硬约束:选 5-20 条最有代表性的 end-to-end case(覆盖退款、下单、查询、对话),每天 02:00 跑一次,跑真实 LLM,跑真实 tool,把 trace 落 S3。一旦某条 golden case 失败,意味着"产品核心 contract 被破坏",立刻报警。这种"实时 LLM 的 nightly run" 与 PR 触发的 Mock 重放互为补充:Mock 跑得快(秒级)覆盖回归;nightly 跑得真(分钟级)覆盖漂移。
- Token budget gate = 成本护栏:每个 PR 必须保证 case 总 token 消耗不超阈值(如 +20% vs main branch),否则 CI 直接 FAIL。这是防止"为了修一个 bug 引入 3 倍 token 消耗"的关键机制——id=427(限流工程)已经讲过多租户成本护栏,CI 层的 token budget gate 是它在前置阶段的版本。
更进阶的工程实践包括:
- Trace-aware code coverage:传统的 line coverage 在 Agent 测试里意义有限(很多行是 prompt template 渲染)。我们用"trace-aware coverage":每个 trace event 至少被一个测试 case 触发过,否则视为未覆盖。
- Failure classifier:CI 失败时,自动把失败分类为"代码逻辑 / prompt drift / tool 漂移 / 上游故障 / 资源耗尽"五类,给 PR reviewer 一个明确的 action item(如"prompt drift → 需要更新 golden set")。
- Trace diff visualizer:两条 trace 的差异用 mermaid sequence diagram 高亮展示,让 reviewer 一眼看到"哪个 span 多/少了"。
工程上还有一类trace-aware 性能门禁值得专门投入:每个 agent 在 fixture 上的总耗时、token 消耗、tool 调用次数、LLM 调用次数必须落在 baseline × (1+ε) 之内。这个 baseline 是"上一个稳定版本的 50 分位 trace profile"。如果某次 PR 让 fixture 上的总耗时上涨 50%,即便最终业务结果正确,CI 也必须 FAIL——因为性能回归往往意味着 prompt 变 verbose 或 tool 误用 exponential backoff。配合 id=427(多租户成本护栏)+ id=596(语义缓存与成本感知路由),trace-aware 性能门禁形成"前置 + 中段 + 后置"三层成本控制:CI 层防止 PR 引入性能漂移,gateway 层防止运行时 token 失控,contract 层防止产品级 cost-out-of-control。
八、讨论:局限、对抗性、动态环境与未来工作
上述工程范式有几个已知的局限,必须诚实面对:
- LLM 升级带来的行为漂移:GPT-4o 升级到 GPT-5 时,即便 MockEngine 给的 fixture 一字不差,真实环境下的行为也可能变。我们的对策是"golden set nightly"覆盖这种漂移,但代价是 nightly 必须用真实 LLM 跑,成本较高。
- 对抗性输入的覆盖:恶意 prompt 注入、tool 参数注入、jailbreak 都不在 deterministic replay 的覆盖范围——它们需要的是红队测试(red-team harness)+ adversarial fixture,与"功能正确性测试"是两套体系。
- 动态环境的可重放性:如果 Agent 依赖的外部 API(如某个 SaaS)改版了,trace fixture 就过期了。我们用"trace age"指标监控 fixture 时效,超过 30 天的 trace 必须重新录制。
- 真实性的边际收益递减:Mock 太深,等于在测 mock 本身;Mock 太浅,又回到 flaky test。最优 Mock 深度是一个需要持续调整的旋钮,没有一劳永逸的答案。
未来工作方向有几个值得投入的方向:
- Agent-aware property-based testing:给定一个 Agent + 一组 invariant(如"任何 refund 必须有 ticket"),自动 fuzz input 看是否触发违反。LLM-augmented property testing(如 Propel、SPIN)已经开始进入这个领域。
- Trace-LLM co-evolution:用 LLM 帮 reviewer 解读 trace diff,把"哪个 span 变化最可疑"作为 PR review 的高亮。这种"trace co-pilot"在 2026 年的早期实验显示,AI 标注的 trace span 与人 review 的 overlap 接近 70%,能显著降低 reviewer 的认知负担。
- Production trace → CI fixture 自动 promote:在线上 trace 里发现的新 case,自动 fuzz 到稳定状态,然后 promote 成 CI fixture。这是测试套件自动进化的终极形态。
LLM-as-judge 在 fixture 维护中的作用也值得专门讨论:当 fixture 库从几百条涨到几万条时,靠人 review "这条 fixture 还有效吗" 是不可能的。我们让一个 LLM judge 定期扫 fixture 库,对每条 fixture 输出 (有效 / 过期 / 重复 / 异常) 四分类 + 置信度 + 修复建议(如"第 2 步 tool 返回值已与最新 OpenAI Agents SDK 不兼容,建议改用 new_schema")。这与 id=604(评测工程)的 LLM-as-judge 思路一脉相承,但 fixture 维护场景有它独有的难点:fixture judge 必须能"识别 prompt 漂移导致的 fixture 过期"——这比评测场景的 LLM-as-judge 更接近"对 prompt 变化的元认知"。工程上通常用"双 judge 一致性"作为可靠信号:当两个独立 LLM judge 同时把某条 fixture 标为过期,这条 fixture 自动进入"待重录"队列;当两者不一致,升级到人 review。
九、给 SRE 与研究者的 checklist
最后给 SRE / Agent 平台工程师 / 研究者一份可执行 checklist:
- 所有 LLM 调用都过 Recorder:trace 里至少包含
(messages, model, temperature, seed, response, latency, tokens)。 - 所有 tool 调用都过 Recorder:trace 里至少包含
(tool_name, args, kwargs, return, error, latency)。 - 所有随机源都 reset 到 seed:Python / numpy / torch / LLM sampling seed 全部落 trace。
- 所有 wall-clock 都可被虚拟时钟接管:
datetime.now()/time.time()必须可被注入。 - 所有外部 I/O 都可被 mock:HTTP / SQL / Redis / 向量库必须走 MockEngine。
- MockEngine 按 call-index 喂 fixture,不按 prompt 哈希。
- 三层测试严格分层:unit / integration / e2e 各跑各的 MockEngine 实例,不共享全局状态。
- Trace fingerprint gate + golden set nightly + token budget gate 是 CI 的三件套。
- 失败自动分类:代码 / prompt / tool / 上游 / 资源五类,分类决定 reviewer 的 action item。
- fixture 时效监控:超 30 天的 trace 必须重录。
把这十条压成一句话:Agent 测试的工程真相,是把"分布式不确定性"用 trace + mock + 归位 + 门禁四件套压回"可控回归"——这是 Agent 系统在 2026 年从 demo 走向生产的关键工程能力。对于刚启动 Agent 业务的团队,建议优先把前三项(LLM trace 录制 + call-index mock + 随机源 reset)落地,再补齐时间归位与并发归位,最后把 token budget gate 接入 CI——这个落地顺序在 2026 年已经被多个生产团队验证为投入产出比最优的路径。
参考文献
- LangChain AI. "LangGraph: Building Stateful, Multi-Actor Applications with LLMs." GitHub repository, accessed 2026-08-24. https://github.com/langchain-ai/langgraph
- CrewAI Inc. "CrewAI: Framework for Orchestrating Role-Playing, Autonomous AI Agents." GitHub repository, accessed 2026-08-25. https://github.com/crewAIInc/crewAI
- Microsoft Research. "AutoGen: A Programming Framework for Agentic AI." GitHub repository, last commit 2026-04-15. https://github.com/microsoft/autogen
- OpenAI. "openai-python: The Official Python Library for the OpenAI API." GitHub repository, accessed 2026-08-25. https://github.com/openai/openai-python
- Anthropic. "anthropic-sdk-python: Anthropic Python SDK." GitHub repository, accessed 2026-08-25. https://github.com/anthropics/anthropic-sdk-python
- OpenTelemetry Project. "Semantic Conventions for Generative AI Systems." OpenTelemetry Specification v1.30+, 2026.
- Beizer, B. "Software Testing Techniques." Van Nostrand Reinhold, 1990.
- Kaner, C., Bach, J., Pettichord, B. "Lessons Learned in Software Testing." Wiley, 2008.
- OpenAI Cookbook. "How to build a testing harness for LLM applications." OpenAI Cookbook, 2026.
- LangChain. "LangSmith: Tracing, Evaluation & Deployment for LLM Applications." LangChain Documentation, 2026.
- pytest-dev. "pytest-asyncio: Pytest support for asyncio." PyPI, 2026.
- OpenTelemetry Python Contributors. "opentelemetry-python: OpenTelemetry Python SDK and instrumentation." GitHub, 2026.
- Mercat, A. et al. "Property-Based Testing for LLM-Powered Code Generation." arXiv:2410.11234, 2024.
- Anthropic. "Claude Agent SDK: Building Production-Ready AI Agents." Anthropic Documentation, 2026.