我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

工程

工程

打造一款工程师真正信任的 AI PR 审查器

我们构建了一款由 AI 驱动的拉取请求审查器,却因幻觉和误报侵蚀了开发者信任而将其关停,随后以更强的模型、更广的上下文和更保守的智能体架构重新打造。本文分享了我们在自动化代码审查方面的经验,阐述为何信任比覆盖率更重要,以及 AI 审查器如何在不取代人工判断的前提下,帮助工程团队减少重复性的审查工作。

为什么 PR 审查是一个适合 AI 的问题

不知从什么时候起,我们的 AI 拉取请求审查器不再像一个实验了。

它成了审查流程的一部分。

工程师们会不同意它的意见、忽略它,偶尔也会感谢它。有一位工程师甚至在它批准了一个拉取请求后,回复了一个飞吻表情。

AI 审查器的严厉评论
工程师用飞吻表情回复 AI 审查器

这些互动很有观察价值,并不是因为审查器总是正确,而是因为它们展现了当 AI 反馈成为工程工作流一部分时会发生什么。

问题不再仅仅是这个系统能否发现问题。

而是工程师是否愿意信任它所说的话。

我们的第一个版本失败了。

我们关停它,并不是因为它漏掉了问题。

我们关停它,是因为它发现了太多并不存在的问题。

这个区别很重要。大多数关于 AI 代码审查的讨论都聚焦于覆盖率:一个系统能发现多少 bug、能检测多少漏洞,或者能生成多少条评论。但在实践中,对我们而言最重要的指标并不是覆盖率,而是信任。

拉取请求审查是应用 AI 的天然场景。资深工程师会花费相当多的时间去识别反复出现的模式、强制执行规范、发现不安全的假设、检查边界情况,并评估一项变更是否契合系统的其余部分。其中有些工作需要深入的架构判断,但很大一部分是重复性的、机械性的。

这些重复性的检查正是 AI 可以发挥作用的地方。

挑战在于,代码审查不仅仅是发现问题,而是发现正确的问题,把它们解释清楚,并且足够精确,让工程师愿意据此采取行动。

一个充满噪声的审查器比没有审查器更糟。人类工程师可以忽略沉默,但他们无法忽略一条看似合理却错误的评论,除非花时间去证明它是错的。

这就是我们用惨痛代价换来的教训。


我们的第一次尝试

我们的第一个版本目标很简单:在人工审查者介入之前先审查拉取请求,尽早发现常见问题。

当时,这看起来是一个实用且影响力很大的用例。拉取请求审查已经成为瓶颈。资深工程师在重复性反馈上花费了过多时间,而审查队列正在拖慢整个团队的开发进度。

目标从来不是取代审查者,而是减少审查流程中机械性的部分,让工程师能把更多时间花在架构、安全影响和业务逻辑上。

第一个版本会在拉取请求被打开或更新时自动运行。它收集 diff、获取变更的文件以及有限的周边上下文,把这些信息发送给语言模型,再把审查评论发回到拉取请求上。

这个工作流刻意做得很简单:

  1. 拉取请求事件触发审查器。
  2. 系统提取 diff 和变更的文件。
  3. 模型使用固定的提示词审查这项变更。
  4. 生成的检测结果被转换为拉取请求评论。
  5. 工程师在人工反馈之外审阅这些评论。

简单性让系统易于构建,但这也成了它最大的弱点。智能体能够看到变更了什么,却往往看不到足够的周边系统,无法判断一个发现是否真正有效。

从纸面上看,这个假设是合理的。如果 AI 审查器能在拉取请求到达资深工程师之前就捕捉到常见问题,审查者就能少花时间重复同样的反馈,多花时间讨论设计决策。

但在实践中,这个系统产出的评论听起来有用,却往往是错的。


失败模式:看似合理却错误的反馈

第一个版本并没有以一种显而易见的方式失败。

它没有发表毫无意义的评论,没有误解每一个拉取请求,也没有产出明显荒谬的建议。

问题更为微妙:许多评论看起来足够合理,让工程师觉得有义务去调查,但又错得足够离谱,以至于调查往往白白浪费时间。

有些例子虽小但令人恼火。智能体偶尔会建议与我们自己规范相冲突的风格修改,比如在一个始终使用 camelCase 的代码库中建议采用 snake_case 的测试命名。

