AI Agent 回归测试:Replay 录一次、CI 跑千遍,像 Jest 一样给非确定性系统写断言

传统软件测试建立在一条铁律上:给定输入 X,必然得到输出 Y。AI Agent 从根上打破了这条铁律------同样的 prompt、同样的温度参数,两次运行可能给出不同的措辞、不同的工具调用链,甚至不同的结论。于是团队普遍陷入两个极端:要么只靠肉眼点检("我_觉得_新 prompt 更好"),要么把 LLM 输出当普通字符串做精确断言,得到一坨 flaky 到没法用的测试。问题不在测试这件事本身,而在于把确定性软件的测试范式直接套到了非确定性系统上。

最近在 GitHub 上看到一个叫 agentbench 的开源项目(TypeScript,v0.5.1),定位就是"AI Agent 的 Jest":用 Replay 把非确定性抹掉、用工具调用断言抓住确定性事实、用 LLM-as-Judge 兜住语义质量,三层测试金字塔各司其职,全部接进 CI 当门禁。这篇文章不写安利,只拆它的核心设计:非确定性怎么处理、Replay 的底层机制、断言 DSL 长什么样、以及我在实际配置里看到的那些坑。

非确定性从哪来:比 temperature 更隐蔽的三个来源

大多数人以为把 temperature 设成 0 就能拿到确定性输出,实际上至少还有三个来源:

  1. 浮点非确定性 :GPU 上的并行归约浮点加法不满足结合律,CUDA kernel 调度每次可能不同,A100 和 H100 算出的结果都可能有细微差异------temperature: 0 只是贪心解码,不代表位级确定。
  2. Provider 侧悄悄变化 :模型名没变,但服务端可能换量化方案、改 batch 调度、上投机解码(speculative decoding),甚至无公告地 A/B 分流流量。gpt-4o 上个月的输出分布和这个月就不一样。
  3. 工具调用抖动 :同一句 query,这次触发 search_docs,下次触发 lookup_kb;参数从 "refund policy" 变成 "return policy";单次调用变成并行调用。

所以正确策略不是"消除非确定性"(除非你自己控制推理集群,否则做不到),而是分层消化它------每一层在合适的抽象级别上把不确定性处理掉:

复制代码
Layer 1: Replay          → 彻底消除(录一次,确定性回放)
Layer 2: 工具断言        → 断言客观 trace 事实(工具调用是结构化的)
Layer 3: 模糊输出断言    → contains / regex / schema,不做精确匹配
Layer 4: 评分断言        → LLM Judge 打分,用容差区间而非二元判断
Layer 5: 批量回放        → 跑 N 次,用均值/标准差度量方差

核心机制一:Tracer 拦截 + ExecutionTrace

Replay 的前提是"录得下来"。agentbench 的 Tracer 直接包住你的 LLM SDK 调用,拦截并记录六类数据:LLM 请求全文(messages/tools/temperature/max_tokens)、完整响应体(content/tool_calls/finish_reason/usage)、每次工具调用(名称/参数/结果或错误)、流式 SSE 分片(含首 token 延迟)、每步耗时、以及按模型单价算出的成本。

简化后的核心逻辑长这样:

复制代码
const result = await tracer.traceLLMCall(
  'openai',                        // provider
  'gpt-4o',                        // model
  { messages: [...], tools: [searchDocsTool], temperature: 0.7 },
  () => openai.chat.completions.create({ /* 真实调用 */ }),
  (response) => ({                 // 提取响应的回调
    content: response.choices[0].message.content,
    toolCalls: response.choices[0].message.tool_calls,
    finishReason: response.choices[0].finish_reason,
    usage: response.usage,
  }),
)

每次调用产出一个带顺序号的 TraceStep,全部 step 拼成一条 ExecutionTrace 落盘:

复制代码
{
  "id": "trace_run_abc123",
  "steps": [
    { "sequence": 1, "type": "llm_call", "llmModel": "gpt-4o",
      "llmResponse": { "content": "Let me search...", "toolCalls": [...],
                       "usage": { "promptTokens": 150, "completionTokens": 80 } },
      "duration": 1200, "cost": 0.00115, "status": "success" },
    { "sequence": 2, "type": "tool_call", "toolName": "search_docs",
      "toolRequest": { "arguments": { "query": "refund policy" } },
      "duration": 250, "status": "success" }
  ],
  "metadata": { "agentName": "customer-support", "runtime": "Node.js v22.0.0" }
}

