设计一个 AI 代码审查助手

深入系统设计场景题约 10 分钟读完

一句话回答

用 Git 平台的 Webhook 在创建或更新合并请求时触发,接收服务验签、去重、入队后立即返回;Worker 拉取 diff 和必要的上下文,过滤掉锁文件和生成代码,把大 diff 按文件或代码块拆分并发审查;Prompt 明确只关注缺陷、安全、性能这类问题并按严重程度分级,要求结构化输出;结果校验行号后写回对应的代码行。难点在于控制误报(置信度阈值、去重、数量上限、收集开发者反馈)、成本,以及代码隐私。

详细解析

第一步:澄清需求

  • 平台:GitHub、GitLab 还是自建平台?决定 Webhook 的格式和写评论的接口
  • 规模:每天多少个合并请求(下文简称 PR)、平均改动多少行,决定并发和成本
  • 定位:只给建议,还是作为合并前的必要检查?一般先做建议,误报率稳定后再考虑卡点
  • 范围:代码风格交给 Lint 和格式化工具,AI 只看工具查不出来的问题
  • 隐私:代码能不能发给云端模型?需不需要私有化部署

第二步:整体架构

文本
Git 平台 ──Webhook(PR 创建、有新提交)──► 接收服务
                                           │ 验签 → 按(仓库, PR, head SHA)去重 → 入队 → 立即返回 2xx
                                           ▼
                                       任务队列
                                           ▼
审查 Worker
  1 拉取 diff、变更文件的全文、PR 标题和描述、仓库里的审查规则文件
  2 过滤:锁文件、生成的代码、二进制文件、超大文件
  3 拆分:按文件,大文件按代码块(带上前后文)
  4 并发调用模型(限制并发数),得到结构化的问题列表
  5 后处理:校验行号、按置信度过滤、去重、限制条数
  6 写回:行内评论 + 一条总结评论
       │                                 │
       ▼                                 ▼
 大模型(云端 / 私有化)             数据库:审查记录、评论指纹、反馈、成本

第三步:核心模块和数据模型

接收 Webhook:用原始请求体校验签名(见代码示例)。平台对响应时间有要求,而审查要跑几十秒甚至更久,所以只入队、不处理。同一个 PR 会因为多次 push、重新打开等收到多个事件,按 head commit SHA 去重;有新提交时,取消旧 SHA 还没跑完的任务。

获取上下文:只看 diff 容易误判,比如看不到被调用函数的定义。在 token 预算内补充:变更文件的完整内容或周边若干行、被修改函数的定义和调用方、PR 描述和关联的需求(理解改动意图)、仓库根目录的审查规则文件(团队约定)。

Prompt 和输出:

  • 明确要关注的:逻辑错误、边界条件、空值和异常处理、并发问题、安全漏洞、明显的性能问题
  • 明确不关注的:代码风格、命名偏好(除非团队规则要求)
  • 没有问题就返回空列表,不要为了凑数提意见
  • 每个问题包含行号、严重程度(blocker / major / minor)、类别、置信度、说明和修改建议,用 JSON Schema 约束输出格式(见让模型稳定输出 JSON)

写回评论:行内评论只能放在 diff 里出现的行上,而模型自己数的行号经常对不上。做法是把 diff 转成带新文件行号的文本交给模型,返回后检查行号是否在可评论的行里,不在就并入总结评论。

数据表:

  • reviews:仓库、PR 编号、head SHA、状态、token 用量、费用
  • review_comments:文件、行号、严重程度、问题指纹、平台上的评论 ID
  • comment_feedback:评论 ID、反馈类型(有用 / 误报)、是否引发了代码修改

第四步:关键难点

控制误报。误报多了,开发者会直接忽略所有评论,这比漏报更伤:

  • 只发布置信度高于阈值的问题,minor 级别只放进总结评论
  • 每个 PR 的评论数量设上限,按严重程度排序
  • 对每个问题再做一次校验(让模型对照代码自查,或者用另一次调用复核),过滤站不住的
  • 用"文件 + 问题类型 + 相关代码"生成指纹去重,有新提交后不重复发同样的评论
  • 收集反馈:评论下的"有用 / 误报"、评论后代码是否被修改(采纳率),误报样本加入评测集

大 diff 和跨文件问题。按文件拆开审查会丢失全局信息,比如改了函数签名而调用方没改。先用一次调用生成整个 PR 的变更摘要,作为每个文件审查的公共上下文;超出预算时按风险排优先级(核心目录、改动大的文件优先),并在总结里说明哪些文件没有审查。

