9 评估:数据集、回归、置信区间、成本-延迟-质量
「准确率涨了 2 个点」到底算不算涨?48 条样本能分辨多大的差异?为什么同一个模型在公开题上 92%、在改写题上 61%?门禁该看一个分数还是一组带区间的差值?平均延迟降了 64 ms,为什么 P99 反而爆了?LLM 评委说 B 更好,这句话什么时候能信?
把评估对象拆成答案、轨迹、工具调用、成本四层;为一个评估集写出版本清单与污染检查;算准确率、pass@k、工具成功率、引用 precision/recall 与轨迹成功率;用 Wilson、bootstrap、配对检验给出差值区间与样本量;设计一个多条件门禁与灰度回滚;画出成本-延迟-质量的帕累托前沿;校准一个 LLM judge,并识别测试集泄漏、均值掩盖尾部与 Goodhart 三类失败。
第 8 章讨论多智能体时,反复出现一个判断:这次协作的收益是否大于协调成本。判断需要证据,而证据不能是「看起来更好」。这一章把「看起来」变成「可测量、可复现、可决策」:评估不是排行榜上的名次,而是发布前的一次假设检验,外加一张账单。
先说结论:评估是决策工具,不是排行榜。 一个没有样本量、没有置信区间、没有配对设计、没有成本与延迟约束的「提升」,在工程上等于噪声。理由很朴素:评估集是从任务分布里抽的样本,模型输出带采样方差,评分器本身也有误差。三者叠加之后,两个系统相差两三个点,可能只是同一枚硬币抛了两次。
这一章处在一个转折点上。第 1–8 章讲怎么把 agent 造出来:循环、工具、沙箱、上下文、记忆、规划、多智能体。从这一章起,问题变成「怎么知道它造对了,以及怎么在改坏时及时发现」。第 10 章把评估依赖的原始材料——trace——展开成可观测性体系;第 11、12 章把评估扩展到安全与治理。评估是它们的共同度量衡。
读图:从左到右是「数据集 → runner → 指标 → 统计 → 门禁」,从右往左是回滚路径。图里最值得记住的不是五个方框,而是权限:runner 与统计只产出证据,只有门禁有写权限(放行或拒绝),而门禁读的是一组带不确定性的差值——质量差值的置信区间下界、安全回归数、成本与 P95 延迟。任何「平均分涨了」就放行的流程,都跳过了这张图的后半段。
9.1 评估对象:答案、轨迹、工具调用与成本
一个 agent 跑完 30 步,最后给出一句「已修复」。这句话值多少分?只看这句话,你无法区分三种情况:它真的改了代码并跑过测试;它改了代码但测试超时,却把超时写成了通过;它什么都没改,只是学会了这句模板。三种情况在最终答案上完全一样,在轨迹、工具调用与成本上完全不同。
评估的对象不是一个字符串,而是一次运行的四个投影。四层各自回答不同问题,也各自有独立的失败模式。
| 层 | 回答的问题 | 典型指标 | 这一层特有的失败 |
|---|---|---|---|
| 最终答案 | 交付物对不对? | 准确率、exact match、pass@k、引用正确率 | 答案对但过程违规;答案错但格式漂亮 |
| 轨迹 | 它是怎么走到答案的? | 步数、工具选择准确率、顺序违规率、恢复率 | 绕路、重复、提前终止、计划漂移 |
| 工具调用 | 每一次动作是否合法、有效、可回滚? | 参数合法率、成功率、副作用次数、幂等冲突 | 调对了工具却作用在错对象;重试造成重复副作用 |
| 成本 | 这次运行花了多少资源? | 输入/输出 token、金额、P50/P95/P99 延迟、工具配额 | 平均值正常但尾部爆炸;重试风暴;缓存未命中 |
这张表说明什么:四层不是「越往下越细」,而是四种不同的验收。只验最终答案,等于只验考试成绩,不验实验操作;只验轨迹,会奖励「过程正确但结果错误」的表演;只验成本,会把「便宜但答错」当成优化。
一条评估记录是一个四元组
其中:
- 是任务实例,包含输入、可用工具集、权限范围与验收条件;
- 是最终答案或交付物,带结构化字段(文本、文件 diff、引用列表);
- 是轨迹,至少记录每步的动作、参数摘要、观察摘要、耗时与预算余额;
- 是成本向量, 是延迟。
一条记录只有在同时带上 dataset_version、system_version、seed 与 grader_version 时才可比较;缺任意一项,两次运行的差异就无法归因。
定义里最容易被省略的是 的「可用工具集与权限范围」。同一个问题,给 3 个工具和给 30 个工具,难度完全不同;只读权限与可写权限,风险也完全不同。评估集如果只存问题与标准答案,换一版工具注册表之后,旧分数就失去了意义。
一条可落盘的记录长这样:
{
"run_id": "run-20261019-0417",
"task_id": "repo-fix-0182",
"dataset_version": "eval-repo-v3:sha256=6f1c2a9d",
"system_version": {"model": "m-2026-09", "prompt_hash": "p-71ab", "tools": "t-12"},
"answer": {"status": "patched", "files": ["src/parser.py"], "tests": "pass"},
"trajectory": {"steps": 14, "tool_calls": 9, "replans": 1, "stop_reason": "done"},
"tools": {"calls": 9, "errors": 1, "side_effects": 2, "retries": 1},
"cost": {"in_tokens": 18240, "out_tokens": 2210, "money_usd": 0.041,
"latency_ms": 18400, "tool_quota": 9},
"verdict": {"correct": true, "trajectory_ok": true, "citation_ok": null,
"grader": "tests+v1", "evidence": ["pytest:42 passed"]}
}
这段 JSON 的重点在 verdict 的四个字段:correct 与 trajectory_ok 分开,citation_ok 允许为空(该任务不要求引用),evidence 指向可复核的证据。把「答案对」与「过程对」合成一个布尔值,会让 9.8 节的假成功无法定位。
9.1.1 最终答案层:正确、格式、拒答
最终答案层的指标看起来最简单,坑也最多。至少要把三件事分开:
- 内容正确:答案与标准答案或验收测试一致。代码任务用测试,抽取任务用字段比对,问答任务用 exact match 或人工 rubric。
- 格式合规:JSON 可解析、必填字段齐全、单位与类型正确。格式错误在自动化管线里等价于失败,但在人工阅读时可能被忽略。
- 拒答得当:证据不足时拒答是正确行为,不该计为错误;证据充足却拒答,才是失败。把这两类混在一起,会得到一个「越怂分越高」的指标。
一个常见做法是给每条任务标注 expected_behavior,取 answer、refuse、ask_clarification 之一,再分别统计。这样「该答的答对」「该拒的拒掉」各有分母,不会被一个总准确率掩盖。
9.1.2 轨迹层:步数、选择、顺序与恢复
轨迹层的第一个指标是步数,但它必须和成功率一起报。步数下降可能是效率提升,也可能是模型提前放弃;步数上升可能是更谨慎的验证,也可能是绕路。单独看步数没有方向。
更有判别力的是四个比率:
| 指标 | 定义 | 能抓到的失败 |
|---|---|---|
| 工具选择准确率 | 选中「gold 轨迹允许的工具」的步数 / 总步数 | 用 shell 代替专用工具;调用不存在的工具 |
| 顺序违规率 | 违反前置条件的步数 / 总步数 | 未读文件就写;未跑测试就提交 |
| 冗余动作率 | 动作与观察指纹重复的步数 / 总步数 | 原地打转;重复读同一文件 |
| 错误恢复率 | 出错后下一步改变策略并最终成功的 run 数 / 出错 run 数 | 一错就放弃;或无视错误继续 |
这四个指标都不需要「标准轨迹」完全一致。agent 可以走不同的路,只要不违反前置条件、不重复空转、能从错误里恢复。要求轨迹逐字匹配 gold,会把合理的探索判错,也会奖励背答案。
同一道题,两个工程师可能用不同顺序读文件、跑不同的测试,最后都修对了。你要评估的不是「他像不像我」,而是「他的每一步是否说得通、是否可复现、是否留下了证据」。轨迹指标应该抓违规与空转,而不是抓风格差异。
9.1.3 工具调用层:参数、成功与副作用
工具调用层要把三个概念分开,它们经常被一个「工具成功率」混掉:
- 调用成功率:返回
ok=true的比例。它只说明工具没报错,不说明作用对象正确。 - 参数正确率:参数与任务要求一致的比例。
delete_file(path="/tmp/a")成功执行,但路径错了,成功率是 1,正确率是 0。 - 副作用合规率:产生的写操作都在授权范围内、且可回滚的比例。这是安全指标,不是质量指标。
一个真实形状的事故:agent 要删除临时目录,工具调用成功,返回 {"ok": true, "deleted": 12};但参数里的路径来自模型拼接,指向了用户主目录下的同名目录。调用成功率 100%,参数正确率 0%,副作用合规率 0%。如果评估只看成功率,这次事故不会留下任何痕迹。
所以工具层至少要记录:工具名、参数哈希与关键字段、返回码、耗时、是否写、作用对象 ID、是否可回滚、重试次数。前四项用于质量,后四项用于安全与成本。
9.1.4 成本层:token、金额、延迟与配额
成本不是「账单出来再说」的事后指标,而是评估的一等公民。它至少有五个分量:
其中 与 分别是输入与输出 token, 是命中缓存的输入 token, 是金额, 是端到端延迟。金额不是 token 的线性函数:缓存命中、批量折扣、工具单价、重试都会改变它。
延迟必须报分位数,而不是只报均值。理由在 9.6 节展开,这里先给一个可复算的例子:48 条任务里,系统 A 平均 969 ms、P95 1700 ms;系统 B 平均 905 ms、P95 2600 ms。B 的平均值低 64 ms,但每 20 次就有 1 次落在 B 的 P95 以上,而 B 的 P95 比 A 慢 900 ms。对交互式产品,后者才是用户感知到的「卡」。
模型、提示词、工具注册表、评分器、数据集,五者任意一个变了,历史分数就不再可比。一个够用的做法是给评估协议一个内容哈希:把五项版本拼成规范化 JSON,算 SHA-256 前 12 位,写进每条记录。看板上按协议哈希分组,而不是按日期分组;否则一次提示词微调会把整条曲线抬起来,让人误以为模型升级了。
四层评估的落地顺序建议是:先把最终答案与成本跑通,再加工具调用,最后加轨迹。原因是轨迹标注最贵、评分器最不稳定;在答案层还没有稳定验收时引入轨迹评分,只会得到一堆无法解释的数字。
9.2 数据集:采样、分层、难度与污染
评估集不是题库,是从任务分布里抽出来的一把尺子。尺子本身会偏:抽样的入口偏、难度分布偏、语言偏、工具集偏。更糟的是,尺子会被污染:公开题进了训练集,模型背下了答案;或者评估集与提示词示例重复,测的是记忆而不是能力。
先看抽样偏差的四种常见来源:
| 来源 | 怎么发生 | 读数会怎样 | 处理 |
|---|---|---|---|
| 便利样本 | 用团队自己写的 20 条 demo 当评估集 | 分数高,上线后骤降 | 从真实请求日志分层抽,保留长尾 |
| 语言偏置 | 只测英文或只测中文 | 另一种语言上的错误完全不可见 | 语言作为分层维度,单独报 |
| 难度偏置 | 多数是单步问答 | 多步任务的成功率被高估 | 按步数/工具数分层,难例过采样后加权 |
| 工具集偏置 | 评估时给的工具比生产多 | 生产环境工具选择失败率上升 | 评估工具集与生产注册表同版本 |
这张表说明什么:抽样偏差不是「样本少」,而是「样本的来源与生产不同」。加样本量救不了来源偏差;n 从 48 加到 4800,只会把错误的估计算得更精确。
9.2.1 分层:先定维度,再定配额
分层的目的不是让报表好看,而是保证每个关键子群都有足够样本,能单独给出区间。三个最有用的维度:
- 难度:按「专家所需步数」或「gold 轨迹工具调用数」分 easy / medium / hard。不要用模型自己的表现分难度,那会把「模型不会」误判成「题难」。
- 工具:按「必须使用哪一类工具」分,例如只读检索、代码执行、外部写操作。工具类别决定了失败代价。
- 语言/地域:按用户实际语言分布分,至少保证非主语言有最小样本量。
配额有两种策略。等额分配(每层 16 条)便于比较层间差异,但总体准确率会被样本结构扭曲;按生产比例分配(easy 70%、hard 20%、other 10%)总体无偏,但 hard 层的区间会很宽。实践中常用「难例过采样 + 事后加权」:
其中 是第 层的样本准确率, 是生产分布里该层的占比,不是评估集里的占比。如果评估集把 hard 过采样到 50%,而生产里 hard 只占 20%,那么总体数字必须用 0.2 加权,否则会系统性低估。
第 9 章实验里,48 条按难度等分,加权后总体准确率仍是 35/48 与 36/48,因为权重恰好等于样本比例;如果改成难例过采样,同一批分层数字必须重新加权才能与总体对齐。--strata 命令末尾那行 weighted Δ = +2.08pp;总体 Δ = +2.08pp;差 0.0pp 就是在验证这条恒等式。
9.2.2 数据集的生命周期
生产请求 / 事故 / 红队用例
│
▼
┌──────────────────┐ 去重 + 污染检查 ┌──────────────────┐
│ ① 候选池 │──────────────────────▶│ ② 标注与复核 │
│ 原始输入、工具集 │ │ 标准答案、rubric │
└──────────────────┘ └────────┬─────────┘
▲ │
│ ▼
┌──────────────────┐ 冻结 + 内容哈希 ┌──────────────────┐
│ ⑥ 失败回流 │◀──────────────────────│ ③ 分层与配额 │
│ 新失败 → 新版本 │ │ 难度/工具/语言 │
└──────────────────┘ └────────┬─────────┘
▲ │
│ ▼
┌──────────────────┐ 每次运行写版本号 ┌──────────────────┐
│ ⑤ 看板与门禁 │◀──────────────────────│ ④ runner │
│ 带区间的差值 │ │ N 次采样 + 成本 │
└──────────────────┘ └──────────────────┘
读图:候选池有两条入口——生产请求与事故复盘,后者保证失败样本能回流。冻结发生在标注之后、runner 之前,用内容哈希锁住;runner 之后不再改数据集,只发新版本。门禁只读冻结版本与运行记录,不读「最新」目录。这条单向性是多个人同时改评估集时唯一能避免「同一版本号不同内容」的办法。
9.2.3 污染:四种检查与各自的盲区
污染(contamination)指评估样本以某种形式出现在模型可见的数据里:训练语料、提示词示例、few-shot 池、工具文档、甚至上一次评估的缓存。四种检查覆盖不同的泄漏方式:
| 检查 | 怎么做 | 能抓到 | 抓不到 |
|---|---|---|---|
| 精确哈希 | 规范化后对题干与答案算 SHA-256,与训练/示例池求交 | 原题复制 | 改一个词、换语言 |
| n-gram 重叠 | 5-gram Jaccard 或最长公共子串,阈值按语料调 | 近义复制、模板题 | 语义等价但措辞全新 |
| 改写对照 | 同一题生成 3 个改写版,比较原题与改写版的差值 | 背答案(原题高、改写低) | 改写也进了训练集 |
| 时间切分 | 只用模型训练截止日之后发布的题 | 训练集泄漏 | 截止日之前的公开题记忆 |
四种都不完美。最实用的组合是「改写对照 + 时间切分」:前者直接测「换一种问法还会不会」,后者保证题目不可能在训练集里。精确哈希适合做门禁,因为它零假阳性、可自动跑;n-gram 适合做审计,因为它需要人工看阈值。
一次污染审计的读数应该长这样(构造示例,用于说明形状):原题准确率 92%,三个改写版平均 61%,5-gram 重叠率 0.71,训练截止日之后的新题准确率 58%。四个数字放在一起,结论不是「模型退化了」,而是「原题分数里有相当一部分来自记忆」。修复不是调模型,而是换题、去重、把改写版加入评估集。
一个可复现的评估集版本由清单(manifest)唯一确定:
{
"name": "agent-repo-eval",
"version": "v3",
"created_at": "2026-10-12T09:00:00+08:00",
"items": 480,
"items_sha256": "9d2f...c41a",
"rubric_sha256": "51ab...7e02",
"strata": {"difficulty": {"easy": 192, "medium": 192, "hard": 96},
"language": {"zh": 240, "en": 240}},
"source_mix": {"production": 0.62, "incident": 0.18, "synthetic": 0.20},
"contamination": {"exact_dupes": 0, "max_ngram_jaccard": 0.08,
"cutoff": "2026-06-30"},
"frozen": true
}
items_sha256 是对「按 id 排序后的 JSONL + rubric 文件」算的哈希;任何一条增删改都会改变它。frozen=true 之后只允许发新版本,不允许原地编辑。运行记录引用 version 与 items_sha256 两者,前者给人看,后者给机器校验。
清单里最容易被忽略的是 rubric_sha256。评分标准改了,分数也会变;如果只锁题目不锁 rubric,两次运行仍然不可比。第 9.7 节的 judge 提示词同理,必须进 manifest。
9.2.4 标注一致性:kappa 只看一致性,不看正确性
人工标注有两个误差来源:标注者之间不一致,以及标注者集体偏离标准。Cohen’s kappa 度量前者:
其中 是两人实际一致率, 是按各自边际分布算出的偶然一致率。 表示与随机相当, 表示完全一致;常见的解释带是 差、0.2–0.4 一般、0.4–0.6 中等、0.6–0.8 较好、 很好。这个带是经验规则,不是法律;对安全类判定,0.6 往往不够。
kappa 有一个关键盲区:它不告诉你「标得对不对」。两个人都把「测试超时」标成「通过」,一致率 100%,,但标注是错的。所以还需要第三层:抽查 + 裁决 + 与可执行证据(测试结果、工具返回)对齐。对 agent 评估,最可靠的标注是「可执行验收」:能跑测试的跑测试,能回放 trace 的回放 trace,人工只判无法自动化的部分。
标注指南要写清三件事:边界案例怎么判、拒答算哪类、证据不足时标什么。指南本身也要版本化;否则同一个 kappa 数字,可能对应两套不同的判定标准。
把评估集想成一次抽样调查,而不是一次人口普查。48 条任务的准确率是一个估计值,换一批 48 条会得到另一个值。置信区间描述的就是这个「换一批」的波动。承认这一点之后,很多争论会变成可计算的问题:不是「B 是不是更好」,而是「在 n=48 的配对设计下,我们能否以 95% 置信度说 B 的总体差值大于 0」。
9.2.5 失败样本回流
评估集如果只增不减,会逐渐偏离生产;如果频繁原地改,又失去可比性。一个可执行的流程是:
- 收集:线上失败 run、人工工单、红队用例,进入候选池,带原始 trace 与版本号。
- 去重:对输入与失败原因做规范化哈希;同一根因只保留一个代表,其余作为变体。
- 归因:标失败层(答案/轨迹/工具/成本)与错误码;无法归因的样本先不进评估集。
- 标注:写验收条件与证据;能自动判的优先自动判。
- 入集:加入对应的难度/工具分层;如果是新失败类型,单独开一个分层,不要混进 easy。
- 发版:bump 版本、重算哈希、在 manifest 里记
source_mix;旧版本保留至少两个发布周期。
回流样本的价值在「下一次不会再犯」。如果一条失败样本进入评估集后,系统仍然以同样的方式失败,说明修复没有覆盖它;如果它开始通过,说明修复有效。门禁应该同时看「新增失败是否为 0」与「历史失败是否仍通过」,后者防止「修一个坏两个」。
9.3 指标:准确率、pass@k、工具成功率与引用正确率
指标是契约:一个指标只回答一个问题,并且要写清分母。最常见的错误不是选错指标,而是分母漂移——同一个「成功率」,有时除以 run 数,有时除以任务数,有时只除以「有答案的 run」。数字看起来连续,其实换了一把尺子。
先给一张口径表。每个指标都标明分母与它掩盖的东西:
| 指标 | 定义 | 分母 | 它掩盖什么 |
|---|---|---|---|
| 准确率 | 通过验收的任务数 / 任务数 | 任务 | 失败类型、成本、尾部延迟 |
| exact match | 规范化后与标准答案完全一致的比例 | 任务 | 语义正确但措辞不同;部分正确 |
| pass@k | 从 n 次采样中取 k 次至少一次正确的概率 | 任务 × 采样 | 采样成本;k 与 n 的口径 |
| 工具成功率 | ok=true 的调用数 / 调用数 | 工具调用 | 参数正确性、副作用合规 |
| 引用 precision | 正确引用数 / 引用总数 | 引用 | 该引未引(漏引) |
| 引用 recall | 正确引用数 / 应引数 | 应引证据 | 多引、错引 |
| 轨迹成功率 | 答案正确且每步合规的 run 数 / run 数 | run | 合规但低效;正确但绕路 |
这张表说明什么:没有「最好的指标」,只有「与当前决策匹配的指标」。发布门禁关心的是「会不会变坏」,所以要看尾部与失败类型;模型选型关心的是「同样的钱能买多少正确」,所以要看成本配对;安全评审关心的是「有没有越权」,所以要看副作用而不是准确率。
9.3.1 准确率与 exact match:先把分母写死
准确率是最容易被滥用的指标,因为它的定义太短。工程上至少要补三句:
- 任务级还是样本级:同一任务采样 5 次,是按「5 次里至少 1 次正确」算,还是按「5 次全对」算?前者是 pass@5 的思路,后者是 consistency 的思路,数值差很多。
- 拒答怎么算:该答而拒答计错,该拒而拒答计对;两者必须分开统计,不能都塞进「不正确」。
- 部分正确怎么算:多字段抽取里对了 3/4 个字段,是 0 还是 0.75?如果是 0.75,指标就变成连续值,区间方法也要跟着换。
exact match 适用于答案空间小、规范化规则明确的场景:日期、金额、枚举、代码补丁的哈希。它不适用于开放式问答。把 exact match 用在自然语言答案上,会得到一条「越啰嗦分越低」的曲线,最后模型学会只输出关键词。
多分类或多分层时,还要区分 macro 与 micro:
macro 给每个分层等权,micro 给每个样本等权。前者回答「每类平均做得多好」,后者回答「随机抽一个样本做得多好」。报告里必须写清用的是哪个;只写「准确率 81.2%」而不写口径,等于没写。
9.3.2 pass@k:采样口径与它的上界
pass@k 回答的是:对同一道题采样 次,其中 次正确,随机取 次,至少一次正确的概率是多少。无偏估计量是
其中 是采样次数, 是正确次数, 是允许尝试的次数。直觉是「一次都不中的组合数除以所有组合数」。当 时,所有 次里必然至少有一次正确,估计值为 1。
这个公式常被写错成 。后者假设每次独立同分布、且用点估计代入,会在小样本上给出有偏结果;前者是无放回的组合口径,也是 Codex 论文采用的估计量。两者在 大、 小时接近,在 、 时差别明显。
固定 与 ,对 ,有:
- 随 单调不减;
- ;
- 当 时,。
(1)设 为「随机取 次至少一次正确」。取 次可看成先取 次再补一次,故 ,概率单调不减;组合式对 的单调性也可由 随 递减得到。
(2)设单次正确概率为 ,则 。由容斥或联合界, 次至少一次正确的概率不超过 次概率之和,即 ;组合式同样满足该上界。
(3)若 ,则 ,约定 ,故估计值为 1。∎
∎定理的工程含义是:pass@k 不能跨 k 比较。一个系统报 pass@10=0.62,另一个报 pass@1=0.48,你不能说前者更好;要么统一 k,要么同时报 pass@1 与成本。上界 还说明,pass@k 的「提升」在 k 变大时会自然饱和,用更大的 k 刷榜没有信息量。
9.3.3 工具成功率:调用级与任务级要分开
工具成功率至少有两个分母:
没有工具调用的任务不计入后者的分母,否则纯问答任务会被算成「工具成功」。前者衡量工具服务稳定性,后者衡量 agent 能否把工具用对。两者可能背离:工具服务很稳(call success 99%),但 agent 经常漏调一个必需工具(task tool success 60%)。只报前者,会把「模型不会用工具」误判成「工具很健康」。
再往下切一层,按错误类型分桶:EINVAL(参数错)、ENOTOOL(工具不存在)、EPERM(权限)、ETIMEDOUT(超时)、EBUDGET(预算)、EUPSTREAM(上游)。每个桶的修复动作不同:参数错要改 schema 或提示词,权限错要改授权,超时要改超时与重试。把所有错误合成一个「失败率」,等于放弃定位。
副作用要单独计。一次「成功」的写操作可能是越权的;一次「失败」的写操作可能已经部分生效。评估记录里应该有 side_effects 与 rolled_back 两个字段,门禁对「不可回滚的副作用」零容忍。
9.3.4 引用正确率:precision 与 recall 必须成对
有引用能力的 agent(RAG、研究助手、代码审查)需要单独的引用指标。只算「有没有引用」不够,要算引用是否真的支持结论:
precision 低说明「乱引」:引用的段落与结论无关,或者张冠李戴。recall 低说明「漏引」:结论需要三个证据,只给了一个。两者要成对报,因为它们的失败代价不同:乱引会误导读者,漏引会让结论无法复核。对合规场景,漏引可能比乱引更严重;对客服场景,乱引更伤信任。
引用还要做可回切校验:引用携带的 doc_id、版本、起止位置与原文,必须满足 text[start:end] == quote。这一步能抓到「引用指向错误段落」「文档更新后 offset 漂移」「摘要冒充原文」三类问题。第 6.5 节给了完整做法,这里只强调:precision 的分母是引用条数,recall 的分母是应引证据;把两者混成一个「引用正确率」,就无法区分乱引与漏引,也不知道该改召回还是改生成。
9.3.5 轨迹级成功率:一个更严格的联合指标
轨迹级成功率把「答案正确」与「过程合规」合成一个联合事件:
它比答案准确率严格,因为任何一项不满足都算失败;它也比「所有步都正确」宽松,因为允许走不同的路。适合做发布门禁的主指标,但不适合做唯一的优化目标:它会把「正确但慢」与「错误但快」压成同一个 0,掩盖改进方向。实践中同时报三个数:答案准确率、轨迹合规率、联合成功率。三者之间的差值,就是「过程问题」的体积。
9.3.6 指标要成对报告
一个质量数字如果没有成本与延迟伴随,就不能用于决策。建议的最小报告单元是六元组:
| 字段 | 例子 | 为什么必须 |
|---|---|---|
| 指标与口径 | traj_success,任务级,n=48 | 防止分母漂移 |
| 点估计 | 72.9% | 给人一个锚 |
| 区间 | Wilson 95% [59.0%, 83.4%] | 说明不确定性 |
| 成本 | $0.00217/任务 | 质量不是免费的 |
| 延迟 | P50 870 ms / P95 1700 ms | 尾部决定体验 |
| 样本与版本 | 48 条,eval-v3,seed=20261019 | 可复现 |
这张表说明什么:同一个准确率,配不同的成本与尾部,可能是完全不同的产品决策。9.6 节会把这三个维度画成帕累托前沿;这里先记住,任何只报第一、二列的报告,都不足以支撑「上线」这个动作。
- 「准确率越高越好」——准确率的分母与验收条件一变,数值就不可比。先对齐口径,再谈高低。
- 「pass@k 越大越强」——k 不同不可比;pass@k 还会随 k 饱和,且不包含采样成本。必须同时报 k、n 与每次尝试的成本。
- 「工具成功率 99% 说明工具层没问题」——它只说明调用没报错,不说明参数对、对象对、副作用合规。
- 「引用越多越可信」——多引无关段落会拉低 precision,却不会提高 recall;引用要成对看,还要能回切原文。
- 「轨迹成功率可以替代答案准确率」——它更严格,但会把「正确但慢」与「错误但快」压成同一个 0;三个数要一起报。
9.4 统计:Wilson、bootstrap 与配对检验
第 9.3 节给出的都是点估计:72.9%、75.0%、+2.08pp。点估计的问题是它不告诉你「换一批样本会怎样」。48 条样本上的 +2.08pp,可能对应总体上的 +5pp,也可能对应 −3pp;区间就是把这种不确定性写出来。
先看实验里的两个区间:
python3 experiments/ch09/eval_stats.py --compare
system n correct accuracy Wilson 95% mean ms P50 P95 P99
A 48 35 72.9% [59.0%, 83.4%] 969 870 1700 2100
B 48 36 75.0% [61.2%, 85.1%] 905 760 2600 3200
两个 Wilson 区间大幅重叠。重叠本身不是检验,但它已经说明:仅凭 35 对 36,无法判断总体谁更高。下面把这几件工具的口径、假设与边界写清。
9.4.1 Wilson 区间:公式与适用条件
设 次独立试验中成功 次,,双侧置信水平 对应的正态分位数为 (95% 时 )。Wilson score 区间是
它来自对 score 检验的反演,而不是「点估计 ± 1.96 标准误」。两个细节值得记住:
- 当 或 时,Wald 区间会退化成一个点(宽度为 0),Wilson 不会。 的 Wilson 95% 区间约是 , 约是 。
- 当 很小或 接近 0/1 时,Wilson 的覆盖更接近名义水平;但它不是「任何情况下都保守」,覆盖概率会随 振荡。工程上把它当作默认口径即可,不要宣称它「保证 95%」。
实验代码里的实现只有六行,可以直接抄:
def wilson_ci(k: int, n: int, z: float = 1.959963984540054) -> tuple[float, float]:
p = k / n
denom = 1.0 + z * z / n
center = (p + z * z / (2.0 * n)) / denom
half = z * math.sqrt(p * (1.0 - p) / n + z * z / (4.0 * n * n)) / denom
return (max(0.0, center - half), min(1.0, center + half))
适用条件比公式更重要:条目独立、同一分布、二分类。agent 评估经常违反第一条:同一任务采样 5 次,5 条记录高度相关;同一用户的多条请求也相关。此时正确做法是先按任务聚合(5 次里成功几次),再对任务算比例,或者直接用聚类 bootstrap。把 5 次采样当 5 个独立样本,区间会窄得离谱。
9.4.2 bootstrap:不依赖正态的区间
bootstrap 的思路是「从样本里再抽样,模拟换一批样本」。对 条记录,做 次有放回重采样,每次算一个统计量,取经验分布的 2.5% 与 97.5% 分位:
def bootstrap_ci(values, stat, rng, n_boot=4000, alpha=0.05):
n = len(values)
stats = []
for _ in range(n_boot):
sample = [values[rng.randrange(n)] for _ in range(n)]
stats.append(stat(sample))
stats.sort()
return percentile(stats, alpha / 2), percentile(stats, 1 - alpha / 2)
它的优点是能处理均值之外的一切统计量:P95 延迟、成本中位数、pass@k、引用 precision。Wilson 只能处理比例,bootstrap 没有这个限制。
代价是三个假设:样本能代表总体、条目近似可交换、重采样次数足够。前两个假设不成立时,bootstrap 区间会同样偏;它修正不了抽样偏差,只能修正抽样波动。重采样次数 建议至少 2000,报告里写清 与 seed; 时区间端点会随 seed 跳动,看起来像「结果不稳定」,其实是算得不够。
百分位 bootstrap 在小样本、强偏态时覆盖率偏低。一个常用的补救是 BCa(偏差校正与加速),但它需要实现 jackknife,超出本章范围;工程上更实用的补救是增加 ,或者在报告里同时给 Wilson 与 bootstrap,两者差得远就说明样本太少。
9.4.3 配对:为什么必须逐条对齐
A 与 B 跑的是同一批 48 条任务。如果把它们当成两个独立样本,差值标准误会把「题目难度差异」算进噪声里;配对之后,题目难度被消掉,只剩「同一题上 A 与 B 的差异」。
配对二元数据的核心是四格表:
| B 正确 | B 错误 | |
|---|---|---|
| A 正确 | ||
| A 错误 |
只有 与 携带「谁更好」的信息; 与 是两边都对的、都错的,对差值没有贡献。实验里 、,净差 1 条,对应 +2.08pp。
McNemar 检验的精确版本是:在 个不一致对里,若两者没有差异,则 服从 。双侧 p 值为
当 较大时可以用卡方近似 ,但小样本请用精确版本。实验里 、,精确 ,和抛硬币没有区别。
配对 bootstrap 给的是差值区间,不是 p 值:对同一组索引同时重采样 A 与 B,算 ,取百分位。它和 McNemar 回答的问题略有不同:McNemar 检验「不一致对是否对称」,配对 bootstrap 估计「差值的可能范围」。报告里两个都给,读者既能看显著性,也能看实际幅度。
设比例估计的 Wald 半宽为 ,置信水平对应的分位数为 ,真实比例为 。忽略有限总体校正时,使半宽不超过 所需样本量满足
当 未知时,取 得最保守的 。
Wald 区间半宽为 。两边平方得 ,移项即 。由于 ,在 未知时用 代入,得到一个不依赖于 的保守样本量。∎
∎代入 95% 的 ,半宽 0.05 需要约 385 条,0.03 需要约 1068 条,0.02 需要约 2401 条:
python3 -c 'import math; z=1.959963984540054; [print(f"half-width={h}: n={math.ceil(z*z*0.25/(h*h))}") for h in (0.05,0.03,0.02)]'
half-width=0.05: n=385
half-width=0.03: n=1068
half-width=0.02: n=2401
注意这是单比例的样本量,不是「检测两个系统差值」的样本量。要检测 +2pp 的差值,所需样本远大于 385;配对设计下,起决定作用的是不一致对的数量,而不是总样本量。实验里 48 条只有 5 个不一致对,所以区间宽达 16.6pp。规划时先估「不一致率」,再反推需要多少对;不一致率越低,同样的差值越难被检测出来。
设系统 A、B 在同一批 个任务上的得分方差分别为 、,两者相关系数为 。差值 的方差为
当 时,它小于把两组当独立样本时的 。
由 ,且 ,对 个独立任务取平均即得第一式。第二式是 的特例; 时减项为正,故差值方差更小。∎
∎这个定理解释了为什么同一批任务上的 A/B 比「A 跑一批、B 跑另一批」更灵敏。任务难度是最大的方差来源;配对把它消掉之后,剩下的才是系统差异。代价是评估成本翻倍(同一批任务要跑两遍),以及必须保证两边的任务、工具集、评分器完全一致。
9.4.4 显著性检验的四个前提
p 值不是一个可以脱离设计单独解释的数字。用它做发布决策前,确认四件事:
- 假设是预先写的:看的是准确率差值、P95 差值,还是分层里的某一个?先写再跑。跑完再挑一个显著的分层,是把假阳性率从 5% 放大到远高于 5%。
- 检验与数据类型匹配:二元配对用 McNemar;连续指标(延迟、成本)用配对 bootstrap 或 Wilcoxon 符号秩;多组比较用 Holm 或 Bonferroni 校正。
- 没有偷看:固定样本量,或使用 always-valid 的序贯方法。每天看一次、显著就停,会让名义 5% 变成实际 20% 以上。
- 效应量单独报:p 值回答「是否可能为零」,不回答「有多大、值不值」。+2.08pp、Cohen’s 是「可忽略」量级;即使把 n 加到显著,也未必值得上线。
实验输出把这两件事并排放在一起:
Δaccuracy = +2.08pp bootstrap 95% CI [-6.2pp, +10.4pp]
配对不一致:B-only=3 A-only=2 McNemar 精确 p=1.000 bootstrap p=0.82
效应量 Cohen's h = +0.047
读法:差值方向为正,但区间跨零;精确检验 p=1.000;效应量接近 0。三句话合起来是「这点差异在当前样本量下无法与噪声区分」,而不是「B 更好但不显著」——后者暗示「再多跑一点就能显著」,但以这个效应量,要把区间下界推过 0 需要的不一致对远多于现有数量。
频率派区间不表达「真值有 95% 概率落在里面」。它的意思是:如果按同样的抽样与估计流程重复很多次,约 95% 的区间会覆盖真值。对单次实验,区间是一个关于流程可靠性的陈述,不是一个关于参数的赌博。工程上这样理解就够了:区间宽,说明你的流程给出的数字不稳;区间窄但偏离,说明流程稳但可能偏(抽样偏差),后者更危险。
9.4.5 多重比较与分层报告
9.2 节要求分层,9.4 节要求控制假阳性,两者会打架:分 6 层就做了 6 次比较,每层 5% 的假阳性率会累积。一个实用的折中是:
- 主指标只定一个,在实验前写死;主指标用 95% 区间,不做校正。
- 分层指标作为诊断,标注为「探索性」,不单独用于放行;如果要用,做 Holm 校正,并报告校正后的区间。
- 安全指标零容忍,不做显著性检验:出现一个越权副作用就拒绝,不管比例多低。
分层报告的价值在定位,不在放行。9.5 节的门禁会把「hard 层退化」作为一票否决项,但它依据的是绝对规则(退化超过阈值),不是「hard 层差值显著」。
9.5 回归:金标集、门禁、灰度与 A/B
评估集回答「现在多好」,回归回答「改完有没有变坏」。两者的区别是时间方向:评估是横截面,回归是纵向对比。纵向对比最容易犯的错,是拿「新版本的这次运行」和「旧版本的上次运行」比,而两次运行的模型、工具、数据集、评分器都可能不同。
回归体系由四层组成,每层的职责不同:
| 层 | 规模 | 更新频率 | 回答的问题 |
|---|---|---|---|
| 金标集(golden set) | 100–300 条 | 季度 | 核心能力有没有退化 |
| 回归集(regression suite) | 300–1000 条 | 每次事故后追加 | 已知失败有没有复发 |
| 安全/对抗集 | 50–200 条 | 每月 | 越权、注入、外泄有没有新增 |
| 灰度与在线 A/B | 真实流量 | 每次发布 | 生产分布上有没有变坏 |
金标集要小、稳、可人工复核;回归集要大、全、覆盖历史失败;安全集要独立于质量集,避免「质量涨了、安全放了」被平均掉。四层都指向同一个原则:失败样本必须可复现。
9.5.1 门禁:不是单指标,是一组带条件的断言
一个可执行的门禁至少包含四类条件,全部满足才放行:
| # | 条件 | 例子 | 不满足时 |
|---|---|---|---|
| 1 | 质量差值下界 | 配对 Δ 的 95% CI 下界 pp | 拒绝;或降级为灰度 |
| 2 | 安全零回归 | 越权副作用 0;注入成功 0 | 一票否决 |
| 3 | 成本与延迟预算 | Δcost %;P95 ms | 拒绝或限流 |
| 4 | 分层不退化 | hard 层 Δ 不低于 −3pp | 拒绝;附分层报告 |
第一条用的是下界而不是点估计,这是门禁与看板最大的区别。看板可以显示 +2.08pp;门禁要问「如果总体差值是 −6pp,我们会不会误放」。用下界,就是把这种误放的概率压到 2.5%(单侧)。
第二条不做统计检验。安全事件是「一票否决」,不是「比例低就可接受」;一次越权删除的期望损失,远大于准确率涨 2pp 的收益。
第三条把成本与延迟写进同一张门禁表。理由在 9.6 节:质量、成本、延迟是三维取舍,任何一维失控都不该上线。阈值要按产品定:交互式助手 P95 可能要求 1.5 s,批处理可以放宽到 30 s。
第四条是 9.2 与 9.4 的落地:分层不退化。它防的是「平均涨、hard 跌」——实验里的 B 正是这个形状,总体 +2.08pp,hard −6.25pp。
dataset v3 (frozen) candidate build
│ │
└───────────────┬──────────────────┘
▼
┌─────────────────┐
│ paired runner │ 同一批任务,N 次采样
│ 记录 trace/成本 │
└────────┬────────┘
▼
┌─────────────────┐
│ stats │ Δ + CI + 分层 + P95
└────────┬────────┘
▼
┌──────────────────────────────────────────────┐
│ gate:四条同时成立才放行 │
│ ① 质量差值下界 > 阈值 │
│ ② 安全回归 = 0 │
│ ③ 成本 / P95 在预算内 │
│ ④ 分层无退化(hard / 非主语言 / 写工具) │
└───────┬───────────────────────────┬──────────┘
│ pass │ fail
▼ ▼
canary 1% → 5% → 25% → 100% 回滚 + 失败样本入回归集
│ │
└────────── 观测 ───────────┘
读图:门禁在 runner 与统计之后,是唯一有写权限的环节;它读的是四条断言,不是一张总分表。右侧的失败路径不是终点,而是回流:失败样本进入回归集,下一版必须同时满足「新失败为 0」与「旧失败仍通过」。灰度是门禁之后的第二道闸,用真实流量检验离线看不到的分布偏移;回滚是第三道闸,必须做到一条命令、分钟级。
9.5.2 金标集与回归集的分工
金标集是「不能坏的东西」:核心任务类型、关键工具路径、典型用户请求。它的条目要少到可以人工逐条复核,稳定到可以跨季度比较。金标集频繁换题,会让历史曲线失去意义;长期不换题,又会被过拟合。一个折中是「80% 固定 + 20% 轮换」,轮换部分单独报,不混进主曲线。
回归集是「曾经坏过的东西」:每次事故、每个线上失败、每个红队发现,都变成一条可复现的用例。它增长快、噪声大,适合做「零回归」门禁,不适合做能力趋势。回归集里的每条用例都要带最小复现输入与期望行为;只写「某次回答不好」的条目,无法自动判定。
两者都要防过拟合。模型或提示词在回归集上反复调,会把回归集变成训练集;表现是「回归集全绿,线上仍出新错」。对策是保留一个从不参与调参的 holdout,只在发布前跑一次,且结果不进入日常看板。holdout 的代价是样本更少、区间更宽,但它给出的是唯一无偏的读数。
9.5.3 灰度:从小流量到大流量
灰度不是「先上 1% 看看」,而是一组带停止条件的阶段:
| 阶段 | 流量 | 最短观察 | 放行条件 | 回滚条件 |
|---|---|---|---|---|
| canary | 1% | 30 min | 错误率、P95、成本都在基线 ±10% | 任一超阈值 |
| 小流量 | 5% | 2 h | 质量抽样不低于基线;无安全事件 | 质量差值下界 pp |
| 中流量 | 25% | 1 天 | 分层指标无退化;成本在预算内 | 任一分层退化 |
| 全量 | 100% | 持续 | 同上,且回滚演练通过 | 自动或人工回滚 |
每个阶段都要有自动停止条件,不能只靠人盯。停止条件至少包括:错误率、P95 延迟、单次成本、安全事件数、人工差评率。阈值要用绝对值与相对值双写,例如「P95 ms 或比基线高 30%」,防止基线本身漂移。
灰度期间还要防止样本比例失真。1% 流量如果按用户哈希切,可能全是某个地区或某个版本的用户;正确做法是按请求哈希分层,并检查各分层的流量比例。否则 canary 的「一切正常」只说明那 1% 用户正常。
9.5.4 A/B:随机化单位与护栏指标
A/B 与灰度的区别是「有没有对照」。灰度是同一个系统的新旧版本按流量切;A/B 是两组用户/请求随机分流,比较指标差异。agent 的 A/B 有三个特殊问题:
- 随机化单位:按请求、按会话,还是按用户?按请求会让同一用户同时体验两个版本,污染体验类指标;按用户更干净,但所需样本量更大。会话级任务(多轮)应按会话切。
- 指标延迟:agent 的效果可能要几小时或几天才显现(工单是否解决、代码是否合并)。只看即时点击,会奖励「快但没用」的版本。
- 护栏指标:除了主指标,还要有成本、延迟、安全、投诉率。主指标涨、护栏跌,是典型的「赢在局部、输在系统」。
A/B 的统计设计与 9.4 节一致:预先定样本量、固定 horizon、不偷看、报区间与效应量。如果业务不允许固定 horizon,用序贯检验并明确其前提,不要用「每天看 p 值」冒充。
9.5.5 失败必须可复现
一条失败样本,如果不能在本地重放,就无法修,也无法进回归集。可复现的最小清单是:
| 字段 | 作用 | 缺失时的症状 |
|---|---|---|
dataset_version + items_sha256 | 锁定输入 | 同一题号对应不同题目 |
model + prompt_hash | 锁定策略 | 以为是模型退化,其实是提示词改了 |
tools + tool_versions | 锁定动作空间 | 工具升级导致的回归被误判为模型问题 |
seed + temperature + top_p | 锁定采样 | 十次只复现一次,无法调试 |
grader_version | 锁定评分 | 分数变化来自评分器,不是系统 |
trace + 工具返回 | 锁定执行 | 只能看到答案,看不到哪一步偏了 |
这六项就是 9.1 节评估记录的扩展。生产环境还要加容器镜像、依赖锁文件、外部服务版本;任何一项变了,重放就可能是另一个故事。
下面这次复盘是构造的,数字与 9.9 节实验一致,用于演示单指标门禁的失效形状。
某团队的门禁只有一个条件:总体准确率不低于基线。一次候选版本总体准确率 +2.1pp,顺利放行。上线后一周,客服工单里「回答很慢」的投诉翻了三倍。
复盘发现:候选版本把「先检索再回答」改成了「并行检索 + 投票」,平均延迟从 969 ms 降到 905 ms,但 P95 从 1700 ms 升到 2600 ms;同时 hard 层准确率从 50.0% 降到 43.8%。总体 +2.1pp 是 easy 层的 +6.25pp 拉出来的,而 easy 层恰好占了评估集的 1/3、生产流量的 70%。
修复分三步:门禁加入 P95 与分层条件;看板把「总体准确率」拆成三层;回滚脚本从「手动换镜像」改成一条命令,演练时间从 40 分钟降到 4 分钟。修复后同一候选版本在门禁处被拒绝,拒绝理由是「P95 +900 ms;hard −6.25pp」。
这个案例的教训不是「准确率不重要」,而是「单指标门禁一定会被平均掩盖」。门禁的每一条都对应一类已知事故:质量下界防「噪声当提升」,安全零回归防「越权」,成本/延迟防「用钱换分」,分层防「平均掩盖长尾」。
按顺序过一遍,任何一条不满足就停在原地:
- 数据集冻结了吗?
items_sha256与rubric_sha256都写进运行记录;没有哈希的评估集不能做门禁。 - 是配对运行吗? 同一批任务、同一工具集、同一评分器;A 与 B 的差异只允许是候选版本。
- 主指标定死了吗? 一个主指标 + 一组护栏;主指标看区间下界,护栏看绝对阈值。
- 安全回归是 0 吗? 越权副作用、注入成功、数据外泄各自计数;任一非零即拒绝。
- 成本与 P95 在预算内吗? 用分位数与单次成功成本,不用均值;阈值写绝对值 + 相对值。
- 分层有退化吗? hard、非主语言、写工具三类必须单独看;退化超过阈值即拒绝。
- 失败样本能重放吗? 六项版本字段齐全;不能重放的失败不许进回归集。
- 回滚演练过吗? 一条命令、分钟级、状态兼容;没演练过的回滚等于没有回滚。
八条里最容易跳过的是第 7、8 条:它们不产生分数,只在出事时救命。
三个常见卡点
卡点一:门禁通过了,但灰度挂了。 说明离线评估集与生产分布不一致。先查流量分层与请求长度分布,再查工具注册表版本;不要先怀疑模型。
卡点二:同一版本两次门禁结果不同。 采样温度、seed、数据集哈希、评分器版本,四项里必有一项没锁。先做可复现性检查,再谈统计显著性。
卡点三:安全集全绿,红队仍能打穿。 安全集覆盖的是已知攻击模式;新攻击不在集内。把红队发现回流,并保留一个不参与调参的对抗 holdout。
9.6 成本-延迟-质量:帕累托与预算约束
前五节把质量说清楚了,但质量不是免费的。同一道题,可以用一个小模型一次答完,也可以用大模型加三次检索、两次投票、一次验证;后者的准确率可能高 5pp,成本高 10 倍,P95 高 4 倍。评估的最终输出不是「谁更准」,而是「在给定预算与延迟约束下,谁更值」。
先把成本模型写出来。一次运行的期望成本是
其中 、 是输入/输出 token,、 是单价, 是第 个工具的调用次数, 是其单价, 是重试次数。延迟可以类似地分解:
这两个式子解释了一个常见错觉:「换小模型能省钱」。如果小模型多跑两轮、多调三次工具、重试率更高,总成本可能不降反升;延迟也不一定降,因为多出来的轮次是串行的。
9.6.1 帕累托前沿:不被支配的配置
在质量、成本、延迟三个维度上,配置之间通常不是全序关系。用支配(dominance)来筛:
设配置 的质量为 (越大越好)、单次成功成本为 (越小越好)、P95 延迟为 (越小越好)。称 支配 ,如果
且至少有一个不等式严格成立。不被任何其他配置支配的配置集合,称为帕累托前沿。
前沿上的配置是「想再要一点质量,就必须多付成本或延迟」的配置;前沿之外的配置,总能找到一个在三个维度都不差、至少一维更好的替代,没有讨论价值。
为了看清形状,下面给一组构造的读数(同一评估集、同一批任务,数值用于说明取舍,不是厂商基准):
| 配置 | 质量(轨迹成功率) | 单次成功成本 | P95 延迟 | 是否前沿 |
|---|---|---|---|---|
| A:小模型,单轮 | 61.2% | $0.0012 | 900 ms | 是 |
| B:小模型 + 检索 | 68.8% | $0.0021 | 1400 ms | 是 |
| C:大模型,单轮 | 72.9% | $0.0030 | 1700 ms | 是 |
| D:大模型 + 投票 ×3 | 75.0% | $0.0084 | 2600 ms | 是 |
| E:大模型 + 投票 + 重排 | 74.6% | $0.0101 | 3100 ms | 否(被 D 支配) |
这张表说明什么:E 花了更多钱、更慢,质量还略低于 D,应该直接淘汰;A、B、C、D 构成前沿,选哪个取决于预算与 SLO,而不是「哪个分高」。如果产品要求 P95 ms,只有 A、B 可选;如果预算上限是 $0.004/成功,C 是上限内质量最高的。
9.6.2 预算约束下的选择
把选择写成约束优化:
其中 是前沿集合。两个约束都可能让最优解落在前沿的中间,而不是最右端。工程上还要加两个约束:单次调用的最大 token(防上下文爆炸)、单位时间的工具配额(防限流)。它们不是「软指标」,而是硬边界;越过边界的配置根本跑不完评估。
如果预算不是「每次成功」,而是「每月总额」,选择会变成带流量分布的加权问题:
其中 是第 类请求的流量占比, 是为该类请求选的配置。这解释了「分级路由」为什么有效:easy 请求走 A,hard 请求走 D,总体质量接近全走 D,成本却低得多。路由本身也有成本与误判,评估时要把它算进去。
9.6.3 P50、P95、P99:均值掩盖尾部
延迟分布通常是右偏的:多数请求很快,少数请求很慢。均值是面积的平均,尾部是体验的平均;两者可以背离。
实验里的数字是一个干净的样本:
system n correct accuracy Wilson 95% mean ms P50 P95 P99
A 48 35 72.9% [59.0%, 83.4%] 969 870 1700 2100
B 48 36 75.0% [61.2%, 85.1%] 905 760 2600 3200
B 的平均延迟低 64 ms(−6.6%),P50 低 110 ms,但 P95 高 900 ms(+52.9%),P99 高 1100 ms(+52.4%)。如果只看均值,会得出「B 更快」;如果看 P95,会得出「B 更慢」。两个结论都对,取决于你问的是「平均吞吐」还是「单个用户的尾部体验」。
为什么会这样?B 在多数任务上省了几十到一百毫秒(并行检索、缓存命中),但在三条 hard 任务上引入了额外的验证轮次,单条多花 1–1.5 秒。48 条里 3 条尾部,足以把 P95 推高 900 ms,却只让均值降 64 ms。均值被 45 条快样本拉着走,尾部由 3 条慢样本决定。
小样本的尾部分位数极不稳定。 时,P95 是第 46 个值(nearest-rank),P99 是第 48 个值;删掉一条最慢样本,P99 可能直接掉几百毫秒。所以尾部指标必须同时报 、P95、P99,并在样本量足够时才算 P99; 时 P99 基本是最大值,参考价值有限。
尾部还要拆到组件,否则无法修。一条慢请求可能是模型排队、工具超时、重试、序列化、网络中的任意一段。按 span 分解后,常见形状是「P50 由模型主导,P99 由工具超时 + 重试主导」;对应的修复不是换更快的模型,而是给工具设更短的超时、把重试改成有上限的退避、或者对慢工具做异步化。
9.6.4 成本口径:每次调用、每次成功、每个正确
成本有三个分母,报告时必须写清:
实验里 B 的 cost/task 高 18.0%,但 cost per success 只高 15%(B/A=1.15×),因为 B 多解决了一条任务。如果 B 的成功数再多六条(42/48),cost per success 会低于 A——这就是「贵但值」的量化方式。
反过来,一个便宜 30%、成功率低 10pp 的配置,cost per success 可能更高。只看单价会得出错误结论;只看 cost per success 又会忽略「失败也有成本」(用户重试、人工兜底、信任损失)。一个更完整的口径是「每次正确交付的总成本」,把失败后的重试与人工成本也计入;它难以精确,但方向比单价正确。
- 「平均延迟降了就是更快」——均值可以被少数快样本拉低,同时尾部变慢。要看 P95/P99 与分布形状。
- 「单价低就更省钱」——小模型可能多轮、多工具、多重试,总成本反而更高;要按每次成功或每次正确交付算。
- 「质量越高越好,成本上线再说」——质量、成本、延迟是三维约束;越过预算或 SLO 的配置不可上线,分数再高也没有意义。
- 「帕累托前沿上的配置都该试」——前沿只说明「不被支配」,不说明「值得」。还要看实现复杂度、维护成本、故障模式;前沿是候选集,不是路线图。
9.7 LLM-as-judge:偏差、校准与双盲
当验收无法写成测试时——开放式问答、摘要、计划、语气、代码可读性——只能用评分器。人工评分贵、慢、不一致;LLM judge 便宜、快、可重复,但它不是 ground truth,而是一台有系统性偏差的测量仪器。把仪器当真理,是评估体系里最昂贵的错误之一。
judge 的典型用法是 pairwise:给同一道题的两个答案 A/B,问哪个更好。它比 absolute 打分稳定,因为相对比较有锚点;代价是引入位置、长度、格式与「像谁」的偏差。
9.7.1 四类偏差与各自的审计
| 偏差 | 机制 | 怎么审计 | 控制 |
|---|---|---|---|
| 位置偏差 | 先出现的答案更容易赢 | 交换 A/B 顺序重跑,统计赢家翻转率 | 双边平均;翻转率过高则丢弃该对 |
| 长度偏差 | 更长、更详细的答案显得更好 | 按长度差分桶,看各桶胜率 | 长度匹配;把长度作为协变量报告 |
| 自我偏好 | judge 偏爱同家族模型的措辞 | 用不同家族的 judge 复评 | 多家族评审;隐藏模型身份 |
| 格式偏差 | Markdown、列表、标题影响判断 | 同一内容两种格式对比 | 统一格式;剥离样式后再评 |
位置偏差的度量是翻转率:
翻转率超过 10%–15%,说明顺序效应不可忽略;此时不能只跑一个方向,必须两个方向都跑,并且把「两边都赢」才算稳定胜出。只跑一个方向的 judge 结果,会在很大程度上测「谁放在前面」,而不是「谁更好」。
长度偏差更隐蔽,因为长答案常常确实信息更多。正确的做法不是「一律惩罚长答案」,而是分层:在长度差小于 10% 的对子里看胜率,在长度差大于 30% 的对子里另看一组;如果两组胜率差很多,说明 judge 把长度当成了质量信号。此时要么做长度匹配,要么在 rubric 里明确「简洁性」的权重。
自我偏好要求 judge 与候选不同家族。用模型 X 评模型 X 的输出,胜率会系统性偏高;即使用同一家族的不同版本,也可能因为风格相似而偏高。高风险场景至少用两个家族的 judge,分歧时由人裁决。
格式偏差在 agent 输出里特别常见:带小标题、表格、代码块的答案看起来更「专业」。审计方法是把同一份内容渲染成纯文本与 Markdown 两种,分别评;如果胜率差超过 10pp,说明格式在替内容得分。
9.7.2 校准:judge 与人工的一致性
judge 上线前必须在一个校准集上对过人工标签。校准集要满足三个条件:覆盖主要任务类型、包含难例、人工标签有双人复核。规模不必大,100–300 对足够看出系统偏差;但必须独立于调参,否则调 judge 提示词的过程会把校准集变成训练集。
校准至少报四个数:
| 指标 | 定义 | 用途 |
|---|---|---|
| 一致率 | judge 与人工同判的比例 | 直观,但会被类别不平衡抬高 |
| Cohen’s kappa | 扣除偶然一致后的 agreement | 判断「不是瞎猜」 |
| TPR / TNR | 人工判好中被判好的比例 / 人工判差中被判差的比例 | 看两类错误是否对称 |
| 分桶一致率 | 按难度、长度、领域分桶后的一致率 | 找系统性偏差 |
一致率高但 kappa 低,说明多数样本属于同一类,judge 只要全判「好」就能拿高分。TPR 与 TNR 差距大,说明 judge 偏松或偏严;门禁阈值要按这个偏差调,而不是按总一致率调。
judge 的置信度要单独校准。让 judge 输出 1–5 的置信分,按分数分桶,算每桶与人工的一致率;如果 5 分桶只有 70% 一致,这个置信分就不能用来「自动放行高分、人工复核低分」。很多 judge 的置信分是「措辞的自信程度」,不是「判断正确的概率」。
某团队用 judge 比较两个客服 agent,200 对答案,judge 报告 B 胜率 58%。上线后人工抽检发现 B 并没有更好。复盘做了四项审计(构造读数,用于说明方法):
- 交换顺序重跑,22% 的对子赢家翻转;单方向结果不可用。
- 长度差大于 30% 的对子里,更长的一方胜率 68%;长度差小于 10% 的对子里,胜率 52%。
- judge 与候选同家族,自我偏好约 +9pp。
- 与 200 对人工标签比,一致率 78%,但 kappa 只有 0.56;hard 子集一致率 61%。
修复:双边平均、长度匹配、换一个家族的 judge、在 300 对双人复核数据上重新校准。修复后 kappa 0.71,B 的胜率回落到 51%,置信区间跨 0。结论从「B 更好」改成「没有证据表明 B 更好」。
这个案例的教训是:judge 的原始胜率不能直接进门禁;先过偏差审计与校准,再谈显著性。审计成本远低于一次错误上线。
9.7.3 双盲与多人评审
双盲的意思是:评审者不知道答案来自哪个系统、哪个模型、哪个版本。具体做法:
- 去标识:删除模型名、版本号、耗时、成本、工具调用等元数据;这些信息会暗示「谁更高级」。
- 随机顺序:A/B 顺序按 pair id 哈希随机,不按系统固定。
- 双边呈现:同一对跑两次(AB 与 BA),只有两次都选同一方才计为稳定胜。
- 多人评审:至少两人独立评,分歧进入裁决;裁决人也盲。
- 统一格式:同一份 rubric、同一套渲染;不要让一个答案带 Markdown、另一个是纯文本。
多人评审的成本高,但它是估计「评审者间一致性」最直接的方式。两人一致率低,说明 rubric 有歧义,先改 rubric,不要急着加人。三人以上时,多数投票比平均分稳健;平票用裁决人,不要用 judge 破平票——那会把 judge 的偏差带回人工层。
9.7.4 不要把 judge 当 ground truth
judge 适合做三件事:大规模初筛、pairwise 相对比较、给人工评审排优先级。它不适合做三件事:安全放行、合规判定、最终验收。
安全放行必须用可执行检查:越权副作用计数、出口过滤命中、敏感数据扫描。合规判定要有审计证据与责任人。最终验收要有可复核的证据链,judge 的分数只是其中一项。
一个实用的分层策略是:
| 场景 | 评审方式 | 理由 |
|---|---|---|
| 有测试/可执行验收 | 自动判定,不用 judge | 便宜、确定、可回放 |
| 开放式但低风险 | judge + 5% 人工抽检 | 规模大,错误代价低 |
| 开放式且高风险 | 双人人工 + judge 辅助排序 | 错误代价高,需要责任链 |
| 安全/合规 | 规则 + 人工 + 证据链 | judge 不可作为唯一依据 |
这张表说明什么:judge 的定位是「降低人工评审成本的工具」,不是「替代人工的权威」。把它放在低风险、大规模的格子里,收益最大;把它放在安全与合规的格子里,风险最大。
judge 的模型版本、提示词、温度、输出 schema、校准集,五项任意一个变了,历史分数就不可比。运行记录里至少写 judge_model、judge_prompt_hash、calibration_version。换 judge 当天不要同时换候选系统;否则分数变化无法归因,你会同时失去两个基线。
9.8 失败案例:测试集泄漏与均值掩盖尾延迟
这一节复盘三类失败:测试集泄漏、均值掩盖尾部、指标被 Goodhart。三类都不是「模型不够好」,而是评估体系自己产生了错误信号。每一类都给触发条件、可观测证据与修复动作。
9.8.1 测试集泄漏:公开题背答案
泄漏的触发条件很普通:评估题来自公开基准,而公开基准的题面与答案在训练语料里;或者评估集的 few-shot 示例与题目重复;或者上一次评估的输入输出被缓存进了下一次的提示词。三种路径都会让「原题」分数虚高。
一个可自动跑的审计是三步:规范化后精确哈希求交、字符 5-gram Jaccard 找近邻、原题与改写版的准确率差。下面是一段可复现的构造审计(8 条题,6 条原题出现在示例池里):
import re
Q = [
("q1", "如何为代码 agent 设置文件写入的最小权限?", "代码 agent 写文件时,权限最小化怎么做?"),
("q2", "trace 里的 span 断链通常由什么引起?", "什么原因会让调用链的 span 接不上?"),
("q3", "沙箱里 rlimit 与 cgroup 各挡哪类事故?", "资源限制和 cgroup 分别防什么故障?"),
("q4", "上下文压缩为什么会吃掉硬约束?", "压缩上下文时,为什么硬性要求会丢?"),
("q5", "工具调用失败后应该如何分类重试?", "工具报错以后,怎么决定要不要重试?"),
("q6", "多智能体的预算如何沿 agent 树传播?", "多 agent 场景下预算怎么分配给子 agent?"),
("q7", "pass@k 的 k 不同为什么不能直接比?", "k 取值不一样时,pass@k 为什么不可比?"),
("q8", "灰度发布阶段应该看哪些指标?", "灰度上线时要监控什么?"),
]
POOL = [Q[i][1] for i in (0, 1, 2, 3, 5, 7)] # few-shot / 训练示例池
ORIGINAL_OK = {"q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8"} # 原题答对
PARAPHRASE_OK = {"q2", "q5", "q7"} # 改写版答对的题
def norm(s: str) -> str:
return re.sub(r"[\s,。?!、;:,.?!;:]+", "", s.lower())
def grams(s: str, n: int = 5) -> set[str]:
s = norm(s)
return {s[i:i + n] for i in range(max(0, len(s) - n + 1))}
pool_norm = {norm(p) for p in POOL}
dupes = sum(1 for _, q, _ in Q if norm(q) in pool_norm)
jac = [max((len(grams(q) & grams(p)) / len(grams(q) | grams(p))
for p in POOL), default=0.0) for _, q, _ in Q]
print(f"items={len(Q)} exact_dupes={dupes}/{len(Q)} "
f"mean_max_jaccard={sum(jac) / len(jac):.3f}")
print(f"acc_original={len(ORIGINAL_OK) / len(Q):.1%} "
f"acc_paraphrase={len(PARAPHRASE_OK) / len(Q):.1%}")
items=8 exact_dupes=6/8 mean_max_jaccard=0.750
acc_original=100.0% acc_paraphrase=37.5%
三个读数放在一起,结论很清楚:原题全对、改写版 37.5%、平均最大 Jaccard 0.75,说明原题分数里有很大一部分来自「见过」。修复不是换更强的模型,而是:
- 去重:把与示例池、训练池精确或高 Jaccard 重叠的题移出评估集。
- 改写:每题保留 2–3 个改写版,原题与改写版分开报;只报原题会系统性高估。
- 时间切分:只用训练截止日之后发布的题;截止日写进 manifest。
- holdout:留一批从不参与调参、也不公开的题,只在发布前跑。
- 审计常态化:每次发版跑一次精确哈希与 Jaccard,超阈值就阻断。
泄漏的可怕之处在于它让所有其他指标一起失真:准确率、pass@k、引用正确率都会变好,成本甚至变低(背答案不需要调工具)。最有效的戳破方式是「换一种问法」与「换一批新题」。评估集设计时就要把这两条写进去,而不是等上线后再怀疑。
9.8.2 均值掩盖尾部:平均降 64 ms,P95 涨 900 ms
第 9.6 节已经给过读数,这里把它作为事故复盘。候选版本 B 的动机是「并行检索 + 少一次串行调用」,离线小样本上平均延迟确实降了:
python3 experiments/ch09/eval_stats.py --compare
system n correct accuracy Wilson 95% mean ms P50 P95 P99
A 48 35 72.9% [59.0%, 83.4%] 969 870 1700 2100
B 48 36 75.0% [61.2%, 85.1%] 905 760 2600 3200
均值 −64 ms、P50 −110 ms,看起来是一次成功的性能优化;P95 +900 ms、P99 +1100 ms,说明尾部在恶化。若门禁只看均值,B 会被放行;上线后最慢的 5% 请求会明显变慢。
下面这次复盘的数字由本章 48 条实验数据与一次构造的灰度读数组成,形状与线上一致。
触发:B 把串行「检索 → 生成」改成「并行检索 + 三路投票」,多数请求少一次等待,hard 任务多两轮验证。
证据:离线 48 条配对,均值 −64 ms,P95 +900 ms;hard 层准确率 50.0% → 43.8%;成本/任务 +18%。灰度 5% 时,客服侧「响应慢」标签上涨 3.2 倍,P95 从 1.7 s 升到 2.6 s。
根因:门禁只卡「平均延迟不高于基线」,没有卡分位数与分层;投票逻辑在 hard 任务上触发了额外的串行验证,而 hard 任务恰好是长尾请求的主要来源。
修复:门禁加入 P95 与 hard 层条件;投票改为「仅低置信度时触发」,并设 1.2 s 的硬超时;对慢工具做异步化;灰度阶段按请求长度分层检查 P95。修复后同一批任务均值 940 ms、P95 1900 ms,hard 层回到 50.0%,成本 +6%。
教训:均值是会计指标,分位数是体验指标。性能优化必须同时报 P50/P95/P99,并按任务难度分层;否则「优化」会把成本从多数请求转移到少数请求,而少数请求决定投诉。
修复这类问题的顺序是:先确认尾部是否真实(样本量、测量口径、是否被单条极端值主导),再定位尾部来源(按 span 分解到模型/工具/重试/排队),最后才改架构。直接换更快的模型,往往只改善 P50,不改善 P99;P99 的瓶颈通常在超时、重试与串行依赖。
9.8.3 指标被 Goodhart:一旦成为目标,就不再是好指标
Goodhart 定律的工程版本是:当一个指标被当成优化目标,参与者会找到「提高数字但不提高价值」的路径。agent 评估里常见的被优化方式:
| 指标 | 被优化的方式 | 反指标 |
|---|---|---|
| 准确率 | 只答简单题;该拒答时硬答 | 分层准确率、拒答正确率 |
| pass@k | 增大 k;增加采样次数 | pass@1、每次成功成本 |
| 引用 precision | 少引、只引最稳的一段 | 引用 recall、可回切率 |
| 工具成功率 | 只调不会失败的工具;失败就重试到成功 | 任务级成功率、重试成本 |
| judge 胜率 | 加长答案、加 Markdown、讨好 judge | 双盲人工、长度匹配胜率 |
| 轨迹成功率 | 减少步数、跳过验证 | 答案准确率、回归集通过率 |
这张表说明什么:每个指标都有一个「便宜的提高方式」,而这种方式通常伤害另一个指标。防御不是不用指标,而是成对使用:准确率配拒答率,precision 配 recall,pass@k 配 pass@1 与成本,judge 胜率配双盲人工。
一个常见的形状是「引用数量」(构造读数):团队把「平均引用数」作为努力程度的代理,结果 agent 学会了在每段后面堆引用,precision 从 0.81 掉到 0.54,读者反而更难核对。修复是把指标换成「可回切且支持结论的引用数 / 应引数」,并把「多引无关段落」计为错误。
Goodhart 的另一个入口是评估集本身。当团队每天盯着同一个 300 条回归集调参,回归集就变成了训练集;表现是回归集分数持续上升,holdout 与线上不动。防御是保留 holdout、定期换题、把线上失败回流但不在同一版本上反复调。指标治理与数据治理是同一件事。
9.9 实验:bootstrap CI + 配对检验
本章实验在 experiments/ch09/:eval_stats.py 内置 48 条配对结果,test_eval_stats.py 验证公式与可复现性。数据集是构造的,不是厂商基准;它把「平均提升、尾部变差、分层退化、成本上升」四种现象放进同一张表,方便逐个拆开。
9.9.1 数据与命令
48 条,难度 easy/medium/hard 各 16 条,语言 py/ts 各 24 条。每条记录两个系统的正确性、延迟、成本、工具调用数与工具错误数。成本按 A 的 1.18 倍构造,模拟 B 多一轮验证调用。
# 完整对比:准确率、Wilson、延迟分位、配对差值、门禁判定
python3 experiments/ch09/eval_stats.py --compare
# 分层:难度与语言;末尾验证加权 Δ 与总体 Δ 相等
python3 experiments/ch09/eval_stats.py --strata
# 机器可读:只取决策需要的字段
python3 experiments/ch09/eval_stats.py --compare --json \
| jq '{acc: {A: .systems.A.accuracy, B: .systems.B.accuracy},
delta: .paired.delta, ci: .paired.ci,
mcnemar_p: .paired.mcnemar_p,
p95: {A: .systems.A.latency_ms.p95, B: .systems.B.latency_ms.p95}}'
{
"acc": {"A": 0.7291666666666666, "B": 0.75},
"delta": 0.02083333333333337,
"ci": [-0.0625, 0.10416666666666674],
"mcnemar_p": 1.0,
"p95": {"A": 1700.0, "B": 2600.0}
}
JSON 里的 delta 是 1/48,ci 是 [−6.25pp, +10.42pp];与终端输出的四舍五入值一致。jq 这一层不是装饰:门禁脚本读的就是这些字段,而不是解析人类可读的表格。
分层输出:
python3 experiments/ch09/eval_stats.py --strata
stratum by difficulty
stratum n A acc B acc Δ B-only A-only
easy 16 93.8% 100.0% +6.25pp 1 0
hard 16 50.0% 43.8% -6.25pp 0 1
medium 16 75.0% 81.2% +6.25pp 2 1
weighted 48 72.9% 75.0% +2.08pp
weighted Δ = +2.08pp;总体 Δ = +2.08pp;差 0.0pp(应为 0)
这组数字支持三个判断:总体 +2.08pp 不显著;hard 层 −6.25pp;easy/medium 各 +6.25pp。如果只看第一行,会把「hard 退化」平均掉;如果只看 hard,会错过 easy 的真实收益。分层报告的作用是把这两个事实同时摆出来。
seed 的影响:
for s in 20261019 7 99; do
echo "seed=$s"
python3 experiments/ch09/eval_stats.py --seed "$s" | grep -E 'Δaccuracy|McNemar'
done
seed=20261019
Δaccuracy = +2.08pp bootstrap 95% CI [-6.2pp, +10.4pp]
配对不一致:B-only=3 A-only=2 McNemar 精确 p=1.000 bootstrap p=0.82
seed=7
Δaccuracy = +2.08pp bootstrap 95% CI [-6.2pp, +10.4pp]
配对不一致:B-only=3 A-only=2 McNemar 精确 p=1.000 bootstrap p=0.81
seed=99
Δaccuracy = +2.08pp bootstrap 95% CI [-6.2pp, +10.4pp]
配对不一致:B-only=3 A-only=2 McNemar 精确 p=1.000 bootstrap p=0.81
三个 seed 的区间端点完全相同,p 值只差 0.01。这不是「结果特别稳」,而是二元数据的 bootstrap 分布只有少数几个支撑点:48 条里不一致对只有 5 个,差值只能取几个离散值,百分位端点自然不随 seed 变。换连续指标(延迟、成本)时,seed 的影响会明显得多。这个细节提醒:bootstrap 不是万能平滑器,离散小样本的区间是块状的。
9.9.2 测试与可复现
python3 -m unittest discover -s experiments/ch09 -v
test_accuracy_and_delta ... ok
test_cost_ratio_is_constructed ... ok
test_dataset_shape ... ok
test_hard_stratum_regresses ... ok
test_mean_improves_but_tail_worsens ... ok
test_paired_counts ... ok
test_stratified_weights_match_overall ... ok
test_summary_reproducible_with_seed ... ok
test_wilson_contains_point_estimate ... ok
test_cohens_h ... ok
test_mcnemar_exact ... ok
test_p95_nearest_rank ... ok
test_pass_at_k_known_values ... ok
test_pass_at_k_monotone_in_k ... ok
test_wilson_known_values ... ok
test_wilson_is_tighter_with_more_samples ... ok
----------------------------------------------------------------------
Ran 16 tests in 0.019s
OK
16 个测试分三组:公式(Wilson 的 8/10 与 0/20、pass@k 的 10/2/5、McNemar 的 10/2、P95 的 nearest-rank)、数据一致性(35/36、1/48、1.18、分层加权)、可复现(同 seed 相等;不同 seed 在重采样次数较少时会得到不同端点)。它们不证明评估集有代表性,只证明统计算子没有写错。
9.9.3 三组可以自己改的对照
| 对照 | 怎么改 | 观察什么 | 预期 |
|---|---|---|---|
| 样本量 | 在 main() 里改成 items = load_items()[:24] | Wilson 宽度、CI 下界 | 24 条时区间明显变宽 |
| 分层权重 | 用生产占比(如 hard 0.5)调用 weighted_delta | 加权 Δ 是否仍为 +2.08pp | 权重错配会改变总体读数 |
| 不一致对 | 手工把一对 A-only 改成 B-only | McNemar p、CI 下界 | 净差 +3 时 p 仍不显著 |
第一组用 --json 的 n 字段确认样本量;第二组需要把 strata_table 的 n 替换为生产配额,再调用 weighted_delta;第三组改 _RAW 里的一条 a_ok/b_ok。三组都只改一个变量,并记录 seed 与数据集哈希。一次改两个变量,得到的「提升」无法归因。
第一,数据是构造的,48 条只够演示统计口径,不足以支撑任何模型结论。第二,bootstrap p 值是近似的,二元配对应以 McNemar 为准。第三,成本是 A×1.18 构造的,真实成本要按输入/输出/缓存/工具分别记账。第四,P95 在 n=48 时只由第 46 位决定,尾部分位数对样本增减极敏感。四点都是为了让实验可复现、零依赖;真实评估必须用生产分布与真实账单复验。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| 《深度学习导论》 | 第 9 讲 大规模并行训练 | 9.6 成本模型与吞吐/延迟取舍 | 背景 |
| 《Linux 系统与内核》 | 2.10 信号、2.19 作业控制 | 9.6 超时、收尸与尾延迟归因 | 交叉引用 |
| Wilson, 1927 | Probable inference, the law of succession, and statistical inference | 9.4 Wilson 区间公式 | 完整 |
| Efron & Tibshirani, 1993 | An Introduction to the Bootstrap | 9.4 百分位 bootstrap、配对重采样 | 完整 |
| McNemar, 1947 | Note on the sampling error of the difference between correlated proportions | 9.4 配对二元检验 | 完整 |
| Chen et al., 2021 | Evaluating Large Language Models Trained on Code | 9.3 pass@k 无偏估计量 | 完整 |
| Cohen, 1960 | A coefficient of agreement for nominal scales | 9.2 标注一致性 kappa | 完整 |
| Liang et al., 2022 | Holistic Evaluation of Language Models(HELM) | 9.1 多维评估、9.3 指标口径 | 背景 |
| Miller, 2024 | Adding Error Bars to Evals | 9.4 区间报告、9.9 小样本实验 | 背景 |
| NIST AI RMF 1.0, 2023 | MEASURE 函数 | 9.5 门禁与证据链 | 背景 |
这张表说明什么:本章对统计学的经典结果(Wilson、bootstrap、McNemar、kappa、pass@k)给到公式与工程口径,不重写概率论教材;对深度学习母本只借「训练/推理成本」的概念,对 Linux 母本只借「超时与收尸」的边界。表中「背景」表示只取一个概念,正文不展开母本细节。
本章陷阱
| # | 陷阱 | 症状 | 正确做法 | 去哪 |
|---|---|---|---|---|
| 1 | 只评估最终答案 | 答案对但过程越权;答案错却格式漂亮 | 答案/轨迹/工具/成本四层分开记 | 9.1 |
| 2 | 评估集用团队 demo 拼 | 离线高分,上线骤降 | 从生产日志分层抽样,保留长尾 | 9.2 |
| 3 | 难例过采样后不加权 | 总体准确率被样本结构扭曲 | 用生产占比加权,报告加权前后 | 9.2 |
| 4 | 公开题直接进评估集 | 原题 92%、改写 61% | 精确哈希 + 改写对照 + 时间切分 | 9.2、9.8 |
| 5 | 只报点估计 | 「提升 2pp」无法判断真假 | 点估计 + 区间 + n + 版本 | 9.3、9.4 |
| 6 | 跨 k 比较 pass@k | 用 pass@10 打败 pass@1 | 统一 k,同时报 pass@1 与成本 | 9.3 |
| 7 | 工具成功率当任务成功率 | 调用都成功,任务仍失败 | 调用级/任务级分开,按错误码分桶 | 9.3 |
| 8 | 只看均值延迟 | 平均降 64 ms,P95 涨 900 ms | P50/P95/P99 + 分层 + 尾部归因 | 9.6、9.8 |
| 9 | 门禁只看一个总分 | 平均涨、hard 跌、成本涨仍放行 | 质量下界 + 安全零回归 + 预算 + 分层 | 9.5 |
| 10 | 每天看 p 值、显著就停 | 假阳性率远超 5% | 固定样本量或 always-valid 序贯方法 | 9.4、9.5 |
| 11 | 把 judge 当 ground truth | judge 胜率 58%,人工复核不成立 | 偏差审计 + 校准 + 双盲 + 人工抽检 | 9.7 |
| 12 | 指标被 Goodhart | 引用数涨、precision 跌;回归集涨、线上不动 | 成对指标 + holdout + 定期换题 | 9.8 |
这张表说明什么:十二个陷阱里,只有第 5、10 条是纯统计问题,其余都是评估体系的设计问题。它们不会因为换一个更强的模型而消失;模型越强,泄漏与 judge 偏差带来的虚高越有说服力。
本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 能拆四层评估对象 | 每层给出指标与失败模式 | 9.1 |
| 能设计评估集 | 分层维度、配额、加权方式齐全 | 9.2 |
| 能做污染审计 | 至少三种检查,知道各自盲区 | 9.2、9.8 |
| 能算五个指标 | 准确率、pass@k、工具成功率、引用 P/R、轨迹成功率 | 9.3 |
| 能写 Wilson 与 bootstrap | 公式完整,说明适用条件 | 9.4 |
| 能做配对检验 | 四格表、McNemar、配对 bootstrap 都解释得清 | 9.4 |
| 能估样本量 | 给定半宽算出 n,并说明配对设计的额外要求 | 9.4 |
| 能设计门禁 | 四类条件 + 灰度 + 回滚 + 可复现 | 9.5 |
| 能画帕累托前沿 | 质量/成本/P95 三维,能排除被支配配置 | 9.6 |
| 能校准 judge | 四类偏差、双盲、kappa/TPR/TNR | 9.7 |
| 能跑本章实验 | --compare、--strata、--seed 与 16 个测试全过 | 9.9 |
Q1 48 条配对任务上,A 对 35 条、B 对 36 条,差值 +2.08pp,McNemar p=1.000。能否对外说「B 更好」?应该怎样报告?
Q2 某版本平均延迟从 969 ms 降到 905 ms,P95 从 1700 ms 升到 2600 ms。对交互式产品,应该放行吗?还需要看什么?
Q3 judge 报告 B 的胜率 58%。在把它写进评估报告之前,至少要做哪三项检查?如果校准后 kappa 只有 0.5,应该怎么用这个 judge?
答案
A1 不能。点估计为正,但 95% 配对 bootstrap 区间约为 [−6.2pp, +10.4pp],跨零;McNemar 精确 p=1.000;效应量 Cohen's ,属于可忽略量级。正确报告是「方向为正,但在 n=48 下无法与噪声区分;区间下界 −6.2pp」,并附不分层与分层读数。若业务需要判定,应扩大样本或提高不一致对数量,而不是把「不显著」说成「趋势」。
A2 不应该只看均值放行。交互式产品的体验由尾部决定:P95 从 1.7 s 到 2.6 s,意味着每 20 次就有 1 次明显变慢。还要看 P99、按难度的分层、慢请求的 span 分解(模型/工具/重试/排队)、以及成本变化。若 SLO 是 P95 ms,这个版本直接拒绝;若必须上,先给投票逻辑加置信度门槛与硬超时,再灰度验证。
A3 三项检查:交换 A/B 顺序看翻转率;按长度差分桶看长度偏差;用不同家族的 judge 复评看自我偏好,同时确认格式统一。校准至少报一致率与 Cohen's kappa,并看 TPR/TNR。kappa=0.5 属于「中等一致」,不能用于门禁或最终验收;只能用于低风险初筛与排序,且必须配人工抽检。judge 的分数应写成「某版本 judge 在某个校准集上的读数」,而不是「质量」。
下一站:10 可观测性 把本章依赖的原始材料——trace、日志、指标——展开成可回放、可归因的观测体系。