上周还能正常退款的客服 Agent,今天换了一版 Prompt,开始先查知识库再调订单接口。结果不一定错,用户甚至可能觉得回答更完整。可对工程团队来说,事情已经变了。

它多走了一步,带来了额外延迟和成本。更麻烦的是,这一步可能越过了原本不该碰的数据边界。要是知识库返回了过期的退款规则,Agent 还会很自信地把错话说得像对话。

这就是 Agent 上线后最别扭的一件事。你很难拿一段「标准答案」去判定它退化了没有,因为两段都通顺、都礼貌、也都可能把事办成。但一次工具调用的选择、参数和先后顺序,才是线上系统真正付费、真正碰权限的地方。

9 月 30 日,OpenRouter 连发两篇关于 Agent 工具调用与回归测试的实践文章。它给出的提醒很朴素,Prompt、模型、工具定义或检索设置只要变了,就该重跑一组固定用例。可我更在意另一层,Agent 的测试对象,正在从「它说了什么」移到「它做了什么」。

这不是给测试团队加一道作业,而是 Agent 工程开始像工程的信号。

代码测试的直觉,在 Agent 身上失灵了

传统回归测试很舒服。给定输入,得到预期输出。断言失败,diff 一眼就能看出来。

Agent 不一样。

同一个问题,两个模型可能写出措辞完全不同的答案,却都能解决用户问题。你如果把自然语言答案逐字比对,测到的大多是文风,不是质量。更荒唐的情况是,测试绿了,Agent 却悄悄多调了一个付费 API,或者把本该让用户确认的动作直接执行掉。

OpenRouter 把工具调用的失败拆成两类,这个拆法很有用。

一类是工具选择错了。用户问退款规则,Agent 调了 lookup_order,而不是 search_docs。另一类是工具选对了,参数却错了。它调用了 refund_order,但把 ord_7281 写成了别的订单号。

这两类错误不能混成一个「工具调用成功率」。前者通常指向 Prompt、工具描述和路由策略。后者更可能是实体抽取、上下文污染或参数约束出了问题。一个笼统指标会把定位线索全抹平。

很多团队第一版 Agent 评测,恰好就卡在这里。他们只检查响应里有没有 tool_calls。这跟检查 Java 服务有没有启动差不多,能排除最明显的故障,却完全回答不了它是不是在做正确的事。

一个能跑的 Agent,可能已经不再是原来的 Agent

模型升级是最容易被忽视的变量。

不少平台提供类似 family-latest 的模型别名。它在生产环境里很省心,供应商发了新版本,你不用改配置就能用上。可拿它跑基线评测,等于每次把实验对象偷偷换掉,再假装结果可比。

OpenRouter 的建议是,回归测试使用具体的 model slug,不用会自动解析到最新版本的别名。做模型替换实验时,Prompt、工具、用例、裁判模型、temperature 和路由配置都固定,只变模型这一项。

这听起来像实验室的基本常识,实际项目里却常常做不到。一个 Spring 服务里,模型名写在环境变量。工具 Schema 来自一份共享依赖。RAG 的召回参数又在另一个配置中心。一次「只是把模型换便宜点」的发布,可能顺手带上了三个变化。

然后出了问题,所有人围着日志猜。

我一直觉得,AI 应用里最贵的不是 token,而是不可归因的变化。你不知道是模型、Prompt、检索还是工具改坏了,就只能继续多跑几轮,拿更多真实用户来帮你找答案。这种省下来的测试成本,最后通常会变成线上排障成本。

先写行为契约,再谈评测集

一个实用的起点不是搭评测平台,而是把 Agent 的行为契约写出来。它不是产品宣传语,而是一组可以被检查的承诺。

拿一个带审批能力的企业采购 Agent 举例,可以写成下面这样。

  • 当金额超过 5,000 元时,必须调用 escalate_to_human
  • 没有明确的采购单号时,不得调用 submit_purchase
  • 用户只问政策时,只能检索知识库,不能修改订单
  • 检索无结果时,要明确说明信息不足,不能编造供应商条款
  • 任何写操作前,必须在轨迹里出现一次用户确认

前两条叫 hard invariant。它们不是「大部分时候做对就行」,一次打破就应该阻断发布。后面几条可以有不同的判定方式,有的适合精确断言,有的要交给人工复核或 LLM judge。

行为契约的价值,在于把团队真正担心的事从脑子里搬到版本库。否则评测集很容易沦为一堆漂亮的问答样本。它能证明 Agent 会回答,但证明不了它不会闯祸。

这和数据库迁移的思路很像。迁移脚本不是为了展示 SQL 写得多优雅,而是为了把不能丢数据、不能重复执行这类底线显式化。Agent 的权限边界,也该有同样的严肃程度。

用例要锁住,别每次都把尺子换掉

OpenRouter 在回归测试文章里强调了一个容易被低估的原则,锁定 case set。每次为了「更像真实用户」改写用例,都会失去与过去运行结果的可比性。

这话听着有点反直觉。真实用户表达本来就会变,为什么不用例也跟着变?

因为你需要两套东西。

一套是锁定集,用来回答这次改动有没有让已知能力退化。它应该包含曾经发生过的事故、边界条件、权限敏感操作和高价值路径。内容一旦入库,就像单元测试一样,轻易不改。