成本。只审查新增的提交(上次审查的 SHA 到新 SHA 之间的 diff);按文件内容的哈希缓存结果;跳过纯文档、纯配置的改动;简单文件用小模型、核心文件用大模型;把规则和 PR 摘要放在 Prompt 前部,利用提示缓存;按仓库设置每月预算。

代码隐私和安全:

  • 代码是核心资产:使用不留存、不用于训练的企业协议,或者私有化部署开源模型
  • 发送前检查 diff 里有没有误提交的密钥,有就直接告警,并且不要把它发给模型
  • 机器人的平台令牌只给读代码和写评论的权限,不给批准和合并的权限
  • PR 内容是不可信输入:有人可能在代码注释里写给 AI 的指令,比如要求它"批准这个 PR",或者对某段代码保持沉默。机器人只能发评论,注入造不成越权操作,但可能让评论失真、漏掉真正的问题,所以 AI 的结论不能代替人工审查

第五步:扩展与优化

  • 支持在评论里 @ 机器人追问,或者让它给出修改补丁,复用同一次审查的上下文
  • 和静态分析结合:把 Lint、类型检查、安全扫描的结果作为输入,让模型解释和补充,而不是重复报告
  • 把被频繁采纳的问题沉淀成团队规则或 Lint 规则,用确定性的工具代替模型

代码示例

TypeScript
import crypto from 'node:crypto'

// 校验 GitHub 的 X-Hub-Signature-256:必须用原始请求体,不能用解析后再序列化的 JSON
export function verifySignature(rawBody: Buffer, header: string | undefined, secret: string) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(header ?? '')
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b) // 定长时间比较,防时序攻击
}

// 把平台返回的单个文件的 patch(从 @@ 开始)转成带新文件行号的文本,并记录可评论的行
export function annotatePatch(patch: string) {
  const lines: string[] = []
  const commentable = new Set<number>()
  let newLine = 0
  for (const raw of patch.replace(/\n$/, '').split('\n')) { // 有的平台的 diff 以换行结尾,去掉末尾的空行
    const hunk = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
    if (hunk) {
      newLine = Number(hunk[1])
      lines.push(raw)
    } else if (raw.startsWith('-')) {
      lines.push(`     ${raw}`) // 删除的行在新文件里没有行号
    } else if (!raw.startsWith('\\')) { // 跳过 "\ No newline at end of file"
      commentable.add(newLine)
      lines.push(`${String(newLine).padStart(4)} ${raw}`)
      newLine++
    }
  }
  return { text: lines.join('\n'), commentable }
}

面试官可能追问

怎么评估这个审查助手的效果?

离线:从历史 PR 里找后来被修复的缺陷(比如有对应修复提交的),看助手能不能在原始 PR 上发现,算召回率;人工标注一批评论是否成立,算精确率。线上:看采纳率(评论后代码被修改的比例)、误报反馈率,以及评论被直接标记为已解决、代码却没改的比例。每次改 Prompt 或换模型,先在离线评测集上对比。

只给 diff,模型经常因为看不到上下文而误报,怎么改进?

按需补充上下文:通过代码搜索或语言服务找到被修改函数的定义、调用方和相关类型;也可以给模型提供"读取文件""搜索代码"的工具,让它在步数和 token 预算内自己查。同时要求模型在缺少信息时说明假设、降低置信度,而不是直接下结论。

能不能让 AI 审查通过后自动批准合并?

不建议。漏报无法避免,PR 内容还可能带有针对机器人的提示注入,自动批准等于把合并权限交给了攻击者能影响的模型。AI 适合作为辅助,批准由人来做。少数确定性高的检查(比如发现提交了密钥)可以作为阻止合并的条件,但它们通常用规则实现,不依赖模型判断。

代码不能发到云端,私有化模型效果又差一些,怎么办?

先用自己的评测集量化差距,再决定取舍:可以按仓库的敏感程度分级,核心仓库用私有化模型,其他仓库在签订数据协议后用云端模型;私有化模型聚焦在误报少的问题类型上,降低对模型能力的要求;把团队规则、静态分析结果作为输入,弥补模型能力的不足。

易错点

  • 在 Webhook 的处理函数里同步执行审查:平台等不到及时的响应,会把这次投递记为失败
  • 用解析后的 JSON 重新序列化去算签名,结果和平台的签名对不上
  • 让模型自己数行号,评论贴错了位置甚至写不进去
  • 只追求"发现更多问题",不控制误报,最后没人看评论

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。