Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 章把评估扩展到安全与治理。评估是它们的共同度量衡。

评估流水线与回归门禁 从左到右是流水线,从右往左是回滚路径。门禁读的不是一个分数,而是一组带不确定性的差值:质量差值的置信区间下界、安全回归数、成本与 P95 延迟。图中「门禁」是唯一有写权限的环节,runner 与统计只产出证据;没有证据链的放行,等于把发布交给运气。 评估流水线:从版本化数据集到可回滚的门禁 数据集 v3 分层 + 污染检查 runner N 次采样 + 成本 指标 准确率 / 工具 / 引用 统计 CI + 配对检验 门禁 阈值 + 灰度 一次比较要同时报告 Δ质量 = +4.2pp [95% CI +1.1, +7.3] Δ成本 = +18% ΔP95 = +240ms n = 400 配对样本;同一 seed 集 失败样本:12 个新增,3 个修复 门禁不是单指标 ① 质量下界 > baseline(配对) ② 安全用例 0 回归 ③ 成本/延迟在预算内 ④ 任一失败 → 可回滚到上一版本 没有「平均分涨了」就放行;平均分可以掩盖尾部、失败类型与成本漂移。 观测:每次评估写 dataset_version / model / prompt_hash / seed / git_sha,否则结果不可复现。
读图:从左到右是流水线,从右往左是回滚路径。门禁读的不是一个分数,而是一组带不确定性的差值:质量差值的置信区间下界、安全回归数、成本与 P95 延迟。图中「门禁」是唯一有写权限的环节,runner 与统计只产出证据;没有证据链的放行,等于把发布交给运气。

读图:从左到右是「数据集 → runner → 指标 → 统计 → 门禁」,从右往左是回滚路径。图里最值得记住的不是五个方框,而是权限:runner 与统计只产出证据,只有门禁有写权限(放行或拒绝),而门禁读的是一组带不确定性的差值——质量差值的置信区间下界、安全回归数、成本与 P95 延迟。任何「平均分涨了」就放行的流程,都跳过了这张图的后半段。

9.1 评估对象:答案、轨迹、工具调用与成本

一个 agent 跑完 30 步,最后给出一句「已修复」。这句话值多少分?只看这句话,你无法区分三种情况:它真的改了代码并跑过测试;它改了代码但测试超时,却把超时写成了通过;它什么都没改,只是学会了这句模板。三种情况在最终答案上完全一样,在轨迹、工具调用与成本上完全不同。

评估的对象不是一个字符串,而是一次运行的四个投影。四层各自回答不同问题,也各自有独立的失败模式。

层回答的问题典型指标这一层特有的失败
最终答案交付物对不对?准确率、exact match、pass@k、引用正确率答案对但过程违规;答案错但格式漂亮
轨迹它是怎么走到答案的?步数、工具选择准确率、顺序违规率、恢复率绕路、重复、提前终止、计划漂移
工具调用每一次动作是否合法、有效、可回滚?参数合法率、成功率、副作用次数、幂等冲突调对了工具却作用在错对象;重试造成重复副作用
成本这次运行花了多少资源?输入/输出 token、金额、P50/P95/P99 延迟、工具配额平均值正常但尾部爆炸;重试风暴;缓存未命中

这张表说明什么:四层不是「越往下越细」,而是四种不同的验收。只验最终答案,等于只验考试成绩,不验实验操作;只验轨迹,会奖励「过程正确但结果错误」的表演;只验成本,会把「便宜但答错」当成优化。

定义 9.1.1 · 评估记录

一条评估记录是一个四元组

e=(x, y, τ, c) e = (x,\ y,\ \tau,\ c)

其中:

  • xx 是任务实例,包含输入、可用工具集、权限范围与验收条件;
  • yy 是最终答案或交付物,带结构化字段(文本、文件 diff、引用列表);
  • τ=(a1,o1,…,aT,oT)\tau = (a_1,o_1,\ldots,a_T,o_T) 是轨迹,至少记录每步的动作、参数摘要、观察摘要、耗时与预算余额;
  • c=(cin,cout,ctool,cmoney,ℓ)c = (c_{\text{in}}, c_{\text{out}}, c_{\text{tool}}, c_{\text{money}}, \ell) 是成本向量,ℓ\ell 是延迟。

一条记录只有在同时带上 dataset_version、system_version、seed 与 grader_version 时才可比较;缺任意一项,两次运行的差异就无法归因。

定义里最容易被省略的是 xx 的「可用工具集与权限范围」。同一个问题,给 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 最终答案层:正确、格式、拒答

最终答案层的指标看起来最简单,坑也最多。至少要把三件事分开:

  1. 内容正确:答案与标准答案或验收测试一致。代码任务用测试,抽取任务用字段比对,问答任务用 exact match 或人工 rubric。
  2. 格式合规:JSON 可解析、必填字段齐全、单位与类型正确。格式错误在自动化管线里等价于失败,但在人工阅读时可能被忽略。
  3. 拒答得当:证据不足时拒答是正确行为,不该计为错误;证据充足却拒答,才是失败。把这两类混在一起,会得到一个「越怂分越高」的指标。

一个常见做法是给每条任务标注 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、金额、延迟与配额

