Published on

大模型有返回等于 Agent 成功运行吗?

Authors
  • avatar
    Name
    hpoenixf
    Twitter

大模型有返回等于 Agent 成功运行吗?

前段时间在做 Agent 模型评测时,我遇到过一个挺别扭的问题。

服务端有候选答案,自动裁判也给了结果,运行状态甚至已经是 completed。如果只看评测报表,这次请求似乎已经结束了。

但继续往前端链路看,会发现事情没有这么简单:候选答案存在,不代表它真的被发送;服务端记录了发送,也不一定等于浏览器最终完整接收和展示。

这几个状态以前很容易被我下意识地当成一回事。

后来我越来越觉得,Agent 评测里最危险的不是某个模型分数低,而是系统把不同层级的“成功”压成了一个 Green。

一旦这样做,报告会越来越漂亮,问题却越来越难找。

这篇文章记录的是我们后来怎么拆这个问题。它不讨论“哪个模型最好”,而是先回答一个更基础的问题:

我们到底在评测什么?又凭什么说一次 Agent 请求真的成功了?


completed 只说明程序走到了某个状态

普通接口里,我们很习惯看一个请求是成功还是失败。

但 Agent 的链路要长很多。

一次看起来普通的问答,可能经历:

用户问题
→ 规划
→ 工具调用
→ 证据整理
→ 生成候选答案
→ 质量检查
→ 流式发送
→ 前端接收和展示

这些环节并不是同时成功或失败的。

比如:

  • JSON 合法,但规划理解错了问题;
  • 规划没问题,但需要的工具根本没调用;
  • 工具调用成功,但证据已经过期;
  • 候选答案写得很好,但发布校验没通过;
  • 服务端开始发流了,但中间断掉;
  • 服务端认为发送完成,客户端却没有足够证据证明完整渲染。

以前如果最后只留一个 success=true,这些差异就全没了。

所以我后来更愿意把一次评测拆成几个独立的问题:

层面真正要确认的事情
协议输出能不能被系统正常解析
规划模型有没有理解任务和需要的证据
证据需要的数据是不是真的拿到了
回答最终内容是否正确、安全、和证据一致
交付这份内容是否真的进入了用户侧链路

前一项通过,不能替后一项背书。

HTTP 200 不能证明回答正确,completed 也不能证明用户看到了答案。

这个区别看起来很基础,但我们后面不少评测问题,根源都在这里。


我不再急着算一个总分

一开始做模型比较时,很自然会想得到一个分数。

比如协议、规划、工具调用、回答质量各自打分,最后加权成 82 分、87 分、91 分。

问题是,有些东西其实不适合被平均。

如果一个模型语言质量很好,但关键证据没有拿到,我不希望“表达能力”把“证据缺失”抵消掉。

如果一次请求根本没有把答案送出去,也不应该因为服务端候选内容很好,就得到一个不错的综合分。

所以现在更接近这样的记录方式:

Protocol = pass
Planning = pass
Evidence = inconclusive
Answer = not_evaluated
Delivery = pass

这组结果没有一个特别漂亮的总分,但它更有用。

我一眼就知道:协议和规划没问题,交付链也跑了,但证据还没有闭合,所以这次结果不能拿去证明模型质量。

这里有个词我觉得很重要:inconclusive。

它不是“比较委婉的 failed”。

failed 表示我们已经有足够证据说明它没达到要求。

inconclusive 表示证据还不够,暂时不知道。

这两种状态后续动作完全不同。前者应该修,后者应该补证据。

如果把它们都算成 0 分,最后得到的模型排名会混入很多基础设施噪音。


比总分更容易骗人的,是分母

后来真正让我警惕的,是另一个更隐蔽的问题:评测分母会偷偷变小。

假设原本计划跑 30 题。

结果其中 20 题因为排队、超时或者工具问题,没有在观察窗口内完成。剩下 10 题里,有 9 题返回了非空内容。

这时候写:

已完成样本中,9/10 返回了答案。

没有问题。

但如果把它写成:

成功率 90%。

意思就完全变了。

因为那 20 个没跑完的题,很可能恰恰是长上下文、多工具、复杂纠错或者更容易暴露问题的题。

它们通常不是随机缺失。

如果只看已经跑完的样本,最后测到的其实是:

模型在“它成功完成的那些题”里的表现。

而不是它在原始题集上的表现。

这也是为什么后来我越来越坚持固定分母。

一次评测开始之前,就把下面这些东西固定下来:

{
  "suite": "agent-cases-v1",
  "sourceHash": "sha256:...",
  "revision": "git-revision",
  "modelId": "provider/model-version",
  "profile": "live-read-only",
  "attempt": 1
}

题集变了,算新窗口。

代码变了,算新窗口。

模型版本变了,也算新窗口。

可以做前后对比,但不能把不同条件下的结果直接拼在一起,再算一个更好看的百分比。

这条规则其实挺反人性的,因为它会让报表变难看。

但失败样本不应该因为“没有完成”就从统计里消失。

有时候,没完成的那些题才是最值钱的样本。


裁判看到的答案,不一定是用户拿到的答案

这可能是整套评测里我最在意的一点。

很多自动评测都发生在服务端。

模型生成一个候选对象,系统把它交给裁判,裁判开始检查事实、格式、引用、语气,然后给出一个 verdict。

技术上完全合理。

但如果真实产品是流式交付,就多了一层问题:

裁判评的那份内容,和用户实际收到的内容,是同一份吗?