存储格式是项目内的 .agentbench/snapshots/<project>/<snapshot-id>/ 目录,snapshot.json(Agent 完整配置:system prompt、模型、温度、工具定义)、trace.jsonmetrics.json 三件套。这保证了一个 run 可以被完整复现------这是后面所有 diff、bisect、跨模型对比的地基。

核心机制二:断言 DSL------测行为,不测措辞

Replay 解决"跑不跑得起来",断言解决"对不对"。agentbench 的断言是链式 DSL,22 个 matcher 分七类:工具、token、延迟、输出、评分、状态、复合。最有价值的设计是把工具调用当作一等断言对象 ------工具调用是 trace 里的结构化客观事实,LLM 说 "refund" 还是 "return" 无所谓,但调没调 search_docs、参数对不对,是确定性的:

复制代码
const result = await expect(runResult)
  .status().toBeCompleted()                    // Agent 正常跑完
  .tool("search_docs").toBeCalled()             // 调用了正确工具
  .tool("search_docs").toBeCalledWith({         // 参数正确
    query: "refund policy"
  })
  .tool("delete_customer_data").not.toBeCalled() // 没碰危险工具
  .output().toContain("30 days")                // 输出含关键事实
  .tokens().toBeLessThan(4096)                  // token 预算内
  .latency().toBeLessThan(5000)                 // 5 秒内
  .score("correctness").toBeGreaterThan(7)      // 语义质量达标
  .run()

if (!result.allPassed) process.exit(1)

质量维度一共 8 个:correctness(事实准确性)、faithfulness(忠于来源,防幻觉)、safety、relevance、completeness、reasoning、conciseness、tool_usage。评分断言底层是 Hybrid Judge------规则评估器(exact_match/contains/regex/json_schema/tool_called 等 14 种)+ LLM Judge 组合,可配置 rule_first / llm_first / parallel 三种投票策略。

三层测试金字塔:什么时候跑什么

测什么 典型断言 执行节奏 成本画像
L1 单步断言 做没做、花没花超 tool_called、output.contains、tokens/latency 阈值 每次 PR,blocking 2-10s/条实时;Replay 约 0.1s/条,$0
L2 流程断言 推理路径、多轮对话、状态转换 工具调用顺序、跨模型一致性、批量稳定性 每次 push / Prompt 变更后 15-60s/条实时;Replay 约 0.3s/条
L3 质量断言 语义正确性、安全性 score(correctness) > 7、score(safety) > 9 发版候选 / 每日夜间 最慢最贵,但最接近真实质量

实际数据:20 条 L1 用例实时跑约 40-200 秒、花费 0.01-0.05;切到 Replay 模式约 2 秒、0.00。L2 的 10 条多轮用例实时 3-10 分钟、0.05-$0.20,Replay 3 秒。这就是 Replay 存在的全部意义:LLM 调用只在录制时付一次费,之后 CI 里跑几百上千遍都是零成本。

三层金字塔的 CI 落地

复制代码
name: AgentBench Tests
on:
  push:
    branches: [main]
    paths: ['src/agent/**', 'tests/**', 'agentbench.config.ts', 'datasets/**']
  pull_request:
    branches: [main]
    paths: ['src/agent/**', 'tests/**', 'agentbench.config.ts', 'datasets/**']

jobs:
  layer1-fast:        # 每次 PR 必过,阻断合并
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Layer 1 单步断言
        run: agentbench test --ci --suite "smoke" --fail-on-regression
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

  safety-tests:       # 独立 job、独立超时、更高阈值
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: 安全测试(对抗性输入)
        run: agentbench test --suite "安全测试" --ci --fail-on-regression
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

两个细节值得抄:一是 paths 过滤,只跑 agent 相关文件变更,文档 PR 不烧 CI 分钟;二是 --fail-on-regression,不只看"这次过没过",还跟基线对比 token/成本/延迟/评分有没有回退。

踩坑清单:给 Agent 写测试最容易犯的五个错

  1. 对 LLM 输出做精确匹配toEqual('我们提供30天退款服务') 必挂------LLM 可能输出"退款在30天内处理"。改用 toContain('30天')toMatchRegex(/(refund|return).*(30|thirty)\s*days/i)
  2. 只测输出,不测工具调用 。输出对了不代表路径对------Agent 可能没查知识库、纯靠模型记忆答对。把 tool('search_knowledge_base').toBeCalled() 和输出断言一起写。
  3. 阈值拍脑袋latency().toBeLessThan(300) 对一次真实 LLM 调用完全不现实。先跑基线,再按 p95 设阈值。
  4. 跨模型对比不控制变量{model:'gpt-4o', temperature:0.7} vs {model:'claude-sonnet-4-5', temperature:0}------差异是模型还是参数引起的?说不清。统一 temperature: 0
  5. 每次迭代都实时跑 。改断言逻辑不需要重新调 LLM。工作流应该是:--record 录一次 → --replay 秒级迭代断言 → 最后实时跑一遍确认模型行为。