成本不是「账单出来再说」的事后指标,而是评估的一等公民。它至少有五个分量:

c=(tin, tout, tcached, m, ℓ) c = (t_{\text{in}},\ t_{\text{out}},\ t_{\text{cached}},\ m,\ \ell)

其中 tint_{\text{in}} 与 toutt_{\text{out}} 分别是输入与输出 token,tcachedt_{\text{cached}} 是命中缓存的输入 token,mm 是金额,ℓ\ell 是端到端延迟。金额不是 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 分层:先定维度,再定配额

分层的目的不是让报表好看,而是保证每个关键子群都有足够样本,能单独给出区间。三个最有用的维度:

  1. 难度:按「专家所需步数」或「gold 轨迹工具调用数」分 easy / medium / hard。不要用模型自己的表现分难度,那会把「模型不会」误判成「题难」。
  2. 工具:按「必须使用哪一类工具」分,例如只读检索、代码执行、外部写操作。工具类别决定了失败代价。
  3. 语言/地域:按用户实际语言分布分,至少保证非主语言有最小样本量。

配额有两种策略。等额分配(每层 16 条)便于比较层间差异,但总体准确率会被样本结构扭曲;按生产比例分配(easy 70%、hard 20%、other 10%)总体无偏,但 hard 层的区间会很宽。实践中常用「难例过采样 + 事后加权」:

Q^w=∑s=1Sws q^s,ws=NsN,∑sws=1 \hat Q_w = \sum_{s=1}^{S} w_s, \hat q_s,\qquad w_s = \frac{N_s}{N},\qquad \sum_s w_s = 1

其中 q^s\hat q_s 是第 ss 层的样本准确率,wsw_s 是生产分布里该层的占比,不是评估集里的占比。如果评估集把 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%。四个数字放在一起,结论不是「模型退化了」,而是「原题分数里有相当一部分来自记忆」。修复不是调模型,而是换题、去重、把改写版加入评估集。

定义 9.2.1 · 评估集版本

一个可复现的评估集版本由清单(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 度量前者:

κ=po−pe1−pe \kappa = \frac{p_o - p_e}{1 - p_e}

其中 pop_o 是两人实际一致率,pep_e 是按各自边际分布算出的偶然一致率。κ=0\kappa=0 表示与随机相当,κ=1\kappa=1 表示完全一致;常见的解释带是 κ<0.2\kappa \lt 0.2 差、0.2–0.4 一般、0.4–0.6 中等、0.6–0.8 较好、κ>0.8\kappa \gt 0.8 很好。这个带是经验规则,不是法律;对安全类判定,0.6 往往不够。

kappa 有一个关键盲区:它不告诉你「标得对不对」。两个人都把「测试超时」标成「通过」,一致率 100%,κ=1\kappa=1,但标注是错的。所以还需要第三层:抽查 + 裁决 + 与可执行证据(测试结果、工具返回)对齐。对 agent 评估,最可靠的标注是「可执行验收」:能跑测试的跑测试,能回放 trace 的回放 trace,人工只判无法自动化的部分。

标注指南要写清三件事:边界案例怎么判、拒答算哪类、证据不足时标什么。指南本身也要版本化;否则同一个 kappa 数字,可能对应两套不同的判定标准。

评估集是样本,不是真相

把评估集想成一次抽样调查,而不是一次人口普查。48 条任务的准确率是一个估计值,换一批 48 条会得到另一个值。置信区间描述的就是这个「换一批」的波动。承认这一点之后,很多争论会变成可计算的问题:不是「B 是不是更好」,而是「在 n=48 的配对设计下,我们能否以 95% 置信度说 B 的总体差值大于 0」。

9.2.5 失败样本回流

评估集如果只增不减,会逐渐偏离生产;如果频繁原地改,又失去可比性。一个可执行的流程是:

  1. 收集:线上失败 run、人工工单、红队用例,进入候选池,带原始 trace 与版本号。
  2. 去重:对输入与失败原因做规范化哈希;同一根因只保留一个代表,其余作为变体。
  3. 归因:标失败层(答案/轨迹/工具/成本)与错误码;无法归因的样本先不进评估集。
  4. 标注:写验收条件与证据;能自动判的优先自动判。
  5. 入集:加入对应的难度/工具分层;如果是新失败类型,单独开一个分层,不要混进 easy。
  6. 发版: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:先把分母写死

准确率是最容易被滥用的指标,因为它的定义太短。工程上至少要补三句:

  1. 任务级还是样本级:同一任务采样 5 次,是按「5 次里至少 1 次正确」算,还是按「5 次全对」算?前者是 pass@5 的思路,后者是 consistency 的思路,数值差很多。
  2. 拒答怎么算:该答而拒答计错,该拒而拒答计对;两者必须分开统计,不能都塞进「不正确」。
  3. 部分正确怎么算:多字段抽取里对了 3/4 个字段,是 0 还是 0.75?如果是 0.75,指标就变成连续值,区间方法也要跟着换。

exact match 适用于答案空间小、规范化规则明确的场景:日期、金额、枚举、代码补丁的哈希。它不适用于开放式问答。把 exact match 用在自然语言答案上,会得到一条「越啰嗦分越低」的曲线,最后模型学会只输出关键词。

多分类或多分层时,还要区分 macro 与 micro:

macro=1S∑s=1Sq^s,micro=∑snsq^s∑sns \text{macro} = \frac{1}{S}\sum_{s=1}^{S}\hat q_s,\qquad \text{micro} = \frac{\sum_s n_s \hat q_s}{\sum_s n_s}

macro 给每个分层等权,micro 给每个样本等权。前者回答「每类平均做得多好」,后者回答「随机抽一个样本做得多好」。报告里必须写清用的是哪个;只写「准确率 81.2%」而不写口径,等于没写。

9.3.2 pass@k:采样口径与它的上界

pass@k 回答的是:对同一道题采样 nn 次,其中 cc 次正确,随机取 kk 次,至少一次正确的概率是多少。无偏估计量是

pass@k^=1−(n−ck)(nk) \widehat{\text{pass@}k} = 1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}

