// WHY THIS MATTERS
选型时你盯着 SWE-bench、MMLU、Arena 排名,但上线后 agent 在你的场景里翻车——这不是偶发,是结构性的。公开 benchmark 测的是「在一个被无数次刷过、且很可能进过预训练语料的固定集合上的平均分」,而你要的是「在我的真实输入分布上、多轮交互里、连续 10 次都不出错」。这两者之间隔着污染、饱和、construct gap 三道鸿沟。Day 6 讲的是 eval 的机械工程(golden set、judge 去偏、回归测试);这一期讲更上游的判断:为什么公开榜单系统性骗你、怎么从生产 trace 反向造出可信 eval、为什么聚合分数是最危险的指标、以及 agent 时代「评估轨迹而非答案」的范式转移。结论先行:能用的 eval 一定是你自己场景的、切片的、可被对抗的。
// 01
公开 Benchmark 的三宗罪:污染、饱和、Construct Gap
论断:排行榜分数系统性高估真实能力——它测的根本不是你部署时面对的那个分布。
背景与原理
三个机制让公开分数和生产可靠性脱钩:
- 污染(Contamination):MMLU、HumanEval 这种存在多年的公开集,几乎必然以某种形式进过新模型的预训练 / 后训练语料。更隐蔽的是 SWE-bench——对 Verified 子集的分析发现,约三分之一的 issue 把修复方案直接写在了 issue 描述或评论里,模型「抄」而不是「解」,分数因此虚高。
- 饱和(Saturation):顶部模型挤在 88%–92% 这种天花板区间,benchmark 失去区分度——分数差落进噪声,你无法据此判断 A 比 B 在你的场景更好。
- Construct Gap:benchmark 测的是 target 的代理,不是 target 本身。HumanEval 测「补全一个独立函数」,你的活是「在 50 万行 repo 里跨文件改一个 bug」。研究发现,把 SWE-bench 的长 GitHub issue 换成真实用户那种简短模糊的提问,同一模型相对成功率掉 20–40%。
底层是 Goodhart 定律:一个指标一旦成为优化目标,就不再是好指标。整个行业在朝公开 benchmark 过拟合,分数涨而真实能力的相关性逐年衰减。
公开 Benchmark 分数 ─────────────────▶ 你的生产可靠性
88% ?
┌──────────────────── 三道泄漏 ────────────────────┐
│ ① 污染:答案进过训练语料 / 写在 issue 里 │ −分数虚高
│ ② 饱和:顶部挤成一团,落进噪声,无区分度 │ −选型失效
│ ③ Construct Gap:测代理任务 ≠ 你的真实任务分布 │ −20~40% 掉幅
└──────────────────────────────────────────────────┘
▼
榜单 #1 ≠ 你场景里最可靠的那个
实战示例
把「能不能信这个 benchmark」做成一张 30 秒诊断表,贴在选型文档里:
# 看到一个亮眼分数,先问这 4 个问题
1. 这个集合公开多久了? > 1 年 → 大概率污染,分数打折
2. 顶部模型分数差多少? < 3pp → 落进噪声,别据此选型
3. 任务形态像我的吗? 单函数/选择题 ≠ 多轮 agent / 长 repo
4. 评分 oracle 严吗? 弱测试集会放过「看起来对」的错答
# 任何一条不过关 → 这个分数不能用来做生产决策
失败模式:拿 benchmark 榜单直接拍板选型并上生产。正确顺序是——用公开榜单做粗筛(排除明显弱的),用你自己的 eval 做终选。公开分数只回答「这个模型大致在什么段位」,永远回答不了「它在我的任务上够不够可靠」。
进阶资源 · SWE-bench (Jimenez et al. 2024, arXiv:2310.06770),
arxiv.org/abs/2310.06770 ·
HumanEval / Codex (Chen et al. 2021)、MMLU (Hendrycks et al. 2021) 原文
// 02
从生产 Trace 反向造 Eval Set
论断:唯一可信的 eval 集来自你自己的真实流量——不是 benchmark,不是凭空合成的样例。
背景与原理
Hamel Husain 在《Your AI Product Needs Evals》里反复说的一件事:失败的 AI 产品几乎都败在没有评估体系,而能用的评估体系起点小得惊人——几十条真实样例 + 一个 scorer 就能跑。关键不是「多」,是分布匹配:你的 eval 输入要长得像生产输入,否则分数再高也是自欺。
方法叫 error analysis / open coding:抽一批真实 trace,逐条人工读输出、给失败贴标签(不是先定 rubric,是先看数据再归纳 failure mode)。EvalGen 论文(Shankar et al. 2024)把这种现象命名为 criteria drift——你需要标准来打分,但恰恰是打分的过程帮你定义标准。所以 eval 不是写一次的静态文档,是和数据共同演化的。
实战示例
# 从生产日志反向造 eval set 的最小 pipeline
import json, random
# 1) 分层采样:别只抽成功的,按 failure 信号过采样
traces = load_prod_traces("2026-05")
hard = [t for t in traces if t.thumbs_down or t.retried or t.fallback]
sample = random.sample(traces, 60) + random.sample(hard, 40) # 故意偏向难例
# 2) open coding:人工读 + 贴 failure 标签(先看数据,后定 rubric)
# 例:wrong_tool / hallucinated_field / ignored_constraint / too_verbose
# 3) 冻结成带 ground-truth 的 eval 集,存进版本库
for t in sample:
eval_set.append({"input": t.input, "context": t.context,
"expected": human_label(t), # 你标的,不是模型标的
"slice": t.category}) # ← §3 切片要用
json.dump(eval_set, open("evals/v1.json", "w"))
注意第 1 步的过采样难例:生产里 95% 是简单请求,若按自然分布抽,eval 会被简单样例稀释,掩盖关键失败。eval 集要刻意偏向你最怕出错的地方。
失败模式:纯靠 LLM 合成 eval 数据。合成集和生产分布有系统性 gap(措辞更规整、歧义更少、长尾缺失),在它上面调到 95% 上线照样崩。合成数据可以补充长尾覆盖,但骨架必须是真实 trace。
// 03
Slice-Based Eval:聚合分数会骗你
论断:整体准确率 90% 可能掩盖一个关键子群体只有 40%——看聚合分数等于把眼睛蒙上。
背景与原理
这是真实场景 eval 里最反直觉、也最值钱的一条。一个「90% 通过」的 agent,可能在「退款类请求」上是 95%、在「改地址类请求」上只有 45%——而改地址恰好是你最高频或最高风险的场景。聚合分数把这种分布内的灾难平均没了。
解法是 slice-based evaluation:按有业务意义的维度切片,分别报告每个切片的分数,盯住最差的那个而不是平均的那个。切片维度通常来自 §2 open coding 出的 failure mode,或者业务类别、输入长度、语言、用户分群。这也是对抗 Goodhart 的天然手段——你优化聚合分时很容易牺牲一个小切片去换大盘,分切片就让这种交易无所遁形。
实战示例
from collections import defaultdict
def sliced_report(eval_set, run_agent, scorer):
buckets = defaultdict(lambda: [0, 0]) # slice -> [pass, total]
for ex in eval_set:
ok = scorer(run_agent(ex), ex["expected"])
b = buckets[ex["slice"]]
b[0] += int(ok); b[1] += 1
# 关键:按分数升序打印,最差的切片顶在最上面
rows = sorted(((s, p/n, n) for s,(p,n) in buckets.items()),
key=lambda r: r[1])
for s, acc, n in rows:
flag = " ⚠️ 门槛线下" if acc < 0.8 else ""
print(f"{s:<22} {acc:5.0%} (n={n}){flag}")
# 输出示例:聚合 90%,但最差切片才 45% —— 这才是该修的地方
# change_address 45% (n=20) ⚠️ 门槛线下
# multi_intent 63% (n=15) ⚠️ 门槛线下
# refund 95% (n=30)
# ── aggregate ── 90%
上线门槛应该挂在最差切片上(「每个切片 ≥ 80% 才放行」),而不是聚合均值。一个被聚合掩盖的 45% 切片,往往就是上线后用户投诉的源头。
失败模式:(1)只追踪聚合分,模型迭代时聚合涨了 2pp,但某个小切片悄悄塌了 20pp,你完全看不见——直到用户来骂。(2)切片切太细,每片 n=3,分数全是噪声。切片粒度要让每片至少有十几条样本才有统计意义。
// 04
Agentic Eval:评估轨迹而非答案
论断:单轮 benchmark 测不了 agent。Agent 时代的指标是多轮一致性和轨迹质量,不是 pass@1 的最终答案。
背景与原理
传统 benchmark 一问一答测一次;真实 agent 是多轮、调工具、和(可能误导的)用户来回交互的轨迹。两个范式转移:
- 从 outcome 到 trajectory:只看最终答案对不对,会放过「答案蒙对了但工具调用顺序全错」的脆弱轨迹。要评估中间步骤——调了哪些工具、有没有违反 policy、有没有多余动作。
- 从 pass@1 到 pass^k(一致性):τ-bench(Yao et al. 2024)提出 pass^k——同一任务连续跑 k 次全部成功的概率。论文里 SOTA 函数调用 agent 单任务成功率已不到 50%,而 retail 域 pass^8 跌破 25%。这个落差就是「demo 能跑」和「生产可靠」之间的鸿沟。它的评分方式也很硬核:不看对话措辞,比对最终数据库状态和标注目标状态是否一致。
对个人 / 小团队,照搬 τ-bench 的两个思想就够:用 state-based 评分(验证副作用,不验证措辞)+ pass^k 一致性(同任务多跑几次看稳不稳)。
单轮 benchmark Agentic eval(轨迹)
┌───────────┐ ┌──────────────────────────────────┐
│ Q ─▶ A ? │ │ 多轮:U⇄Agent,调工具,可能被误导 │
│ pass@1 │ │ ├ 轨迹检查:tool 序列 / policy 合规 │
└───────────┘ │ ├ 状态检查:最终 DB == 目标 state? │
测一次 │ └ 一致性:pass^k(连跑 k 次全过) │
蒙对也算赢 └──────────────────────────────────┘
实战示例
def pass_hat_k(task, run_agent, check_final_state, k=8):
# pass^k:连续 k 次「全部」成功才算稳;任一次失败即 False
return all(check_final_state(run_agent(task)) for _ in range(k))
def check_final_state(result):
# state-based:只验证副作用,不在乎它话怎么说
return (db.get("order#123").address == "new addr"
and not charged_extra_fee()) # 还要验证「没干坏事」
# 轨迹合规检查:流程对不对,不止结果对不对
def check_trajectory(trace):
if "refund" in trace.tools_called and not trace.asked_confirmation:
return "VIOLATION: 退款前没走确认" # policy 违规
return "ok"
对 agent,「跑一次成功」毫无意义——pass^8 才是上线信号。一个 pass@1=80% 但 pass^8=30% 的 agent,意味着平均每三个用户就有两个会在某一轮撞上失败。
失败模式:(1)只判最终答案,不查轨迹——agent「歪打正着」(绕过确认却恰好没出事)被记成成功,上线后必爆。(2)只跑一次。LLM 有随机性,单次成功不代表稳定;不测一致性,等于把 70% 的概率当 100% 报给老板。
// 综合实战 · 给你的 agent 配一套上线门禁
把四点串成一个能进 CI 的 eval gate,半天搭完:
- 粗筛:公开榜单只用来排除明显弱的模型,不做终选(§1)。
- 造集:从近一个月生产 trace 分层采样 100 条,过采样 thumbs-down / retry / fallback 的难例,人工标 ground truth + 切片标签,冻进
evals/v1.json(§2)。
- 切片:scorer 按切片出分,门槛挂在最差切片 ≥ 80%,不是聚合均值(§3)。
- 一致性:核心任务跑 pass^5,state-based 验副作用 + 轨迹查 policy 合规(§4)。
- 防过拟合:留一个从不用于调 prompt 的 held-out 切片,每月用新 trace 轮换 20%——一旦某个集合被你反复优化,它就被 Goodhart 了,得换血。
从此「能不能上线」是 pytest evals/ 输出的红绿,不是某人拍脑袋说「感觉还行」。这套门禁挡掉的每一次 regression,都是一次本会发生的线上事故。
// ENGLISH GLOSSARY
- Contamination
- 测试集(部分)进过训练语料,导致分数虚高。公开老 benchmark 几乎必有。
- Saturation
- 顶部模型分数挤在天花板,benchmark 失去区分度,无法据此选型。
- Construct Validity
- 指标是否真的测到了你想测的能力。benchmark 测代理任务 ≠ 你的真实任务。
- Goodhart's Law
- 「一个指标一旦成为目标,就不再是好指标」。整个行业对 benchmark 的过拟合。
- Slice-Based Eval
- 按业务维度切片分别报分,盯最差切片而非聚合均值。
- Open Coding / Error Analysis
- 先读真实数据、归纳 failure mode,再定 rubric 的 eval 构建法。
- Criteria Drift
- EvalGen 提出:打分过程本身会帮你重新定义评分标准,eval 与数据共演化。
- Trajectory Eval
- 评估 agent 的中间步骤(工具序列、policy 合规),而非只看最终答案。
- pass^k
- τ-bench 指标:同一任务连跑 k 次全部成功的概率,衡量可靠性而非单次能力。
- Held-out Set
- 从不用于调 prompt 的留出集,防止 eval 被过拟合(Goodhart)。
// 深入思考
如果公开 benchmark 系统性高估,为什么整个行业还在拿它打榜、还在涨分?
因为它解决的是协调问题而非测量问题:行业需要一个可比、可复现、零边际成本的公共坐标系来沟通「谁大概更强」,benchmark 满足这个需求,哪怕它测不准你的场景。问题出在使用方把「公共坐标系」误当「生产决策依据」。正确姿势是二分:benchmark 做行业级粗筛和趋势观察,你自己的 eval 做产品级终选。涨分本身也未必假——可能是真能力提升 + 过拟合各占一半,你无法从分数本身分离这两者,这正是为什么必须有自己的 held-out eval。
§3 说门槛要挂在最差切片。但切片可以无限细分,细到每片 n=1 时人人都「有切片不及格」。这个度怎么把握?
切片是统计单位,不是放大镜。每片至少要十几到几十条样本,分数才脱离噪声——n=3 的 33% 和 67% 没有区别。原则:切片维度要有业务可操作性(这个切片差,你知道该改什么)且样本量足够。无法同时满足时,合并相近切片。另一个信号:如果某切片只有 1-2 条样本却很重要,那不是 eval 切片问题,是你的 eval 集对该场景采样不足,回到 §2 去补样本,而不是在统计噪声上做决策。
pass^k 听起来很硬核,但它把一次 99% + 一次 50% 的任务都算「不稳」。会不会过于严苛、误杀实际可用的 agent?
pass^k 严苛是故意的——它对齐的是「用户体验的下界」而非平均。一个 pass@1=95% 的 agent,连用 8 次全顺的概率只有 0.95^8≈66%,意味着 1/3 的多步会话里至少撞一次失败。对话型产品里用户感知的是最差那次,不是平均。是否「误杀」取决于失败的代价:高代价场景(支付、医疗)就该用高 k;低代价、失败可廉价重试的场景,可以放宽到 pass@1 或 pass^2。k 是你对失败容忍度的旋钮,不是绝对真理。
用 LLM-as-judge 给轨迹打分,但 judge 本身也是会幻觉的 LLM。这是不是「让小偷看守金库」?
是真问题,所以 EvalGen 的核心论点正是「validate the validators」——judge 必须先和人工标注对齐再用。两条防线:1) 能用确定性 scorer 就别用 LLM——§4 的 state-based 检查(验数据库状态、验工具序列)是确定性的,比 LLM-judge 可靠得多,agentic eval 应尽量往这上面靠;2) 必须用 LLM-judge 时(如评「回答是否有帮助」这种主观维度),先在一批人工标注上测 judge 和人的一致率,达标才信,且定期重测。judge 是需要被 eval 的组件,不是免检的裁判。
held-out 集每月轮换 20% 防 Goodhart,但新换进来的样本未必和老的等难度,分数波动里混进了「集合难度变化」的噪声。怎么办?
这是 eval 工程的经典张力:稳定性(固定集合可比)vs 抗过拟合(轮换防 Goodhart)。实践上分两层:稳定核(一个长期不变的 anchor 集,用于看跨版本趋势,接受它会慢慢被过拟合)+轮换层(每月换血,用于抓 anchor 集掩盖不了的新 failure)。两个分数分开看:anchor 涨而轮换层不涨 = 在过拟合;两者齐涨 = 真进步。难度漂移可以用分层采样缓解(轮换时保持各切片比例不变),但无法完全消除——所以才需要 anchor 做相对基准,而不是只看绝对分。