在 400 行 diff 后人类必然 skim,提出「确定性 gate 优先 + LLM 审查其次 + LLM 只在机器无法判断处评论」的流水线设计,附实战调教经验。
人类审查员擅长判断——这个抽象是否正确、这个应该放这里吗、下一个人能看懂吗——但不擅长注意力。到 diff 的第 400 行时,大家都在泛读。真正漏出去的 bug 几乎从来不是那些精巧的,而是被吞掉的异常、缺失的 await、循环里每行都查一次数据库。好的 AI 审查流水线不是对人类判断的替代,而是把注意力工作交给一个永远不会疲劳的东西,这样人类可以把审查预算花在真正需要大脑的地方。
本文是我在自己项目上运行的流水线的演示:确定性检查在前,LLM 审查器在后,并且有一条硬性规则——LLM 只在机器无法判断的地方发表评论。截至 2026 年中期,模型 API 已经足够便宜和快速,每次 pull request 的成本只有几美分,但设计比模型更重要——范围界定不当的 LLM 审查器会产生大量噪音,导致人们在一周内将其静音。
因为它会对所有内容发表评论,而一个对所有内容发表评论的审查员最终会被忽视。这种失败模式在每个尝试过它的团队中都有记录:机器人留下了十四条评论,其中十二条是格式检查器已经处理的样式问题,一个是针对不可能为 null 的代码的虚假"可能的空引用",而真正的 bug 被埋在中间,没人去看。
解决方案是分层。任何确定性工具能判断的东西,都应该由确定性工具来判断——它更快、更免费,而且在自己的规则上永远不会出错。LLM 只在剩下的内容上运行:语义层面的、跨文件的、"看起来正确但实际不对"的分类,这是 linters 结构上无法看到的。
核心要点: LLM 应该是最后一层,而不是第一层——它永远不应该看到 linter 会捕获的问题。
这些是我衡量 LLM 层最常捕获的类别,所有这些在技术上都可以在 diff 中看到,但很容易被滑过:
吞掉的错误 —— catch (e) {} 或 except: pass,或者捕获的错误被记录但执行继续就好像什么都没发生一样。
缺失的 await —— 一个从未被 await 的异步调用,所以代码"正常工作"直到负载下的时序改变。
N+1 查询 —— 循环内的数据库调用,人类读为"一次查询",不会乘以循环次数。
分页和切片中的 off-by-one —— 经典的 >= vs >,或者在页面边界丢弃或重复一行的偏移量。
与 PR 声明意图不一致 —— 描述说"修复超时",但 diff 也悄悄地改变了重试行为。
最后一个是 LLM 真正优于 linter 的地方,因为它可以将 PR 标题和描述作为上下文来理解。Linter 不知道这个变更应该做什么。
核心要点: 将 LLM 限制在语义和意图层面,它就不再与 linter 竞争,而是开始添加新的价值。
首先将确定性检查作为阻塞性的 CI 步骤。以下是 GitHub Actions 中的基本形式——审查任务只有在廉价检查通过后才运行:
name: review
on: pull_request
jobs:
gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint # blocking
- run: npm run typecheck # blocking
- run: npx secretlint "**/*" # blocking
ai-review:
needs: gates # only spend tokens on diffs that already passed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: node scripts/ai-review.mjs
env:
MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
needs: gates 这一行是成本控制的全部技巧:在 diff 通过每项廉价检查之前,永远不会将其发送给模型。审查脚本拉取 diff,用严格范围的 prompt 发送,并在审查评论中发布结果:
// scripts/ai-review.mjs (sketch)
import { execSync } from "node:child_process";
const diff = execSync(
`git diff origin/${process.env.GITHUB_BASE_REF}...HEAD`
).toString();
const system = `You are a code reviewer. Report ONLY:
- swallowed or ignored errors
- missing await / unhandled promises
- database queries inside loops (N+1)
- off-by-one errors in slicing or pagination
- changes that contradict the PR description
Do NOT comment on style, formatting, naming, or anything a linter
handles. If you find nothing in these categories, return an empty list.
For each finding return: file, line, one-sentence reason, suggested fix.
Return JSON only.`;
// send { system, diff, prDescription } to your model API,
// parse the JSON, and post each finding via the GitHub review API.
两个 load-bearing 指令是"只报告这些类别"和"如果没发现任何东西,返回空列表"。没有第二个指令,模型会觉得有义务说些什么,而这些东西就是噪音。让模型明确地被允许保持沉默。
核心要点: prompt 的作用是让沉默成为默认行为,让评论成为例外。
让 AI 层成为非阻塞的,并将其误报率作为一个主动管理的指标。两个习惯保持它的诚实。首先,将发现类别保持在版本控制的 prompt 文件中,当机器人发布了不好的评论时,在修复底层问题的同一个 PR 中收紧 prompt——prompt 是代码,值得进行同等的审查。其次,允许人类自由地解决机器人的评论;如果某个类别在一个月内产生了大量被驳回的评论,就从 prompt 中删除它。
如果你想要一个托管版本而不是自己维护脚本,GitHub 的 Copilot code review 和第三方机器人如 CodeRabbit 会在每个 PR 上运行 LLM 审查器,而无需你维护这些管道——权衡是你对它们评论的确切类别控制较少。本文中自己动手的方法的真正缺点恰恰相反:你永远拥有误报调优的所有权,这不是一次性设置,而是真正的持续工作。
核心要点: 一个非阻塞的 AI 审查器,其 prompt 你主动修剪,保持有用;一个阻塞的 AI 审查器,其噪音你容忍,最终会被静音。
AI 代码审查能否取代人类审查者?不能。它取代的是审查中的注意力部分——捕捉人类因疲劳而忽略的机械 bug——而不是判断部分。设计、架构以及这个变更是否应该存在仍然需要人类,所以保持 AI 层非阻塞且仅提供建议。
LLM 代码审查流水线的运行成本是多少?截至 2026 年中期,审查一个正常大小的 pull request 花费几分钱,因为你只发送已经通过 linter 和类型检查器的 diff。将 AI 任务放在廉价的确定性检查后面是保持 token 账单可以忽略不计的原因。
为什么我的 AI 审查器会留下这么多无用的评论?几乎总是因为它是第一层而不是最后一层,而且 prompt 没有给它保持沉默的权限。将其限制在几个简短的语义类别中,告诉它在没发现任何东西时返回空列表,让 linter 处理所有机械性的工作。
如果你要给代码审查添加 AI,把它放在链的最后而不是最前面:formatter、linter、type checker 和 secret scanner 捕捉所有机械性的问题,LLM 只看到这些工具在结构上无法检测到的语义问题。使其非阻塞、将其 prompt 限制在几个高价值类别中,并明确允许它不发言。个人开发者和小团队从手工脚本版本中获得最多收益,因为他们可以精确地调整它;不想拥有 prompt 维护的团队最好使用托管审查器并接受较少的控制。目标从来不是审查更多——而是将人类注意力从无聊的 bug 上转移,专注于真正需要人来做决定的地方。