传统eval套件无法检测到模型悄然退化——contains("refund")即使规则被删除仍能通过,muteval通过结构化检测发现charge_card失败后仍返回成功并告知用户已发货的致命问题。
上周我写了关于 tracelint 的文章,它是一个能捕捉 agent trace 结构 Bug 的 linter——经典场景是:agent 调用了 charge_card,收到一个失败响应,却继续往下走,告诉客户订单已发货。
反响超出我的预期,有一个问题以不同形式反复出现:"好吧——但我的 eval 套件难道不会捕获这种情况吗?"
问得有道理。于是我验证了一下。在我将展示的这个案例里,套件没有捕获。CI 是绿的。扣款被拒绝了。agent 说了"payment successful",所有 eval 都通过了。
本文要说的是我用来发现这个问题的工具——muteval,以及将它对准 tracelint 所针对的那类故障时发生了什么。
关键在于:绿色通过的 eval 只表明你的系统通过了今天的测试。它无法说明当系统悄然退化时,这些测试是否能够察觉。
这并非空穴来风。你的 contains("refund") 检查通过,并不代表模型仍然遵循上周被人删除的退款规则。只要答案建立在上下文基础上,faithfulness judge 就会给出一个自信但错误的答案。eval 是绿的,而回归就这样悄然通过。
在传统软件中,我们有一个术语来描述"我的测试实际有效性如何"——突变测试。你在代码中人为注入缺陷(mutants),重新运行测试套件,然后测量有多少被捕获。mutmut 和 Stryker 做的就是这种事。muteval 对 eval 套件做同样的事——只不过它变异的"代码"是正在测试的系统:prompt、检索到的上下文、工具输出、model。
降低系统表现 → 重新运行现有 evals → 报告它们捕获的注入回归百分比。那些被遗漏的就是幸存者——具体的覆盖缺口。
回到 agent。这里有一个故意做得简陋的支付 agent:它调用 charge_card 并报告成功。它的 eval 套件有一个现实的语义检查——回复是否确认扣款已成功?
muteval 有一个操作符叫 deny_tool_output。它接收一个工具的结果,将其转化为最棘手的失败类型:域名失败以传输成功形式返回——HTTP 200 但 body 里是 {"status": "declined"}。结构化错误处理永远不会触发,因为从技术上讲没有错误发生。agent 继续执行。最终答案仍然写着"Your payment was successful."
针对这个变异体重新运行 eval 套件:
[HIGH] SURVIVED [deny_tool_output]
tool output #1 returned a domain failure (HTTP 200 + status:declined)
突变得分:0%。套件什么都没捕获。卡片被拒绝,agent 撒了谎,但 contains("successful") 检查通过了——因为答案确实仍然包含"successful"。这就是一个幸存者,而这正是你从绿色 CI 运行中永远看不到的那种盲点。
(明确一下这个数字的含义:0% 意味着在我注入的回归中,你的 evals 一个都没有捕获。不是"你的系统坏了"——而是"你的测试不会注意到"。)
这里才是我认为真正有趣的部分。
捕获"扣款被拒绝却报告为成功"的不是更聪明的输出判断器——输出看起来没问题。这是 trace 上的结构性检查,而这正是 tracelint 所做的。
所以我把 tracelint 作为 muteval 的一个 eval 接入。tracelint 读取 agent 的 trace,如果工具声明了失败的样子(failure_when: {"pointer": "/status", "in": ["declined"]}),它就会确定性地标记被拒绝的扣款——无需 judge,无需 key。
加入这个检查,然后重新运行:
verdicts on the declined-charge mutant:
semantic eval (confirms "successful") -> PASS (misses it)
tracelint (declared failure contract) -> FAIL (kills it)
mutation score: semantic eval alone 0% -> + tracelint 100%
muteval 不仅告诉我差距存在——还让我能证明修复能关闭它:在语义套件中存活的变异体,一旦结构性检查加入套件就会死去,而基线保持绿色。这就是整个循环——幸存者指出一个缺失的 eval,你加入它,分数就上升。
工具故障是变异体;trace-lint 规则是一个 eval。突变测试在一边,确定性的 trace 检查在另一边,二者相组合。
你无法信任的诊断比没有诊断更糟,所以:
幸存者是候选,不是判决。 muteval 告诉你,你的 eval 漏掉了一个注入的变更。这个变更是否真的可能发生、是否真的有害,需要人来判断。有些幸存者经不起这个问题。
突变覆盖率不等于有效性。 一个套件可能对突变高度敏感但仍然错误——敏感不等于与 ground truth 或人类判断一致。这需要标注;没有免标注的工具能帮你做到这一点。
突变是基于规则的合成编辑。 它们建模真实的回归;它们与真实回归并不相同。变异体是否预测你的系统实际经历的失败,是我仍在追逐的开放问题。
这是针对每个套件的诊断,不是通用的 flaw 发现器。 它告诉你你的套件在哪里有漏洞——而不是关于 evals 的新通用真理。
它需要一个可重新运行的系统。 muteval 降低系统表现,需要每个变异体产生新的输出。冻结的 CSV 输出无法被变异。
muteval 在所有这些方面都是故障关闭的:基线标红、变异体太少、或太多变异体出错,意味着它拒绝给出分数,而不是给你一个自信但无意义的数字。
有一个无需 key 就能运行的 demo——使用 mock model,这样你可以在大约一分钟内观看 mutate → survive → fix 的完整循环。(在你自己的套件上运行需要调用你的 model 和 key,和其他 eval 一样。)
git clone https://github.com/AshwinUgale/muteval && cd muteval
pip install -e ".[tracelint]"
python examples/agent_tool_fault/run_demo.py # keyless, ~1s
Repo: https://github.com/AshwinUgale/muteval (Apache-2.0) · tracelint: https://github.com/AshwinUgale/tracelint (MIT).
还有一个真诚的请求,和上次一样:如果你为 agents 或 RAG 编写 evals,我想知道你的套件会遗漏什么。你最担心上线哪个回归——如果你把它注入进去,它能在你的 evals 中存活下来吗?Issues、回复和踩坑故事都欢迎。