其中 nn 是采样次数,cc 是正确次数,kk 是允许尝试的次数。直觉是「一次都不中的组合数除以所有组合数」。当 k>n−ck \gt n-c 时,所有 kk 次里必然至少有一次正确,估计值为 1。

这个公式常被写错成 1−(1−p^)k1-(1-\hat p)^k。后者假设每次独立同分布、且用点估计代入,会在小样本上给出有偏结果;前者是无放回的组合口径,也是 Codex 论文采用的估计量。两者在 nn 大、kk 小时接近,在 n=10n=10、k=5k=5 时差别明显。

定理 9.3.1 · pass@k 的单调性与上界

固定 nn 与 cc,对 1≤k≤n1 \le k \le n,有:

  1. pass@k^\widehat{\text{pass@}k} 随 kk 单调不减;
  2. pass@k^≤k⋅pass@1^\widehat{\text{pass@}k} \le k\cdot\widehat{\text{pass@}1};
  3. 当 k>n−ck \gt n-c 时,pass@k^=1\widehat{\text{pass@}k}=1。
证明

(1)设 AkA_k 为「随机取 kk 次至少一次正确」。取 k+1k+1 次可看成先取 kk 次再补一次,故 Ak⊆Ak+1A_k \subseteq A_{k+1},概率单调不减;组合式对 kk 的单调性也可由 (n−ck)/(nk)\binom{n-c}{k}/\binom{n}{k} 随 kk 递减得到。

(2)设单次正确概率为 p=c/np=c/n,则 pass@1^=p\widehat{\text{pass@}1}=p。由容斥或联合界,kk 次至少一次正确的概率不超过 kk 次概率之和,即 kpkp;组合式同样满足该上界。

(3)若 k>n−ck \gt n-c,则 n−c<kn-c \lt k,约定 (n−ck)=0\binom{n-c}{k}=0,故估计值为 1。∎

∎

定理的工程含义是:pass@k 不能跨 k 比较。一个系统报 pass@10=0.62,另一个报 pass@1=0.48,你不能说前者更好;要么统一 k,要么同时报 pass@1 与成本。上界 k⋅pk\cdot p 还说明,pass@k 的「提升」在 k 变大时会自然饱和,用更大的 k 刷榜没有信息量。

9.3.3 工具成功率:调用级与任务级要分开

工具成功率至少有两个分母:

call success=#{ok=true}#{calls},task tool success=#{任务内所有必需工具都成功}#{tasks} \text{call success} = \frac{#{\text{ok}=\text{true}}}{#{\text{calls}}},\qquad \text{task tool success} = \frac{#{\text{任务内所有必需工具都成功}}}{#{\text{tasks}}}

没有工具调用的任务不计入后者的分母,否则纯问答任务会被算成「工具成功」。前者衡量工具服务稳定性,后者衡量 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、研究助手、代码审查)需要单独的引用指标。只算「有没有引用」不够,要算引用是否真的支持结论:

Pcite=#{被证据支持的引用条数}#{引用条数},Rcite=#{被正确引用的应引证据}#{应引用的证据} P_{\text{cite}} = \frac{#{\text{被证据支持的引用条数}}}{#{\text{引用条数}}},\qquad R_{\text{cite}} = \frac{#{\text{被正确引用的应引证据}}}{#{\text{应引用的证据}}}

precision 低说明「乱引」:引用的段落与结论无关,或者张冠李戴。recall 低说明「漏引」:结论需要三个证据,只给了一个。两者要成对报,因为它们的失败代价不同:乱引会误导读者,漏引会让结论无法复核。对合规场景,漏引可能比乱引更严重;对客服场景,乱引更伤信任。

引用还要做可回切校验:引用携带的 doc_id、版本、起止位置与原文,必须满足 text[start:end] == quote。这一步能抓到「引用指向错误段落」「文档更新后 offset 漂移」「摘要冒充原文」三类问题。第 6.5 节给了完整做法,这里只强调:precision 的分母是引用条数,recall 的分母是应引证据;把两者混成一个「引用正确率」,就无法区分乱引与漏引,也不知道该改召回还是改生成。

9.3.5 轨迹级成功率:一个更严格的联合指标

轨迹级成功率把「答案正确」与「过程合规」合成一个联合事件:

