在 GitHub 仓库里配一个 AI Code Reviewer,自动审查 PR

在之前公司实习的时候,参与了小组内GitLab里面嵌入一个AI审查工具;当你每次MR时候,欸,你就会发现,ld不审查你代码了,随之而来的是AI更加严厉的批评,更加专业更加仔细,让你无处可逃;于是我这次也把他嵌入到了我的开源项目里面,狠狠的批阅一下我自己的代码。

一、问题:没人审和审不细

举个例子:仓库推了一个 PR,改了 22 个文件。等了半小时没人 review,自己看了一遍,有个 SQL 拼接没注意,合了。上线后被安全团队找上门。

这种场景不陌生。代码审查有两个老问题:

  • 没人审:团队忙的时候 PR 堆半天没人看,阻塞合入
  • 审不细:人连续审 5 个 PR,第 6 个基本就在走形式了,diff 翻到第三屏注意力就散了

AI 编码工具(Copilot/Cursor)普及后单 PR 代码量还在涨,但 Reviewer 的注意力带宽没变。于是我自己搭了一个 AI 代码审查机器人------PR 一创建就自动触发,45 秒内把行级评论贴到 Files changed 页面,零手动操作。

它不是资深架构师,更像一个不知疲倦的初级审查员,先把机械性检查(安全漏洞、复杂度、命名)过一遍,人只需要审它搞不定的业务逻辑部分。

先看效果:PR #2 提交后,GitHub Actions 自动触发审查,在 22 个文件上贴了 30 条行级评论:

当你有邮箱,那么你就会收到来自邮箱的提醒

二、整体架构:一条流水线

整个系统就是一条五步流水线:

bash 复制代码
GitHub PR 事件 (opened/synchronize)
        ↓
拉 PR Diff (GitHub API: GET /pulls/:number/files)
        ↓
解析 Diff → 还原「新文件行号 → 代码行」映射
        ↓
逐文件跑 Skills 审查(规则预检 + LLM 语义分析)
        ↓
汇总评论 → POST 行级评论到 PR

技术栈极简:Node.js 原生 ESM,零第三方依赖 ,只用了 fetch(Node 18+ 内置)。大模型用 DeepSeek(OpenAI 兼容接口),成本约 0.03 元/PR

分层设计