它们可能不是。

一个候选答案可能已经完整生成,但在发布校验时被拒绝。

也可能开始发送,前半段到了,后半段因为超时没有出去。

甚至服务端已经记录了 send,但如果没有客户端 ACK、接收事件或者渲染侧证据,我们最多只能说:

服务端确认发送过这些内容。

不能再往前一步说:

用户一定完整看到了这些内容。

这个边界我觉得有必要说得很死。

所以评测里最好至少区分三层:

模型候选内容
    ↓
服务端实际发送内容
    ↓
客户端接收 / 展示证据

我们当前最容易可靠重建的是第二层:从真实发送事件按顺序拼出“服务端交付材料”。

如果客户端还有 ACK、消息序号或渲染回执,可以继续证明第三层。

如果没有,就不要为了让指标名字好听,把“已发送”写成“用户已看到”。

服务端交付材料怎么重建

对流式系统来说,答案不是一个最终字符串,而是一段事件历史。

例如:

run started
tool call
tool result
text delta #1
text delta #2
text delta #3
terminal

评测用的正文应该尽量从真实对外发送的 text delta 重建,而不是直接拿后台保存的候选答案。

这样做之后,一个很常见的假 Green 会自然消失:

裁判认为答案完整,但用户侧实际上只收到前半段。

如果流在中途断了,评测材料也只能包含已经真正发送出去的部分。

后台后来补写了一个更完整版本,也不能反过来修改那次运行的历史。

这一点让我觉得,Agent 评测和普通 benchmark 很不一样。

它评的不只是模型输出,还在评一个分布式系统的交付过程。


评测系统自己也会犯错

做到这里,还有一个问题绕不过去:

如果错的是评测器本身怎么办?

我们确实遇到过这种情况。

某一类输出因为夹具或者校验合同的问题,被机械地判成失败。后来修正评测逻辑后,同一批受影响样本的结果发生了明显变化,但仍然保留了一部分真实失败。

最简单的做法当然是直接改旧结果。

但这样一来,第一次为什么失败、后来为什么改判,就再也说不清了。

所以后来采用的思路更像账本:

原始评测结果不改
        +
追加 correction
        ↓
生成新的修订视图

一条 correction 至少要知道:

  • 它修改的是哪一次评测;
  • 哪些 case 受影响;
  • 原来的 verdict 是什么;
  • 新 verdict 是什么;
  • 为什么改;
  • 它对应的原始输入和版本是不是同一份。

可以简单理解成:

RevisedView = RawLedger + ValidCorrections

这里的重点不是公式,而是 RawLedger 不被覆盖。

如果以后发现第二次修正也有问题,可以再追加一条,而不是继续篡改历史。

这对调模型也很重要。

否则几轮之后,你看到的是一张“修过很多次但不知道修过什么”的榜单,很难再判断提升到底来自模型、Runtime,还是评测器自己。


几个真实数字,为什么我现在会刻意少解释一点

在受控验证窗口里,我们曾经有过这样的结果:

  • 一组离线评测是 583/583;
  • 另一组证据闭环测试是 59/59;
  • 更大的模型评测集合是 677/677。

以前看到这种数字,我可能会很兴奋地写“全部通过”。

现在反而会多补一句:

它们证明的是对应测试合同和当次本地逻辑通过,不证明某个候选模型已经在真实长期环境里更好。

这不是故意保守。

而是我们后来见过太多“数字是真的,结论说过头了”的情况。

一次受控批次可以证明协议能走通。

一组真实调用可以证明链路曾经工作。

一批自动裁判结果可以提供比较信号。

但长期质量、尾延迟、供应商稳定性、真实工具可用率,都需要自己的证据。

这些东西不能互相借。


最后,我们把发布模型结论变成了一个门槛问题

做到后面,我发现其实不需要设计一个特别复杂的总评分公式。

先问三个问题就够了。

1. 这次比较是不是同一场实验?

题集、代码、模型、配置、数据快照和分母是否对得上。

对不上,就不要硬比。

2. 有硬门没有通过吗?

比如协议、安全、隐私、关键证据、交付。

有任何一项明确失败,这个候选在当前角色下就不应该进入后面的排名。

3. 还有关键状态未知吗?

如果关键证据还是 inconclusive,那就保持 provisional。

先补证据,再谈冠军。

只有通过这几层之后,质量、稳定性、延迟和成本这些指标才适合真正拿来比较。

伪代码大概是:

if not same_experiment:
    return inconclusive

if hard_gate_failed:
    return not_eligible

if hard_gate_unknown:
    return provisional

return compare_quality_reliability_latency_cost()

它看起来没有一个漂亮的“智能评分公式”。

但实际用下来,我反而更信任这种东西。

因为它不会让一个 95 分掩盖掉“答案根本没发出去”。


写在最后

这轮评测改造之后,我最大的变化不是更会测模型了,而是没那么相信一个绿色数字了。

completed、模型产出了答案、裁判给了 pass、服务端发送了正文、客户端实际展示成功,这些事情彼此有关,但不是同一件事。

同样,20 个样本没完成之后算出来的 9/10,也不是整套题的 90%。

Agent 的链路越复杂,越容易出现这种“每一小段都像成功,拼起来却不是成功”的情况。

所以现在再看评测,我会先问:

这个 Green 到底在证明哪件事?

能把这句话回答清楚,再去比较模型,排名才有意义。

否则我们很可能只是在非常精确地计算一个定义错了的问题。