先测判据,再测 Agent:业务评测样本的构造方法与一次作弊下界实测

我按六轴坐标亲手造了 12 条电商售后样本和三代判据,让 5 个不理解业务的确定性作弊脚本去跑:第一版判据被拿到 9/12(75%),12 条样本全部被攻破,加固后降到 1/12。剩下那条是任务效度问题——题面漏了答案,改判据无解。附可复跑代码与判据体检清单。

我造了 12 条电商售后的业务评测样本,写了一版看起来很合理的判据,然后写了 5 个完全不理解业务、连订单都不查的脚本去跑它。最强的那个拿到了 9/12(75%),12 条样本全部至少被一个脚本攻破。

这不是判据写得潦草——它检查了「回复是否像样」和「该变的状态有没有变」,也就是多数团队上线时用的那一版。问题在于我从头到尾没有测过判据本身。

《什么是一个好的评测样本?》 给了七个条件和一张样本卡,但结尾自己声明了一句:「本文给的是设计与审核框架,不是一套被我重新跑过的 benchmark 实验」。这篇就是补上那次实验。上游的《业务评测覆盖地图》 回答「要测哪些格子」,本文回答一个更靠后、也更容易被跳过的问题:格子里那条样本,它的判据自己可信吗?

一句话主线:在业务评测里,判据不是量 Agent 的尺子,判据本身是第一个被测对象。 顺序错了,后面所有分数都在浮沙上。

名词速查

术语一句话解释
判据(verifier / oracle)判定一次任务算成功还是失败的那段代码或规则
outcome validity(结果效度)判据说「过了」的时候,任务是真的完成了
task validity(任务效度)只有真正具备目标能力才能通过;不具备就一定通不过
作弊下界(cheat floor)一个不理解任务的固定策略能在你的样本上拿到多少分。这个分数是你所有成绩的减法基准
假阳 / 假阴判据把错误解判成通过 / 把正确解判成失败
pass^kk 次独立重跑全部成功的概率,衡量可靠性;对比 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(明确错误解)、判据本体。referencewrong 是这套方法的关键,下一节就靠它们给判据做体检。

四、第一次跑分:判据的假阳率 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;
}

先用双向对照给它体检——正确解必须判过,错误解必须判挂

判据版本假阴(正确解被判挂)假阳(错误解被判过)
V11/125/12
V20/120/12
V30/120/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 条样本,通过数):

脚本V1V2V3
mute8 (67%)00
parrot8 (67%)10
appeaser4 (33%)00
escalator8 (67%)10
exchanger9 (75%)21
至少被一个脚本攻破的样本12/124/121/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 没有。我实测了「要求必须查过政策」这条补丁:

referencewrongmuteparrotappeaserescalatorexchanger
get_policy 断言

它确实拦住了 exchanger,而且没有产生假阴。但代价出现在 parrot 那一列:它因为无脑把所有只读工具调了一遍,反而通过了。

这就是「过程断言」的通病——它奖励调用形式,不奖励业务理解。用它换来的不是一条更严的样本,而是把作弊路径从「抄题面」换成了「无脑全调」。所以我选择不采用这条补丁,在代码里留了注释说明理由。

T12 的正确修法是改样本,不是改判据:让新地址不出现在用户话里(比如「寄到我上次那个地址」,逼 Agent 去查地址簿),或让 SKU 需要查商品表推导。判据加固能修结果效度,修不了任务效度——题面漏答案,只能改题。

我保留这条不修,并诚实标注:V3 的 1/12 作弊下界不是「几乎完美」,是「还有一条样本的设计是坏的」。

最终账本:

V1V2V3
最强作弊脚本得分9/12(75%)2/12(17%)1/12(8%)
被攻破样本数12/124/121/12
假阳 / 假阴5 / 10 / 00 / 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 小时以上骤降,我这套样本整体落在容易区间,对强模型区分度不足。

这张表的价值就在这里:六轴不是发布前的自我表扬清单,是一张会打自己脸的覆盖热图。 出题人的偏好会自动向「好写的样本」倾斜——不可逆动作好写(断言清楚),欠定信息难写(要设计追问的判据)。不量化就看不见。

八、带得走的东西:判据体检清单

如果这篇只留一件事,是下面这个顺序。每条样本进入评测集之前过一遍:

  1. 配一个必须判挂的错误解。 判据判它通过 = 假阳,直接退回。这是成本最低、收益最高的一步。
  2. 配一个必须判过的正确解变体。 判据判它失败 = 假阴,同样退回。别只测一个方向。
  3. 跑作弊下界。 至少 mute(什么都不做)、parrot(倾倒查询结果)、appeaser(无脑讨好)、escalator(一律甩人工)四个脚本。任何一个能通过的样本,判据未完成。
  4. 断言「不该发生什么」。 只写期望状态变更,漏掉无关写入,是最高频的洞。
  5. 检查答案是否能从题面抄走。 抄得走就是任务效度问题,改样本,别改判据。
  6. 慎用「必须调过某工具」这类过程断言。 它拦得住抄题面的脚本,但会放过无脑全调的脚本(本文实测),把作弊路径换了个方向而已。
  7. 标注人工基线时长。 没有它,难度无法跨批次比较。
  8. 报告四元组,不报单一分数(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 条,看它拿到几分。

那个分数,就是你所有历史评测结论需要减掉的部分。

参考来源

方法论与清单

评测基准(一手)

站内相关