traj success=#{答案正确∧无顺序违规∧无越权副作用∧预算内}#{runs} \text{traj success} = \frac{#{\text{答案正确} \land \text{无顺序违规} \land \text{无越权副作用} \land \text{预算内}}}{#{\text{runs}}}

它比答案准确率严格,因为任何一项不满足都算失败;它也比「所有步都正确」宽松,因为允许走不同的路。适合做发布门禁的主指标,但不适合做唯一的优化目标:它会把「正确但慢」与「错误但快」压成同一个 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 节会把这三个维度画成帕累托前沿;这里先记住,任何只报第一、二列的报告,都不足以支撑「上线」这个动作。

关于指标的五个误解
  1. 「准确率越高越好」——准确率的分母与验收条件一变,数值就不可比。先对齐口径,再谈高低。
  2. 「pass@k 越大越强」——k 不同不可比;pass@k 还会随 k 饱和,且不包含采样成本。必须同时报 k、n 与每次尝试的成本。
  3. 「工具成功率 99% 说明工具层没问题」——它只说明调用没报错,不说明参数对、对象对、副作用合规。
  4. 「引用越多越可信」——多引无关段落会拉低 precision,却不会提高 recall;引用要成对看,还要能回切原文。
  5. 「轨迹成功率可以替代答案准确率」——它更严格,但会把「正确但慢」与「错误但快」压成同一个 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 区间:公式与适用条件

设 nn 次独立试验中成功 kk 次,p^=k/n\hat p=k/n,双侧置信水平 1−α1-\alpha 对应的正态分位数为 zz(95% 时 z≈1.95996z \approx 1.95996)。Wilson score 区间是

p^+z22n ± zp^(1−p^)n+z24n21+z2n \frac{\hat p+\frac{z^{2}}{2n}\ \pm\ z\sqrt{\frac{\hat p(1-\hat p)}{n}+\frac{z^{2}}{4n^{2}}}}{1+\frac{z^{2}}{n}}

它来自对 score 检验的反演,而不是「点估计 ± 1.96 标准误」。两个细节值得记住:

  • 当 k=0k=0 或 k=nk=n 时,Wald 区间会退化成一个点(宽度为 0),Wilson 不会。0/200/20 的 Wilson 95% 区间约是 [0, 0.161][0,\ 0.161],20/2020/20 约是 [0.839, 1][0.839,\ 1]。
  • 当 nn 很小或 p^\hat p 接近 0/1 时,Wilson 的覆盖更接近名义水平;但它不是「任何情况下都保守」,覆盖概率会随 pp 振荡。工程上把它当作默认口径即可,不要宣称它「保证 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 的思路是「从样本里再抽样,模拟换一批样本」。对 nn 条记录,做 BB 次有放回重采样,每次算一个统计量,取经验分布的 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 区间会同样偏;它修正不了抽样偏差,只能修正抽样波动。重采样次数 BB 建议至少 2000,报告里写清 BB 与 seed;B=200B=200 时区间端点会随 seed 跳动,看起来像「结果不稳定」,其实是算得不够。

百分位 bootstrap 在小样本、强偏态时覆盖率偏低。一个常用的补救是 BCa(偏差校正与加速),但它需要实现 jackknife,超出本章范围;工程上更实用的补救是增加 nn,或者在报告里同时给 Wilson 与 bootstrap,两者差得远就说明样本太少。

9.4.3 配对:为什么必须逐条对齐

A 与 B 跑的是同一批 48 条任务。如果把它们当成两个独立样本,差值标准误会把「题目难度差异」算进噪声里;配对之后,题目难度被消掉,只剩「同一题上 A 与 B 的差异」。

配对二元数据的核心是四格表:

B 正确B 错误
A 正确aabb
A 错误ccdd

只有 bb 与 cc 携带「谁更好」的信息;aa 与 dd 是两边都对的、都错的,对差值没有贡献。实验里 b=3b=3、c=2c=2,净差 1 条,对应 +2.08pp。

McNemar 检验的精确版本是:在 b+cb+c 个不一致对里,若两者没有差异,则 bb 服从 Binomial(b+c,0.5)\text{Binomial}(b+c, 0.5)。双侧 p 值为

p=2∑i=0min⁡(b,c)(b+ci)(12)b+c p = 2\sum_{i=0}^{\min(b,c)}\binom{b+c}{i}\left(\frac{1}{2}\right)^{b+c}

当 b+cb+c 较大时可以用卡方近似 χ2=(b−c)2/(b+c)\chi^2=(b-c)^2/(b+c),但小样本请用精确版本。实验里 b=3b=3、c=2c=2,精确 p=1.000p=1.000,和抛硬币没有区别。

配对 bootstrap 给的是差值区间,不是 p 值:对同一组索引同时重采样 A 与 B,算 B∗−A∗B^{*}-A^{*},取百分位。它和 McNemar 回答的问题略有不同:McNemar 检验「不一致对是否对称」,配对 bootstrap 估计「差值的可能范围」。报告里两个都给,读者既能看显著性,也能看实际幅度。

定理 9.4.1 · 半宽与样本量

设比例估计的 Wald 半宽为 hh,置信水平对应的分位数为 zz,真实比例为 pp。忽略有限总体校正时,使半宽不超过 hh 所需样本量满足