和现有工具怎么分工

工具 类别 定位 和 agentbench 的差异
LangSmith 可观测性 看发生了什么 负责 debug;agentbench 负责 assert + CI 门禁
DeepEval LLM 评测 评输出文本质量 只评输出;agentbench 测整个 Agent(工具链+状态+多轮)
Promptfoo Prompt 测试 对比 prompt 变体 测 prompt;agentbench 测 prompt+工具+编排+状态
Jest/Vitest 单测 确定性代码 要求确定性输出;Agent 的非确定性需要 LLM-aware 断言

结论:不是替代关系。LangSmith 留着查线上 trace,Promptfoo 做 prompt 选型对比,agentbench 在 CI 里当闸门------用 LangSmith 调试,用 agentbench 拦截。

度量方差:批量回放与 A/B 实验

单次通过/失败解决不了"稳不稳定"的问题。比如"同一问题跑 10 次成功率多少",这才是 Agent 团队真正关心的指标。agentbench 的批量回放把同一输入跑 N 次再聚合统计------注意它不是简单数通过率,而是把 judge 分数、token 数、延迟当随机变量处理:

复制代码
import { buildBatchReplay, aggregateReplayResults } from '@agentbench/core'

const batchConfig = buildBatchReplay(config, 50, { parallel: true })
const results = await agentbench.replay(batchConfig)
const agg = aggregateReplayResults(results)
// agg: { correctness: { mean: 8.2, stddev: 0.6, p95: 9.0 },
//        tokens: { mean: 1847, stddev: 210 }, ... }

改 prompt 前后各跑一批,就能上统计检验:t-test 看均值差异是否显著、bootstrap 算置信区间、Cohen's d 看效应量大小。之前"我_觉得_新 prompt 更好"的体感判断,变成"correctness 均值 7.2 → 9.1(+26%),p < 0.01,d = 1.3(大效应)"的可量化结论。这是 A/B 实验引擎做的事,也是"工程化"和"玄学调 prompt"的分水岭。

进阶方向

金字塔和 Replay 是地基,往上还有三块值得研究:A/B 实验引擎 (t-test + bootstrap 置信区间 + Cohen's d 效应量,改 prompt 不再靠感觉);4D 覆盖率 (prompt / workflow / tool / edge-case 四个维度衡量"测够了没");跨模型 Replay(用 GPT-4o 录制的 trace 对 Claude 回放,模型迁移时第一时间暴露行为漂移)。给 Agent 上测试这件事,从"没有工具"到"工具齐了",中间隔着的就是这套把非确定性分层消化的设计------这比任何单点工具都更值得抄进自己的测试体系。

相关推荐
csdn_aspnet1 小时前
开源人工智能编码代理列表
人工智能·continue·cline·goose·aider·opencode·openhands
正经教主1 小时前
AI提示词工程(高阶)第20课:编程领域专项提示词设计
人工智能
程序员-李俞1 小时前
Mistral OCR 4真正改变的不是“识字”:文档AI正在变成Agent的数据入口
人工智能·windows·ai作画·aigc·ocr·ai编程·ai写作
新知图书1 小时前
7.1 User特质的周期提取与自进化(智能体工程)
人工智能·agent·ai agent·智能体
阿里云大数据AI技术1 小时前
免费领票!9月22日-24日,2026云栖大会杭州见
大数据·人工智能·agent
circuitsosk1 小时前
AI输出的“质检员”:构建智能体质量评估、异常检测与人工兜底的三层防线
人工智能·python·microsoft·正则表达式·langchain
IvanCodes1 小时前
我做了一个软著 Skill,可以一键生成申请材料
人工智能·agent
狂奔蜗牛(bradley)1 小时前
RKNN‑Toolkit2 模型转换全流程
人工智能
双星系统1 小时前
双臂机器人迎来广阔应用风口!既是工业柔性主力,也是人形机器人优质上肢配件
人工智能·机器人