参考解运行与不可解任务

11 分钟读完

E15
深入解析 · 评估智能体

你那套评测集有一个你从未测量过的天花板,它会自行向下漂移,而这种漂移看起来与模型退步一模一样。

你报出的每一个分数,都是一个分母靠假定、分子靠测量的分数。任务会悄悄死掉——某个传递依赖解析不出来了、某个基础镜像标签挪了位置、某个曾经够用的超时现在不够了——而由于一个死掉的任务只会做减法,六周的寻常腐坏就能在一个根本没变的模型上造出四个百分点的「退步」。补救办法是多跑一次只花容器时间、不花任何模型 token 的运行:把参考解送进同一套脚手架,每个周期都跑,然后按「本可达成」而不是按「你发布时的总数」来报分。

步骤 1任务会在你没看的时候,以三种方式死掉。

这些都不是什么离奇的失败。它们是「用真实软件搭评测集」这件事的寻常后果,而三者的检测特征并不相同。

  • 环境腐坏。依赖解析器挑了一个更新的传递版本,把某个 import 弄坏了。某个镜像源变慢了。你钉住的某个基础镜像标签在你脚下挪了位。任务触达的某个外部服务改了响应结构。这些都不会产出一条写着「此任务现已失效」的报错——它们产出的是一个失败的测试,而这与「智能体答错了」无从区分。
  • 额度不再够用。任务仍然可解,只是在脚手架给出的挂钟时限、token 上限、内存上限或步数上限之内不可解。这是三者中最阴险的一种,因为对足够快的智能体它会通过,对较慢的则失败,于是它读起来像一个能力差异。这正是 Terminal-Bench 的维护者单独计数的那一类,而他们在八十九个里找到了八个。
  • 题面说明与校验脱钩。任务写下来的时候,说明与测试是一致的。后来有人为了解开一次不稳定的构建而把某个测试收紧了,或者上游项目重命名了说明里仍在描述的某个接口;现在要通过,唯一的办法是做一件提示词并没要求的事。仓库里没有任何东西记录这次分岔。

三者对你正在盯的那个指标都不可见,而三者都把它朝同一个方向偏。这就是整个问题:你的评测集是一件精度单调下降、却不报告自身校准状态的测量仪器。

步骤 2参考解运行是唯一能找出它们的测量——而它花的是算力,不是 token。

一次参考解运行(gold run)是对整套题目跑一遍,其中用已知正确的答案顶替智能体的位置:代码任务用参考补丁,智能体任务用录下来的正确工具序列,分类任务用标注好的答案。其余一切完全相同——同一个容器、同样的上限、同一个评分器、同一天。参考解运行回答一个问题:我这 N 个任务里,当下究竟还有多少是能通过的?

有三条性质让它负担得起,而这也正是这个做法「被少用」而非「太贵」的原因。

  • 回路里没有模型。参考解是一件固定产物,所以一次参考解运行只计容器秒数、零推理。在一套「智能体跑一遍要花掉几十美元 token」的题目上,参考解运行是个舍入误差——钱究竟花在哪里,见评测的成本。
  • 它是确定性的,这意味着一次参考解失败是一个真信号,而不是一次抽样。参考补丁若没通过,那这个任务就是坏的;这里没有什么「再试五次」可争,不像评测方差与统计功效里讨论的智能体侧方差。
  • 它完美并行,而且可以收窄范围。按计划跑一次全量参考解,再对「自上次全绿以来镜像摘要、锁文件或评分器有变动」的任务做一次定向参考解运行,就能用一小部分时间拿到大部分覆盖。
# one cycle, two sentinels, one measurement
gold   = run(suite, solver=reference_patch)   # upper bound: what is passable
null   = run(suite, solver=no_op)             # lower bound: what passes for free
agent  = run(suite, solver=system_under_test)

achievable = gold.passed - null.passed      # tasks that both work AND discriminate
score      = agent.passed / achievable        # the only fraction worth reporting

# gold costs container time and no model tokens — it is the cheapest run you make

步骤 3分母会对你的数字做什么。

