可评测性入门:从零到一
这是一份快照
本文的数字、常量、行数取自 2026-08-29 对 sid-code 源码的一次实读。 代码在动,这些数字会腐坏——引用其中任何一个之前,请按文中给出的命令在你自己的仓库里复跑一次。
这份文档写给谁:没做过 agent 评测、但需要在短期内既能听懂别人在说什么、又能自己动手设计一套的人。 用途是知识梳理与 agent 开发面试准备。
它和同目录另外五份的关系:那五份是执行文档——写给已经懂的人, 满篇是「这条裁决作废」「这个数字错了」「第四次踩同一个坑」。信息密度极高, 但它们默认你已经知道 harness、grader、reward、holdout 是什么,所以第一次读会卡住。
本文补的正是那一层:先把概念讲通,再把那五份文档里真正值钱的结论放回它该在的位置上。 每一章末尾都有「回到原始文档」的指路,想深挖时照着走。
它不是摘要。 摘要会把结论抽出来变成一句正确但没用的话。 本文的写法相反:每个结论都从「为什么会有人搞错」讲起, 因为面试里能拉开差距的从来不是结论本身,是你能不能说清它的反面为什么诱人。
怎么读这份文档
按顺序读。这份文档是一条链,不是清单——后面每一章都在用前面章节建立的概念。
| 章 | 讲什么 | 读完你能回答 |
|---|---|---|
| §0 | 名词地图 | 别人说 harness / reward / holdout 时,你知道指什么 |
| §1 | 为什么评测 agent ≠ 测试代码 | 为什么单元测试那套方法在这里不够用 |
| §2 | 三件套:一条 case 的最小可执行单元 | 拿到一个任务,你能判断它「能不能做成 case」 |
| §4 | 判据的五种形态(谱系) | 一个需求进来,你知道该用哪种判据、代价是什么 |
| §5 | LLM-as-Judge 与校准 | 为什么「让大模型打分」不能直接用,怎么让它可用 |
| §6 | 非确定性:pass@k 与方差 | 同一题跑两次不同分,这不是 bug,怎么处理 |
| §7 | 外部锚 vs 内部集 | 该接现成 benchmark 还是自建,各买到什么 |
| §8 | 执行底座:一次 trial 里到底发生了什么 | 能画出从「起容器」到「出分数」的完整时间线 |
| §9 | 会「绿着坏掉」的失效模式 | 这一章是本文最值钱的部分 |
| §10 | 评测系统自己是会死的 | 一个真实死亡案例的完整解剖 |
| §11 | 口径陷阱:为什么你查不到别人有没有评测 | 调研一个项目时不会得出反的结论 |
如果只有 20 分钟:读 §2、§4、§9。这三章是这个领域的骨架,其余都是它们的展开。
§0 名词地图:先把词认全
这一节是查询表,不用背。往后每章第一次用到某个词时都会重新解释, 这里放一份集中的,是为了你读那五份执行文档时能随时回来查。
按「一次评测从头到尾」的顺序排列,不按字母序——因为这些词之间是有位置关系的。
0.1 被测的东西
| 词 | 中文 | 是什么 |
|---|---|---|
| LLM | 大模型 | 输入一段文字,输出一段文字。没有手脚 |
| agent | 智能体 | 大模型 + 工具(读文件 / 改文件 / 跑命令)+ 循环。有手脚,能看到自己动作的结果再决定下一步 |
| coding agent | 编码智能体 | 专门干编程活的 agent。Claude Code、Cursor、Codex CLI 都是 |
| agent loop | 智能体循环 | 「想 → 调工具 → 看结果 → 再想」这个转圈的过程。它是 agent 与 LLM 的分水岭 |
| trajectory | 轨迹 | agent 干活的全过程记录:调了哪些工具、读了哪些文件、改了什么。它是过程的证据 |
0.2 题目侧
| 词 | 中文 | 是什么 |
|---|---|---|
| case / task / instance | 用例 / 任务 | 一道题。三个词基本同义,不同项目叫法不同 |
| benchmark / eval set | 评测集 | 一批题打的包。SWE-bench 是 2294 道题的包 |
| dataset | 数据集 | 同上,偏「数据」视角的叫法 |
| instruction / problem statement | 任务描述 | 题面。喂给 agent 的那段话 |
| initial state / 初态 | 初始状态 | 开考前那个代码仓长什么样。必须能精确复现,否则题目无意义(§2 详解) |
| base_commit | 基线提交 | 用 git commit 号来锚定初态。最常见的初态复现手段 |
| gold patch | 标准答案补丁 | 人类真实提交的那个修复 diff。用来自检环境,不是用来对比 agent 的答案(§9 详解) |
| holdout | 留出集 | 刻意藏起来、不给任何人看的一批题。防止「拿考题当复习资料」 |
| contamination | 污染 | 题目和答案已经进了模型训练数据,模型是背出来的不是做出来的 |
0.3 判分侧(这一组最容易混,注意区分)
| 词 | 中文 | 是什么 | 关键区别 |
|---|---|---|---|
| grader / scorer | 判分器 | 决定「这次做得怎么样」的那段代码 | 两词常混用;有的项目 grader 指规则、scorer 指打分函数 |
| verifier | 验证器 | 在容器里跑测试、根据退出码给结论的那段脚本 | 比 grader 窄:verifier 特指「跑起来看结果」这一种 |
| judge / LLM-as-Judge | 评审 | 用另一个大模型来打分 | 它是 grader 的一种实现方式,不是并列概念(§5 详解) |
| rubric | 评分细则 | 给 judge 看的打分标准,自然语言写的 | 「是否定位到文件路径 + 类名」这种 |
| FAIL_TO_PASS | 修前失败、修后通过 | 一份测试清单:改之前必须红,改之后必须绿 | SWE-bench 的核心判据,也是「程序化验证器」的黄金形态 |
| reward | 奖励 / 得分 | 一次评测的最终数字,通常 0 或 1,也可以是小数 | 从强化学习借来的词,别被吓到,就是分数 |
| assertion | 断言 | 「这个文件必须存在」这类机械检查 | 最便宜的判据,也最容易随重构烂掉(§10) |
0.4 执行侧
| 词 | 中文 | 是什么 |
|---|---|---|
| harness | 执行底座 / 脚手架 | 把题目喂给 agent、收集结果、判分的那一整套外围代码。agent 本身不是 harness,题目也不是 |
| trial | 一次试验 | 一道题跑一遍 = 一个 trial。同一道题跑 3 遍 = 3 个 trial |
| job / run | 一批次 | 一次评测活动整体。一个 job 含 N 道题 × K 次重复 = N×K 个 trial |
| adapter | 适配器 | 让底座能驱动某个具体 agent 的胶水代码。「接入 Harbor」主要就是写一个 adapter |
| sandbox | 沙箱 | 隔离的执行环境,通常是 Docker 容器。防止 agent 把你的机器搞坏 |
| oracle agent | 标准答案 agent | 一个假 agent,行为是「直接执行标准答案脚本」。它应该拿满分(§9 详解) |
| nop agent | 空 agent | 一个假 agent,行为是「什么都不做」。它应该拿 0 分(§9 详解) |
| pass@k | k 次通过率 | 同一题跑 k 次,至少成功一次就算过(§6 详解) |
0.5 治理侧
| 词 | 中文 | 是什么 |
|---|---|---|
| baseline | 基线 | 上一次的成绩。用来判断这次是进步还是退步 |
| regression gate / 门禁 | 回归门禁 | 「分数掉了就不许合并代码」的拦截机制 |
| calibration | 校准 | 检验判分器本身准不准。评的是尺子,不是被测对象(§5 详解) |
| Spearman ρ | 斯皮尔曼相关系数 | 衡量「judge 的排序和人的排序像不像」,−1 到 1,越接近 1 越好 |
| flaky | 不稳定用例 | 有时过有时不过、但被测对象没变的题 |
💡 一个能立刻用上的记忆法:把评测想成一场考试。 case 是题目,instruction 是题面,initial state 是考卷发下来时的样子, agent 是考生,trajectory 是考生的草稿纸,grader 是阅卷老师, rubric 是阅卷标准,reward 是分数,harness 是整个考场的组织工作—— 发卷、计时、收卷、送阅、登分。
这个类比后面还会用到好几次,尤其是 §9——那一章讲的就是「考场坏了,但成绩单看起来很正常」。
§1 为什么评测 agent 不是「多写点测试」
大多数人第一次听到「给 agent 做评测」,脑子里的画面是单元测试:写一堆断言,跑,看绿还是红。 这个直觉会让你在前三个决策上全错。 这一节把差别讲清楚。
1.1 先看单元测试为什么好用
一个普通的单元测试长这样:
expect(add(2, 3)).toBe(5)它能work,靠三个隐含前提:
- 输入确定 → 就是
2和3 - 输出确定 → 同样的输入,永远返回同一个值
- 正确答案唯一 →
5是对的,别的都是错的
这三条成立时,测试可以是机械的、瞬时的、无争议的。
1.2 三个前提在 agent 上全部不成立
现在换成给一个 coding agent 出题:「修复这个仓库里的登录超时 bug」。
前提 1 破了:输入不确定。 你给的是同一句话,但 agent 实际"看到"的输入包括:仓库当前状态、它自己上一步的输出、 工具返回的内容、还有大模型采样时的随机性。同一句话,两次跑出来的中间过程可以完全不同。
前提 2 破了:输出不确定。 跑两遍,第一遍 agent 改了 auth.ts 的 3 行,第二遍改了 session.ts 的 8 行。 两个都可能是对的。 你不能用 toBe() 比对。
前提 3 破了:正确答案不唯一。 这是最要命的一条。「修好这个 bug」有无穷多种正确写法。 你没法枚举正确答案,只能验证某个性质——比如「原来失败的那个测试现在过了」。
🔑 第一个要记住的转变: 从「比对输出」转向「验证性质」。 单元测试问「结果等于 5 吗」,agent 评测问「原先红的测试现在绿了吗」。
这句话就是 SWE-bench 的
FAIL_TO_PASS设计的全部思想。它不关心 agent 怎么改的、 改了几行、用了什么思路——只问那批指定的测试有没有从红变绿。
1.3 还多了一个单元测试根本没有的维度:过程
单元测试只有「结果对不对」。agent 多了一个正交的问题:它是怎么做到的。
举个具体的例子,同一道题,两个 agent 都把测试跑绿了:
| agent A | agent B | |
|---|---|---|
| 结果 | 测试绿 ✅ | 测试绿 ✅ |
| 过程 | 读了 3 个文件,改了 4 行 | 读了 47 个文件,改了 600 行,顺手删掉了两个它看不懂的测试 |
| 花费 | $0.12,8 轮 | $4.30,96 轮 |
在「结果正确性」这一个维度上,两者完全相同。 但没有任何团队会认为它们一样好。
这就带出了三类互相独立的评测目标:
| 类别 | 问什么 | 怎么测 |
|---|---|---|
| 结果正确性 | 事情办成了吗 | 跑测试看红绿。最成熟,外部 benchmark 基本都在这一格 |
| 过程合规 | 办的方式对吗 | 有没有遵守项目约定、有没有绕过门禁、有没有删测试骗过检查。看轨迹 |
| 成本效率 | 代价可接受吗 | token / 美元 / 墙上时间 / 轮数 |
⚠️ 一个必须提前知道的事实:这三格的成熟度差异极大。
结果正确性有几十个现成 benchmark 可以直接接。 成本效率只要底座愿意记就有数。 而过程合规这一格,业界几乎没有可直接复用的东西—— 最贴近的 OctoCodingBench(评「agent 有没有遵守项目里的 CLAUDE.md / Skills / Memory 约定」) 评测脚本没有开源。
🔑 第二个要记住的转变: 「评测」不是一个维度,至少是三个正交维度。 面试里如果只答「跑 SWE-bench 看通过率」,你答的是三分之一,而且是最容易外包出去的那三分之一。
更狠的一句:结果正确性可以整块交给外部 benchmark,过程合规交不出去。 所以一个团队真正的护城河在第二格——这也是为什么自建评测集不能省。 详见 §7。
1.4 于是「评测」这件事的形态变了
把上面三条合起来,agent 评测和单元测试的差别可以收成一张表:
| 单元测试 | agent 评测 | |
|---|---|---|
| 输入 | 确定 | 不确定(含采样随机性) |
| 正确答案 | 唯一,可枚举 | 开放,只能验证性质 |
| 单次运行 | 毫秒级、免费 | 分钟到小时级、要花钱(每题几美分到几美元) |
| 跑一次的结论 | 可信 | 不可信,要跑多次看分布(§6) |
| 判分 | 机械比对 | 五种形态的谱系,从机械到主观(§4) |
| 被测对象 | 只有结果 | 结果 + 过程 + 成本 |
| 环境 | 本机进程 | 容器,且环境本身会坏(§9) |
| 失败的含义 | 代码有 bug | 可能是代码有 bug,也可能是考场坏了(§9) |
最后一行是本文最重要的一行,也是 §9 整章的主题。
🔑 第三个要记住的转变(面试高频,也是最容易讲出深度的一条): 单元测试红了,你知道是代码的问题。 agent 评测得了 0 分,你不知道是 agent 不行、还是评测系统自己坏了。 而这两种情况在数据上长得一模一样。
整套 agent 评测工程里,一半的复杂度都在处理这一件事。
§2 三件套:一条 case 的最小可执行单元
这是全文最重要的一节。如果只能记住一句话,记这个:
一条 coding agent case 的最小可执行单元是三件套:① 可复现初态 + ② 明确任务 + ③ 程序化验证器。缺任何一件,这条 case 就不成立——不是"质量差",是"不成立"。
这句话看起来平淡,但它是一把极其锋利的判据。 它能在五秒内回答一个真实项目里反复出现的问题:「我们攒了一堆数据,能不能做成评测集?」
2.1 三件套分别是什么
① 可复现初态(reproducible initial state)
开考时代码仓长什么样。为什么必须精确可复现: 如果第一次跑的时候仓库是 A 状态,一个月后跑的时候变成了 B 状态, 那两次的分数没有可比性——你不知道分数变化是 agent 变了还是题目变了。
常见实现手段,从强到弱:
| 手段 | 强度 | 说明 |
|---|---|---|
| Docker 镜像(打包好整个环境) | 最强 | 连依赖版本、系统库都锁住。SWE-bench 用这个 |
| git commit + 构建脚本 | 强 | base_commit 锚定代码,脚本装依赖。要求那个 commit 永远可达 |
| 一个初始化脚本(造几个文件) | 中 | 适合自建的小题 |
| 「在你的仓库当前状态下……」 | ❌ 不成立 | 这不是初态,这是一句祝福 |
② 明确任务(unambiguous instruction)
题面。要求是不看答案也能判断做完没做完。
反例:「优化一下这个模块」——什么叫优化完了? 正例:「login() 在 token 过期时抛 TypeError,让它改为抛 AuthError 并保留原始 message。」
③ 程序化验证器(programmatic verifier)
一段能自动跑、返回通过/不通过的代码。注意三个词:
- 程序化 → 是代码,不是一段「请人类判断」的说明
- 验证 → 检查性质,不是比对答案
- 器 → 是可执行的东西,不是一份标准
最经典的形态就是 FAIL_TO_PASS:一份测试清单,改之前必须红,改之后必须绿。
2.2 这把判据的锋利之处:一个真实的判断
现在用它来看一个真实案例。这是 03-内部评测集重建与轨迹回流方案.md 里最硬的一段发现。
背景:一个团队有 6025 条真实的 agent 使用轨迹(用户真的用 agent 干过的活的完整记录)。 直觉上这是宝库——真实任务、真实场景,比人工编题好得多。团队的计划是「写个数据管道,把轨迹转成评测集」。
用三件套一条条对(下面的数字都是实测抽样,不是估计):
| 三件套 | 轨迹里有吗 | 实测覆盖率 |
|---|---|---|
| ② 明确任务 | ✅ 有,用户的原始请求就是 | 稳定提供 |
| ① 可复现初态 | ❌ 几乎没有 | 带 git 基线信息的:1/398 = 0.25% |
| ③ 程序化验证器 | ❌ 完全没有 | 已治理过的 847 条里,outcome_grader 有 844 条是 none,0 条有验证器 |
结论:
数据治理管道产不出验证器。验证器只能人写。
这不是"管道写得不好",是信息在源数据里就不存在。 用户当时干活的时候,没有人要求他先记下 commit 号、再写一份测试来证明自己修对了。 这个信息从未被产生过,所以任何清洗、任何 LLM 加工都变不出来。
这个判断的价值在于它改变了执行顺序。原计划是「写管道 → 自动产出几百条 case」, 修正后变成:
管道做「筛选 + 打包」→ 人写判据 → 产出几十条预期从"几百条"降到"几十条",并接受一个硬后果: 几十条的内部集当不了回归门禁(量太小、覆盖不住),只能当归因样本。
🔑 面试可以直接用的一句话: 「判断一批数据能不能做成评测集,我会先拿三件套过一遍:可复现初态、明确任务、程序化验证器。 真实轨迹通常只满足第二条——它有任务描述,但没记 commit,更没有人当时写测试来证明自己做对了。 所以轨迹的正确用法是筛选候选 + 提供任务描述,判据必须人写。 指望管道自动产出完整 case,会得到一堆看着像 case 的空壳。」
2.3 「空壳」这个坑值得单独说,因为它比缺失更危险
上面那个案例里,第一版管道确实跑出了 27 条 case。它们长这样:
rubric:
completeness: "TODO: 由 importer 占位,请人审补充"形态上「case 存在了」,实质上是 27 条不可用的东西混在可用 case 里一起被统计。
这引出一条设计铁律,它的适用面远超评测:
🔴 缺的东西就不要生成。 让「未完成」在文件系统层面就是可见的—— 没有
tests/test.sh这个文件,门禁直接拒绝,它永远进不了正式目录,只能待在暂存区。反面做法(写占位内容)的代价是:「未完成」被伪装成「已完成」,然后进了分母。 你的「235 条 case」里有 27 条是空壳,而统计脚本不知道,于是所有基于总数的结论都偏了。
这一条和 §9 是同一个主题的两种表现:评测系统最大的敌人不是"报错",是"看起来正常"。
2.4 三件套的一个重要推论:题目比 agent 贵
写一条合格的 case 需要:搭环境(Docker)、锚定初态(commit)、 手写验证器(一批测试,且必须验证过「改前红改后绿」)。
这就是为什么:
- 现成的 benchmark 有巨大价值——别人已经付过这笔钱了
- 自建评测集的成本会被系统性低估——人们估的是"写题面"的时间,忘了写验证器
- 「AI 批量生成 case」这个想法通常会失败——AI 能生成题面(第二件), 但生成不了另外两件里最难的那件:一个真的能跑、且真的在初态下会失败的验证器
§3 agent 评测 vs LLM 评测:一条容易接错的边界
这一节解决一个具体的坑:你想评 agent,但接进来一堆其实在评模型的题库。
这个错误不会报错,也不会有人指出来——你会得到一批看起来很正常的分数, 然后基于它做优化决策,而那些优化对 agent 的实际表现没有影响。
3.1 四条判据
区分二者要有可操作的判据,不能靠感觉。这四条来自 01 号文档:
| # | 判据 | 为什么它能区分 |
|---|---|---|
| 1 | 有 agent loop(多轮) | 单轮 = 一问一答,没有"看到结果再调整"的机会 |
| 2 | 有工具调用 | 能不能自己 grep、自己读文件、自己跑命令 |
| 3 | 在容器里跑 | 要真正执行代码,就必须有隔离环境 |
| 4 | 能看测试报错再改 | 这条最关键——反馈闭环是 agent 的本质能力 |
四条越齐,越是 agent 评测。
3.2 拿真实的评测集来判
这张表的价值在于它包含了边界案例和反例,比单纯列"哪些是好的"有用:
| 评测集 | 判定 | 判据 |
|---|---|---|
| SWE-bench 家族 | ✅ agent | 容器内改真实仓库,跑真实的 FAIL_TO_PASS |
| Terminal-Bench | ✅ agent | 终端内长程作业 |
| SWE-Lancer | ✅ agent | 全栈任务 + Playwright 端到端测试 + Docker |
| SlopCodeBench | ✅ agent | 多 checkpoint,agent 要扩展自己上一轮写的代码 |
| ProgramBench | ✅ agent | 只给编译后的二进制 + 文档,从零重建 |
| OctoCodingBench | ✅ agent | 评 CLAUDE.md / Skills / Memory 遵循度——只有 agent 才有这些概念 |
| Aider Polyglot | ⚠️ 边界 | 有编辑循环、能看报错重试,但不能自主调工具(不能自己 grep / 读文件)。判为「带编辑循环的 LLM 评测」 |
| BigCodeBench | ❌ LLM | 官方数据卡自证:serves as a fundamental benchmark for LLMs instead of LLM Agents |
| LiveCodeBench | ❌ LLM | 单轮竞赛题,无工具无容器 |
💡 注意 Aider Polyglot 那一行的处理方式。它满足判据 1 和 4,不满足 2。 正确的做法不是硬塞进某一类,而是给它一个准确的名字:「带编辑循环的 LLM 评测」。
面试里被问到边界案例时,能给出这种带修饰的分类, 比强行二分要显得清楚得多。分类法的价值在于它能容纳中间态。
3.3 一个反直觉但很重要的结论:最有名的不等于最有用
这一条来自 01 号文档,它推翻了一个几乎所有人的默认选择。
SWE-bench Verified 不适合当主口径。 理由三条:
- 污染 —— 题目来自公开 GitHub PR,大概率已进训练数据
- 测试缺陷 —— 部分题的测试本身有问题
- 饱和 —— 榜顶已挤到 95%+
第 3 条最致命,而且它的后果非常具体:
拿一个已经饱和的 benchmark 做「每个版本一条曲线」,你会得到一条平线。 不是因为产品没进步,而是因为尺子的刻度用完了。
OpenAI 已于 2026-02-23 停报 SWE-bench Verified。SWE-bench Pro 更糟: OpenAI 2026-07-08 官方估算约 30% 的 public 题是坏的,并公开撤回推荐。
那 SWE-bench Verified 还有什么用? 有,但角色要换: 当回归哨兵——不指望它涨,只要它别掉。掉了说明出大问题了。
🔑 面试可以用的一句话: 「选 benchmark 我会先看它还有没有刻度。SWE-bench Verified 榜顶 95%+ 已经饱和, 拿它画版本曲线会得到一条平线——不是产品没进步,是尺子用完了。 所以我会把饱和的 benchmark 降级为回归哨兵(只看跌不看涨), 主口径要找还在区分度区间里的评测集。」
3.4 更贴近真实需求的方向:过程合规
01 号文档一个反直觉的结论是:最贴近实际产品目标的两个评测,都不是 SWE-bench 系。
| 评测集 | 它评什么 | 为什么重要 |
|---|---|---|
| OctoCodingBench | agent 对 CLAUDE.md / Skills / Memory 的遵循度 | 这是 §1.3 说的「过程合规」那一格。用户真正抱怨的往往是"它不听我的项目约定",不是"它不会写代码" |
| SlopCodeBench | 多轮迭代中的代码质量退化 | 单次 diff 看起来没问题,连续改 5 轮之后代码烂成一团——这个问题只有多 checkpoint 评测能抓到 |
⚠️ 但 OctoCodingBench 有个硬限制:评测脚本未开源(只有论文和数据)。 所以「过程合规」这一格目前没有可直接接入的现成方案。
这就回到 §1.3 那个结论:结果正确性可以外包,过程合规交不出去。
§4 判据的五种形态:一条从机械到主观的谱系
§2 说「必须有程序化验证器」,但没说验证器长什么样。这一节把选择空间铺开。
核心心智模型:判据不是「有或没有」,是一条谱系。 从左到右,客观性递减、表达力递增、维护成本先降后升。 选哪一档不是品味问题,是由「你要测的东西是什么形态」决定的。
① 存在性断言 ②结构断言 ③ 执行测试 ④ 工具序断言 ⑤ LLM Judge
←—— 越左:越机械、越便宜、越不会有争议 ——|—— 越右:越能测复杂性质、越贵、越需要治理 ——→4.1 五种形态逐个讲
① 存在性断言(existence assertion)
最便宜的一档。「这个文件必须存在」「这个函数必须被定义」。
- type: assert
check: file_exists
path: packages/core/src/auth.ts- 优点:零成本、零争议、毫秒级
- 缺点:测不到任何行为。文件存在不代表里面写对了
- 适用:脚手架类任务(「创建一个符合规范的新模块」)
② 结构断言(structural assertion)
在 ① 之上加一层:不只看文件在不在,还看内容的结构。 「auth.ts 里必须 export 一个叫 AuthError 的类」「这个模块不许 import 那个模块」。
- 优点:能测架构约束(分层、依赖方向、边界),仍然完全机械
- 缺点:随重构极易烂掉——这是它最大的坑,§10 会用一个真实案例专门讲
- 适用:架构红线(「core 不许依赖 cli」这种)
③ 执行测试(execution test)
跑真实测试,看退出码。FAIL_TO_PASS 就在这一档,它是整个领域的黄金标准。
- 优点:真正验证行为,且客观性和 ① 一样强
- 缺点:需要可运行环境(Docker),慢,且写一条合格的验证器很贵(§2.4)
- 适用:结果正确性。所有严肃的外部 benchmark 都在这一档
④ 工具调用序断言(tool-call sequence assertion)
这一档专属 agent,单元测试世界里没有对应物。它断言的不是结果,是过程: 「必须先 Read 再 Edit」「不许在没读文件的情况下直接改」「Bash 调用里不许出现 git push --force」。
判据作用在 §0.1 说的 trajectory 上。
- 优点:唯一能机械测「过程合规」的手段——不用 LLM 就能测
- 缺点:表达力有档次差别(下面 4.2 单独讲),且容易过度约束(把一种正确做法写死成唯一做法)
- 适用:过程合规、安全红线
⑤ LLM-as-Judge
让另一个大模型读 agent 的输出/轨迹,按 rubric 打分。
- 优点:唯一能测主观性质的手段(「这个解释清楚吗」「这段代码可读吗」)
- 缺点:贵、慢、本身可能是错的——所以必须校准(§5 整节讲这个)
- 适用:确实无法机械判定的性质。注意:是"无法",不是"懒得写"
4.2 一个容易被忽略的深度点:④ 的表达力是分档的
工具序断言不是一个东西,它有两种表达力差距很大的形态:
| 形态 | 长什么样 | 能表达 | 不能表达 |
|---|---|---|---|
| 枚举式 | 「轨迹里必须出现 Read」 | 「用过某工具」 | 顺序、次数、相邻关系 |
| 索引序关系式 | 「Read(x) 的下标 < Edit(x) 的下标」 | 顺序、依赖、"改之前必须先读" | — |
这个区别的实际后果很大。 「不许在没读文件的情况下直接改」这条规则,枚举式根本表达不出来—— 因为轨迹里既有 Read 也有 Edit,枚举式检查会通过, 但实际上 agent 可能先 Edit 了 a.ts 再 Read 了 b.ts。
🔑 面试里如果聊到过程评测,能指出这个档次差别是一个明显的加分点, 因为它说明你真的想过怎么实现,而不只是知道"可以评过程"。
4.3 选哪一档:一张决策表
| 你要测的东西 | 用哪档 | 别用哪档 |
|---|---|---|
| 功能是否正确 | ③ 执行测试 | ⑤(有客观判据时用 judge 是浪费+引入噪声) |
| 架构约束、依赖方向 | ② 结构断言 | ③(跑不出架构问题) |
| 有没有遵守项目约定 | ④ 工具序断言 | ⑤(能机械判的先机械判) |
| 安全红线(不许 force push) | ④ | ⑤(红线不能交给概率性判据) |
| 输出的可读性/解释质量 | ⑤ 唯一选择 | — |
| 脚手架产出物齐不齐 | ① | — |
🔴 一条铁律:能机械判定的,绝不交给 LLM judge。
理由不是省钱,是噪声。judge 有方差、有偏差、要校准、会随模型升级漂移。 每一个本可以机械判定却交给 judge 的判据,都在给你的分数里注入一份不必要的噪声, 而这份噪声后面会让你分不清「分数变了」是产品变了还是尺子抖了。
4.4 一个真实的判据分布,以及怎么读它
理论讲完,看一个真实项目的分布(共 235 条 case):
| 判据类型 | 条数 | 属于哪档 |
|---|---|---|
structured_arch(结构断言) | 103 | ② |
capability(工具序断言) | 49 | ④ |
binary_redline(红线断言) | 14(仅 architecture/ 下) | ②/④ |
rubric_5d(五维 judge 打分) | 大部分剩余 | ⑤ |
execution_test | 少量(个位数) | ③ |
⚠️ 先说口径,这本身是一课。 上表按 case 数计。同一批数据按 yaml 字段出现次数计会得到另一组数 (
structured_arch176 /binary_redline28)——因为一条 case 可以带多条断言。 两组数都对,但混用就全错。源文档明确记着这是一个已知的「字段口径问题」。 这正是 §11 那条纪律的由来:数字旁必须写清取数口径, 否则半年后没人能判断 176 和 103 哪个是真的——而实际上两个都是真的。
怎么读这张表——这里有两个层次:
表层读法:「确定性判据(①②③④)占了大头,看起来挺健康。」
深层读法(这才是有价值的):注意 ③ 执行测试只有个位数。 也就是说这套评测集大量测的是结构,很少测行为。 结构断言便宜,但它测不到「功能对不对」—— 所以这套集的结果正确性覆盖是薄的,它的强项在架构约束。
这不一定是错的(如果这个团队就是想守架构),但它是一个必须被说出来的事实, 否则「我们有 235 条 case」会被误读成「我们的功能正确性有 235 条覆盖」。
💡 通用的读数纪律(来自
00号文档的方法论): 看判据构成要看比例,不是有无。 「有 LLM judge 吗?有。」——这个回答没有信息量。 「judge 占 30% 还是 90%?」——这个才有。这条纪律有一个专门的名字,在那份文档里叫维度 E3: 「判据类型与可靠性:确定性断言 / 脚本验证 / LLM judge / 多评审加权,各占多少比例(不是「有无」)」。
§5 LLM-as-Judge:为什么不能直接用,以及怎么让它可用
「让大模型给大模型打分」听起来是个优雅的方案,实际上它是整套评测系统里最需要治理的一环。 这一节讲它为什么危险,以及一套让它可用的工程手段。
5.1 先说清它的地位:它是手段,不是目的
一个常见的误解是把 judge 当成「更高级的判分方式」。它不是。 按 §4.3 那条铁律,它是最后手段——只在无法机械判定时才用。
但它确实不可替代。有些性质根本没有机械判据:
- 「这段代码可读吗」
- 「agent 给用户的解释准确吗」
- 「这个方案的取舍讲清楚了吗」
对这些,judge 是唯一的选项。所以问题不是「用不用」,是「怎么用才可信」。
5.2 校准(calibration):评的是尺子,不是被测对象
这是理解 judge 治理的第一个关键概念。
校准 = 让 judge 给一批「人类已经打好分」的样本打分,然后比较两份分数是否一致。 它测的是尺子准不准,与被测的 agent 完全无关。
衡量一致性最常用的指标是 Spearman ρ(斯皮尔曼等级相关系数):
- 范围 −1 到 1
- 它衡量的是排序的一致性,不是数值的一致性
- 也就是说:judge 打 2/4/6 分,人打 1/3/5 分——ρ = 1.0,完美, 因为排序完全一致。系统性的偏移不影响排序,而排序才是我们真正需要的(我们要判断 A 是否比 B 好)
- 通常 ρ ≥ 0.6 被视为可用门槛
5.3 一个真实的校准过程:三个版本,ρ 从 0.289 到 0.921
抽象概念不如看真事。这是一个真实项目的校准历史:
| 指标 | v1 | v2 | v3 ✅ |
|---|---|---|---|
| Spearman ρ | 0.289 | 0.354 | 0.921 |
| 平均绝对偏差 |Δ| | 2.29 | 1.00 | 0.78 |
| 有效判定数 | 21/30 | 24/30 | 45/45 |
| JSON 解析失败 | 9/30 | 6/30 | 0/45 |
v1 和 v2 都远低于 0.6 门槛,不可用。v3 做了三件事让它跳到 0.921:
① 改校准集的构造方式(最关键的一件)
- v1/v2:10 条 gold case 全部用满分答案去校准
- v3:5 条 case × 好/中/差 三种答案
为什么这是关键?因为 v1/v2 的做法有一个致命缺陷: 分数分布太窄(全是 3-5 分区间)。 在一个窄区间里算相关系数,信号被噪声淹没—— 你没给 judge 机会展示"它能不能区分好和差",只测了"它能不能在好和更好之间排序"。
🔑 这是一条可迁移的统计直觉,面试里很好用: 校准集必须覆盖被评维度的完整值域。 只用满分答案校准,等于只在尺子的一小段上验证刻度。 这个错误 v2 自己的诊断里已经写出来了,v3 才照着修。
② 换 judge 模型 —— 从中等模型换到最强模型。judge 的能力上限决定它能判什么。
③ prompt 加 few-shot —— 给几个「这样的答案应该打几分」的例子。
注意 JSON 解析失败从 9/30 降到 0/45 这一行—— 它提醒一件很实际的事:judge 的失败一半是格式问题,不是判断问题。 一次解析失败就是一条样本丢失,而丢失的样本会让你的有效样本数悄悄缩水。
5.4 校准通过 ≠ 可以放心用:一个精细但重要的区分
上面那个案例 ρ=0.921,看起来可以收工了。但完整的结论是:
judge 校准不是「偏低待修」,是「gold case 上已达标、真实回答上未验证」。
差别在哪?校准用的是人工构造的好/中/差答案。 而生产环境里 judge 要判的是真实 agent 产出的答案——两者的分布不一样。
真实 agent 的答案可能有校准集里从未出现的形态:半途放弃的、跑偏的、 格式怪异的、在错误的前提下写得很漂亮的。judge 在这些形态上的表现是未知的。
同一份记录里还有一条已知的残余偏差:medium 档系统性偏低 (judge 给 1.8 分,作者给 3.0 分,Δ=1.20)。 也就是说这个 judge对中等质量的答案打分偏严。ρ 高(排序对)但档位偏移存在。
🔑 面试里这个区分很能体现层次: 「校准通过」是一个有作用域的结论,不是全局许可。 正确的表述是「在什么样本上、用什么方法、达到了什么指标」。 只说「我们校准过了 ρ=0.92」是不完整的。
5.5 三个反直觉的结论(来自生产源码实证)
这三条来自对一个真实生产系统的源码分析,每一条都和"教科书写法"相反。
① 生产 rubric 没有分数维度
教科书写法是「任务完成度 1-5 分,代码质量 1-5 分,……」这种李克特量表。
实际生产环境里那份 rubric(153 行,纯 prompt)没有任何分数维度, 全部内容是反偷懒契约——明确规定什么行为算作没做、 什么证据缺失就判失败。Galtea 2026 的指南同样建议跳过 1-5 分量表。
为什么? 因为分数维度制造了一种虚假的精确性。 「代码质量 3.5 分」这个数字无法行动。而「没有 Command run 块的 PASS 一律算 skip」可以行动。
② Agentic Judge 的首要失败不是打分偏差,是过程伪造
如果你的 judge 本身也是一个 agent(能读文件、能跑命令), 它最危险的失败模式不是打分不准,是:
它读了一遍代码就写 PASS,根本没有真的跑过验证。
这一条为什么这么重要:校准系数无法修正一个从未发生的测量。 ρ=0.92 说明"当它真的测了的时候,它测得准", 但对"它假装测了"这种情况,ρ 完全无能为力——因为它在校准集上也可以伪造。
防它的手段不是校准,是结构性的: 「没有 Command run 块(即没有真的执行过命令的证据)的 PASS 一律降级为 skip」
- 上游重放证据抽查。用证据的存在性来约束,而不是用分数的准确性。
③ fail-closed 不是通用默认值
fail-closed(失败时保守处理 = 判为危险/不通过)常被当成安全默认值。 但在同一份源码里,两个模块的方向是相反的:
| 模块 | 方向 | 为什么 |
|---|---|---|
| 权限判定 | fail-closed(宁错勿漏) | 漏放一个危险操作的代价 >> 误拦一次的代价 |
| 安全审查报告 | fail-open(宁漏勿错) | 审查报告里充满误报会被整体忽略 = 最终全部漏报 |
🔑 结论:方向由失败成本不对称的方向决定,不是由「安全」这个词决定。 第二个例子的推理链很精彩:过度谨慎会导致人类不再看这份报告, 于是过度谨慎的最终后果是零覆盖——比适度宽松更差。
5.6 顺带一个重要的区分:在线 judge 与离线评测是两套系统
这是很多人(包括很多面试题库)会混掉的地方。
| 离线评测 | 在线 judge | |
|---|---|---|
| 触发 | CI / 定时 | 用户每次工具调用 |
| 延迟预算 | 分钟级 | 2 秒(由人的感知阈值定,不是 P95) |
| 失败处理 | 可以重跑 | 必须立刻给出有明确语义的裁决 |
| 多次采样 | ✅ 靠采样求成功率 | ❌ 不能,只能靠 temperature 0 + 架构确定性 + fail-closed 兜底 |
| 输入规模 | 每条独立且短 | 随会话增长,会撞上下文上限 |
一个具体的架构手段值得记:两阶段不对称级联。 Stage 1 用极少 token(64)优化 recall,说"放行"才采信; 说"阻断"必须升级到 Stage 2(4096 token)优化 precision。 便宜的那一级只被信任它擅长的那一半结论。
🔑 这条区分在面试里很有用,因为大部分「agent 评估」题目问的都是离线那一侧。 主动指出「还有在线判定这套约束完全不同的系统」,能显示视野宽度。
§6 非确定性:同一道题跑两次得到不同结果,这不是 bug
§1.2 说过 agent 的输出不确定。这一节讲这件事的工程后果——它比想象的深。
6.1 先接受一个事实:单次运行的分数没有意义
同一道题、同一个 agent、同一份代码,跑两次可能一次过一次不过。原因至少四层:
| 层 | 来源 | 能不能消除 |
|---|---|---|
| 模型采样 | LLM 本身按概率生成 token | temperature=0 能减弱,但不能归零(浮点非结合性、批处理差异、后端调度) |
| agent 决策路径 | 前一步的输出影响后一步,误差会放大 | ❌ 不能。这是 agent loop 的固有性质 |
| 环境 | 网络抖动、依赖拉取失败、容器调度 | 部分可控(锁镜像、锁依赖版本) |
| 超时 | 慢一点就被杀 | 部分可控(但见 §9 R2 的坑) |
第二层是最关键也最容易被低估的:agent 的非确定性是被放大的非确定性。 单次 LLM 调用的微小差异,经过 20 轮循环会演化成完全不同的解题路径。 这就是为什么 agent 评测的方差远大于单次 LLM 评测的方差。
🔑 第一条纪律: 报告单次运行的分数是错的,无论那个数字多好看。 正确的输出是「跑了 k 次,分布是什么」。
6.2 pass@k:一个必须搞清楚含义的指标
pass@k = 同一道题跑 k 次,至少成功一次就算通过。
这个定义很简单,但它的含义常被误读。看两个 agent:
| agent A | agent B | |
|---|---|---|
| 10 次运行 | 过 3 次 | 过 3 次 |
| 3 次成功分布 | 均匀分散在 10 次里 | 集中在前 3 次 |
| pass@1 | 0.3 | 0.3 |
| pass@10 | 1.0 | 1.0 |
pass@k 相同,但它们对用户的价值完全不同吗? 不一定—— 这取决于用户能不能重试。
这就是 pass@k 的真正含义:
pass@k 隐含了一个使用场景假设:「用户愿意重试 k 次,并且能识别哪次是对的」。
第二个条件常被忽略。pass@10 = 1.0 听起来很好, 但如果用户无法判断 10 次里哪一次是对的,这个指标就是虚的—— 它假设了一个"完美验证者"的存在。
| 指标 | 假设的场景 | 什么时候该用 |
|---|---|---|
| pass@1 | 用户只跑一次,直接用结果 | 面向产品体验的主口径。它最诚实 |
| pass@k(k>1) | 用户会重试,且能验证 | 衡量能力上限(模型知不知道怎么做);或有自动验证器时(如能跑测试) |
🔑 面试可以直接用的一句话: 「pass@1 和 pass@k 测的是两件不同的事。pass@1 测产品体验,pass@k 测能力上限。 而 pass@k 隐含了『用户能识别哪次成功』这个假设—— 有自动验证器时这个假设成立,纯生成任务里往往不成立。 所以我会同时报两个,并说明差距:差距大说明能力有但稳定性差, 这是两种完全不同的优化方向(前者调模型,后者调 agent 架构和提示)。」
6.3 方差处理的四个工程手段
| 手段 | 做什么 | 代价 |
|---|---|---|
| 多次采样 + 报分布 | 跑 k 次,报均值 + 标准差,而不是单个数 | 成本 ×k。这是最基础也最必要的一条 |
| flaky 隔离 | 识别「被测对象没变但结果反复变」的题,单独归类 | 需要历史数据支撑 |
| 政策分档 | 明确规定「差异 < X% 视为噪声,不判定为退步」 | 需要先测出噪声底线 |
| 固定随机源 | 锁 temperature、锁 seed(如果 API 支持) | 能减弱不能消除;且过度固定会掩盖真实脆弱性 |
最后一条的括号值得展开:如果你把所有随机性都锁死, 测出来的是"在这一个特定路径上的表现", 而用户实际遇到的是分布。过度追求可复现会让评测偏离真实体验。
6.4 一个必须先做的前置工作:测出噪声底线
上面「政策分档」说要规定「差异 < X% 算噪声」,那 X 怎么定?
不能猜,要测。方法是:
同一份代码、同一个 agent、什么都不改,连续跑 3-5 轮完整评测。 这几轮之间的分数波动,就是你这套系统的噪声底线。
这件事的价值极大,因为它直接决定了你的评测能检测出多小的改进:
- 噪声底线 = ±1% → 2% 的改进是可信信号
- 噪声底线 = ±8% → 5% 的"改进"完全可能是噪声,你据此做的所有优化决策都建立在空气上
⚠️ 而这件事绝大多数团队没做。 它不产出任何"新功能", 所以永远排在优先级末尾——直到某天有人问「这次涨了 3%,是真的涨了吗」, 而没有人能回答。
🔑 面试里这是一个很好的"反问"素材: 被问「你怎么评估 agent 改进有没有效果」, 可以答:「我先要知道这套评测的噪声底线是多少。 不改任何东西连跑 5 轮,看分数抖多少。 如果抖 ±8%,那 5% 的提升就不能宣布为改进。 这一步不做,后面所有 A/B 结论都是不可靠的。」
6.5 非确定性还有一个容易忽略的方向:判分侧也会抖
前面讲的都是 agent 侧的非确定性。但如果你用 LLM judge(§5), 判分侧也有非确定性——同一份答案,judge 两次可能打不同分。
于是总方差 = agent 侧方差 + judge 侧方差,而这两份是独立叠加的。
后果:用 judge 判分的评测,噪声底线会显著高于用执行测试判分的评测。 这是 §4.3 那条铁律(能机械判定的绝不交给 judge)的另一个理由—— 每引入一个 judge,你的检测灵敏度就下降一档。
§7 外部锚 vs 内部集:两者买到的是不同的东西
到这里你已经知道怎么设计判据、怎么处理方差。现在是一个战略性的选择: 接现成的 benchmark,还是自己攒题?
答案是都要,但理由不是"两个总比一个好",而是它们买到的东西不重叠。
7.1 各自买到什么
| 外部锚(现成 benchmark) | 内部集(自建) | |
|---|---|---|
| 买到 | 可比性 —— 能和竞品在同一把尺子上比 | 相关性 —— 测的是你真正在意的东西 |
| 题目成本 | 别人付过了(§2.4 说的最贵的那笔) | 你自己付,且会被系统性低估 |
| 公信力 | 高,外部可验证 | 低,"自己出题自己考"总是可疑的 |
| 覆盖你的独特性 | ❌ 完全不覆盖 | ✅ 这是它唯一的理由 |
| 污染风险 | 高(公开题目大概率进了训练数据) | 低(如果你不公开) |
| 会饱和吗 | ✅ 会(§3.3) | 不容易,因为你可以持续加新题 |
| 维护成本 | 低(跟上版本就行) | 高,且会腐烂(§10) |
7.2 分工的判据:按 §1.3 的三个维度切
这是最清晰的切法:
| 维度 | 交给谁 | 为什么 |
|---|---|---|
| 结果正确性 | ✅ 整块交给外部锚 | 别人的题更多、更权威、更便宜(你不用写验证器) |
| 过程合规 | ❌ 交不出去,必须自建 | 测「有没有遵守我们项目的 CLAUDE.md 约定」——这个"我们的约定"外人不可能有 |
| 成本效率 | 两边都能出,但要统一口径 | 只要底座记 token/时间就有数 |
🔑 面试可以用的一句话: 「外部锚和内部集买的不是同一样东西。外部锚买可比性,内部集买相关性。 所以分工不是按'哪个更好',是按维度切: 结果正确性整块外包,过程合规必须自建—— 因为过程合规测的是'有没有遵守我们的项目约定',这个约定外部 benchmark 里根本不存在。」
7.3 内部集的一个残酷现实:它可能小到当不了门禁
§2.2 那个案例的最终结论值得单独强调,因为它推翻了一个常见期待:
内部集在可见未来只有几十条量级。 几十条当不了回归门禁(覆盖太窄,一次代码改动可能一条都碰不到), 只能当归因样本。
这个区分很重要,两者的用法完全不同:
| 用途 | 需要什么 | 几十条够不够 |
|---|---|---|
| 回归门禁(分数掉了就拦住合并) | 覆盖面广、稳定、快 | ❌ 不够。覆盖不住,且方差会导致误拦 |
| 归因样本(出问题时用来定位原因) | 深度、真实、有代表性 | ✅ 够。几十条深题比几百条浅题有用 |
接受这个定位,比假装几十条能当门禁要好得多。 假装的后果是:门禁误拦 → 大家学会绕过它 → 门禁事实上失效,但看起来还在。
7.4 污染:外部锚最大的隐性成本
§3.3 提过污染,这里说清它为什么是结构性问题而不是"选题不慎"。
机制:主流 benchmark 的题目来自公开 GitHub。模型训练数据也来自公开 GitHub。 于是题目和答案很可能都在训练集里。模型是背出来的,不是做出来的。
为什么难以解决:
- 无法证明未污染 —— 你不知道别人的训练集里有什么
- 时间不站在你这边 —— 一个 benchmark 越有名,被爬走的概率越大,寿命越短
- "更新题目"只能延缓 —— 新题发布后,下一轮训练又会吃进去
实践中的应对:
| 手段 | 说明 |
|---|---|
| holdout(留出集) | 一批永不公开的题。这是唯一真正有效的手段 |
| 看时间线 | 优先用发布日期晚于被测模型训练截止的题 |
| 看饱和度 | 分数异常高往往是污染的信号(§3.3) |
| sanitize 仓库内引导 | ⚠️ 容易漏的一条:如果题目仓库里有 CLAUDE.md / AGENTS.md,它可能直接写着答案或提示。这些文件必须清理 |
最后一条是 00 号文档 E8 维度里专门列出的判据,很容易被忽略—— 你锁了 commit、锁了镜像,但没注意到那个仓库的 AGENTS.md 里写着「注意:这个 bug 在 auth.ts 第 42 行」。
7.5 一个反直觉的选型原则:先看它还活着吗
01 号文档里最实用的一条经验,是两个"看起来还活着,其实已经死了"的陷阱:
| 陷阱 | 表现 | 后果 |
|---|---|---|
| LiveCodeBench | 名字里带 "Live",实际已静默停更 12–14 个月 | 你以为在用持续更新的新题,实际在用一年前的老题(且已污染) |
| EvalPlus | 半停滞,issue 堆积 | 同上 |
"静默"是关键词——没有公告、没有 archive 标记,仓库还在,README 还写着 Live。
🔑 选型时的机械检查(三条,都是一分钟能做完的):
- 最后一次 commit 是什么时候(不是最后一次 star,是 commit)
- 最近的 issue 有人回吗
- 数据集本身最后更新是什么时候(代码在更新 ≠ 题目在更新)
第 3 条最容易漏:harness 代码可以一直有人修,但题目集三年没加过新题。
还有一个更隐蔽的形态:官方自己撤回了推荐,但你不知道。 BigCodeBench 已在 2026-07-20 archive,而它的官方数据卡里早就写着 serves as a fundamental benchmark for LLMs instead of LLM Agents—— 它自己声明了不是 agent 评测,但很多人还在拿它评 agent。
§8 执行底座:一次 trial 里到底发生了什么
前面七节都在讲"评什么、怎么判"。这一节讲怎么跑起来—— 把题目喂给 agent、收集结果、判分的那一整套外围代码,也就是 §0.4 说的 harness。
8.1 为什么需要"底座"这个东西
假设你要评一个 coding agent。最朴素的做法:写个脚本, 把题面用管道喂给 agent 的命令行,等它跑完,然后跑测试看红绿。
这个脚本很快会长出下面这些东西:
- 每道题要起一个干净容器(不然上一题的改动污染下一题)
- 容器里得先装上 agent(可能是装 npm 包,可能是传二进制)
- 要注入 API key,但不能让 key 泄漏到日志里
- agent 可能跑飞了不停,需要超时杀掉
- 要记录用了多少 token、花了多少钱
- 要把 agent 的轨迹从容器里捞出来
- 要跑验证器,而验证器得跑在 agent 改完之后、且不能被 agent 篡改
- 一道题要跑 k 次(§6),得并发调度 + 控预算
- 换一个 agent 评,上面全部要重写一遍
这一堆东西加起来就是 harness。 它跟"评测题目"完全正交—— 题目变了它不用改,agent 变了它只需要换一个适配器。
于是 2026 年出现了一个明显的收敛:大家不再各写 harness,而是共用执行底座。 现在的事实标准是 Harbor(Apache-2.0)。它的价值一句话说完:
接一次底座,拿到几十个 benchmark。 Terminal-Bench / SWE-bench 家族 / DeepSWE / SWE-Atlas / cline-bench / SlopCodeBench / Aider Polyglot 全部已有适配器(实测 85 个)。 你只写一个 adapter(让底座会驱动你的 agent),就同时接上了所有这些题库。
这是投入产出比最高的一步,没有别的选项能接近。
8.2 一次 trial 的完整时间线
这是本节的核心,值得记住。 一个 trial = 一道题跑一遍:
job(一次评测活动)
└─ 每道题 × 每次重复(-k) = 一个 trial
① 环境构建 起容器(Docker),挂载 /logs/* 目录
② agent.setup() 底座已实现,内部调用你的 install()
③ agent.run() ← 你实现:在容器里跑你的 agent
④ verifier 上传 /tests/ → 跑 test.sh → 读 /logs/verifier/reward
⑤ 日志回传 容器里 /logs/agent/ 的内容同步回宿主机
⑥ post_run() ← 你实现:在宿主侧解析日志,回填 token/成本
⑦ 清理 删或留容器七个阶段里,你只写两个(③ 和 ⑥)。 其余全是底座的活。
8.3 三个对设计影响最大的细节
① 验证器是在 agent 跑完之后才上传的(阶段 ④)
注意时间顺序:/tests/ 目录在 agent 运行期间不存在,是 agent 结束后才上传进容器的。
这是一个刻意的防作弊设计。 如果测试文件一开始就在容器里, agent 可以读到测试内容然后针对性地"作弊"—— 甚至直接改测试让它通过。agent 看不到判据,判据也不在 agent 的可写范围内。
🔑 这一点在面试里可以体现设计敏感度: 判据的可见性和可写性都必须在 agent 的能力范围之外。 这不是靠"agent 应该守规矩",是靠时序和文件系统隔离来保证的。
② agent 完全不参与判分
这是接入底座最大的一笔净收益。在底座模式下:
- agent 不需要产出 patch(不用自己算 diff)
- agent 不参与判分(不用自己声明成功失败)
- 判分完全由 verifier 在容器里跑
test.sh得出
对比自建 harness:「提取 patch / 防作弊 / 判分」这套逻辑动辄上千行, 在底座路径上整块不存在。
⚠️ 但这同时是一条风险:判分链路变成了黑盒, 如果它坏了返回恒定值,你不会收到任何报错——见 §9 R1。
③ 日志目录是 bind mount,不是 copy
/logs/agent/ 是双向挂载的:容器内往这个目录写东西,宿主侧当场可见。
这个细节决定了你怎么把轨迹和成本数据带回来:不需要专门做导出, 让 agent 把轨迹直接写进 /logs/agent/,宿主侧就能读到。 阶段 ⑥ 跑在宿主机上、且在日志同步之后,所以它能直接解析这些文件。
8.4 接入面到底有多小
「接入一个执行底座」听起来是大工程,实际的必填面非常小:
| 你要实现 | 干什么 |
|---|---|
name() | 返回一个字符串名字 |
install(env) | 把你的 agent 装进容器(传二进制 / 装包 + 写配置) |
run(instruction, env, ctx) | 在容器里执行你的 agent,把题面传给它 |
populate_context_post_run(ctx) | 可选但建议做:解析日志,回填 token 和成本 |
一个类、三个必填方法。 而且不用改底座源码、不用提 PR、不用进官方注册表—— 底座支持直接吃本地导入路径。
💡 这个"接入面小"本身是选型判据。 评估一个底座值不值得接,最实际的问题是: 「我要实现的抽象方法有几个,签名有多复杂?」 如果一个底座要求你实现十几个方法、还要理解它的内部状态机,那个成本会淹没收益。
8.5 两个必须提前想清楚的工程约束
① 网络:任务要断网,但模型 API 要通
这是一个看起来矛盾的需求:
- 任务侧要断网 —— 不然 agent 可能直接上网搜到这道题的答案(污染)
- 模型 API 要通 —— agent 得调 LLM 才能工作
早期的底座只有一个"允许/禁止联网"的开关,一关就把 LLM API 一起断掉了。 现在的正解是分阶段网络策略:agent 运行阶段只放通模型 API 的域名,其余全断。
🔑 这条在面试里可以作为「你怎么防污染」的具体答案之一。 比"我们会注意污染"要具体得多。
② 密钥绝不进容器
agent 要调 LLM,就需要 API key。但 key 不能写进容器,理由:
- 容器可能被保留下来(
--no-delete)供调试 - 轨迹和日志会被归档、可能被分享
- agent 本身可能把环境变量打印到日志里
标准做法是让容器通过一个网关访问模型,key 只存在于网关侧, 容器里只有一个作用域受限的临时凭据,或者干脆只有一个内网地址。
8.6 一个反直觉的选择:先跑 10 题,不要跑 500 题
接通底座后的第一件事,直觉是"跑一个完整的 benchmark 看看分数"。这是错的。
正确的顺序是四步递进,每一步都有明确的"过不了就停"判据:
| 步 | 跑什么 | 过关判据 |
|---|---|---|
| 0 | 底座自带的假 agent | 链路能通,能出分数 |
| 1 | 10 题的 sample 集 | 你的 agent 能被驱动起来,能出非零分 |
| 2 | 同 10 题,跑 oracle 和 nop 对照(§9 R1) | oracle 满分、nop 零分 |
| 3 | 完整集 | — |
为什么不能跳到第 3 步:一次完整的 benchmark 运行要花几小时和几十到几百美元。 如果链路有问题,你付了全款买到一堆无意义的 0 分。 更糟的是——你可能不知道它无意义(§9 整章的主题)。
🔑 面试里这个顺序本身是一个信号。 「我会先用 10 题的 sample 集跑通,并且用 oracle/nop 做双向对照, 确认链路正确之后才跑完整集」—— 这句话说明你知道评测系统自己需要被测试。
§9 会「绿着坏掉」的失效模式(本文最重要的一章)
前面八节讲怎么把评测系统建起来。这一章讲它会怎么骗你。
为什么这是最重要的一章:一个报错的评测系统不危险——你会去修它。 危险的是不报错、流程全绿、数字看起来正常,但结论是错的那种。 因为你会相信它,并据此做决策。
这一章的组织方式:每条给出形态(怎么表现)和判据(怎么机械地发现它)。
9.1 R1 🔴 验证器恒返 0:把「链路坏了」伪装成「没解出来」
形态:所有题的分数都是 0,任务正常结束,无任何报错。 看起来像「我们的 agent 一题都没做出来」。
为什么它是最危险的一条:
它和"真实的低分"在数据上完全不可区分。
一个刚接通的评测链路跑出全 0,有两种可能: ① agent 确实很差 ② 判分链路坏了(验证器脚本没上传成功、退出码读错了、 reward 文件路径写错了、scorer 里有个 return 0 的占位没删)。
两者的数据长得一模一样:一列 0,没有报错。 而人的默认反应是接受它——「哦,看来还需要优化」—— 然后开始优化一个根本没被测量的东西。
判据(三条,必须全做):
| # | 做什么 | 期望 | 说明什么 |
|---|---|---|---|
| 1 | 用 oracle agent 跑一遍 | 应该满分 | oracle 直接执行题目自带的标准答案脚本。oracle 也 0 分 = 验证器或环境坏了,与你的 agent 无关 |
| 2 | 用一个已知能用的第三方 agent 跑一遍 | 有非零分 | 横向对照 |
| 3 | 看验证器的 stdout 日志 | 能看到测试真的跑起来了 | 区分"测试跑了但失败"和"测试根本没跑" |
⚠️ 反过来的形态也要防:reward 恒为 1。
如果验证器的测试在初态就能通过(题目没有正确设置"破损初态"), 那所有 agent 都会拿满分——包括什么都没做的。
oracle 对照防不住这个(oracle 满分是预期的)。要用 nop agent—— 一个什么都不做的假 agent:
nop 应该拿 0 分。nop 拿到分 = 题目的初态本来就是通的,这道题是废的。
🔑 这一条是全文最值得记住的工程手段,因为它体现了一个通用思想: 双向对照 = 上界 + 下界。 oracle 验证「做对了能拿到分」(防假阴性),nop 验证「没做不会拿到分」(防假阳性)。 只做一边,另一边的失效模式对你完全隐形。
面试里被问「你怎么保证评测系统本身是对的」, oracle/nop 双向自检是最硬的答案。
9.2 R2 🔴 多层超时互相掩蔽:修了一层只是换了个杀手
形态:agent 被杀掉了,但归因指向错的那一层。
一个真实的 agent 评测里,至少有两层超时同时存在:
| 层 | 谁设的 | 表现 |
|---|---|---|
| agent 内部限制(如"最多 30 轮") | 你的 agent 自己 | agent 优雅退出,日志完整,有明确的退出状态 |
| 底座的阶段超时 | harness | 外部 kill,agent 内部看不到 |
问题在于:如果这两层的值接近,你修一层只是把杀手换成另一层。 你以为放宽了限制,实际上被杀的题数一点没变,只是死因换了。
外部 kill 还会造成一个隐蔽的数据缺失: 因为 agent 是被强杀的,它的"会话结束"钩子没跑, 于是成本数据缺失——那条 trial 既没有成功结果,也没有花费记录。
判据:
退出状态缺失或异常 且 成本来源标记为「缺失」 → 判定为「被外部 kill」。 这类样本和"内部限制耗尽"是两类不同的样本,不能混算。
⚠️ 一条重要的推论(这句话本身很有价值):
被截断的分布不能论证自己的上限。
意思是:如果你看到"90% 的题都在 28-30 轮完成", 不能据此得出"30 轮够用"的结论—— 因为那些需要 50 轮的题在 30 轮时就被杀了,它们根本没进入你看到的分布。 分布的右尾被切掉了,而切掉的部分不会在图上留下痕迹。
要放宽超时,必须同批抬高所有层,否则只是换了个杀手。
9.3 R3 🟠 环境噪声污染耗时数据
形态:某条 trial 的耗时是 4 小时,看起来是"这道题极难", 实际是宿主机中途休眠了(笔记本合盖),墙上时间在走但什么都没在跑。
为什么值得单独列:因为它会复活已经被认为解决的超时问题—— 你调好了超时阈值,某天换个环境跑,一批题莫名超时。
判据:耗时分布里的极端离群值要先怀疑环境,不要先怀疑题目难度。 更彻底的做法是同时记录 CPU 时间和墙上时间,两者严重背离就是环境噪声。
9.4 R4 🟠 提示模板不一致 → 跨 agent 对照失效
形态:你对比 agent A 和 agent B 的分数,得出"A 更强"。 实际上 A 拿到的题面里多了一句"请先跑测试确认问题",B 没有。
这不是作弊,是很容易无意发生的: 两个 adapter 由不同的人写,各自加了点"帮助 agent 理解任务"的话。
判据:跨 agent 对比时,题面必须逐字节相同。 把题面的 hash 记进结果里,对比前先比 hash。
9.5 R5 🟠 结构断言随重构大规模烂掉
这一条 §10 会用一个完整案例讲,这里先给形态:
形态:103 条结构断言,实测通过率 0.0%。 不是断言写错了,是代码目录结构变了(重构后 src/ 变成了 packages/*/src/), 而断言里写死的路径全部指向了不存在的位置。
为什么它属于"绿着坏掉":因为在重构发生的那一刻, 没有任何东西报错——评测系统只是不再跑了,或者跑出一堆"失败"而被当成"待优化"。
判据:断言里引用的每一个路径,都要有一个"这个路径必须存在"的元检查。 路径不存在时应该报「断言本身失效」,不是报「断言未通过」—— 这两者是完全不同的信号,混在一起就丢失了信息。
9.6 R6 🟡 「接了但没人用」—— 系统自己变成死功能
形态:评测系统建好了,跑通了,然后没有人再跑它。 三个月后有人想用,发现全都不 work 了。
这一条听起来不像技术问题,但它是最常见的失效模式, 而且它有一个可怕的性质:它会让前面所有的工作归零。
为什么会发生:评测的收益是延迟的、间接的(它防止的是"没发生的退步"), 而成本是即时的、直接的(每次跑要花钱花时间)。 在任何优先级排序里,它都会排在功能开发后面。
判据(这是要点,判据必须机械):
看「最后一次真跑」的日期,而不是看「系统是否存在」。
具体到几条可以直接查的东西:
- 结果账本里最新一条记录是什么时候
- 门禁配置里有没有真的挂上这个检查(挂了但被跳过等于没挂)
- 有没有任何自动触发(定时 / PR 触发),还是全靠人手动跑
9.7 一个统一的心智模型:所有失效模式都是同一件事
回头看 R1–R6,它们表面上很不同(判分、超时、环境、模板、路径、组织), 但有一个共同结构:
「没有信号」被误读成「信号是负的」。
| 失效 | 真实情况 | 被误读成 |
|---|---|---|
| R1 | 没测到 | 分数低 |
| R2 | 被外部杀了 | 能力不足 |
| R3 | 环境休眠 | 题目难 |
| R4 | 题面不同 | A 比 B 强 |
| R5 | 断言失效 | 断言未通过 |
| R6 | 没在跑 | 在跑且没问题 |
所以整个防御思想可以收成一句话:
🔑 必须让「没有信号」和「信号是负的」在数据上可区分。
这就是为什么要有 oracle/nop(区分"没测到"和"没做到")、 为什么要区分"断言失效"和"断言未通过"、 为什么要把"被 kill"和"限制耗尽"分成两类样本、 为什么要看"最后一次真跑"而不是"系统是否存在"。
在评测系统里,最重要的一类信息是"这个测量到底有没有发生"。
面试里如果能把这句话讲出来,并用 oracle/nop 举例, 这个回答的层次会明显高于"我们会做 code review 和写测试"。
§10 评测系统自己是会死的:一个真实死亡案例的完整解剖
§9 讲的是"单次运行骗你"。这一章讲一个更大的时间尺度:整套评测系统会腐烂,而且是静默的。
这一章用一个真实案例贯穿。它的价值在于:死因和几乎所有人的第一直觉都不一样。
10.1 案发现场
一套自建的 agent 评测系统,规模不小:
| 项目 | 数字 |
|---|---|
| case 总数 | 235 条 |
| 评测框架代码 | 31 个文件的独立包 |
| 相关文件总数 | 623 个 |
| 判分器 | 5 种 |
| judge 校准 | 已做,ρ=0.921 ✅ |
看起来是一套认真建设过的系统。实际状态:已停跑 85 天,且这件事在任何地方都没有告警。
停跑的证据很直接:结果账本里最后一条记录是 6 月 1 日, 判分校准最后更新 5 月 25 日,而现在是 8 月 25 日。
10.2 第一个反直觉:死因不是「题目质量差」
系统的所有者对死因的归因是:「题目是 AI 生成的,太通用、不权威,所以没意义了。」
这个归因听起来非常合理,而且它推出的动作也很自然:清空重建,这次认真出题。
实测死因完全不同:
| 所有者的归因 | 实测死因 | |
|---|---|---|
| 问题定位 | 题目质量差(AI 生成、太通用) | 基线不可复现 + 判据不可执行 |
| 证据 | 主观判断 | 见下面两条硬数据 |
硬数据一:结构断言的路径存活率 10.5%
103 条结构断言,实测通过率 0.0%。原因不是断言写错,是重构:
代码从 src/xxx.ts 变成了 packages/core/src/xxx.ts(分包重构), 而 169 处断言里的路径全部指向旧的 src/,指向 packages/ 的:0 处。 去重后 120 条唯一路径里 102 条已不存在。
硬数据二:引用的 git commit 全部不可达
另一组 case 用 commit 号锚定初态(§2.1 的正确做法),但那 3 个 commit 全部不可达—— 可能是分支被删、可能是仓库被重写历史。初态没了,题目就不成立(§2)。
🔑 这就是为什么归因必须回到证据。 「AI 生成所以不权威」是一个关于题目内容的判断; 「路径存活率 10.5%」是一个关于题目可执行性的事实。
两者推出的动作完全不同:
- 按"内容差"治 → 重新出题 → 新题会以完全相同的方式再死一次(因为重构还会发生)
- 按"可执行性"治 → 建立路径与断言的联动机制,让重构时断言跟着改或至少报警
10.3 第二个反直觉:清空是零收益动作
所有者计划的第一步是「清空旧题目和分数」。这一步被否决了,理由值得完整理解。
否决理由一:它现在骗不了任何人。
清空的动机是"这些分数是假的,会误导人"。但实测:
- 分数不进门禁(不拦任何代码合并)
- 分数不进产品曲线(没有任何地方展示它)
- 官网没有评测页
也就是说,没有任何人正在被这些数字误导。清空解决的是一个不存在的问题。
否决理由二:清空的成本远超预期。
要删这 235 条 case,会撞上:21 个文件 / 39 处代码引用, 外加 40 个 yaml/md 的自引用,还有一条"结果账本只增不改"的哈希封存约定。
正确的第一步是「冻结存档」 —— 零风险,一天,不删任何东西。 把旧的标记为归档、说清它为什么不可用,然后把精力放在新建上。
🔑 这里的通用教训: 删除旧东西感觉像进展,但它通常是零收益的。 判断要不要清理,问一个具体问题:「现在有谁在被它误导?」 没有人 → 它是历史包袱,不是活跃风险 → 优先级排在最后,不是最前。
这条在面试里可以体现优先级判断力—— 很多人会把"技术债清理"排在最前面,因为它看起来负责任。
10.4 第三个反直觉:它的死亡有一个精确的机制
系统为什么会停 85 天没人发现?这里有一条完整的因果链,每一步都合理,但合起来是死亡:
第 1 步:把评测挂进 PR 门禁。 合理——这样每次改代码都会跑。
第 2 步:发现门禁会误红。 因为用了 LLM judge,而 judge 有方差(§6.5)。 红了之后分不清是真的退步还是 judge 抖动。
第 3 步:把它从 PR 门禁摘掉。 ✅ 这个决定是对的,理由写在配置注释里:
「LLM judge 天然有方差,设成必需门禁红了分不清是真回归还是模型抖动, 会养成『重跑一次看看』的习惯。」
这个理由完全成立——一个会误报的门禁比没有门禁更糟, 因为它会训练所有人学会忽略它。参照的两个知名开源项目,PR CI 里同样不跑真实 LLM 评测。
第 4 步:摘掉之后,没有任何替代的定期信号。 4 个定时任务全部被注释掉了(当初的理由是"密钥还没配")。
结果:评测只在有人主动想起来的时候才跑。而人已经 85 天没想起来了。
🔴 这条因果链的精妙之处在于:没有任何一步是错的。 挂门禁是对的,发现误红是对的,摘掉是对的。 错的是"摘掉"之后没有补上替代品—— 而这一步不是决策失误,是没有人负责的空白。
用一句更狠的话总结: 这道防线自己变成了它当初要消灭的那种死功能。
10.5 从这个案例提炼的三条可迁移原则
① 新增防线的验收判据不是「代码能跑」,是「真实场景里被触发过」
这是最重要的一条。一个门禁、一条告警、一个检查:
- ❌ 错的验收:
build过、单测过、手动跑一次成功 - ✅ 对的验收:在真实的一次代码提交里,它真的被触发了,并给出了正确结论
原因很简单:build 过只证明代码语法对,不证明它接线接对了。 配置里少一个触发条件、密钥没配、条件判断写反——这些都能通过 build。
② 门禁的方差必须低于它要检测的信号
这解释了第 2-3 步。用高方差的判据(LLM judge)做门禁,注定失败:
| 判据 | 方差 | 适合做门禁吗 |
|---|---|---|
| 结构断言、执行测试 | 极低 | ✅ 适合 |
| 工具序断言 | 低 | ✅ 适合 |
| LLM judge | 高 | ❌ 不适合。适合做趋势观察,不适合做单次拦截 |
正确的分工:确定性判据做门禁(拦截),judge 做周期性趋势(观察)。 把 judge 放进门禁,等于用一把会抖的尺子做通过/不通过的二元裁决。
③ 「有实现」「能手动跑」「有自动化」是三个不同的状态
案例里有两个功能(pass@k 计算、轨迹对比):代码写好了,能手动跑, 但不在任何自动链路里。
这三档必须在评估时分开,因为它们的实际价值差异巨大:
| 状态 | 实际价值 | 常见误判 |
|---|---|---|
| 有实现、无法运行 | ❌ 接近零 | 被当成 ✅ |
| 有实现、能手动跑、无自动化 | ⚠️ 只在有人想起来时兑现 | 最容易被当成 ✅ |
| 有实现、有自动化触发 | ✅ 真实价值 | — |
🔑 中间那一档是最大的认知陷阱。 「我们有 pass@k」这句话是真的,但它兑现的条件是"有人记得跑"—— 而 §10.4 已经证明了人不会记得。
10.6 评测系统的腐烂清单:会烂的六个地方
把这个案例和其他观察合起来,一套评测系统会从这六处烂掉。 每一处都值得在设计时就想好对策:
| # | 烂在哪 | 触发条件 | 对策 |
|---|---|---|---|
| 1 | 断言里的路径 | 重构 | 路径存在性做元检查(§9.5);报「断言失效」而非「未通过」 |
| 2 | 锚定的 commit | 分支删除、历史重写 | 优先用 Docker 镜像而非 commit;或把 commit 归档到自己的仓 |
| 3 | 定时触发 | 密钥缺失、临时关闭后忘记开 | 验收判据是"真实触发过"(§10.5①) |
| 4 | judge 的模型版本 | 模型下线、升级 | 锁版本;模型换代后必须重新校准 |
| 5 | 文档里的现状描述 | 时间流逝 | 数字旁边写取数命令(§11) |
| 6 | 人的注意力 | 一个季度 | 唯一对策是自动化 + 有主的告警 |
第 6 条是根因,前五条某种程度上都是它的表现—— 如果每周有人真的看一次评测结果,前五条都会在一周内被发现。
10.7 一个正确的执行顺序(对比错的那个)
案例最后给出的修正顺序,值得作为模板记住:
| ❌ 直觉顺序 | ✅ 修正后 | |
|---|---|---|
| 第 1 步 | 清空旧 case 和分数 | 冻结存档(零风险,不删任何东西) |
| 第 2 步 | 重新出一批高质量题 | 保住外部锚的信号(已有的对照先别中断) |
| 第 3 步 | 写数据管道自动产出 case | 补初态埋点(新会话开始记 commit)← 回流的物理前提 |
| 第 4 步 | 跑起来看分数 | 定 case 格式 + 唯一入库门禁 |
| 第 5 步 | — | 手工做 3–5 条端到端跑通的 case(不写管道) |
| 第 6 步 | — | 跑通之后,才判断哪一段值得自动化 |
| 第 7 步 | — | 最后归档旧 case(纯成本动作,放最后) |
其中最关键的是一条硬判据:
🔴 在"至少 1 条内部 case 真的跑出满分"之前,不许写任何管道代码。
理由:拿假数据设计转换层,等于给一个还没见过的数据形状写适配器。 你会写出一堆看起来合理、但和真实数据对不上的代码。
这条判据的形态很通用——先端到端打通一条,再考虑批量化。 它防的是"我先把基础设施建好"这种诱人但常常报废的顺序。
§11 口径陷阱:为什么你查不到别人有没有评测
这一章换一个视角:不是"怎么建评测",而是**"怎么调研别人的评测"**。
为什么这值得单独一章:面试里很可能被问「你了解业界都怎么做评测的吗」, 而更实际的场景是——你入职后第一件事就是摸清现状(自家的和竞品的)。 这件事看起来只是"搜一下代码库",实际上有一整套陷阱,每一个都会让你得出反的结论。
下面 13 条全部来自真实调研中踩到的,编号 P-1 到 P-13。
11.1 假阳性陷阱:搜到一堆,但都不是评测
P-10 eval 是最差的检索词,双向都不可靠 🔴
这一条最反直觉,所以放第一个。搜 eval 会怎样:
某个项目搜 eval 命中 235 个文件,真正的评测实体:0 个。 原因是 eval 是 Value 的子串——PermissionRuleValue 里就有 eval,这一个词就命中 34 处。
正确做法:禁用裸 eval 判定有无,改用三条: ① 扫目录名 ② 找实体文件(*.eval.ts / case_*.yaml / task.toml / verify.sh) ③ 看 package.json 的脚本名
P-1 harness 是评测领域的伪信号词
搜 harness 在三个项目分别命中 2311 / 314 / 119 个文件, 全部是"测试 harness"或"agent harness",跟评测无关。
处理:必须组合搜(eval harness / bench harness),或直接看目录。
P-2 snapshot 测试会碾压真评测的命中数
搜 snapshot:一个项目命中 703 文件、insta::assert 97 处,真评测实体 0。 另两个项目命中 4620 和 568。
处理:矩阵里必须分开两列——「代码确定性测试」和「agent 行为评测」, 不许合并计数。它们是完全不同的东西,混在一起的数字毫无意义。
P-7 评测框架 ≠ 被评测对象
promptfoo / langfuse / litellm / langchain 都会在 eval 检索里高命中—— 但它们是工具,不是产品。Harbor 也属这一类。
处理:准入先判「它是 coding agent 吗」。否则你会拿工具跟产品对比, 得出"promptfoo 的评测能力比 Cursor 强"这种毫无意义的结论。框架应进"架构参照",不进主矩阵。
P-9 产品内的 evaluator 不是评测系统
搜 evaluat* 会命中运行时代码——比如一个 agent 内部判断"目标达成了吗"的模块。 那是产品功能,不是离线评测。
P-6 同一作者的另一个项目会伪装成外部对象
调研中发现一个"外部项目"的评测布局与自家高度相似, 查了才知道:43 个 commit 的作者全是同一个人(就是自己)。
处理:准入加一步 git log --format='%an' | sort | uniq -c,作者重合的移出主矩阵。 否则你以为在做外部对照,实际在照镜子。
11.2 假阴性陷阱:明明有,但你搜不到
这一组比假阳性更危险,因为你会得出"他们没有"的结论,而这是错的。
P-4 「评测」在产品化项目里可能叫 qa
一个项目的 514 个评测场景全在 qa/ 目录下,搜 eval 零命中。
处理:词汇表必须包含 qa / scenario / suite / maturity / parity。
P-3 评测系统可能不在仓库根,而在子包里
一个项目的评测实体全在 packages/console 和 packages/stats 里,仓库根零命中。
处理:架构摸底必须扫全部 workspace 包。
P-4′ 最坑的一条:自己的能力也会因命名不同被自己漏掉 🔴
这条值得单独讲,因为它是镜像形态:
自家有 49 个 case 做的是工具调用序断言(§4.1 的第④档), 但因为目录不叫 flow 而叫 capability、字段不叫 flow 而叫 assert, 调研报告把"工具序断言"写成了「我们没有的形态」。
也就是说:在对比矩阵里,自己那一栏被自己填错了, 结论变成"竞品有这个能力我们没有",据此派生的优化任务是去建一个已经存在的东西。
🔑 处理办法很具体,值得记住: 摸自己也要两轮检索——第一轮用我们的词,第二轮用对手的词回头搜自己。
这个手法的通用性很强:任何"我们 vs 他们"的对比, 都要用对方的词汇表回来搜一遍自己,否则命名差异会被误读成能力差异。
11.3 「存在」不等于「在运行」:三个递进的陷阱
这一组是 §10 那个案例的方法论版本。
P-8 「有 N 个用例文件」≠「评测系统在运行」
819 个文件、260 条 case,但最后一次真实产出是三个月前。
处理:每个 ✅ 都必须附上"最近一次产出数据的时间"。
P-8′ 加强版:报告文件存在 ≠ 报告有内容 🔴
这一条更细,也更容易上钩:
有一个 eval-latest.json,时间戳看起来很新,文件名叫"最新报告"。 打开一看:只含 1 个 case、成本 0、总耗时 17 毫秒—— 它是一个 mock 冒烟测试的产物,不是真实报告。
处理:查"最近一次产出"不能只看文件的修改时间, 必须打开看 case 数与关键值是否非零。
P-12 README 与实现漂移,而且它会自称「就绪」
- 一份 README 写着「框架就绪,真实接入待后续阶段」——那是计划,不是现状
- 一个配置文件自称「10 条精挑」,实测含 15 条,且文件头部明确标着「骨架阶段、未校验」
处理:联网或读 README 得到的形态,必须落地到源码验证。
11.4 两个关于"失效会互相掩盖"的陷阱
P-5 两个失效串联时,上游失效会掩盖下游失效 🔴
一个具体的例子,值得完整看清结构:
- 一个定时任务的配置文件里,有个文件匹配模式写错了,匹配 0 个文件
- 但没人发现——因为这个定时任务从来没被触发过(代码托管平台不匹配)
上游的失效(从不触发)完全掩盖了下游的失效(模式写错)。 你修好了上游,下游的 bug 才会显形——而那时你以为已经修好了。
处理:验收 CI 类能力必须两步: ① 配置内容对不对 ② 在当前环境下是否真能触发。 只做 ① 会把"死脚手架"记成 ✅。
P-11 / P-11′ 判"有没有门禁"要把全部层级列出来逐个验 🔴
原以为「CI 不跑测试没关系,本地提交钩子在拦」。 实测:提交钩子和推送钩子都不含跑测试的命令(三个月前被主动移除了)。 真正在拦的只有发布脚本。
处理:门禁通常有多层(CI / 本地钩子 / 发布脚本), 每一层都要实测它到底跑什么,不能假设某一层在兜底。
11.5 最贵的一条:引用旧文档的「现状」
P-13 引用自家旧文档的「现状」= 引用一个已过期的快照 🔴
这一条造成的代价在所有陷阱里最大,而且它重复发生了四次:
| 第几次 | 表现 | 代价 |
|---|---|---|
| 1 | 把已修复的问题当新缺陷写进报告 | 一条假缺陷 |
| 2 | 把全仓 6 个配置的问题写成 3 个的问题 | 范围低估,任务归错子系统 |
| 3 | 差点把两个月前已调研过的东西当新情报 | 差点重做一份 2591 行文档的调研 |
| 4 | 三条已作废的结论被当现状引用 | 🔴 一条错误的路线裁决 + 一条不存在的 P0 任务 |
第 4 次的代价第一次超过了"假缺陷"的量级——它改变了路线决策。
具体是这样:旧文档里记着「judge 校准 ρ=0.354,偏低」, 引用者据此裁决「judge 校准修不动,不投入」。 而实际上 ρ 早已是 0.921(§5.3 那个案例)——裁决的前提整个不存在。
但错误的机制比结论更值得看:
那份旧文档已经更新了。它用「
0.354→ ✅ 已修复:0.921」的写法记录了修正。 引用者取了被删除线划掉的那半句。
所以问题不是文档没更新,是读法出了问题。
🔑 由此得到的纪律不是"要查文档扩散点",而是一句更小、更机械的话: 读到句末。
新旧结论并存的文档很常见(删除线、"已废弃"标注、"更新:"前缀), 而人的眼睛会停在第一个匹配到的数字上。
11.6 把这 13 条收成四条纪律
调研方法论最终可以压成四条。顺序有意义:
| # | 纪律 | 防的是 |
|---|---|---|
| 1 | 数字旁必须有取数命令 | 半年后无法判断真假,也无法在下一个对象上保持同一口径 |
| 2 | 计数一律写脚本,禁目测 | 实测教训:目测三处错三处 |
| 3 | 🔴 对自己的核验强度必须 ≥ 对对手的核验强度 | P-4′。对手要两轮检索、要记 commit,自己也要 |
| 4 | 🔴 引用旧文档时读完整句,不摘半句 | P-13 |
第 3 条是最容易违反的,而它的违反形态很隐蔽: 调研外部对象时你会很谨慎(查两遍、记 commit、找源码), 摸自己时会凭印象——因为"自家的事我还不知道吗"。 结果是:对比表里最不可靠的一栏,是你自己那一栏。
🔑 面试里这一条特别值得说,因为它体现的是方法论上的诚实: 「做横向对比时我有一条硬纪律:基准列的核验强度不能低于对手列。 人的自然倾向是查别人查得很细、摸自己凭印象, 结果对比表里最不可靠的恰恰是自己那一栏。 我们踩过一次——自家已有的一种判据形态因为命名不同被写成了'我们没有', 据此派生的优化任务是去建一个已经存在的东西。」
附录 B · 术语速查(中英对照,按字母序)
| 英文 | 中文 | 一句话 | 详见 |
|---|---|---|---|
| adapter | 适配器 | 让底座能驱动某个具体 agent 的胶水代码 | §8.4 |
| agent loop | 智能体循环 | 想→调工具→看结果→再想。agent 与 LLM 的分水岭 | §0.1 |
| assertion | 断言 | 机械检查。最便宜也最易随重构烂掉 | §4.1 |
| base_commit | 基线提交 | 用 commit 号锚定初态 | §2.1 |
| baseline | 基线 | 上一次的成绩,用来判断进步还是退步 | §0.5 |
| calibration | 校准 | 评的是尺子,不是被测对象 | §5.2 |
| case / task | 用例 | 一道题 | §0.2 |
| contamination | 污染 | 题目答案已进训练数据,模型是背出来的 | §7.4 |
| FAIL_TO_PASS | 修前红修后绿 | 程序化验证器的黄金形态 | §2.1 |
| fail-closed | 失败时保守 | 不是通用默认值,方向由成本不对称决定 | §5.5 |
| flaky | 不稳定用例 | 被测对象没变但结果反复变 | §0.5 |
| gold patch | 标准答案补丁 | 人类真实的修复 diff。用来自检环境,不是对比答案 | §0.2 |
| grader / scorer | 判分器 | 决定得分的那段代码 | §0.3 |
| harness | 执行底座 | 喂题+收集+判分的一整套外围代码 | §8.1 |
| holdout | 留出集 | 永不公开的题。防污染唯一真正有效的手段 | §7.4 |
| judge | 评审 | 用另一个模型打分。是 grader 的一种实现,不是并列概念 | §5 |
| nop agent | 空 agent | 什么都不做。应该拿 0 分 | §9.1 |
| oracle agent | 标准答案 agent | 执行标准答案脚本。应该拿满分 | §9.1 |
| pass@k | k 次通过率 | 跑 k 次至少成功一次。隐含"用户能识别哪次对" | §6.2 |
| reward | 得分 | 一次评测的最终数字 | §0.3 |
| rubric | 评分细则 | 给 judge 的打分标准。生产 rubric 往往没有分数维度 | §5.5 |
| sandbox | 沙箱 | 隔离执行环境,通常是容器 | §0.4 |
| Spearman ρ | 斯皮尔曼系数 | 衡量排序一致性,不是数值一致性。≥0.6 可用 | §5.2 |
| trajectory | 轨迹 | agent 干活的全过程记录。过程评测的证据 | §0.1 |
| trial | 一次试验 | 一道题跑一遍 | §8.2 |
| verifier | 验证器 | 在容器里跑测试给结论。比 grader 窄 | §0.3 |