LLM 评测在 CI 中有多嘈杂?
How noisy are LLM evals in CI?
作者统计了公开仓库 GitHub Actions 默认分支上连续两次运行的评测结果,发现通过的评测在下一次运行中失败的比例约为 2.5%,五个仓库合计 2690 条评测中有 68 条出现这种情况。
你修改了一个提示词,提交了一个拉取请求,然后你的评估套件从 48 个通过变成了 46 个。是你的改动破坏了两个评估,还是它们本来就会失败?我们测量了在公共仓库的 CI 中,通过的评估在下一次运行时失败的概率,以及一个套件能透过这种噪声看到多大的破坏。
通过的评估大约每 40 次会有 1 次偶然失败
我们提取了每个仓库从 GitHub Actions 在其默认分支上上传的评估结果,将连续的运行对齐,并统计了在一次运行中通过、在下一次运行中失败的评估。从未到达模型的失败(缺少 API 密钥、速率限制、超时)被排除在外。
| 仓库 | 格式 | 每次运行的评估数 | 运行次数 | 通过的评估在下一次运行中失败 |
|---|---|---|---|---|
| JRichlen/agent-plugins | promptfoo | 约 12 | 33 | 389 个中的 2 个(0.5%) |
| PrefectHQ/prefect-mcp-server | pytest、JUnit XML | 21 | 27 | 508 个中的 0 个(0%) |
| abpai/skills | JUnit XML | 28 | 12 | 272 个中的 2 个(0.7%) |
| launchdarkly/ai-tooling | promptfoo | 59 | 13 | 651 个中的 18 个(2.8%) |
| evloghq/evlog | JUnit XML | 约 17 | 53 | 870 个中的 46 个(5.3%) |
在这五个仓库中,2,690 个通过的评估中有 68 个在下一次运行中失败:2.5%。提交在运行之间落到了默认分支上,因此其中一些失败可能是真实的更改。这些比率是仅由偶然因素导致的上限。
更大的套件本身就显示出偶然性。onyx-dot-app/agent-wiki 每晚运行约 2,268 个评估。在 56 个夜晚中,中位数为 73 个评估在某一晚通过后下一晚失败,且计数从未达到零。在代码完全没有变化的 22 个夜晚中,中位数为 73.5。在整个期间,2,268 个评估中有 541 个至少通过和失败各一次。
在 2.5% 到 3% 的情况下,一个包含 50 个评估的套件在任意两次运行之间会丢失一两个,而一个包含 300 个评估的套件会丢失八九个。一个没有改变任何内容的拉取请求经常会显示“回归”。
我们排除的两个套件,以及原因
另外两个仓库被测量但被排除在表格之外,因为它们的数字测量的不是模型。
- 0% 但什么都没测量到。 TimMoyence/Innov-mind-museum 的结果中唯一的评估在每次运行中都通过。它调用的端点返回 HTTP 401“需要令牌”,而评分器将错误消息接受为答案。
- 45%。 chmonitor/chmonitor 的评估调用其位于 Cloudflare 之后的生产端点。其许多输出是 Cloudflare 质询页面(“稍等片刻……”)和“Worker 超出资源限制”错误,而不是答案。
这很常见。在 PrefectHQ/prefect-mcp-server#200 上,20 个评估中有 14 个在第一次推送时失败。这 14 个都是同一个 404:评判模型 claude-opus-4-1-20250805 已被弃用。该拉取请求没有破坏任何东西,下一次推送更换了评判模型,全部 20 个都通过了。(案例研究)
在你从通过率中读出任何含义之前,请检查每个结果,无论通过还是失败,都来自模型,而不是来自错误页面。
你的套件能看到多大的破坏?
假设套件中每个通过的评估在任意一次运行中偶然失败的概率为 p。那么一次没有真实更改的运行会丢失大约 n × p 个评估。一个拉取请求的失败只有在数量超过 20 个拉取请求中 19 个由偶然因素产生的失败时才会凸显出来。以下是更改必须破坏多少个评估才能达到这一点:
| 套件 | 每次运行偶然失败 | 超出偶然的阈值 | 一半情况下能看到的破坏 | 80% 情况下能看到的破坏 | 看到 2 个评估破坏的概率 |
|---|---|---|---|---|---|
| 20 个评估,2% 噪声 | 0.4 | 3 个失败 | 3 个评估 | 3 个评估 | 30% |
| 50 个评估,3% 噪声 | 1.5 | 5 个失败 | 4 个评估 | 5 个评估 | 17% |
| 100 个评估,3% 噪声 | 3 | 7 个失败 | 4 次评估 | 6 次评估 | 17% |
| 300 次评估,5% 噪声 | 15 | 22 次失败 | 8 次评估 | 11 次评估 | 11% |
| 2,268 次评估,3.2% 噪声 | 73 | 88 次失败 | 16 次评估 | 23 次评估 | 6% |
当你在整个套件中统计失败次数时,更大的套件并不能更好地发现小的破坏。它的偶然失败会随之增长,而一些真正的失败会淹没其中。上面那个 2,268 次评估的套件可能因一次真实回归而丢失 15 次评估,而且它往往看起来和任何一个平常的夜晚没什么两样。
它能发现的是某个从不失败的评估出现了破坏。在 HolmesGPT/holmesgpt#1452 中,一次重构停止向 agent 发送其 runbook 目录,一个 8 次评估套件中的某个评估失败了:162_get_runbooks,而它自 1 月初以来的全部 230 次运行都通过了。该仓库自己的评估检查仍然显示绿色。像这样的评估出现单次失败并非偶然。(案例研究)
什么有帮助
测量你自己的噪声。在未更改的默认分支上重新运行你的评估,或者保留每次默认分支运行的结果,并统计通过的评估失败的频率。其他每一个决策都取决于这个数字,而且不同套件之间差异很大,如表所示。
将每个评估与其自身历史进行比较。不要将通过率与固定阈值进行比较。将拉取请求的结果与基础分支上相同的评估配对,并根据该评估此前失败的频率来判断一次失败。一个从不失败的评估出现失败,比每周都失败的评估出现三次失败更有意义。
将错误与答案分开。缺少密钥、速率限制、已退役的模型或错误页面都不是结果。将其计为“无法运行”,而不是通过或失败。
以稳定的评估作为门禁,其余则跟踪观察。与特定行为绑定、每次运行都通过的评估,可以在单次失败时阻止合并。广泛的质量扫描更适合作为趋势来观察,而不是用作门禁。
重复重要的评估。将一次评估运行 3 到 10 次并判断通过的比例,可以降低其噪声。在 HumanEval 上,Meta 的研究人员发现,配对并取平均可将达到显著性的最小差异从 12 个百分点降低到 2 到 4 个百分点。如果你随后单独测试每个评估,请针对你测试的数量进行校正,否则有些评估会因偶然看起来像是坏了。
在相信单次失败之前先重新运行。一个偶然失败率为 3% 的评估连续失败两次的概率约为千分之一。在 launchdarkly/ai-tooling#189 中,该仓库自己的评估报告显示 configs-create 相对于 5 月记录的基线下降了 25 个百分点,并且其门禁在 onboarding(4 个中的 2 个)上失败。导致下降的那个测试在此拉取请求之前,也曾在 main 分支的 12 个夜晚中有 7 个夜晚失败,而重新运行同一提交则通过了门禁。(案例研究)
evalship 如何做到这一点
evalship 读取你的 GitHub Actions 已经上传的评估结果,并使用上述方法将每个拉取请求与你的默认分支进行比较。它会在默认分支上最近 30 次运行中测量每个套件的噪声,将偶然可以解释的失败标记为“在偶然范围内”,将无法运行的结果搁置一旁,并在拉取请求上评论剩下的内容。PR 分析如何工作 中有详细信息。它对公共仓库免费,设置只需将一段提示粘贴到你的编码 agent 中:开始使用。
延伸阅读
- Evan Miller,Adding Error Bars to Evals(Anthropic,2024):评估的标准误差、聚类、配对比较和功效分析。
- Wang 等人,测量 LLM 评估中的所有噪声(Meta,2025):为什么对重复运行取平均有助于配对比较。
- OpenRouter,如何在 CI 中基于 LLM 评估对拉取请求进行门控:在设定阈值之前先测量噪声底限。
来源:Hacker News · evalship.com