跳到主要内容

将 Eval 作为 Pull Request 评论而非任务:在代码审查中嵌入 LLM 质量门禁

· 阅读需 12 分钟
Tian Pan
Software Engineer

许多自称“有评估(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 评论背后的工程投入

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates