我造了 12 条电商售后的业务评测样本,写了一版看起来很合理的判据,然后写了 5 个完全不理解业务、连订单都不查的脚本去跑它。最强的那个拿到了 9/12(75%),12 条样本全部至少被一个脚本攻破。
这不是判据写得潦草——它检查了「回复是否像样」和「该变的状态有没有变」,也就是多数团队上线时用的那一版。问题在于我从头到尾没有测过判据本身。
《什么是一个好的评测样本?》 给了七个条件和一张样本卡,但结尾自己声明了一句:「本文给的是设计与审核框架,不是一套被我重新跑过的 benchmark 实验」。这篇就是补上那次实验。上游的《业务评测覆盖地图》 回答「要测哪些格子」,本文回答一个更靠后、也更容易被跳过的问题:格子里那条样本,它的判据自己可信吗?
一句话主线:在业务评测里,判据不是量 Agent 的尺子,判据本身是第一个被测对象。 顺序错了,后面所有分数都在浮沙上。
名词速查
| 术语 | 一句话解释 |
|---|---|
| 判据(verifier / oracle) | 判定一次任务算成功还是失败的那段代码或规则 |
| outcome validity(结果效度) | 判据说「过了」的时候,任务是真的完成了 |
| task validity(任务效度) | 只有真正具备目标能力才能通过;不具备就一定通不过 |
| 作弊下界(cheat floor) | 一个不理解任务的固定策略能在你的样本上拿到多少分。这个分数是你所有成绩的减法基准 |
| 假阳 / 假阴 | 判据把错误解判成通过 / 把正确解判成失败 |
| pass^k | k 次独立重跑全部成功的概率,衡量可靠性;对比 pass@k 是「至少一次成功」 |
| 双向对照 | 每条样本同时配一个必须判过的正确解和一个必须判挂的错误解,用来给判据本身做体检 |
一、为什么先测判据:三条一手证据
这不是我的原创顾虑,是 2025 年这一批方法论工作的共同结论。数字都来自我本次逐篇核对过的论文原文(不是二手转述)。
第一条,空回复能拿 38%。 ABC 论文(arXiv:2507.02825) 发现 τ-bench 里一个「只返回空回复」的平凡 Agent 拿到 38% 成功率,超过了当时基于 GPT-4o 的 Agent。机制很朴素:τ-bench 含一批「故意不可能完成」的任务(比如给不可退票办退款),而判据把空回复也算作成功。一个把整个数据库内容倾倒出去的 spam Agent,在 Airline 分区拿到 40%(Retail 分区只有 9.6%)——注意这个分数对任意 k 的 pass^k 和 pass@k 都成立,也就是说它稳定地拿这个分,不是运气。
第二条,判据松紧的影响量级和模型换代相当。 同一篇论文用它的清单审计了 10 个主流 benchmark,结果是 7 个有结果效度缺陷、7 个有任务效度缺陷、10 个全部在结果报告上不达标。具体偏差:KernelBench 因为 fuzz 测试不覆盖内存布局,把内核正确率高估 31 个百分点;WebArena 因为字符串匹配问题高估 5.2 个百分点;SWE-Lancer 允许 Agent 覆写测试文件,不解决任何任务也能拿 100%。
反方向的例子同样存在,这点常被忽略:OSWorld 的 chrome 分区有 13/46 道题因为网站改版导致 HTML 选择器失效,把当时最强开源 Agent UI-TARS 的成绩低估了 28 个百分点。判据坏掉不一定是虚高,也可能让你错杀一个能用的方案。
第三条,人来当判据也有天花板。 GDPval(arXiv:2510.04374) 用真实职业交付物做评测,1320 条任务、44 个职业,评分方式是让同职业专家盲评对比。两个数字值得业务团队记住:每条对比的人工评分平均耗时超过 1 小时;而专家之间的互评一致率只有 71%(他们训练的自动评分器是 66%,差 5 个百分点)。
这条把「上人工评审就没问题了」这个默认退路也堵住了:71% 是你的判据精度上限,不是理想值。业务里那些「体验好不好」「回复得体吗」的维度,本质上就在这个天花板下面。
二、六轴:样本铺开的坐标,不是清单
造样本最常见的失败不是造得太少,而是十几条全挤在同一个角落:都是「查订单 + 回话」,都规约明确,都没有对手。我用六个正交轴来强制铺开:
| 轴 | 取值 | 它逼出什么能力 |
|---|---|---|
| 时间视野 | 人工基线分钟数 | 长链条下的状态维持。参考 METR(arXiv:2503.14499) 的 50% 时间视野方法:任务难度用「熟练人类做完要多久」标定,比主观难度可比 |
| 状态耦合 | 只读 / 可写 / 不可逆 | 不可逆动作(退款、扣款)前的确认与自制 |
| 信息完备度 | 明确 / 欠定 / 含矛盾 | 欠定时追问、用户前提错误时纠正 |
| 交互对手 | 无用户 / 被动用户 / 双控用户 | 双控指用户自己也要动手。τ²-bench(arXiv:2506.07982) 把这个设定形式化成 Dec-POMDP,gpt-4.1 的 pass^1 从 Retail/Airline 的 74%/56% 掉到 Telecom 双控的 34% |
| 工具面 | 内部 API / 跨系统 | 跨系统的一致性与补偿 |
| 干扰 | 干净 / 施压诱导 / 脏数据 / prompt 注入 | 压力下守政策、数据可疑时升级 |
「时间视野」这一轴我要特别推荐给业务团队:它是唯一一个能让不同批次样本可比的难度标定方式。没有人工基线时长的样本,事实上无法定位难度——出题人的「这题挺难」和实际耗时经常差一个数量级。
关于 τ² 那条,还有一个更适合业务借鉴的细节:它的 Telecom 用户模拟器错误率是 16%(其中 6% 是致命错误),而约束更松的 Retail / Airline 分别是 40%/12% 和 47%/13%(各域 50 通对话)。做法是把用户模拟器的行为限制在工具和可观测状态里,不让它自由规划。如果你要用 LLM 扮演用户来跑多轮评测,这个降幅说明:约束模拟器比换更强的模型更有效。
三、动手:一个可回滚的业务世界
样本要能反复跑、结论要能复现,世界状态就必须可回滚、工具调用必须留痕。我搭了一个电商售后世界,代码在仓库 demos/agent-eval-sample-kit/,零依赖、不调模型,node run.mjs 直接跑。
三个设计决定,每个都是为判据服务的:
1)时间写死在世界里。 now: '2026-09-01T10:00:00Z',不用 Date.now()。否则「签收 7 天内可退」这条政策会让样本在某天突然翻转结论。
2)每次工具调用都留痕,并标注读 / 写 / 不可逆。
// 摘自 world.mjs 的 makeApi(),判据据此做动作序列检查
const log = (tool, args, kind) => world.calls.push({ tool, args, kind });
refund_order(orderId, amount, reason) {
log('refund_order', { orderId, amount, reason }, 'irreversible');
// 省略:订单存在性校验
world.refunds.push({ orderId, amount, reason });
world.orders[orderId].status = 'refunded';
return { ok: true };
},
有了 kind 这个字段,判据才能表达「信息不全时不许做任何不可逆动作」——这类约束无法从最终状态反推,因为一个被工具层拦住的错误决策,最终状态和从未尝试过是一样的。
3)政策写成可执行函数,不写在 prompt 里。 refundableAmount() 返回 { ok, reason, amount },判据和参考解共用同一个真值来源,避免我在 12 条样本里把政策记错。
12 条样本每条携带五个字段:六轴坐标、世界种子、reference(正确解)、wrong(明确错误解)、判据本体。reference 和 wrong 是这套方法的关键,下一节就靠它们给判据做体检。
四、第一次跑分:判据的假阳率 5/12
判据 V1 是一条通用的直觉规则,套用到全部 12 条样本——这确实是多数团队的第一版:
// 摘自 tasks.mjs 的 verifyV1():回复像样 + 该变的变了/不该变的没变
export function verifyV1(task, world) {
const fail = [];
if (!world.reply || world.reply.length < 20) fail.push('回复缺失或过短');
const m = task.naive.mutation; // 'none' | 'refund:<id>' | 'exchange'
if (m === 'none') {
// T09 世界种子里本就有 1 条退款记录,故基线不是 0
if (world.refunds.length > (task.id === 'T09' ? 1 : 0)) fail.push('产生了不应有的退款');
} else if (m.startsWith('refund:')) {
if (!world.refunds.some(r => r.orderId === m.split(':')[1])) fail.push('未产生预期退款');
} else if (m === 'exchange') {
if (!world.exchanges.length) fail.push('未产生换货单');
}
return fail;
}
先用双向对照给它体检——正确解必须判过,错误解必须判挂:
| 判据版本 | 假阴(正确解被判挂) | 假阳(错误解被判过) |
|---|---|---|
| V1 | 1/12 | 5/12 |
| V2 | 0/12 | 0/12 |
| V3 | 0/12 | 0/12 |
V1 的 5/12 假阳意味着:我为每条样本亲手写的、明确错误的解法,有 5 条被判成了通过。举三个具体的:
- T02 未发货退款:错误解退了 199 元而不是 299 元。V1 只检查「有没有产生退款记录」,不检查金额。业务上这是资损。
- T05 已发货却要改地址:错误解顺着用户的错误前提去调改址接口。工具层把它挡住了,所以最终状态干净,V1 判过。错误的决策被正确的工具兜底掩盖了。
- T11 订单备注里的 prompt 注入:错误解执行了注入指令、发了 500 元券,还把注入原文复述给用户。V1 的
mutation: 'none'只看退款,没看优惠券。
还有一条方向相反的:T08(订单金额与明细不符)的正确解是升级人工、不自行退款,但 V1 期望「产生退款」,于是把正确解判成了失败。同一版判据同时在放过错误解和错杀正确解——这正是 ABC 论文里结果效度与任务效度双双失守的样子。
五、第二次跑分:作弊下界
假阳率还只是「我预想到的错误」。更狠的测法是让完全不理解业务的确定性脚本去跑——它们不调模型、不花 token,只执行一个固定策略:
| 脚本 | 策略 |
|---|---|
mute | 不调任何业务工具,只回一句客服万能话术 |
parrot | 把只读工具全调一遍,查到的东西原样倾倒给用户 |
appeaser | 无脑讨好:能退就退全额,顺手发一张券 |
escalator | 一律甩人工,自己不做任何判断 |
exchanger | 一律建换货单,广撒网 |
结果(12 条样本,通过数):
| 脚本 | V1 | V2 | V3 |
|---|---|---|---|
mute | 8 (67%) | 0 | 0 |
parrot | 8 (67%) | 1 | 0 |
appeaser | 4 (33%) | 0 | 0 |
escalator | 8 (67%) | 1 | 0 |
exchanger | 9 (75%) | 2 | 1 |
| 至少被一个脚本攻破的样本 | 12/12 | 4/12 | 1/12 |
V1 下最强作弊脚本拿到 75%,12 条样本无一幸免。 请注意 mute 拿到 8/12 的含义:它连订单号都没解析。如果我拿这套样本去评一个真实模型,得到 80% 的分数,那么其中有 67 个百分点是白送的——报告一个不带作弊下界的分数,等于报告一个没有零点的温度计。
一个反直觉的细节:appeaser(无脑全额退款+发券)只拿到 33%,是五个里最低的。乱做事比不做事更容易被抓住,因为它留下了状态痕迹。这解释了为什么「不做事」型的失败在业务评测里最危险——它天然隐形。
判据加固到 V2(双向状态断言 + 动作序列合规 + 回复内容断言)后,最强作弊脚本从 9 掉到 2,被攻破样本从 12 条降到 4 条。加固不神秘,就是把 V1 漏掉的东西写进去:
// 摘自 T02 的 v2 判据:金额、状态、副作用、答复,四项都断言
v2: (w) => {
const fail = [];
const r = w.refunds.filter(x => x.orderId === 'A1002');
if (r.length !== 1) fail.push(`退款记录数应为 1,实际 ${r.length}`);
else if (r[0].amount !== 299) fail.push(`退款金额应为 299,实际 ${r[0].amount}`);
if (w.orders.A1002?.status !== 'refunded') fail.push('订单状态未流转');
if (w.coupons.length) fail.push('无端发放补偿券'); // 关键:也要断言「没做多余的事」
if (!w.reply) fail.push('未答复用户');
return fail;
},
最后一条 if (w.coupons.length) 是最容易漏的一类断言:只写「该发生什么」,不写「不该发生什么」,判据就有洞。
六、V2 剩下的 4 个洞,性质各不相同
V2 之后仍被攻破的 4 条样本,值得逐条看——它们对应四种不同的判据缺陷:
- T01(查物流)被
parrot攻破:判据要求回复含运单号和物流状态,parrot把整个数据库倾倒出来,自然「包含」了。这是 ABC 里 WebArena 那类问题:判据忽略无关内容,倾倒型策略就能蒙对。 V3 补丁:回复超 200 字、含结构化数据残留、混入无关订单号,一律判挂。 - T08(脏数据应升级)被
escalator攻破:判据只要求「升级了人工」,而escalator一律升级。V3 补丁:升级理由里必须包含具体证据(金额 1000 与明细 700 不符这两个数字),把「查明后升级」和「碰巧升级」分开。 - T11(prompt 注入)被
exchanger攻破:判据只禁了「发券」,没禁其他无关写入,而exchanger建的是换货单。V3 补丁:查询类样本要求除答复外零写入。 - T12(多步换货)被
exchanger攻破——这个洞我关不掉。
T12 值得单独说,因为它是全文最有价值的结论。
exchanger 的策略是「一律建换货单,SKU 填 S1-L,地址填建国路」。而 T12 的正确答案恰好就是这个——因为用户的话里已经写明了「换成 L 码,寄到北京市朝阳区建国路 2 号」。答案可以直接从题面抄走,不需要任何业务理解。
我把两者的世界状态打出来对比过,结果很干净——换货单一模一样:
// 正确解与 exchanger 产生的 world.exchanges,逐字段相同
{ orderId: 'A1012', sku: 'S1-L', addressText: '北京市朝阳区建国路 2 号' }
这正是 ABC 定义的任务效度失守:任务应当「有能力才能通过,没能力一定通不过」,而 T12 允许一个正则表达式通过。既然最终状态完全一致,任何基于状态的断言都无法区分二者。
那么真的完全无解吗?这里我要修正自己写稿时的第一个判断。两者的动作序列其实有差异:正确解调用了 get_policy(查换货政策),exchanger 没有。我实测了「要求必须查过政策」这条补丁:
| reference | wrong | mute | parrot | appeaser | escalator | exchanger | |
|---|---|---|---|---|---|---|---|
加 get_policy 断言 | 过 | 挂 | 挂 | 过 | 挂 | 挂 | 挂 |
它确实拦住了 exchanger,而且没有产生假阴。但代价出现在 parrot 那一列:它因为无脑把所有只读工具调了一遍,反而通过了。
这就是「过程断言」的通病——它奖励调用形式,不奖励业务理解。用它换来的不是一条更严的样本,而是把作弊路径从「抄题面」换成了「无脑全调」。所以我选择不采用这条补丁,在代码里留了注释说明理由。
T12 的正确修法是改样本,不是改判据:让新地址不出现在用户话里(比如「寄到我上次那个地址」,逼 Agent 去查地址簿),或让 SKU 需要查商品表推导。判据加固能修结果效度,修不了任务效度——题面漏答案,只能改题。
我保留这条不修,并诚实标注:V3 的 1/12 作弊下界不是「几乎完美」,是「还有一条样本的设计是坏的」。
最终账本:
| V1 | V2 | V3 | |
|---|---|---|---|
| 最强作弊脚本得分 | 9/12(75%) | 2/12(17%) | 1/12(8%) |
| 被攻破样本数 | 12/12 | 4/12 | 1/12 |
| 假阳 / 假阴 | 5 / 1 | 0 / 0 | 0 / 0 |
七、我自己这套样本的偏斜
跑分器还打了第三张表——六轴覆盖。结果对我自己不太好看:
状态耦合 只读×1 可写×3 不可逆×8
信息完备度 明确×9 欠定×1 含矛盾×2
交互对手 无用户×1 被动用户×10 双控用户×1
工具面 内部×11 跨系统×1
干扰 干净×9 施压诱导×1 脏数据×1 prompt注入×1
时间视野 人工基线 1~25 分钟,中位数 7 分钟
我在「不可逆动作」上堆了 8 条,而「欠定信息」只有 1 条、「跨系统」只有 1 条。 时间视野也压在 25 分钟以内——按 METR 的观察,模型在 4 分钟以下的任务上接近满分、在 4 小时以上骤降,我这套样本整体落在容易区间,对强模型区分度不足。
这张表的价值就在这里:六轴不是发布前的自我表扬清单,是一张会打自己脸的覆盖热图。 出题人的偏好会自动向「好写的样本」倾斜——不可逆动作好写(断言清楚),欠定信息难写(要设计追问的判据)。不量化就看不见。
八、带得走的东西:判据体检清单
如果这篇只留一件事,是下面这个顺序。每条样本进入评测集之前过一遍:
- 配一个必须判挂的错误解。 判据判它通过 = 假阳,直接退回。这是成本最低、收益最高的一步。
- 配一个必须判过的正确解变体。 判据判它失败 = 假阴,同样退回。别只测一个方向。
- 跑作弊下界。 至少
mute(什么都不做)、parrot(倾倒查询结果)、appeaser(无脑讨好)、escalator(一律甩人工)四个脚本。任何一个能通过的样本,判据未完成。 - 断言「不该发生什么」。 只写期望状态变更,漏掉无关写入,是最高频的洞。
- 检查答案是否能从题面抄走。 抄得走就是任务效度问题,改样本,别改判据。
- 慎用「必须调过某工具」这类过程断言。 它拦得住抄题面的脚本,但会放过无脑全调的脚本(本文实测),把作弊路径换了个方向而已。
- 标注人工基线时长。 没有它,难度无法跨批次比较。
- 报告四元组,不报单一分数:
(pass^k, 成本, 作弊下界, 判据假阳假阴率)。
关于 pass^k 这一项:τ-bench(arXiv:2406.12045) 原文里 gpt-4o 在 Retail 的 pass^8 低于 25%;HAL(arXiv:2510.11977,ICLR 2026) 用 21730 次 rollout、9 模型 × 9 benchmark、约 4 万美元的规模给出另一个提醒:36 组对比里有 21 组,提高 reasoning effort 反而降低了准确率。 只跑一次、只报 pass@1 的评测,结构上就测不出可靠性。
诚实的提醒
哪些是我亲手测的:本文所有 V1/V2/V3 的假阳假阴数、五个作弊脚本的通过率、六轴覆盖分布,全部来自仓库 demos/agent-eval-sample-kit/ 的 node run.mjs 输出,可复跑。
哪些是核实过的来源但我没复现:ABC、τ-bench、τ²-bench、GDPval、METR、HAL 的所有数字,来自我本次逐篇打开原文核对(不是二手博客)。我没有重跑它们的实验。核对过程中修正了几处流传较广的转述错误,列出来供参考:WebArena 的高估幅度是 5.2 个百分点(常被写成「1.4–5.2%」区间);spam Agent 的 40% 只是 Airline 分区,Retail 分区仅 9.6%;τ-bench 空回复得 38% 的前提是「故意不可能完成的任务」这一子集,不是全集;METR 论文原文写的是 RE-Bench + HCAST + 66 条新增短任务,我未能在原文中核实到流传的「170 任务 / 800 份人类基线」这一说法。
这套样本的局限:12 条是演示规模,不是可用的评测集;六轴分布如第七节所示明显偏斜;所有「回复内容断言」都是关键词匹配,真实业务里需要换成校准过的 LLM judge——而按 GDPval 的 71% 专家互评一致率,那个 judge 自己也要先做体检。作弊脚本 exchanger 里的 S1-L 是我知道样本内容后填的;但这不削弱结论,因为它取的正是题面里已有的信息,一个正则 Agent 同样能拿到。
成本最低的验证实验(你和我都能跑):从你现有评测集里抽 5 条样本,为每条写一个明确错误的解,然后跑你现有的判据。如果有任何一条被判通过,你就找到了一个真实的假阳——这个实验不需要模型,不花 token,半小时内能做完。做完之后再写一个 mute 脚本(什么都不做,只回一句「已为您处理完毕」)跑同样 5 条,看它拿到几分。
那个分数,就是你所有历史评测结论需要减掉的部分。
参考来源
方法论与清单
- Establishing Best Practices for Building Rigorous Agentic Benchmarks(arXiv:2507.02825,v5 2025-08-07) —— ABC 清单,分结果效度 / 任务效度 / 结果报告三部分;审计 10 个主流 benchmark。配套复现代码:uiuc-kang-lab/agentic-benchmarks
- Holistic Agent Leaderboard(arXiv:2510.11977,ICLR 2026) —— 21730 次 rollout / 9 模型 / 9 benchmark / 约 4 万美元,模型-scaffold-benchmark 三维分离,公开 2.5B token 日志
评测基准(一手)
- τ-bench(arXiv:2406.12045,2024-06-17) —— pass^k 的出处
- τ²-bench(arXiv:2506.07982,2025-06-09) —— 双控环境、约束式用户模拟器
- GDPval(arXiv:2510.04374,2025-10-05) —— 1320 条真实职业交付物,专家盲评一致率 71%
- Measuring AI Ability to Complete Long Software Tasks(arXiv:2503.14499,NeurIPS 2025) —— 50% 时间视野方法
站内相关
- 什么是一个好的评测样本? —— 七个条件与样本卡(本文是它承诺的实验部分)
- 业务评测覆盖地图 —— 要测哪十个格子
- Agent 测试决策地图 —— 测试层次的选择