用具体数字走一遍,因为这个效应的量级就是论证本身。

一套 180 个任务的题目。在你建成它的那天,参考解运行通过 180 之 180,而你的候选智能体通过 112 个——分数 62.2%,这个数上了看板。

六周之后,你把同一个、未经改动的智能体在同一套题上重跑一遍。它通过 104 个,也就是 57.8%。那是 4.4 个百分点的下跌,而在多数组织里,它会触发一次模型退步调查、一场回滚讨论,或一封发给厂商的邮件。

现在跑参考解。它通过 180 之 168。十二个任务已经死了:九个是某个传递依赖解析不出来了,两个是某个基础镜像标签挪了位,一个是 600 秒的上限已经容不下一个变慢了的镜像源。而你的智能体此前正通过着这十二个里的八个——恰恰就是它丢掉的那八个。

按「本可达成」的分母算,这个智能体得分 104 / 168 = 61.9%——与它出发时相差三十分之一个百分点。什么都没有退步。你花了一周去调查一件测量仪器。

这个诊断既便宜又决定性:如果参考解运行的通过数下降的幅度与你的智能体大致相同,那变化在脚手架里。如果参考解稳住了而智能体掉了,那变化在被测系统里。没有参考解,这两种情形产出的看板一模一样。

步骤 4这个偏差是单向的,所以一个哨兵不够。

这是真正不显然的那一部分。环境腐坏只能拿掉通过、永远不会添上通过,所以它不是噪声——它是一个带符号的漂移。在一年未被测量的维护之后,一套评测集会报出一条没有任何模型造成的缓慢下行线,而每一次真实的改进都要先去抵掉它。与这种状况共处久了的团队,会学会不信自己的评测——而那比根本没有评测更糟。

参考解运行抓得住那个方向,对另一个方向则完全失明。相反的那种失败——一个任务变成「不做功也能通过」——会抬高你的分数,而参考解运行依然全绿,因为参考补丁也照样通过。它发生在:为了解开一次不稳定的构建而削弱了某个测试;修复落进了基础镜像、于是仓库里本来就带着它;或者评分器的一次改动让子串匹配就足够了。这件事的对抗性版本是评测完整性与评分器博弈的主题;而偶然版本更常见,且没人在找它。

所以也要跑一次空解运行(null run):同一套题,换上一个什么都不做的求解器——一个空补丁、一个空操作的工具序列、一次拒答。不变式是:空解运行通过零个任务。它通过的任何任务,都是一个已经不再具备甄别力的任务,应当被隔离而不是被计入。两个哨兵把你的题目从两侧夹住,而 STEP 2 里的算术会把空解通过数从分母里减掉,这样一个被敞开的任务就无法替你贴金。

  • 如果负担得起,还有一个便宜的第三哨兵:一个刻意写错的求解器——把参考补丁里某一行取反,或者用错一个参数去发那个正确的工具调用。它应当处处失败。凡它通过的地方,你的评分器对那个任务而言就是太松了。
  • 按摘要钉住,不要按标签。多数环境腐坏是从一个「解析到了新东西」的标签进来的;这与未钉住的厂商默认值在生产侧描述的是同一个隐患,而解法完全一致。
  • 把资源额度当作任务规格的一部分,并与任务一起做版本。一个你在快机器上调出来的超时,是你这个基准的一个隐藏参数;换了执行机,分数就变。比较侧的同一个要点见预算对齐的评测。

步骤 5大约十分之三,是已公布的常态,不是最坏情况。

唯一公布过「自己任务有效性」测量结果的维护团队是 Terminal-Bench 的,而那个数字正是「该严肃对待这件事、而不是把它归入卫生习惯」的理由。

Terminal-Bench 2.0 在 2025 年 11 月发布,89 个任务,从 93 人贡献的 229 个候选中选出、并由三位资深人类评审复核——通过率 39%,这在手工构建的评测集里差不多已是最谨慎的水准。半年后,维护者发布 2.1,去修那 89 个任务中 28 个的问题(2.1 仓库自己的 README 写的是修改了 26 个,所以诚实的说法是:来自两个维护方来源的 26–28 这个区间)。九个是基准构建之后外部依赖变了。八个是资源额度小到容不下至少一条有效解法。还有第三组,在公告里没有计数:题面说明已不再与它们的测试对齐。

