评测集怎么变成门禁
评测跑一次,产出的是一份报告。评测跑在 CI 里,它才成为门禁——而这是两件难度完全不同的事。
难点不在"跑",在于 LLM 的输出不是确定性的:同一个输入、同一份提示词、同一个模型,两次跑出来的分数可能不一样。用断言布尔值的思路去守一个带方差的测量值,得到的一定是个 flaky 门禁,然后团队会在两个星期内把它关掉。
1. 为什么不能照搬单元测试
1.1 分数是样本,不是布尔值
单元测试的前提是"确定性输入映射到唯一正确输出"。LLM 评测不是:它测的是一个随机系统的抽样结果,一次跑分只是一个样本,"通过率 0.84" 是对真实质量的一个带噪声的估计。
于是"分数是否 ≥ 0.85"这个问题本身就没有良好定义——0.84 和 0.86 的差距,可能只是噪声。
1.2 temperature 设为 0 也不解决问题
常见的第一反应是"那我把温度设成 0 不就确定了吗"。并不。
主因不是采样,而是 GPU kernel 不是 batch-invariant 的:同一个提示词,因为同批次里还塞了别的请求,浮点数走到 logits 的路径会略有差异,贪婪解码在临界处就可能分叉。所以即使 temperature=0,同一条 case 也可能这次过、下次不过。
能做的是降低方差(固定 seed、固定模型 ID、同一批次规模),做不到的是消除方差。
1.3 flaky 门禁是怎么形成的
典型过程:团队设了 faithfulness >= 0.85 的硬门禁 → 隔三差五 CI 红,但代码没变 → 大家学会 "re-run" → 后来加个 continue-on-error → 门禁名存实亡。
问题出在把测量值当成了断言值。
2. 断言要分层,确定性优先
一条 case 应该先过所有"能确定性判断"的检查,过不了就直接判失败,不必请教 judge。
2.1 第一层:确定性断言
这些是免费的、毫秒级的、可以硬门禁的:
- 输出能否解析成目标 schema(见《结构化输出与自愈》)
- 必含/必不含关键词或正则
- 引用数量、引用 ID 是否都来自召回集合
- 拒答场景是否真的拒答了
- 语言、长度、格式约束
这一层应该覆盖大部分 case,且每个 PR 都跑。
2.2 第二层:检索指标
如果系统有检索环节,检索质量可以单独测,且不需要模型——前提是你标了"这条 query 应该命中哪些文档"。
标注方式是给 golden set 里每一条带上 expected_doc_ids,然后算 Recall@K、MRR。这一层是确定性的(检索是确定性的),可以硬门禁。具体指标怎么选见《RAG 评测指标怎么选》。
2.3 第三层:LLM-as-Judge
只有前两层判不了的主观维度才交给 judge:表达是否清楚、是否答非所问、语气是否合适。
judge 是兜底而不是默认。它每次调用都要再花一次推理,成本翻倍、耗时翻倍、还引入自己的偏差。让 80% 的 case 在前两层结束,judge 只处理剩下那部分。
2.4 judge 自身也要被校准
judge 是一个没人 review 却在决定哪些版本能发布的程序。三点要求:
- 用不同的模型家族。用被评模型同家族的模型当 judge,会倾向于认同自己的输出风格,这个自偏好偏差是可复现的。
- 先和人工标注对齐。拿 50 条人工标过的样本,看 judge 的判定和人的一致率。一致率低就先改 rubric,别急着上 CI。
- rubric 写成可核查的句子。"回答是否有帮助"太主观;"回答是否明确指出退款需要 3–5 个工作日"是可核查的。
3. 门禁比 delta,不比绝对值
这是把测量值接进 CI 的关键一步。
做法:PR 分支和 main 各跑一遍同一个 golden set,只有当分数下降超过容差时才失败。
这就把一个无法回答的问题("0.84 够好吗")换成了可以回答的问题("0.84 比 main 的 0.87 差得超过噪声了吗")。
# .github/workflows/evals.yml
name: evals
on:
pull_request:
paths:
- "src/prompts/**"
- "src/ai/**"
- "src/retrieval/**"
- "evals/**"
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- name: Run smoke set
run: npx tsx evals/run.ts --suite smoke --baseline main
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Gate
run: npx tsx evals/gate.ts --max-overall-drop 0.03 --max-tag-drop 0.06
3.1 容差怎么定
不要拍脑袋。把 main 跑几次,看分数的波动范围,容差取标准差的 2–3 倍——这就是"噪声的量级",超过它才更可能是真实回退。前提是多跑几次再估:只跑三次估出来的标准差本身就不稳,按指标类型分开看(通过率类指标看比例波动,生成类指标看分数分布)。更稳的做法是直接拿 PR 与 main 的配对样本做显著性检验,差异不显著就不拦——否则门禁会拦出大量假阳性,最后被大家学会绕过。另外,API 侧的 seed 参数只能降方差、不保证确定性,别把它当硬保证用。
不同指标方差不同:格式合规率几乎没方差,faithfulness 这类主观分方差大。所以容差要按指标分别设,噪声大的指标给宽容差,否则就是制造告警疲劳。
3.2 按 tag 分开设门禁
总分下降 2% 可能意味着某个关键类别崩了 40%,而其余类别没变。只守总分会漏掉这个信号。
给每条 case 打 tag(按业务类别、按难度、按来源),门禁按 tag 分别比较。关键路径(退款、账号安全)设紧,创意类设松——同样是 0.02 的下降,在事实准确性上和在文案语气上完全不是一个量级。
3.3 确定性失败永远阻塞
schema 解析失败、安全违规、越权内容——这些不是"分数下降",是故障。无论基线如何、方差多大,有一条就阻塞。
把确定性失败和统计性回退分开处理,是让门禁既不 flaky 又不放松的关键。
4. 分档跑,别每个 PR 都跑全套
三档的定位:
| 档 | 规模 | 时机 | 判据 |
|---|---|---|---|
| Smoke | 20–50 条 | 每个 PR | 确定性断言 + 少量检索指标,60 秒内 |
| Full | 300–1000 条 | 每晚 / 发布前 | delta 对比,含 judge |
| Stress | 30–100 条 | 换模型 / 每周 | 人工看,测分类行为 |
Smoke 集必须快。超过两三分钟,工程师就会想办法绕开它,门禁就废了。
Stress 集测的是"分类行为"而非质量分:注入尝试是否被拦、该拒答的是否拒答、不该拒答的是否答了。冻起来版本化,每次换模型快照都跑一遍——新模型经常在这些边界行为上悄悄变化。
4.1 成本控制
judge 调用是评测成本的大头。几个省法:
- 确定性断言不花钱,尽量扩大第一层覆盖
- judge 结果按
(prompt_hash, output_hash)缓存 - 非阻塞的全量跑用 Batch API,异步折扣可观
- rubric 简单时用 judge 模型家族里的便宜档
一条经验:如果 judge 每次跑的花费接近被测功能本身的成本,这个评测迟早会被人关掉。
4.2 顺带记三个数
一条 case 除了通过与否,还应该记录:token 消耗(输入、输出分开)、延迟(p50 和 p95 都要)。
平均分掩盖长尾,而用户感知的是长尾。另外,一次提示词改动让通过率涨 2 个点但输出长度翻倍,这未必是赢——把成本一起展示,PR 评审时才有判断依据。
5. 数据集会腐烂
一个绿色门禁的可信度,不会高于它背后的数据集。而数据集是会坏的,坏的方式有三种。
泄漏进训练数据。 下一个模型可能已经"见过"你的评测样本,分数虚高。这不是能解决的问题,只能意识到:新模型在你的 golden set 上分数暴涨时,先怀疑数据集污染,再怀疑模型真的变强了。
和真实流量脱节。 用户问法变了,数据集还是半年前那批。定期从线上流量里采样补充。
过拟合。 每次出 bug 就加一条能抓住它的 case,加了一年之后,数据集测的是去年的 bug,不是今年的风险。
对应的三条纪律:数据集像源码一样版本化和 review;judge 提示词的改动要在 PR 里被看见;重新基线要有意为之,并在提交信息里写明原因。
5.1 基线什么时候更新
这是最容易被滥用的口子。如果每次部署后都更新基线,包括那些其实很糟的部署,那么被劣化的质量就被"正常化"了,门禁再也发现不了它。
只在确认良好的部署之后更新基线——要么人工确认,要么在生产环境稳定跑一段时间之后。
5.2 数据集从哪来
- 线上失败案例:每一个用户投诉都是一个天然测试用例,而且带业务权重
- 按类别铺覆盖:每个主要查询类别至少几条,别只测你擅长的
- 对抗样本:注入、越界、诱导拒答
- 边界样本:正好卡在系统能力边缘的问题,这类最先回退
初始不用多,50 条真实流量手标一遍就能开始。
6. 最小可启动方案
如果现在一个 case 都没有,不要一上来搭四层架构。一周可以做完的版本是:
- 拉 50 条真实请求,人工标注
- 写第一层确定性断言(schema + 必含必不含),CI 里每个 PR 跑,硬门禁
- 针对工单里最常见的那类失败写一条 rubric,加 judge,先以 PR 评论形式跑两周不阻塞
- 团队信任这个分数之后,再翻成阻塞
一周的活,抓到的回退比计划中的那个大屏要多。
这套流程针对的是"一次生成、判结果对不对"。如果被测对象是一个会调工具、会走多步的 Agent,样本和判据都要换——多一个轨迹维度,而且每个用例要跑多次看分布,见《Agent 怎么评测》。