将 Eval 作为 Pull Request 评论而非任务:在代码审查中嵌入 LLM 质量门禁
许多自称“有评估(evals)”的团队,其实际情况是:有一个仪表板,某人每周运行一次测试套件,然后将数据粘贴到没人看的 Slack 频道。评审人员批准提示词(prompt)更改时,甚至根本没看过它是否影响了测试套件,而回归问题(regression)两周后才在客户反馈单中显现。评估确实存在,但评估并未进入开发循环。
解决办法在于结构,而非意愿。只有当评估存在于变更发生的地方——即 Pull Request(PR)评论中,紧挨着代码差异(diff),并带有单个 PR 的增量变化和评审员无法忽视的回归提醒时,评估才能真正起到质量把关的作用。在其他任何地方,它们都只是表演性的产物:投入了大量精力构建,却什么也拦截不到。
值得效仿的模式早已存在于你的代码库中。代码覆盖率(Code coverage)在 20 年前也经历过同样的历程:从一个没人点开的 HTML 报告的夜间任务,演变为在 PR 中以置顶评论形式发布的增量报告,并带有逐文件提醒。大多数工程师已经不记得覆盖率不在行内显示的时候了,而这正是重 点——一旦反馈与变更相邻,这种实践就无需推销而能自发延续。
什么是实践中所谓的“我们有评估”
走进任何一个开展大语言模型(LLM)工作超过一个季度的团队,询问他们的评估在哪里运行。答案几乎总是一样的:有一个脚本,针对“黄金数据集”运行,结果保存在 Notebook、电子表格中,或者如果团队更成熟的话,保存在一个托管的仪表板上。在发布前、通过定时任务(cron)或是在产品经理(PM)询问“我们的摘要功能改进了吗?”时,手动调用该脚本。
这种设置将评估变成了一项“任务”——一种你需要触发、观察运行并进行解读的事。任务具有惯性。一个需要 10 分钟的任务不会在每次提交时触发;一个生成百行表格的任务不会在每次提交时被阅读;一个存在于代码评审工具之外的任务不会阻碍代码合并。团队制造了一个测量仪器,却把它放在了离实验现场三个房间远的地方。
自然的偏移是显而易见的。评估只会在想起来运行的日子运行。涉及提示词修改的 PR 往往基于工程师“感觉还行”以及评审员在阅读新模板时没发现明显错误而合并。评估在运行时,往往在某个没人注意的片段中倒退了两三个点,而团队并不会将这种回归与导致它的合并关联起来,因为在合并发生的地方,这种关联是不可见的。
这并非评估框架的工具性失败。框架本身没问题——DeepEval、promptfoo、Braintrust、Latitude、Traceloop 以及其他数十种工具都能在黄金数据集上生成称职的指标。失败的原因在于,这些指标产生的地方并非做出合并决策的表面。
代码覆盖率做对了什么
作为指标,代码覆盖率一直都很薄弱。运行到某行代码不等于测试了它;一个返回错误类型的函数如果有 100% 的覆盖率,也依然是 100% 覆盖;追求数字的团队会写出只跑行数而不验证行为的测试。尽管如此,覆盖率作为一种“实践”能够存续,是因为一个具体的原因:报告进入了 PR 评论。
演进过程至关重要。早期的覆盖率工具生成 HTML 报告——这是一个在浏览器中打开的目录,逐行显示颜色标注。它们确实有用,但几乎没人用。下一步是仪表板,托管在团队 Wiki 中某个逐渐失效的链接上。更有用一点,但对合并决策依然没有承重作用。让覆盖率坚持下来的原因是每个 PR 上自动置顶的机器人评论,它包含三项信息:整体覆盖率百分比、与基准分支的增量,以及覆盖率下降的已修改文件列表。
第三项信息才是真正起作用的。知道整体覆盖率从 87.2% 降到 87.1% 是一个可以被合理化忽视的事实。但知道在这个特定的 PR 中,payments/refund.ts 的覆盖率从 94% 降到了 71%,这是评审员一定会质问你的事情。差异(diff)与指标并列,且指标范围限定在变更内,而不是汇总在整个代码库中。
现代覆盖率操作进一步加强了这一点:当你推送新提交时,它们会重新编辑同一条评论,而不是堆叠便签;它们仅显示 PR 中修改的文件以减少噪音;如果变更文件的增量超过阈值,它们会让检查失败。这些单看 都不稀奇。但结合在一起,它们让覆盖率成为了代码评审中自然发生的一环,无需任何人特意要求。
有效 PR 评论背后的工程投入
为 LLM 评估复制这种态势,不仅仅是在 GitHub 工作流中连接 promptfoo eval 或 braintrust eval-action。连线是容易的部分——两者都提供了发布评论的操作。难点在于让评论携带的信息能够经受住繁忙评审员的审视。必须实现以下四点:
针对每个 PR 的增量,范围限定在变更内容。 在每个 PR 上发布“事实性:0.87”是犯了“整体代码库覆盖率百分比”的错误。评审员需要看到的是,在变更实际触及的评估套件切片上,与基准分支相比的增量。如果 PR 修改了 prompts/extraction/invoice.md,评论应该报告发票提取(invoice-extraction)子集的评估数据,而不是发票提取仅占 4% 的全局平均值。聚合指标会掩盖细分领域的回归:模型更换可能使全局得分保持稳定,但某个 15% 的切片可能会暴跌 30 分,由于全局数字没有变化,你的评审员永远不会发现。
影响范围提醒——触及了哪些下游消费者。 提示词具有组合性。对三个产品界面使用的“摘要”提示词进行修改,与对十二个界面使用的提示词进行修改是截然不同的。PR 评论应列出导入或调用该修改模板的消费者——代理(agents)、链(chains)、下游提示词——并同时运行它们的评估。如果没有这一点,对“摘要”提示词的修改在 diff 中看起来很小,结果却成了下个冲刺阶段 所有评估回归的上游根源。对提示词单体仓库(monorepo)进行静态分析,再加上谁调用谁的注册表,可以让这变得可行。
行内回归高亮,而不只是汇总数字。 覆盖率工具发出的最有用的产物是未覆盖的代码行列表。评估评论的对应物是新失败(或新通过)的黄金数据集示例列表:显示输入、预期输出、先前输出、新输出以及评判器(judge)裁决的 3 到 5 行数据。如果评审员看到“此 PR 导致示例 #142 开始失败——输入是带有嵌入 HTML 的退款请求,新提示词丢失了 HTML,提取器返回为空”,他们就会发现这个 Bug。如果评审员只看到“事实性:0.87 → 0.84”,他们会将其合理化为分数波动。
不破坏开发循环的延迟预算。 代码覆盖率报告在几秒钟内就能运行完毕,因为测试原本就需要运行。而针对远程评判器后的一千个示例黄金集进行的 LLM 评估可能需要 20 分钟,并且耗费真金白银。这两者都会以不同的方式破坏开发循环:20 分钟足够让评审员去做别的事,再也不回来读评论;而每个 PR 产生的真金白银成本会让团队在财务部门询问时第一时间关闭该门禁。解决办法是分层评估——在每个 PR 上进行快速确定性检查(模式验证、正则表达式、长度限制),在每个 PR 上对未更改的输入进行缓存并进行启发式评分,仅对受影响的切片而非全量套件使用“LLM 作为评判器”。全量评估仍然每晚运行,PR 评审则只看切片。
隐藏在绿色勾选标记背后的失效模式
在那些已经配置了评估流水线(eval action)但尚未完成上述工作的团队中,有几种失效模式会反复出现。
最常见的是聚合绿色勾选(aggregate green check)。PR 评论显示“事实性:0.87,所有检查均已通过”,随后代码被合并,结果导致了切片级的回归。这就是细分问题。解决方法是:在仓库中通过显式的细分定义来设置各切片阈值。这样,当任何定义的切片退化超过其特定阈值时,流水线就会报错,而不仅仅是在全局平均值下降时报错。
第二种是裁判漂移(judge drift)。LLM-as-judge(以大模型作为裁判)的指标依赖于一个行为本身就在不断变化的裁判模型。更换裁判模型或升级版本会默默地重新定义测试集中的每个分数,下一个 PR 显示的“回归”实际上只是裁判模型的变化。缓解措施是:固定裁判模型版本,并为每个发布版本快照基准分数。这样,在与基准分支对比时,比较的是由同一个裁判模型产生的分数。
第三种是成本增加但质量未变的缺口。评论中只显示质量指标,PR 将流量迁移到更小的模型,质量保持不变,但由于新提示词(prompt)更冗长,token 使用量翻了三倍,成本账单却没能及时体现。解决方法是将成本和延迟作为一等公民与质量指标并列放在评论中,并附带它们各自的增量和阈值。一个不仅检查质量、还检查性能预算的合并门禁,能捕捉到那些虽然单次输入的推理变便宜了、但新模板却让输入变得更长的情况。
第四种是将评估作为对评估本身的测试。团队针对黄金数据集优化提示词,直到该数据集成为它唯一擅长的事情。保护措施是使用一个留出评估集(held-out evaluation set)。团队在迭代提示词时不查看该集合,并将其作为一项单独的、非阻塞指标列在 PR 评论中。当黄金数据集表现提升而留出集表现下降时,说明团队对评估集产生了过拟合,PR 评论会在生产环境出问题前指出这一点。
“评估卡点合并”究竟是什么样子
最终状态不是更多的仪表盘。而是合并按钮——那个工程师在每个 PR 中都会点击的同一个按钮——只有在评估评论显示受影响切片的增量为绿色,且审阅者已经浏览过三到五个因更改而新通过或失败的具体示例后,才会变为可用状态。
具体来说,产出物是每个涉及提示词或 Chain 配置的 PR 上的置顶评论。评论包含四个部分。一个摘要行:“评估了 4 个受影响的切片,事实性 +0.02,拒绝率 -0.01,成本 +12%,延迟 +0ms。” 一个带有增量以及针对阈值的通过/失败情况的分切片表格。一个爆炸半径提示:“此提示词被 7 个 Chain 调用;也运行了它们的评估——除 support-triage 在紧急工单切片上下降了 3 分外,其余全部通过。” 以及一个回归高亮块:三个新失败的示例,包含输入、预期结果、之前的输出、新的输出以及裁判裁定,每个示例都链接到评估平台中的完整记录。
审阅者只需 15 秒即可读完。摘要行让他们可以跳过并查看表格;表格让他们可以跳过并查看发生变动的切片;回归高亮让每个存在 Bug 的 PR 至少暴露一个 Bug。批准是有据可查的;拒 绝是具体明确的。所有这些都不需要审阅者离开 GitHub 的 PR 视图,也不需要打开仪表盘。
实现这一目标的投入不在于评估框架——那部分是基础工具。投入在于上述四个部分:单 PR 作用域划分、爆炸半径分析、行内回归高亮以及保持评论生成速度的延迟预算。构建好这些,“我们有评估”就会变成“评估卡点合并,且审阅速度变快而非变慢”。跳过它们,评估流水线虽然上线了,评论也发了,但团队在切片级回归影响到客户的两周前,仍然还在对着聚合数据自我安慰。
从代码覆盖率中得到的教训是:指标本身的重要性不如它所呈现的界面。PR 评论中的一个弱指标每次都能胜过仪表盘上的完美指标。将你的评估移入评论中。其余的都只是实现细节。
- https://www.braintrust.dev/articles/best-ai-evals-tools-cicd-2025
- https://www.braintrust.dev/articles/eval-driven-development
- https://github.com/braintrustdata/eval-action
- https://www.promptfoo.dev/docs/integrations/github-action/
- https://github.com/promptfoo/promptfoo-action
- https://www.promptfoo.dev/docs/integrations/ci-cd/
- https://www.kinde.com/learn/ai-for-software-engineering/ai-devops/ci-cd-for-evals-running-prompt-and-agent-regression-tests-in-github-actions/
- https://www.traceloop.com/blog/automated-prompt-regression-testing-with-llm-as-a-judge-and-ci-cd
- https://latitude.so/blog/ultimate-ci-cd-llm-evaluation-guide
- https://newsletter.pragmaticengineer.com/p/evals
- https://www.datadoghq.com/blog/llm-evaluation-framework-best-practices/
- https://github.com/marketplace/actions/jest-coverage-report
- https://github.com/marketplace/actions/code-coverage-summary
- https://josh-ops.com/posts/github-code-coverage/