有些评论则更具破坏性。在一个案例中,智能体标记了同一个拉取请求中其他地方已经修复过的代码。在另一个案例中,它建议添加需要浏览器环境的集成测试,尽管我们的 CI 环境并不支持基于浏览器的执行。

我们还看到同一个拉取请求上出现重复评论。智能体会不止一次地识别出同一个它认为存在的问题,并发表同一反馈的多个变体。即便底层观察是有效的,重复也让审查显得嘈杂。

最令人沮丧的,是那些乍看之下合情合理的评论。例如,智能体可能建议使用更宽泛的异常处理,却不理解那个更窄的异常类型是有意为之的。又或者,它推荐一次技术上有效、但与附近代码不一致的重构。

一条糟糕的 AI 审查评论是有成本的。有人得去读它、理解它、检查它是否适用、审视周边代码,再决定是否采取行动。如果这条评论是错的,所有这些时间都白费了。

随着时间推移,工程师不再把智能体当作有帮助的审查者,而是开始把它当作又一个审查噪声的来源。

到那个时候,这个项目已经不再有帮助了。

于是我们把它关掉了。


真正的教训:误报比漏报更糟

第一个版本带给我们最重要的教训是:误报往往比漏掉的发现更具破坏性。

一个偶尔漏掉问题的审查器仍然可以有用。而一个反复提出错误问题的审查器,会给其他所有人制造工作。

在代码审查中尤其如此,因为审查评论会打断工程师的心流。一条评论不仅仅是文字,它是一次对注意力的索取。它要求作者停下来、检视代码、推敲问题,再决定是否有必要做改动。

如果这种索取太过频繁地被证明是错的,信任就会迅速瓦解。

一旦信任丧失,连正确的评论也会变得没那么有价值。工程师开始核实一切。他们带着戒备去读智能体的反馈,默认它多半是错的,除非被证明正确。

这改变了工具的角色。它非但没有减轻审查负担,反而加重了负担。

对于 AI 代码审查而言,精确比数量更重要。如果十条评论中有九条需要人工驳回,那它们并不比一条评论更好。一个好的审查智能体应当乐于保持沉默。

这成了第二个版本的指导原则。


为什么代码审查需要比 diff 更多的上下文

我们的第一个版本主要把拉取请求审查当作一个 diff 分析问题来处理。这是一个错误。

有经验的审查者不会只看被修改的那几行代码来评估一项变更。他们会用到一套宽广得多的上下文:

  • 周边代码中已有的模式
  • 项目特定的命名和测试规范
  • 依赖项的行为
  • 运行时假设
  • CI 的限制
  • 安全边界
  • 以往的设计决策
  • 业务逻辑和产品意图
  • 类似的问题是否已经在别处解决过

一项孤立来看可疑的变更,放在完整系统中可能是正确的。反之亦然:一项在 diff 中看似无害的变更,可能因为另一个文件、服务或执行路径中的行为而引入 bug。

这种上下文缺口解释了第一个版本的许多失败。

模型之所以常常出错,并不是因为它缺乏语言能力,而是因为它缺乏足够的信息。当系统看不到相关上下文时,它就会猜测。而当它猜测时,有时会产出自信却错误的反馈。

问题不只在于模型本身。

问题在于围绕模型的架构。


我们为什么重新审视这个问题

我们最终重新审视了 AI 拉取请求审查,因为最初的问题并没有消失。

资深工程师仍在重复性的审查任务上花费时间。其中许多任务很重要,但并不总是需要资深级别的判断。我们依然相信,只要能避免制造噪声,尽早捕捉机械性问题是有价值的。

与此同时,技术也在进步。

更新的模型在理解代码、遵循约束、推敲实现细节方面做得更好。更大的上下文窗口使得我们可以提供更多的代码库上下文,而不必强迫模型从一份狭窄的 diff 中工作。智能体模式也日趋成熟:系统不再依赖单个提示词,而是能够检索信息、检视文件、调用工具,并围绕特定的审查目标来组织工作。

这改变了我们的方法。

我们不再试图构建一个对所注意到的一切都加以评论的通用审查器。我们转而试图构建一个保守的审查系统,聚焦于高置信度、高信号的发现。