另一套是生长集,用来持续吸收生产流量。OpenRouter 的另一篇文章建议从 20 到 50 条经人工复审的样本起步,再逐步扩展到 100 到 1,000 条完整回归集。这里的关键不是追求数量,而是保留真实分布和失败模式。

我会把生产样本先做脱敏,再按任务类型聚类。比如订单查询、政策问答、写操作、异常恢复、拒绝处理各抽一批。相似样本太多,只留一个代表项。每一条都补上预期工具、关键参数、允许和禁止的动作,以及它来自哪次事故或哪个业务风险。

别急着做 500 条。20 条足够坏的样本,比 200 条「帮我查一下订单」更有价值。

断言要分层,别迷信一个万能裁判

Agent 的轨迹不是只有对和错。不同层次,应该用不同的尺子。

第一层检查结构。工具名是否在允许列表里,JSON 是否能通过 Schema,必填参数是否齐全。这层最便宜,也最稳定。比如 order_id 缺失、日期格式错误、金额不是数字,没必要交给一个 LLM 去猜。

第二层检查关键参数值。Schema 合法不代表业务正确。ord_7281 和 ord_7287 都符合 Schema,可它们绝不是一回事。对于订单号、审批阈值、地域、时间范围这类确定信息,直接做确定性断言。

第三层检查轨迹。某些工作流不只在乎调没调到工具,还在乎顺序。先查询订单,再发起退款,最后通知用户,这条链路可能需要被约束。不过也别把所有轨迹写死。多个路径都能安全抵达结果时,强行要求唯一顺序,只会制造假失败。

第四层才是语义判断。比如用户表述模糊时,Agent 有没有追问缺失信息,解释是否符合政策。这类题没有唯一文本答案,使用有明确 rubric 的 LLM judge 或人工抽检更合适。

这块需要注意一下,LLM judge 不是法官,只是另一个有偏好的模型。候选池里 Arena 的研究提到,模型裁判会偏爱自己的答案,且与人类投票的一致程度有限。评测模型和被测模型同源时,这个风险尤其明显。对权限、安全和钱相关的断言,宁可笨一点,用确定规则。

一次发布,应该留下什么证据

如果你在做 LangGraph 或 Spring AI 这类 Agent,完全可以把回归测试接到 CI/CD 里。一次合格的发布,至少要能回看下面几项。

  • 本次提交改了什么,Prompt、模型、工具还是检索配置
  • 使用了哪一版固定用例集
  • 实际解析到的具体模型版本
  • 每条用例的工具轨迹与关键参数
  • baseline 与 candidate 的差异
  • hard invariant 是否全部通过
  • 成本、延迟、工具调用次数是否显著变化

最后两项经常被漏掉。一个功能正确但工具调用数翻倍的 Agent,未必该直接放行。尤其在长链路任务里,额外一步不只是几分钱,它会放大超时、限流和下游故障概率。

可以先用一个很朴素的目录开始。

1
2
3
4
5
6
7
8
9
10
agent-evals/
contracts/
purchase-agent.yaml
cases/
locked.jsonl
incoming.jsonl
baselines/
2026-09-30.json
reports/
pr-184.md

locked.jsonl 放不会随意改的回归用例。incoming.jsonl 放刚从生产归档、等待人工标注的样本。purchase-agent.yaml 放行为契约。报告里不只写 pass rate,还要把失败的轨迹摘出来,让评审者能看见 Agent 到底做了哪一步。

这个结构没什么魔法。它的好处是每个人都知道,出现一次线上事故后,应该把什么东西留下来。

别用总体分数掩盖一次越权

团队喜欢一个大数字,准确率 92%,成功率 87%,看板也好看。但 Agent 的风险并不平均分布。

把 100 次普通查询答好,抵消不了一次未经确认的付款。把所有测试合成一个平均分,会让严重问题藏在漂亮曲线下面。

所以我更倾向于两段式门禁。hard invariant 只要失败一条,直接阻断。其余体验和能力指标再看趋势、阈值与人工复核。前者保护底线,后者帮助迭代,不要让它们互相稀释。

这话听着有点保守,但不是所有 Agent 都需要上火箭发射级别的流程。一个只会整理会议纪要的内部助手,容错空间当然更大。可一旦它能发邮件、改数据、动资金、访问客户资料,行为回归就不再是锦上添花。

它是权限系统的一部分。

从明天开始,做三件小事

别等评测平台采购完再开始。明天就能做的事情很具体。

第一,挑最近一次线上异常,写成一条固定回归用例。不要只记在复盘文档里,让它每次改 Prompt 都重新出现。

第二,为每个有写权限的工具补一条 hard invariant。比如没有确认不能发信,没有工单号不能退款,金额超阈值必须升级人工。

第三,把模型的具体版本、Prompt hash 和工具 Schema hash 写进运行记录。以后再遇到「昨晚还好的」问题,至少先能确定到底变了什么。

OpenRouter 的文章里有一句我很认同,固定用例集让「感觉没问题」变成可以辩护的结论。对 Agent 来说,这份可辩护性比一次惊艳 Demo 更稀缺。

因为用户不会为一个漂亮回答付费。

用户是在把下一步操作交给你。

参考资料