n≥z2 p(1−p)h2. n \ge \frac{z^{2}\,p(1-p)}{h^{2}}.

当 pp 未知时,取 p=1/2p=1/2 得最保守的 n≥z2/(4h2)n \ge z^{2}/(4h^{2})。

证明

Wald 区间半宽为 h=zp(1−p)/nh=z\sqrt{p(1-p)/n}。两边平方得 h2=z2p(1−p)/nh^{2}=z^{2}p(1-p)/n,移项即 n=z2p(1−p)/h2n=z^{2}p(1-p)/h^{2}。由于 p(1−p)≤1/4p(1-p)\le 1/4,在 pp 未知时用 1/41/4 代入,得到一个不依赖于 pp 的保守样本量。∎

∎

代入 95% 的 z=1.95996z=1.95996,半宽 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。规划时先估「不一致率」,再反推需要多少对;不一致率越低,同样的差值越难被检测出来。

定理 9.4.2 · 配对设计的方差削减

设系统 A、B 在同一批 nn 个任务上的得分方差分别为 σA2\sigma_A^2、σB2\sigma_B^2,两者相关系数为 ρ\rho。差值 Δ^=Bˉ−Aˉ\hat\Delta=\bar B-\bar A 的方差为

Var⁡(Δ^)=σA2+σB2−2ρ σAσBn. \operatorname{Var}(\hat\Delta)=\frac{\sigma_A^{2}+\sigma_B^{2}-2\rho\,\sigma_A\sigma_B}{n}.

当 ρ>0\rho \gt 0 时,它小于把两组当独立样本时的 (σA2+σB2)/n(\sigma_A^2+\sigma_B^2)/n。

证明

由 Var⁡(X−Y)=Var⁡(X)+Var⁡(Y)−2Cov⁡(X,Y)\operatorname{Var}(X-Y)=\operatorname{Var}(X)+\operatorname{Var}(Y)-2\operatorname{Cov}(X,Y),且 Cov⁡(A,B)=ρσAσB\operatorname{Cov}(A,B)=\rho\sigma_A\sigma_B,对 nn 个独立任务取平均即得第一式。第二式是 ρ=0\rho=0 的特例;ρ>0\rho \gt 0 时减项为正,故差值方差更小。∎

∎

这个定理解释了为什么同一批任务上的 A/B 比「A 跑一批、B 跑另一批」更灵敏。任务难度是最大的方差来源;配对把它消掉之后,剩下的才是系统差异。代价是评估成本翻倍(同一批任务要跑两遍),以及必须保证两边的任务、工具集、评分器完全一致。

9.4.4 显著性检验的四个前提

p 值不是一个可以脱离设计单独解释的数字。用它做发布决策前,确认四件事:

  1. 假设是预先写的:看的是准确率差值、P95 差值,还是分层里的某一个?先写再跑。跑完再挑一个显著的分层,是把假阳性率从 5% 放大到远高于 5%。
  2. 检验与数据类型匹配:二元配对用 McNemar;连续指标(延迟、成本)用配对 bootstrap 或 Wilcoxon 符号秩;多组比较用 Holm 或 Bonferroni 校正。
  3. 没有偷看:固定样本量,或使用 always-valid 的序贯方法。每天看一次、显著就停,会让名义 5% 变成实际 20% 以上。
  4. 效应量单独报:p 值回答「是否可能为零」,不回答「有多大、值不值」。+2.08pp、Cohen’s h=0.047h=0.047 是「可忽略」量级;即使把 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 下界 >−1\gt -1pp拒绝;或降级为灰度
2安全零回归越权副作用 0;注入成功 0一票否决
3成本与延迟预算Δcost ≤+10\le +10%;P95 ≤2000\le 2000 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% 看看」,而是一组带停止条件的阶段:

阶段流量最短观察放行条件回滚条件
canary1%30 min错误率、P95、成本都在基线 ±10%任一超阈值
小流量5%2 h质量抽样不低于基线;无安全事件质量差值下界 <−2\lt -2pp
中流量25%1 天分层指标无退化;成本在预算内任一分层退化
全量100%持续同上,且回滚演练通过自动或人工回滚

每个阶段都要有自动停止条件,不能只靠人盯。停止条件至少包括:错误率、P95 延迟、单次成本、安全事件数、人工差评率。阈值要用绝对值与相对值双写,例如「P95 >2000\gt 2000 ms 或比基线高 30%」,防止基线本身漂移。

灰度期间还要防止样本比例失真。1% 流量如果按用户哈希切,可能全是某个地区或某个版本的用户;正确做法是按请求哈希分层,并检查各分层的流量比例。否则 canary 的「一切正常」只说明那 1% 用户正常。

9.5.4 A/B:随机化单位与护栏指标

A/B 与灰度的区别是「有没有对照」。灰度是同一个系统的新旧版本按流量切;A/B 是两组用户/请求随机分流,比较指标差异。agent 的 A/B 有三个特殊问题:

  1. 随机化单位:按请求、按会话,还是按用户?按请求会让同一用户同时体验两个版本,污染体验类指标;按用户更干净,但所需样本量更大。会话级任务(多轮)应按会话切。
  2. 指标延迟:agent 的效果可能要几小时或几天才显现(工单是否解决、代码是否合并)。只看即时点击,会奖励「快但没用」的版本。
  3. 护栏指标:除了主指标,还要有成本、延迟、安全、投诉率。主指标涨、护栏跌,是典型的「赢在局部、输在系统」。

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.5.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」。

