1. 为什么我会自己搭一套代码审查机器人
1.1 团队评审中的真实痛点
代码评审这件事,做得好是质量闸门,做不好就是另一种形式主义的打卡。我经历过不少团队,PR 挂了两天没人点开,合并前临时抓两个人 rubber stamp,review 意见清一色是命名、缩进、注释这类 style nit,真正要紧的并发问题、空指针风险、异常吞掉反而没人提。也不能全怪同事,人脑在短时间扫几百行 diff 时,注意力天然会被细节带走,越大的 PR 越容易漏掉逻辑层面的问题。所以我第一次看到 open-code-review 这个项目时,第一反应不是"又一个 AI 玩具",而是想验证一件事:它能不能把评审从"人海战术"变成"机器先打底、人来做裁决"。
open-code-review 本质上是一个把大模型接进代码评审流程的开源工具。它的工作方式很直白:拉取 PR 的变更内容,把 diff 整理成大模型能理解的指令,让模型以审稿人的身份逐文件、逐块分析,再把结论以行级评论或者 review 汇总的形式写回代码托管平台。和那些只能做静态检查的 linter 不同,它看的是语义层的东西——分支条件写得对不对、资源有没有释放、状态变更有没有覆盖所有路径——这些恰恰是传统工具覆盖不到、又最消耗人工精力的部分。
1.2 open-code-review 能接住哪些场景
我实际用下来,它最适合接三类场景:
- PR 首轮初筛:开发提 PR 后,机器人先跑一遍,把明显的问题列出来,作者在上线前自己就能改掉一大部分,评审人看到的是已经"过滤过"的版本。
- 评审意见补盲:人工 reviewer 看过的代码,机器人再从不同角度检查一遍,专门找那些"人容易忽略但模型擅长发现"的漏洞,比如异常路径、边界条件、跨文件的状态一致性。
- 历史代码存量扫描:把机器人挂在 main 分支的定时任务上,对新合并代码做回顾式分析,很多团队的隐性技术债就是这样一点点被挖出来的。
当然它也有限制。它没有编译环境,不会帮你真的跑测试,对大型重构类 PR 的全局把握也远不如一个熟悉业务的老手。它的定位是"第一层筛子",不是"最终裁决者"。想清楚这一点,后面所有的配置逻辑都顺了。
2. 审查链路是怎么跑通的:从 Push 到行级评论
2.1 获取变更集(Diff)的几种方式
整个审查链路的第一步,是把"这次 PR 改了什么"这个信息准确拿到手。常见做法有三种,各有利弊:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| GitHub API 拉取 PR files 列表 | 按文件返回 patch 内容,结构干净,带新旧行号 | 受 API 速率限制,超大 PR 会分页 | 独立服务部署,批量处理 |
本地git diff命令 | 完全不受 API 限制,速度最快 | 需要先完整 checkout 代码,占用 CI 时间和磁盘 | GitHub Action 里最常用 |
| Webhook 事件携带的 payload | 实时性最强,事件驱动 | payload 里只有 patch 摘要,拿不全全部内容 | 实时通知、触发后续任务 |
我在 Action 里用的是 checkout +git diff的组合。关键点在 checkout 的深度配置:如果你只设置了fetch-depth: 0,那没问题,完整的提交历史都在;如果为了省时间只拉单个 commit,diff 对比就会失败。下面这段配置是我实测可以稳定工作的:
- name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Compute diff id: diff run: | BASE_SHA=$(git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD) HEAD_SHA=${{ github.event.pull_request.head.sha }} git diff $BASE_SHA...$HEAD_SHA -- '*.go' '*.py' '*.ts' '*.tsx' > /tmp/pr.diff echo "diff_file=/tmp/pr.diff" >> $GITHUB_OUTPUT注意这个git merge-base。直接拿 base 分支的最新 commit 去 diff 是不对的,PR 是基于某个历史节点拉出来的,base 分支可能在这段时间里前进过了,差出来的内容会包含别人的改动。merge-base找到共同祖先,再对比到 head,拿到的才是这个 PR 真正引入的变化。
2.2 把 Diff 翻译成大模型能理解的结构化指令
拿到 diff 之后,下一步是把它组装成 prompt。这里最容易犯的错误是把整个 diff 一次性塞进去,几百行代码的大模型往往抓不住重点。我的经验是按文件分组、再按 hunk 分块,每个块单独送审。系统提示词里必须写清楚三件事:角色是什么、要看什么、评论的格式是什么。
我用的角色设定大概长这样:你是一名资深代码评审专家,只分析用户提供的代码变更,不臆测未展示的逻辑。检查重点按优先级排列:会导致崩溃或数据丢失的问题、并发和竞态条件、资源泄漏、异常处理缺失、逻辑边界错误。对于纯粹的代码风格问题不要提出评论,除非它直接影响可读性和维护性。
输出格式我要求它返回 JSON,每一条意见包含文件路径、起始行号、结束行号、严重级别、问题描述、以及修改建议。有了结构化输出,后面回填评论、分级过滤、统计报表都好做。这里有个小技巧:在 prompt 里明确告诉模型"如果某一行无法确定,不要编造行号,宁可不报",能显著减少行号错位的情况。
2.3 评论回填与 GitHub API 的协作时序
模型返回意见后,写回评论这一步看着简单,其实是个坑最多的地方。GitHub 的 PR 行级评论 API 要求你提供path、line、side和commit_id,其中line必须是 diff hunk 中出现的行号,也就是说,要么是新增内容里的新文件行号,要么是删除内容里的旧文件行号。你把模型返回的行号直接填进去,经常会被 API 拒绝,报错"line must be part of the diff"。
解决方法是做一个行号映射表。我先把 diff 解析成{新文件行号 -> diff position}的映射,然后把模型意见里的行号翻译过去。还有一个更省事的选择:用 Pull Request Review API 而不是单条评论 API,POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews,body 里带comments数组,一次性提交所有行级意见,这样能避开很多繁琐的分页和速率限制问题。这个接口有个很好的特性,就是注释会天然归并成一个 review,在 GitHub 界面里呈现为一次完整的评审会话,而不是一堆散落的留言。
3. 落地部署时的关键选择与配置
3.1 部署形态:GitHub Action 还是独立服务
open-code-review 的部署方式主要有两种,我建议按团队规模来选。团队小、仓库少,直接用 GitHub Action 最省事,配置一个 workflow 文件,每次 PR 事件触发,跑完即走,不需要维护任何服务。团队大、仓库多,或者用的是自建的 GitLab,那就要考虑部署成独立 Web 服务,通过 Webhook 接收事件,自己做任务排队和并发控制。
两种方式的核心区别在于"评论状态的保存"。Action 模式是瞬态的,每次 run 都是全新的进程,要把"这个 PR 上次已经审查过哪些 commit"这个状态存下来,只能借助 GitHub 自身的机制,比如检查 commit 上的 label、或者用 PR 描述附加字段。独立服务模式就没这个烦恼,直接在本地数据库存一张review_records表,记录 repo、PR、head_sha、审查结果,天然支持增量审查。
如果你也准备上独立服务,结构可以简单一点:一个 Webhook 接收端点,一个任务队列,一个调用模型的 worker,一个回填评论的 writer。不要一上来就上消息中间件,先用一张带状态的数据库表就能撑住小团队的流量。
3.2 模型选型与参数设定
模型选择直接决定了审查质量和成本,这部分我问过很多用过类似工具的人,结论高度一致:能用大参数模型就别用小的,但也不是无脑上最好的。
| 模型档位 | 适合场景 | 实测感受 | 成本 |
|---|---|---|---|
| 旗舰级(长上下文、强推理) | 复杂逻辑、跨文件依赖、并发类问题 | 分析质量明显高,意见更有依据 | 高,只建议用于重点文件 |
| 中端平衡型 | 默认全部文件 | 常规 bug 检出率基本够用 | 中 |
| 轻量模型 | 规则审查、风格检查、可跳过 | 容易漏错,幻觉也偏多 | 低,适合跑第一遍粗筛 |
参数上,temperature我建议设低一点,0 到 0.2 之间。审查是严谨活,不需要模型的创造性,温度越高越容易编造问题。max_tokens按单文件 diff 的大小给 800 到 2000 之间,太长容易把无关信息带进来,太短评论会被截断。还有一个很多人忽略的参数是frequency_penalty,如果接口支持,稍微调高一点(0.1-0.3)可以有效减少模型反复唠叨同一个问题的情况。
3.3 规则配置与仓库级忽略
规则配置是让机器人"懂规矩"的关键。open-code-review 支持通过仓库根目录的配置文件做细粒度控制,我强烈建议每一个接入的仓库都单独维护一份,而不是全公司套同一个模板。每个项目的技术栈、架构约定、痛点都不一样,一套通用规则必然产生大量噪声。
看一个我实际在用的配置片段:
# open-code-review.yml version: 1 review: enabled: true model: default ignored_paths: - "**/test/**" - "**/migrations/**" - "**/*.lock" - "**/*.min.js" - "docs/**" severity: default_threshold: warning critical: always warning: always suggestion: never # 默认不显示纯建议类评论 rules: - name: no-swallow-errors paths: ["**/*.go"] instruction: "检查错误处理是否吞掉了 error,要求必须向上返回或显式处理" - name: sql-injection-audit paths: ["**/*.py"] instruction: "重点检查 SQL 拼接场景,发现字符串拼接查询时标记为 critical" labels: - "type:automated-review"ignored_paths非常重要。测试代码、迁移脚本、自动生成的锁文件,这些内容送给模型纯属浪费 token,还会引入噪声。规则里的instruction字段是针对特定路径的额外审查要求,相当于给模型开小灶,让它在这类文件上多留一个心眼。
4. 噪声控制:让机器人从"话痨"变成"审稿人"
4.1 重复评论与并发更新的处理
机器人跑起来之后,第一个让人头疼的问题就是刷屏。开发每 push 一次,bot 就重新跑一遍,然后对同样的代码重复评论,PR 页面直接变成批斗大会。这个问题绕不开,因为大模型没有记忆,每次收到的是同一份 diff,自然会产生高度相似的输出。
我的解法分两层。第一层是"审查触发条件":默认只在 PR 从 draft 转为 ready、或者有人手动评论/review时才执行,而不是每次 push 都跑。第二层是"状态去重":在数据库里记录last_reviewed_sha,只有当新的 head commit 与上次审查的 commit 不同时,才触发新一轮审查。如果只是 force push 或 rebase 没改实质内容,就直接跳过。
还有一个细节值得注意:上一轮已经评论过的意见,新一轮要不要重新发?我的做法是把同一 PR 的旧轮行级评论先标记为 outdated(GitHub 原生支持),然后只评论新增 diff 部分的问题。这样既保留历史记录,又不重复打扰作者。
4.2 严重级别分级和过滤阈值
分级是控制噪声的另一只手。模型天然倾向于把问题说得严重,因为它默认你要的是"严格审查"。所以我在输出 schema 和提示词里都强制它输出严重级别,并约定:critical 是会导致崩溃、数据错误、安全问题;warning 是在特定条件下可能出错;suggestion 是改进建议,可有可无。
然后我在配置里做了过滤:suggestion 级别默认不展示,除非仓库 owner 手动打开。跑了一周之后,我把历史数据拉出来看了一眼,suggestion 级别里有价值的信息大约只占一成,剩下的不是风格建议就是泛泛而谈。把这个级别一屏蔽,PR 页面立刻清爽很多。警告级别保留,但会限定单条评论最多 60 个中文字符,强迫模型把问题说清楚,而不是长篇大论写小作文。
4.3 自定义 Prompt 的经验
很多部署教程里都有自定义 prompt 的功能,但真正把它用好的人不多。我踩过一轮坑之后的体会是:让 prompt 做减法比做加法重要。一开始我往提示词里堆了大量的规则,什么"检查命名规范""确保注释完整""提醒写测试",结果模型处处都想管,处处都管不深。
后来我改成只保留三到五条对当前仓库最有价值的检查目标,每一条都用"场景 + 判定标准 + 示例"写清楚。比如对于 Go 服务端代码,我会写:"检查 context 是否可能被 cancel 后继续执行阻塞操作。判定标准:如果调用方传入的 context 已经 Done,函数仍执行超过 100ms 的 IO 操作,标记为 warning。示例:select 里有 context.Done() 分支但在执行数据库查询前没有重新检查。"大模型在这种小而具体的指令下,表现比一堆泛泛规则好得多。
5. 成本、性能与运行观测
5.1 Token 消耗的实测数据
聊完质量聊成本。这是团队决策时最容易被问到的部分。以我接入的两个中等规模仓库为例,实测数据如下:
| 指标 | 数值 |
|---|---|
| 平均每个 PR 的 diff 大小 | 300-500 行 |
| 平均 token 消耗(含 prompt 和输出) | 1.2 万 - 2.5 万 |
| 平均单 PR 成本(按中端模型计价) | 1-3 元人民币 |
| checkout + 审查总耗时 | 3-6 分钟 |
这个成本作为"人工评审之外再加一道保险"来说,是完全合理的。但要注意一个隐藏成本:如果配置不当,同一个 PR 反复触发审查,token 消耗会成倍上涨。我有一次调试配置时,一个 PR 被重复跑了 7 遍,账单直接翻了三倍。所以一定要把 4.1 里的 SHA 去重逻辑做实。
5.2 超时与重试策略
模型接口不稳定是常态,尤其高峰期,单次请求 30 秒以上、连接被重置,都遇到过。如果审查任务跑了一半超时,直接放弃整个 PR 太浪费,我的做法是分片重试:把 diff 切成多个独立块,每个块独立请求、独立记录结果,失败的重试两次,超过 2 次就跳过该块,但保证已完成的部分能正常回填评论。这样至少不会出现"全程报废"的情况。
重试时机上,指数退避比固定间隔靠谱得多,第一次失败等 2 秒,第二次等 8 秒,第三次放弃。同时给整个流程设一个硬超时,我设的是 10 分钟,超过就直接降级为"本次审查跳过,不阻塞合并"。代码评审机器人永远不应该成为发布流程的卡点,这个原则一定要坚持。
5.3 日志和召回率复盘
机器人不是装上就完事了,它需要被"驯化"。我每周会做一次审查结果复盘,具体做法是把每条评论连同对应的代码片段导出成 JSON,然后人工打标:真实有效的 bug、有启发的改进、误报、无意义噪声。跑一个月之后画个简单表格,就能清楚地看到误报率集中在哪些文件类型、哪类规则上,然后再针对性调整 prompt 或忽略列表。
还有一种更务实的评估方式,叫"种子 bug"测试。我在自己的测试仓库里故意埋了几个经典问题——一个忘记释放的资源、一个 off-by-one 的边界、一个没有考虑空指针的分支——然后让机器人审查,看它能抓住几个。这个测试每次改 prompt 后跑一遍,能快速验证改动是正向还是负向,比我肉眼一条条看评论高效得多。
6. 我踩过的坑和目前的使用建议
6.1 行号偏移问题
这是几乎每个做过这类工具的人都会踩的坑。模型读完的是拼接好的 diff 文本,它输出的行号基于的是自己看到的文本,而不是 GitHub API 认定的 diff position。两者经常对不上,尤其是当 diff 里包含多个 hunk、或者某些文件被编辑器做了大面积格式调整时。
我后来干脆不在提示词里让它返回行号了,而是让它在 JSON 里返回一个"意见序号"和对应的代码片段引用(比如func validateInput),我在后处理阶段根据这个函数名或代码片段去 diff 里定位实际行号。这个方法准确率比直接信任模型输出的行号高很多。如果代码片段匹配失败,就让这条意见降级为 PR 汇总评论而不是行级评论,避免把评论贴到错误的代码附近,造成更大的困惑。
6.2 大模型幻觉与"假 Bug"
幻觉是这类工具逃不掉的宿命。模型有时候会一本正经地指出一个"严重问题",追根溯源是它自己脑补出来的规则。我遇到过最离谱的一次,它声称某段 Go 代码存在"隐式的接口断言可能 panic",实际上那段代码根本没有做接口断言。这种假阳性特别消耗团队信任。
防幻觉的办法:一是在系统提示词里写死"如果你对代码行为不确定,不要提出意见;必须引用具体代码来支撑你的结论";二是把评论的策略从"有问题就报"改成"只在满足明确规则条件时才报";三是保持人的裁决权,机器人永远不直接 block 合并,只做建议,合并决定权留给人。三个手段叠加,假阳性率能压到可接受范围。
6.3 团队落地节奏
最后聊落地节奏。我的建议是分三步走。第一步,先在 1-2 个非核心仓库跑两周,配置完全照默认来,只观察噪声率,不推广,让团队成员先熟悉机器人的存在。第二步,根据反馈把规则收紧,打开 warning 级别评论,在周会上用 10 分钟过一遍"本周机器人发现了哪些人工 review 漏掉的问题",把这个价值具象化。第三步,形成团队规范后再逐步扩大覆盖范围。
我在实际使用中最大的感受是:AI 审查机器人最怕的不是技术问题,而是"信任赤字"。一开始团队都会怀疑它是来替代人工评审的,这个误解必须第一时间澄清。它的价值是人类评审的"前置过滤器",把人均 40 分钟的评审时间压缩到 15 分钟,让人把精力集中在机器人看不透的业务逻辑与架构决策上。真正跑通之后,团队里的态度会从"这机器人真烦"慢慢变成"诶,这个 PR 怎么机器人没报问题,我心里反而有点不踏实了"。到这个阶段,这套工具才算是真正融进了团队的开发流程。