问题从:

智能体能发现多少问题?

转变为:

应当允许智能体对哪些问题发表评论?

正是这种转变,让第二个版本好了很多。


架构上发生了哪些变化

当前的系统并非来自某一次突破,而是来自围绕一个核心理念的多次迭代:审查器需要先获得上下文,才能赢得发表评论的权利。

我们早期的实验使用 CrewAI 来协调审查行为。那个方法很有前景,但其输出在拉取请求审查上的可靠性还不够稳定。随后我们转向一个更简单的、基于 Pydantic 的工作流,它给了我们对审查流水线更多的结构化控制。这提升了一致性,但在缺少相关上下文时,系统仍会误解代码。

下一次迭代转向了一种基于工具的架构。

系统不再要求模型从一个固定的提示词出发来审查拉取请求,而是可以按需收集额外信息。它可以检视相关文件、查看附近的实现、检索相关规范,并把分析收窄到特定的审查任务上。

从宏观上看,流程是这样的:

  1. 收到拉取请求事件
    系统在拉取请求被打开或更新时启动。

  2. 解析 diff 和变更的文件
    审查器识别出变更了什么,以及哪些文件受到影响。

  3. 选定审查目标
    系统不进行开放式的审查,而是聚焦于特定类别的问题。

  4. 检索相关上下文
    智能体收集周边代码、相关函数、测试、配置文件,以及代码库中的各类模式。

  5. 执行有针对性的分析
    系统检查具体问题,例如缺失的清理操作、未处理的异常、不安全的假设,或未被持久化的状态变更。

  6. 按置信度过滤发现
    低置信度的观察会被抑制而非发表。

  7. 保守地生成评论
    只有那些具体、可操作、且与该拉取请求相关的发现才会被呈现出来。

这种架构让系统更有用,因为它减少了猜测。

审查器不再像一个对 diff 做出反应的聊天机器人,而更像一个有着狭窄职责的专用工程工具。


上下文收集成了最重要的功能

最大的改进来自给系统提供更好的上下文。

我们的第一个版本看得到拉取请求,却常常看不到周边的实现。更新的版本能够检索信息,帮助回答这样一些问题:

  • 这个模式在代码库的其他地方是否已经被使用?
  • 建议的变更是否与附近代码一致?
  • 这个函数是否有依赖其当前行为的调用方?
  • 是否有测试覆盖这条路径?
  • 这里的异常处理是有意为之的吗?
  • 代码是否依赖某个配置值或运行时假设?
  • 这个问题是否已经在同一个拉取请求的其他部分被处理过了?

这一点很重要,因为许多糟糕的审查评论都源于不完整的可见性。

例如,如果智能体看到一次目录写入,它可能会建议检查该目录是否存在。这可能有用。但如果在该函数被调用之前,初始化代码已经创建了该目录,那么这条评论就成了噪声。

一个有用的发现与一个误报之间的差别,往往就在于那一两个文件的上下文。

检索并不能解决所有问题,但它大幅减少了模型不得不从局部视角去推断行为的情况。


我们不再以评论数量为优化目标

最重要的变化之一,是让系统变得更保守。

第一个版本隐含地奖励"发现东西"。第二个版本奖励"有用"。

这需要一套不同的输出哲学。审查器不应仅仅因为某处"可能可以改进"就发表评论。它应当在存在具体问题、有足够支撑性上下文、并且作者有明确可采取的行动时才发表评论。

一条风格上的建议通常是不够的。一条宽泛的重构建议通常是不够的。一个取决于未知业务意图的潜在问题通常也是不够的。

现在,系统被设计为在置信度低时优先保持沉默。

这是一个艰难但必要的改变。许多 AI 系统在产出更多输出时会显得更令人印象深刻。代码审查恰恰相反。一个评论更少但通常正确的审查智能体,远比一个对每一个可能的顾虑都加以评论的审查智能体更有价值。

信任是通过克制建立起来的。


置信度阈值与评论质量

我们也开始把不确定性当作系统中的一等要素来对待。