这个案例的教训不是「准确率不重要」,而是「单指标门禁一定会被平均掩盖」。门禁的每一条都对应一类已知事故:质量下界防「噪声当提升」,安全零回归防「越权」,成本/延迟防「用钱换分」,分层防「平均掩盖长尾」。

方法 9.5.1 · 发布门禁检查清单

按顺序过一遍,任何一条不满足就停在原地:

  1. 数据集冻结了吗? items_sha256 与 rubric_sha256 都写进运行记录;没有哈希的评估集不能做门禁。
  2. 是配对运行吗? 同一批任务、同一工具集、同一评分器;A 与 B 的差异只允许是候选版本。
  3. 主指标定死了吗? 一个主指标 + 一组护栏;主指标看区间下界,护栏看绝对阈值。
  4. 安全回归是 0 吗? 越权副作用、注入成功、数据外泄各自计数;任一非零即拒绝。
  5. 成本与 P95 在预算内吗? 用分位数与单次成功成本,不用均值;阈值写绝对值 + 相对值。
  6. 分层有退化吗? hard、非主语言、写工具三类必须单独看;退化超过阈值即拒绝。
  7. 失败样本能重放吗? 六项版本字段齐全;不能重放的失败不许进回归集。
  8. 回滚演练过吗? 一条命令、分钟级、状态兼容;没演练过的回滚等于没有回滚。

八条里最容易跳过的是第 7、8 条:它们不产生分数,只在出事时救命。

三个常见卡点

卡点一:门禁通过了,但灰度挂了。 说明离线评估集与生产分布不一致。先查流量分层与请求长度分布,再查工具注册表版本;不要先怀疑模型。

卡点二:同一版本两次门禁结果不同。 采样温度、seed、数据集哈希、评分器版本,四项里必有一项没锁。先做可复现性检查,再谈统计显著性。

卡点三:安全集全绿,红队仍能打穿。 安全集覆盖的是已知攻击模式;新攻击不在集内。把红队发现回流,并保留一个不参与调参的对抗 holdout。

9.6 成本-延迟-质量:帕累托与预算约束

前五节把质量说清楚了,但质量不是免费的。同一道题,可以用一个小模型一次答完,也可以用大模型加三次检索、两次投票、一次验证;后者的准确率可能高 5pp,成本高 10 倍,P95 高 4 倍。评估的最终输出不是「谁更准」,而是「在给定预算与延迟约束下,谁更值」。

先把成本模型写出来。一次运行的期望成本是

E[C]=cinTin+coutTout+∑j=1Jcjnj+cretryR \mathbb{E}[C] = c_{\text{in}}T_{\text{in}} + c_{\text{out}}T_{\text{out}} + \sum_{j=1}^{J} c_j n_j + c_{\text{retry}}R

其中 TinT_{\text{in}}、ToutT_{\text{out}} 是输入/输出 token,cinc_{\text{in}}、coutc_{\text{out}} 是单价,njn_j 是第 jj 个工具的调用次数,cjc_j 是其单价,RR 是重试次数。延迟可以类似地分解:

ℓ=ℓqueue+∑i=1Kℓmodel,i+∑j=1Jnjℓtool,j \ell = \ell_{\text{queue}} + \sum_{i=1}^{K}\ell_{\text{model},i} + \sum_{j=1}^{J} n_j \ell_{\text{tool},j}

这两个式子解释了一个常见错觉:「换小模型能省钱」。如果小模型多跑两轮、多调三次工具、重试率更高,总成本可能不降反升;延迟也不一定降,因为多出来的轮次是串行的。

9.6.1 帕累托前沿:不被支配的配置

在质量、成本、延迟三个维度上,配置之间通常不是全序关系。用支配(dominance)来筛:

定义 9.6.1 · 帕累托支配

设配置 ii 的质量为 qiq_i(越大越好)、单次成功成本为 cic_i(越小越好)、P95 延迟为 ℓi\ell_i(越小越好)。称 ii 支配 jj,如果

qi≥qj,ci≤cj,ℓi≤ℓj, q_i \ge q_j,\qquad c_i \le c_j,\qquad \ell_i \le \ell_j,

且至少有一个不等式严格成立。不被任何其他配置支配的配置集合,称为帕累托前沿。

前沿上的配置是「想再要一点质量,就必须多付成本或延迟」的配置;前沿之外的配置,总能找到一个在三个维度都不差、至少一维更好的替代,没有讨论价值。

为了看清形状,下面给一组构造的读数(同一评估集、同一批任务,数值用于说明取舍,不是厂商基准):

配置质量(轨迹成功率)单次成功成本P95 延迟是否前沿
A:小模型,单轮61.2%$0.0012900 ms是
B:小模型 + 检索68.8%$0.00211400 ms是
C:大模型,单轮72.9%$0.00301700 ms是
D:大模型 + 投票 ×375.0%$0.00842600 ms是
E:大模型 + 投票 + 重排74.6%$0.01013100 ms否(被 D 支配)