职责 关键文件
Adapter 层 和代码托管平台交互(拉 diff、回贴评论) src/adapters/github.js
Parser 层 解析 unified diff,还原行号映射 src/parser.js
Skill 层 审查规则封装(规则 + LLM prompt) src/skills/*.js
Reviewer 层 编排主流程:load → precheck → LLM → filter → post src/reviewer.js

为什么要 Adapter 层?因为这套逻辑同样跑在 GitLab 上(我实习时的版本),GitHub 和 GitLab 只是 API 路径和评论参数不同。换平台只需写一个新 Adapter,Skill 层零改动。

三、核心实现

3.1 GitHub Actions 自动触发

触发逻辑写在 .github/workflows/ai-cr.yml,监听 PR 的三个事件:新建(opened)推新 commit(synchronize)重新打开(reopened)

yaml 复制代码
name: AI Code Review

on:
  pull_request:
    branches: [main, develop]
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - name: Run AI Code Review
        working-directory: ai-code-review
        env:
          ARK_API_KEY: ${{ secrets.ARK_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          node bin/ai-cr.mjs \
            --platform github \
            --repo ${{ github.repository }} \
            --pr ${{ github.event.pull_request.number }} \
            --post \
            --min-severity warning \
            --skills security,complexity

两个关键 token:

  • GITHUB_TOKEN:GitHub Actions 每次运行自动注入 的临时 token,有 pull-requests: write 权限就能回贴评论,不需要手动配
  • ARK_API_KEY:DeepSeek 的 API key,需要在仓库 Settings → Secrets 里手动添加一次

3.2 Diff 解析:还原行号是最核心的一步

GitHub API 返回的 patch 是 unified diff 格式,长这样:

diff 复制代码
@@ -10,7 +10,9 @@ function getUser(id) {
   if (!id) return null
   const user = db.query('SELECT * FROM users WHERE id = ' + id)
+  const profile = await fetchProfile(user)
+  if (!profile) throw new Error('no profile')
   return user
-}
+}

要把评论精准锚定到行,必须搞清楚新文件里每一行对应的行号@@ -10,7 +10,9 @@ 的意思是:旧文件从第 10 行开始共 7 行,新文件从第 10 行开始共 9 行。

解析逻辑的核心(src/parser.js):

javascript 复制代码
for (const raw of diffText.split('\n')) {
  if (line.startsWith('@@')) {
    const m = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
    newLine = Number(m[3]) // 新文件起始行号
    continue
  }
  if (line.startsWith('+') && !line.startsWith('+++')) {
    // 新增行:记录行号,行号+1
    hunk.lines.push({ type: 'add', text: line.slice(1), newLine })
    newLine++
  } else if (line.startsWith('-') && !line.startsWith('---')) {
    // 删除行:在新文件里不存在,newLine 不变
    hunk.lines.push({ type: 'del', text: line.slice(1), newLine: null })
  } else if (line.startsWith(' ')) {
    // 上下文行:行号+1
    hunk.lines.push({ type: 'ctx', text: line.slice(1), newLine })
    newLine++
  }
}

坑点 :GitHub 要求回贴评论时 line 必须落在该文件 diff 的 hunk 范围内,否则返回 422。我们的 changedLines 天然取自 patch,不会越界。

3.3 Skills:规则 + LLM 混合审查

我把审查规则封装成可插拔的 Skill,每个 Skill = { systemRule, precheck }

  • precheck确定性规则校验,用正则或算法硬查,0 成本 0 幻觉
  • systemRule:喂给 LLM 的 prompt 模板,做语义层面的判断

目前实现了三个 Skill:

Skill 规则预检做什么 LLM 做什么
安全审查 正则扫 SQL 拼接、硬编码密钥、eval/exec 语义分析 XSS、命令注入风险
命名规范 正则扫单字母变量、驼峰下划线混用 语义判断命名是否清晰、给出具体改名建议
圈复杂度 算法算函数圈复杂度,超 10 报 warning 补充"怎么拆"的具体建议

以安全审查的 precheck 为例(src/skills/security.js):

javascript 复制代码
const DANGEROUS = [
  {
    re: /(['"`])\s*(SELECT|INSERT|UPDATE|DELETE|DROP)\b[\s\S]*?\+\s*[A-Za-z_$]\w*\s*/i,
    msg: '疑似 SQL 字符串拼接,存在注入风险,建议使用参数化查询',
  },
  { re: /api[_-]?key\s*[:=]\s*['"`][^'"`]{4,}['"`]/i, msg: '代码中疑似硬编码密钥,应改用环境变量' },
  { re: /\b(eval|exec)\s*\(/g, msg: '禁止直接使用 eval/exec', skipRegex: true },
]

为什么不用纯 LLM? 三个原因:

  1. 成本:规则预检免费,90% 的确定性问题直接命中,LLM 只处理剩下 10% 的语义问题,单次 PR 成本从几毛钱降到几分钱
  2. 稳定性 :LLM 会幻觉------比如把正则定义里的 /eval\(/ 误判为 eval 调用(后面会讲这个坑),规则是确定性的,能兜底
  3. 可解释性:开发者看到"正则命中 SQL 拼接"比"LLM 觉得不安全"更有说服力

加新审查点?只需在 src/skills/ 下新建一个文件,导出到 src/skills/index.js,零改动其他代码。这就是 Monorepo 管理 Skills 的思路。

3.4 给 LLM 的 Prompt:行号标记 + JSON 约束

Reviewer 在组装 prompt 时,会给每一行加行号和变更标记,让 LLM 只对变更行给意见:

javascript 复制代码
const matrix = lines.map((text, i) => {
  const no = i + 1
  const mark = file.addLines.has(no) ? '+' : file.changedLines.has(no) ? '~' : ' '
  return `${String(no).padStart(4)}|${mark}| ${text}`
})

给 LLM 看到的代码长这样:

javascript 复制代码
  40| | function getUser(id) {
  41| |   if (!id) return null
  42|+|   const sql = 'SELECT * FROM users WHERE id = ' + id
  43|~|   const user = db.query(sql)
  44|+|   const profile = await fetchProfile(user)

+ 是新增行(重点审查),~ 是变更附近上下文,空格是未变更行。Prompt 明确要求"只对新增行给出意见,输出 JSON 数组"。

LLM 返回后做宽松解析(src/llm.js)------先剥离 Markdown 代码块,再找 [] 的区间,兼容 LLM 输出 Markdown 包裹或加废话前缀的情况:

javascript 复制代码
export function parseJsonArray(text) {
  if (!text) return []
  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/)
  const candidate = fenced ? fenced[1] : text
  const start = candidate.indexOf('[')
  const end = candidate.lastIndexOf(']')
  if (start === -1 || end === -1) return []
  try {
    return JSON.parse(candidate.slice(start, end + 1))
  } catch {
    return []
  }
}

3.5 回贴行级评论:GitHub API + 限流处理

回贴接口是 POST /repos/{owner}/{repo}/pulls/{pr}/comments,body 长这样:

json 复制代码
{
  "body": "🔴 [严重] 疑似 SQL 字符串拼接,存在注入风险",
  "commit_id": "abc123def...",
  "path": "src/db.js",
  "line": 42,
  "side": "RIGHT"
}
  • commit_id:PR 的 head SHA(从 GET /pulls/:numberhead.sha 获取)
  • side: "RIGHT":锚定在新文件(PR 合并后的版本),不是旧文件

四、三个真实踩过的坑

坑 1:GitHub 422 "was submitted too quickly"

第一次跑回贴时,10 条评论全部 422 失败。查文档没找到限流说明,反复试验发现:GitHub 对同一 PR 的行级评论有隐式速率限制,同一文件连续快速发评论会被拒。

解决方案(src/adapters/github.js):

javascript 复制代码
// 按文件分组
const byFile = new Map()
for (const c of comments) {
  if (!byFile.has(c.file.path)) byFile.set(c.file.path, [])
  byFile.get(c.file.path).push(c)
}

for (const [, fileComments] of byFile) {
  for (let i = 0; i < fileComments.length; i++) {
    // 每条评论间 100ms 延时
    await postOne(fileComments[i])
    if (i < fileComments.length - 1) await sleep(100)
  }
  // 文件间 800ms 延时
  await sleep(800)
}

再加指数退避重试(遇 422 等 1s→2s→3s),从 0/10 成功变成 10/10 全成功。

坑 2:Security 规则误报------正则定义里的 eval

安全规则用 \beval\s*\( 扫危险函数,但代码里 /eval\(/ 这种正则字面量也会命中:

javascript 复制代码
// 这行是正则定义,不是 eval 调用,却被误报
const evalRegex = /eval\(.*\)/

解决方案:检查 eval 前后的 / 数量,两侧都是奇数说明在正则里,跳过:

javascript 复制代码
function isInRegex(line, keyword) {
  const idx = line.indexOf(keyword)
  const slashBefore = (line.slice(0, idx).match(/\//g) || []).length
  const slashAfter = (line.slice(idx).match(/\//g) || []).length
  return slashBefore % 2 === 1 && slashAfter % 2 === 1
}

这就是规则+LLM 混合的价值:规则可以精确兜底 LLM 会犯的错。

坑 3:GITHUB_TOKEN fine-grained 权限不足

一开始用 fine-grained token,只勾了 Contents: Read and write,结果创建 PR 和回贴评论都 403。后来发现:

  • 开 PR 和回贴评论 需要 Pull requests: Read and write 权限
  • 推代码 需要 Contents: Read and write 权限
  • 两者是独立的 Repository permissions

嫌 fine-grained 界面麻烦的同学,直接用 Classic token 勾 repo 作用域一步到位。

五、5 分钟接入你自己的仓库

步骤 1:Copy 代码

把项目里的 ai-code-review/ 目录和 .github/workflows/ai-cr.yml 复制到你的仓库。

步骤 2:配 LLM Key

仓库 Settings → Secrets and variables → Actions → New repository secret:

  • Name:ARK_API_KEY
  • Secret:你的 DeepSeek API Key(也可以用任何 OpenAI 兼容接口,改 ARK_BASE_URL 即可)

步骤 3:开个 PR 试试

随便改几行代码提个 PR,30-60 秒后就能在 Files changed 页面看到 AI 评论。

也可以本地先跑 mock 看效果(不需要任何账号):

bash 复制代码
cd ai-code-review
npm run review:sample

六、它能做什么,不能做什么

能做的

  • 检测安全漏洞(SQL 注入、硬编码密钥、eval/exec)
  • 发现圈复杂度过高的函数,建议拆分
  • 检查命名规范、魔法数字、代码风格问题
  • 逐文件、逐行精准锚定评论到变更行

不能做的

  • 理解跨文件的复杂业务逻辑(判断不了"已发货"和"已签收"是否写反)
  • 判断功能是否符合产品需求
  • 替代资深工程师的架构审查

它是第一道过滤,把机械性检查接过去,人只需要集中审业务逻辑和架构设计。

七、后续迭代方向

  1. 增量审查:目前是对整个文件审查,后续可以只审查 diff 范围内的代码片段,减少 LLM 调用量
  2. 审查门禁:设置 severity=error 的评论 block 合入,需要开发者回复"已知,暂不修改"才能合并
  3. 去重评论:同一 PR 多次 push 后,避免重复发相同评论
  4. Skill 市场:把审查规则做成可共享的包,不同团队可以定制自己的规则集

仓库地址

完整代码已开源,clone 下来 cp .env.example .env 填个 API Key 就能跑:

github.com/Maxkim321/m...

关键文件索引:

如果对你有帮助,给个 Star ⭐ 支持一下。有问题欢迎在评论区交流。

相关推荐
忆江南1 小时前
Flutter 图片库与 Dio 库优化实战指南
前端
用户921080262861 小时前
AI 对话为什么需要 Markdown 解析器:从纯文本气泡到专业内容渲染
前端
NeverSettle_1 小时前
Agent 如何快速调用公司接口?——CLI + Skill 实践与踩坑
前端·javascript·后端
环境栈笔记1 小时前
指纹浏览器怎么用:从 Profile、代理到环境检测的完整上手流程
前端·人工智能·后端·自动化
sugar__salt1 小时前
三列布局与 TypeScript 工具类型 Pick / Omit / Partial 详解
前端·javascript·typescript
名字还没想好☜1 小时前
Next.js Route Handler 做 SSE 服务端推送:实时进度条、自动重连与什么时候别用 WebSocket
开发语言·javascript·websocket·react·sse·next.js
IMPYLH2 小时前
HTML 的 <html> 元素
前端·javascript·html
IT_陈寒2 小时前
SpringBoot自动配置的坑,把我整不会了
前端·人工智能·后端
赵大仁2 小时前
Human-in-the-loop:前端确认流与后端幂等
前端·后端·ai·agent·人机协作