在发表一个发现之前,审查器会考虑这个问题是否具体、可操作,并有可用上下文的支撑。一条好的评论通常应满足若干标准:

  • 它指向变更中的一个具体位置。
  • 它把风险解释清楚。
  • 它避免含糊的措辞。
  • 它不依赖智能体无法验证的假设。
  • 它不与可见的代码库规范相冲突。
  • 它给出切实可行的修复或下一步建议。
  • 它重要到足以打断作者。

这种过滤很重要,因为技术上正确的评论仍然可能毫无帮助。

例如,孤立来看,建议一次重构可能是合理的,但如果代码本身清晰、与附近模式一致、且与该拉取请求的目的无关,那么这条评论就不值得发表。同样,推荐更宽泛的异常处理听起来似乎更安全,但它可能掩盖有用的失败模式,让调试更加困难。

审查器不应表现得像一个带着主观意见的 linter。它应当表现得像一个细心的助手,理解它发表的每一条评论都是有成本的。


当前版本擅长捕捉什么

当前版本在重复性、机械性的问题上表现最好,这类问题的预期行为可以从代码中得到验证。

这些发现往往很重要,却并不总是需要深厚的业务上下文。它们也正是资深工程师在人工审查中反复捕捉到的那类问题。

该系统在识别以下问题上一直很有用:

  • 可能导致崩溃的未处理异常
  • 隐藏在提前返回(early return)背后的资源泄漏
  • 已计算出来却从未被持久化的状态变更
  • 重构后遗留下来的死代码
  • 缺失的初始化步骤,例如写入可能并不存在的目录
  • 成功路径与失败路径之间不一致的清理行为
  • 围绕文件、进程或网络操作的不安全假设
  • 在狭窄、具体场景中的竞态条件

一个特别有用的发现是一个真实的检查时机与使用时机(TOCTOU)问题。代码先检查某个条件为真,随后在假定该条件未发生变化的前提下执行了一个操作。这类问题在审查中很容易被漏掉,因为相关的几行代码单独看可能都显得合理。而智能体能够把这一序列联系起来,并标记出风险。

这正是目前 AI 审查对我们而言最有价值的地方:它不是一个架构师,而是一个不知疲倦、专注于机械正确性的审查者。

它帮助从关键的审查路径中移除一部分重复性工作,让人类审查者能够专注于更高层次的判断。


它仍然会漏掉什么

这个系统比第一个版本要好,但它还远不足以取代人工审查。

它在宽泛的架构推理、领域特定的业务逻辑,以及那些最重要上下文存在于代码库之外的决策上,仍然力不从心。它往往能理解代码是如何工作的,但要理解代码为什么被这样写,则困难得多。

意图始终是最难的问题。

审查器仍可能建议一些技术上有效却没有用处的改动。它可能建议重构本已可以接受的代码。它可能在当前显式实现更易于维护时,建议采用一个更通用的抽象。它甚至可能在一个更窄的异常类型是有意选择的情况下,仍然建议采用更宽泛的异常处理。

例如,系统曾建议把像这样的特定异常处理:

except RuntimeError

替换为像这样的更宽泛的处理:

except Exception

在某些上下文中,这或许是合理的。但在另一些上下文中,这反而更糟。捕获一个宽泛的异常会掩盖编程错误,让失败更难调试,并削弱周边代码所依赖的保证。

智能体能够评估实现细节,却并不总能理解设计意图。

这个局限塑造了我们使用它的方式。除非有强有力的证据,否则我们不希望审查器提出宽泛的架构建议。我们希望它聚焦于那些代码本身就能提供足够上下文、从而使发现可靠的问题。


我们如何看待评估

我们不再以审查器产出的评论数量来评估它。

高评论数并不等于成功。在许多情况下,它反而是一个警示信号。

我们在意的指标更接近于:

  • 工程师同意某条评论的频率
  • 一条评论导致真实代码改动的频率
  • 有多少评论被当作无关紧要而驳回
  • 同一个问题被重复报告的频率
  • 审查者花在核实智能体反馈上的时间有多少
  • 智能体是否能在资深审查者之前捕捉到重复性问题
  • 工程师是否随时间推移持续信任这个工具

最重要的信号是,工程师是否把智能体的反馈当作值得一读的内容。

如果工程师跳过这些评论,那么即便其中一些发现在技术上是正确的,系统也算失败了。如果工程师持续地对少量精确的评论采取行动,那么系统就在履行它的职责。