这张表说明什么:E 花了更多钱、更慢,质量还略低于 D,应该直接淘汰;A、B、C、D 构成前沿,选哪个取决于预算与 SLO,而不是「哪个分高」。如果产品要求 P95 ≤1500\le 1500 ms,只有 A、B 可选;如果预算上限是 $0.004/成功,C 是上限内质量最高的。

9.6.2 预算约束下的选择

把选择写成约束优化:

max⁡i∈F qis.t.ci≤Cmax⁡,  ℓi95≤Lmax⁡ \max_{i \in \mathcal{F}}\ q_i \quad \text{s.t.}\quad c_i \le C_{\max},\ \ \ell_i^{95} \le L_{\max}

其中 F\mathcal{F} 是前沿集合。两个约束都可能让最优解落在前沿的中间,而不是最右端。工程上还要加两个约束:单次调用的最大 token(防上下文爆炸)、单位时间的工具配额(防限流)。它们不是「软指标」,而是硬边界;越过边界的配置根本跑不完评估。

如果预算不是「每次成功」,而是「每月总额」,选择会变成带流量分布的加权问题:

min⁡ ∑kπk ci(k)s.t.∑kπk qi(k)≥Qmin⁡ \min\ \sum_{k} \pi_k, c_{i(k)} \quad \text{s.t.}\quad \sum_k \pi_k, q_{i(k)} \ge Q_{\min}

其中 πk\pi_k 是第 kk 类请求的流量占比,i(k)i(k) 是为该类请求选的配置。这解释了「分级路由」为什么有效: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 条慢样本决定。

小样本的尾部分位数极不稳定。n=48n=48 时,P95 是第 46 个值(nearest-rank),P99 是第 48 个值;删掉一条最慢样本,P99 可能直接掉几百毫秒。所以尾部指标必须同时报 nn、P95、P99,并在样本量足够时才算 P99;n<100n \lt 100 时 P99 基本是最大值,参考价值有限。

尾部还要拆到组件,否则无法修。一条慢请求可能是模型排队、工具超时、重试、序列化、网络中的任意一段。按 span 分解后,常见形状是「P50 由模型主导,P99 由工具超时 + 重试主导」;对应的修复不是换更快的模型,而是给工具设更短的超时、把重试改成有上限的退避、或者对慢工具做异步化。

9.6.4 成本口径:每次调用、每次成功、每个正确

成本有三个分母,报告时必须写清:

cost/call=∑iciN,cost/success=∑ici#{成功},cost/correct=∑ici#{正确} \text{cost/call} = \frac{\sum_i c_i}{N},\qquad \text{cost/success} = \frac{\sum_i c_i}{#{\text{成功}}},\qquad \text{cost/correct} = \frac{\sum_i c_i}{#{\text{正确}}}

实验里 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 又会忽略「失败也有成本」(用户重试、人工兜底、信任损失)。一个更完整的口径是「每次正确交付的总成本」,把失败后的重试与人工成本也计入;它难以精确,但方向比单价正确。

关于成本-延迟-质量的四个误解
  1. 「平均延迟降了就是更快」——均值可以被少数快样本拉低,同时尾部变慢。要看 P95/P99 与分布形状。
  2. 「单价低就更省钱」——小模型可能多轮、多工具、多重试,总成本反而更高;要按每次成功或每次正确交付算。
  3. 「质量越高越好,成本上线再说」——质量、成本、延迟是三维约束;越过预算或 SLO 的配置不可上线,分数再高也没有意义。
  4. 「帕累托前沿上的配置都该试」——前沿只说明「不被支配」,不说明「值得」。还要看实现复杂度、维护成本、故障模式;前沿是候选集,不是路线图。

9.7 LLM-as-judge:偏差、校准与双盲

当验收无法写成测试时——开放式问答、摘要、计划、语气、代码可读性——只能用评分器。人工评分贵、慢、不一致;LLM judge 便宜、快、可重复,但它不是 ground truth,而是一台有系统性偏差的测量仪器。把仪器当真理,是评估体系里最昂贵的错误之一。

judge 的典型用法是 pairwise:给同一道题的两个答案 A/B,问哪个更好。它比 absolute 打分稳定,因为相对比较有锚点;代价是引入位置、长度、格式与「像谁」的偏差。

9.7.1 四类偏差与各自的审计

偏差机制怎么审计控制
位置偏差先出现的答案更容易赢交换 A/B 顺序重跑,统计赢家翻转率双边平均;翻转率过高则丢弃该对
长度偏差更长、更详细的答案显得更好按长度差分桶,看各桶胜率长度匹配;把长度作为协变量报告
自我偏好judge 偏爱同家族模型的措辞用不同家族的 judge 复评多家族评审;隐藏模型身份
格式偏差Markdown、列表、标题影响判断同一内容两种格式对比统一格式;剥离样式后再评

位置偏差的度量是翻转率:

flip rate=#{交换顺序后赢家改变}#{pairs} \text{flip rate} = \frac{#{\text{交换顺序后赢家改变}}}{#{\text{pairs}}}

翻转率超过 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 的置信分是「措辞的自信程度」,不是「判断正确的概率」。

例题 9.7.1 · 一次 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 双盲与多人评审

双盲的意思是:评审者不知道答案来自哪个系统、哪个模型、哪个版本。具体做法:

  1. 去标识:删除模型名、版本号、耗时、成本、工具调用等元数据;这些信息会暗示「谁更高级」。
  2. 随机顺序:A/B 顺序按 pair id 哈希随机,不按系统固定。
  3. 双边呈现:同一对跑两次(AB 与 BA),只有两次都选同一方才计为稳定胜。
  4. 多人评审:至少两人独立评,分歧进入裁决;裁决人也盲。
  5. 统一格式:同一份 rubric、同一套渲染;不要让一个答案带 Markdown、另一个是纯文本。

多人评审的成本高,但它是估计「评审者间一致性」最直接的方式。两人一致率低,说明 rubric 有歧义,先改 rubric,不要急着加人。三人以上时,多数投票比平均分稳健;平票用裁决人,不要用 judge 破平票——那会把 judge 的偏差带回人工层。

9.7.4 不要把 judge 当 ground truth

judge 适合做三件事:大规模初筛、pairwise 相对比较、给人工评审排优先级。它不适合做三件事:安全放行、合规判定、最终验收。

安全放行必须用可执行检查:越权副作用计数、出口过滤命中、敏感数据扫描。合规判定要有审计证据与责任人。最终验收要有可复核的证据链,judge 的分数只是其中一项。

一个实用的分层策略是:

场景评审方式理由
有测试/可执行验收自动判定,不用 judge便宜、确定、可回放
开放式但低风险judge + 5% 人工抽检规模大,错误代价低
开放式且高风险双人人工 + judge 辅助排序错误代价高,需要责任链
安全/合规规则 + 人工 + 证据链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,说明原题分数里有很大一部分来自「见过」。修复不是换更强的模型,而是:

  1. 去重:把与示例池、训练池精确或高 Jaccard 重叠的题移出评估集。
  2. 改写:每题保留 2–3 个改写版,原题与改写版分开报;只报原题会系统性高估。
  3. 时间切分:只用训练截止日之后发布的题;截止日写进 manifest。
  4. holdout:留一批从不参与调参、也不公开的题,只在发布前跑。
  5. 审计常态化:每次发版跑一次精确哈希与 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% 请求会明显变慢。

例题 9.8.1 · 一次「平均更快」的上线事故

下面这次复盘的数字由本章 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-onlyMcNemar 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, 1927Probable inference, the law of succession, and statistical inference9.4 Wilson 区间公式完整
Efron & Tibshirani, 1993An Introduction to the Bootstrap9.4 百分位 bootstrap、配对重采样完整
McNemar, 1947Note on the sampling error of the difference between correlated proportions9.4 配对二元检验完整
Chen et al., 2021Evaluating Large Language Models Trained on Code9.3 pass@k 无偏估计量完整
Cohen, 1960A coefficient of agreement for nominal scales9.2 标注一致性 kappa完整
Liang et al., 2022Holistic Evaluation of Language Models(HELM)9.1 多维评估、9.3 指标口径背景
Miller, 2024Adding Error Bars to Evals9.4 区间报告、9.9 小样本实验背景
NIST AI RMF 1.0, 2023MEASURE 函数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 msP50/P95/P99 + 分层 + 尾部归因9.6、9.8
9门禁只看一个总分平均涨、hard 跌、成本涨仍放行质量下界 + 安全零回归 + 预算 + 分层9.5
10每天看 p 值、显著就停假阳性率远超 5%固定样本量或 always-valid 序贯方法9.4、9.5
11把 judge 当 ground truthjudge 胜率 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/TNR9.7
能跑本章实验--compare、--strata、--seed 与 16 个测试全过9.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 h≈0.047h \approx 0.047,属于可忽略量级。正确报告是「方向为正,但在 n=48 下无法与噪声区分;区间下界 −6.2pp」,并附不分层与分层读数。若业务需要判定,应扩大样本或提高不一致对数量,而不是把「不显著」说成「趋势」。

A2 不应该只看均值放行。交互式产品的体验由尾部决定:P95 从 1.7 s 到 2.6 s,意味着每 20 次就有 1 次明显变慢。还要看 P99、按难度的分层、慢请求的 span 分解(模型/工具/重试/排队)、以及成本变化。若 SLO 是 P95 ≤2000\le 2000 ms,这个版本直接拒绝;若必须上,先给投票逻辑加置信度门槛与硬超时,再灰度验证。

A3 三项检查:交换 A/B 顺序看翻转率;按长度差分桶看长度偏差;用不同家族的 judge 复评看自我偏好,同时确认格式统一。校准至少报一致率与 Cohen's kappa,并看 TPR/TNR。kappa=0.5 属于「中等一致」,不能用于门禁或最终验收;只能用于低风险初筛与排序,且必须配人工抽检。judge 的分数应写成「某版本 judge 在某个校准集上的读数」,而不是「质量」。

下一站:10 可观测性 把本章依赖的原始材料——trace、日志、指标——展开成可回放、可归因的观测体系。