有三个细节值得抽出来:

  • 这些类别与 STEP 1 完全吻合,这说明那套分类是真实存在的,而不是一个工整的发明。依赖漂移、额度漂移、规格漂移。
  • 专家筛选并没有防住它。39% 的通过率、每个任务三位人类评审,买到的是一套更好的题,不是一套更耐久的题。腐坏是流逝时间与外部变化的函数,不是你写任务时有多仔细的函数。
  • 他们因此能做出的那句主张,才是有价值的产物。修复之后,维护者声明 2.1 中没有任何任务是未被解出的。那是一句关于天花板的话,而那正是你自己的看板应该有能力产出的那句话。另请注意,它也让 2.0 与 2.1 的分数不可比——修复坏掉的任务会抬高多数智能体——这是「对分母诚实」的代价,而且值得付。

这个领域围绕此事建起来的基础设施指向同一个方向。SWE-bench 在 2024 年中改用了完全容器化的 Docker 脚手架,如今建议本地运行准备大约 120 GB 空闲存储;SWE-rebench 为每个实例都附上一个预构建镜像。两者都是对「这个任务为什么不工作了」这个问题给出的昂贵答案,而两者都比替代方案便宜。更宽的那套生命周期——替换率、留出集、重设基线——是评测集维护;而本页讲的是那一个能告诉你「生命周期已经跟不上了」的测量。

步骤 6报三种状态,并对第三种设告警。

报告方式的改动很小,而价值正落在这里。一个任务有三种结局,不是两种。

  • 通过——智能体解出了它,而这个任务当下可通过。
  • 失败——智能体没解出它,而这个任务当下可通过。
  • 不可用——参考解没通过,或者空解求解器通过了。本周期内,该任务从分子与分母中一并排除。

第三种状态并不是一个新想法——当检查本身必须动手在校验器才是失败那一方时,主张的正是同一个逃生口。本页的要点是:它需要第二个触发条件。一个任务可能因为评分器坏了而不可用,也可能因为任务本身死了而不可用,而只有一次参考解运行能把两者分开。

然后把三个数字一起公布:按「本可达成」分母算出的分数、这个可达成计数本身,以及不可用率。「180 个任务中 168 个可达成、12 个不可用,得分 61.9%」是一个可读的结果。「57.8%」则是一个把一次模型测量与一次基础设施故障静默合并了的数字。

  • 对不可用率越过阈值设告警,而不是对分数。一套题在一周之内从 2% 不可用变成 9%,那是一次基础设施事件;而如果你放着不管,它会以质量退步的面貌出现。
  • 对「参考解运行缺席」设告警。一个缺失或过期的参考解结果,绝不可以默认成「假定 100% 可达成」——那会悄悄把原来的 bug 装回去。一个被冻住的天花板,看起来与一个健康的天花板一模一样。
  • 绝不静默丢弃一个不可用的任务。把它从分母里剔掉却不作记录,正是一套题在两年里从 180 缩到 120、而标称分数却一路上行的那种机制;这是真实且常见的失败。
  • 修复之后要重设基线。修掉十二个任务改变了分母,所以前后两段序列并不连续。请像对任何其他测量那样记录版本(见可复现性与确定性),并在同一版本之内作比较。

这周先做这一件:拿你现有的那套评测题,把参考解送进当前这套活的脚手架跑一遍,数数有多少通过。如果答案不是 N 之 N,那你现在就知道你上次报出的分数是错的、错了多少,并且手里有了一个站得住的分母。如果答案就是 N 之 N,那你确立了一条基线,而这次运行只花了你容器时间、没花 token。

相关:基准污染讲的是那种抬高分子而非缩小分母的泄漏;对着活系统做评测讲的是环境并不由你来钉住的那种情形;不可满足的任务则是同一问题面向智能体的那一版。