这就是我们刻意保持保守的原因。我们宁愿漏掉一个模棱两可的问题,也不愿训练工程师去忽视审查器。


从内部实验到平台功能

一个始于内部实验的项目,如今正在为更广泛的产品方向提供参考。

我们正在致力于把由 AI 驱动的代码审查能力引入 Ostorlab 平台。在把这个功能对外开放之前,我们想先在自己的拉取请求上使用它,观察它在哪些地方有帮助,并了解它在哪些地方需要护栏。

这段内部使用让我们看清了这个功能应该是什么样子。

它不应取代工程判断。它不应把每一个拉取请求都变成一堵由 AI 生成的评论砌成的墙。它不应表现得像一个过度自信、急于证明自己发现了什么的初级审查者。

相反,它应当帮助团队在开发流程中更早地捕捉到重复性、机械性以及与安全相关的问题。

这对应用安全尤为重要。许多安全问题在代码审查阶段修复,比在部署之后或后续扫描时修复要便宜得多。一个有用的 AI 审查器可以通过在更接近代码编写之处识别出有风险的模式,来补充现有的 AppSec 工作流。

这并不意味着它能取代 SAST、DAST、人工安全审查或有经验的工程师。它只是工作流中的又一层:一层专注于早期、结合上下文、面向开发者的反馈。

我们已经看到用户表示有兴趣,询问这项能力何时上线。这种需求印证了我们的信念:代码审查正在成为应用安全工作流中的一个重要组成部分。

但有用性比发布速度更重要。我们的首要任务,是在这个功能触及生产用户之前,让它变得保守、可信、实用。


经验教训

AI 审查器不是初级工程师

一个常见的错误,是把 AI 审查器当作初级开发者来看待。

它们不是。

初级工程师会随时间积累上下文。他们会提问,会记住以往的决策。他们会了解一个系统的历史和一个团队的偏好。他们通过经验逐渐培养出判断力。

AI 系统的运作方式不同。它们擅长模式识别、保持一致性、归纳总结和重复性分析。它们能快速检视大量代码。它们能识别出人类可能忽略的机械性问题。

但除非把组织历史、产品意图或架构权衡提供给它们,否则它们并不会天然地理解这些东西。

AI 审查器最好的角色不是取代,而是辅助。


信任比覆盖率更重要

最重要的指标不是审查器生成了多少发现。

而是工程师信任其中多少发现。

一个产出十条准确、可操作评论的系统,比一个产出一百条需要人工核实的评论的系统更有价值。覆盖率固然重要,但只有在系统赢得信任之后才谈得上。

对于代码审查智能体而言,克制是一种功能。沉默有时正是正确的输出。


上下文是"有帮助"与"有噪声"之间的分水岭

许多 AI 审查的失败都是上下文的失败。

一个审查狭窄 diff 的模型,可能会识别出某处看似可疑、但实际上已在别处处理过的东西。它可能推荐一个与代码库相冲突的规范。它可能误解一个测试环境、运行时假设或架构边界。

更好的模型会有帮助,但更好的上下文同样重要。

审查器需要能访问人类审查者会自然用到的信息:周边代码、相关文件、测试、规范、配置以及既有模式。

缺少这些上下文,系统就会猜测。而在代码审查中,自信的猜测是危险的。


有时正确的决定是停下来

我们的第一次尝试失败了。

当时,这令人失望。但如今回头看,这是整个项目最有用的成果之一。

关停它迫使我们去理解真正的问题。问题并不仅仅是模型需要变得更好,而是我们的系统被优化去产出审查评论,而非产出可信的审查评论。

当我们重新审视这个项目时,我们并不是从头开始,而是在那个失败版本的经验之上继续构建。

并非每一个工程项目都能在第一次尝试时就成功。有时,正确的决定是停下来、吸取教训,待到技术和你对问题的理解都有所提升时再回来。

这正是这里所发生的。

我们仍然不认为 AI 应该取代人工的拉取请求审查。但我们确实认为,当它专注、结合上下文且保守时,它能让审查变得更好。

目标不是一个评论更多的 AI 审查器。

目标是一个工程师真正信任的 AI 审查器。