跑分是一次尝试的摘要。生产是一个分布。

这个区分听上去像咬文嚼字,直到两者不一致。一个在你盯着看的那次运行里答对的 agent,会在没人盯着的时候运行一千次 —— 真正要问的不是它能不能做到,而是它多久做到一次,以及做不到的那些次它做了什么。这是两种不同的测量。大多数团队只做了第一种。

单次运行藏起了什么

有三个性质不会出现在单次成功率里。

方差。 同一个问题反复问,每一次的轨迹都不一样 —— 工具顺序不同、中间摘要不同、“证据够了可以回答了”这个判断的时机也不同。其中一些被判为正确的运行,是碰巧正确的:模型走错了路又绕了回来,或者跳过了某个步骤,而这个步骤的缺失恰好对这条输入不重要。分数无法区分”正确”和”走运”。轨迹可以。

故障行为。 在 eval 框架里,工具都会应答。在生产里,它们会超时、会限流、会返回半页结果,偶尔还会返回一个结构合法但语义错误的东西。我们最在意的失败模式是安静的那种:模型收到一个坏掉的工具响应,很少会因此停下。它会绕过这个缺口继续往下走,带着已经被污染的上下文,而它产出的答案和正确答案一样流畅、一样自信。输出本身不会透露上游发生过什么。

输入漂移。 跑分把输入固定住。应用不会。文档会被重新排版、某个字段会变成空、上游服务会开始截断、用户会粘进来一段比 eval 集里任何样本都长一倍的文本。系统不需要对见过的输入鲁棒,它需要对没见过的输入鲁棒。

关于 agent 评估的公开工作正在从外部得出同一个结论:只看最终答案的评估,通过率明显高于检查完整轨迹的评估;而单次运行的跑分数字,高估了同一系统在反复执行、且工具真会出故障的条件下的表现。我们自己系统上的调用链是同样的形状。这个差距的方向从来不会对我们有利。

评估轨迹

我们不再把”答案对不对”当作首要问题,转而问一组关于路径的问题:

最后一条是最便宜的信号,也是最常被跳过的。固定输入下的轨迹离散度,直接度量了有多少行为是被系统控制的,而不是被采样器控制的。当离散度很大时,再怎么改 prompt 都没用:要么任务本身欠定义,要么工具面允许太多条通往同一个地方的路。

我们现在怎么跑

由此长出来的规则,按重要性排列:

  1. 每条 eval 用例跑很多次,我们读最差的那一成,而不是均值。 均值描述的是一个没有人经历过的系统。产生工单的是尾部。
  2. 故障注入属于 eval 套件,不属于混沌测试。 超时、畸形载荷、空结果集、过期索引,都是寻常状况,所以它们属于寻常评估,并且每一种都要定义期望行为。
  3. 拒绝回答是一个通过的结果。 如果证据不足而系统说了”证据不足”,它做对了。把这个判为失败,等于精确地教会系统错误的行为 —— 这是我们见过最常见的评分错误。
  4. 存下来的是轨迹,不只是分数。 分数告诉你数字动了。只有轨迹告诉你为什么动,也只有存下来的轨迹,允许你在事后问一个当时没想到要问的问题。

代价

上线变慢了,以及至少有一个 agent 最终没有上线。它分数很好,演示也很好,但在反复执行加故障注入的条件下,它的行为对于它当时要做的那个决策来说不够稳定。我们本可以凭那个离线数字把它上了。那个数字是真的;它只是在回答一个我们并不需要答案的问题。

eval 的用途不是给系统发合格证。它是搞清楚系统在”一切都稍微有点不对”的那天会做什么 —— 而在足够长的生产窗口里,大多数天都是那一天。