☰
open-code-review:用大语言模型实现自动化代码审查的工程实践
2026/9/26 12:21:25 网站建设 项目流程

1. 从"PR 挂了三天没人理"到搭建 open-code-review

我印象很深,上上个月周三下午,群里弹出一条消息:"各位,我的 PR 挂了两天半了,有没有人有空 review 一下?"三分钟后没人回,五分钟后还是没人回。这种事在我们团队不是第一次了,代码审查永远排在写代码、改 bug、开会的后面,成了名副其实的"待办事项的终点站"。

做 open-code-review 这个开源项目的念头就是这么来的:既然人肉 review 会拖延、会遗漏、会看漏低级错误,那我能不能让机器人先顶上去,把第一轮粗筛做掉,人再上来做真正需要判断力的审查。

先说清楚,它是一个"开放式的自动化代码审查"工具,核心做法是把 GitHub 或者 GitLab 上的 Pull Request 拉下来,用大语言模型逐文件、逐 diff 地审一遍,然后把审查意见以评论的形式贴回 PR 里。听起来不复杂,但真的从"能用"做到"好用",中间有大量细节。

这篇文章我会从项目初衷、内部架构、部署踩坑、质量调优、团队实践五个维度完整拆一遍。目标读者是想给团队搭一套自动 code review 基础设施的开发者,也包括那些维护开源仓库、每天被 PR 淹没的独立维护者。

1.1 传统 code review 的三个失效场景

我把团队里的 code review 失效场景归结为三类,相信很多人都有同感。

第一类是"LGTM 式走过场"。PR 很简单、改动很小,reviewer 不好意思拒绝,点开扫一眼就回一句 Looks Good To Me。但这种"零反馈"的审查对代码质量没有任何提升,反而会养成"反正没人看"的心态。

第二类是"滞后式审查"。PR 提交后两三天才有人看,此时开发者的上下文早断了,重新解释、重新回忆的成本比做审查本身还高。更麻烦的是,如果主线已经推进,开 review 讨论时会跟新代码冲突,最终很多讨论不了了之。

第三类是"新手沉默"。团队里经验尚浅的成员往往不敢在陌生模块的 PR 里发言,不是没想法,是怕说错。而没有多元视角参与,review 的覆盖度其实很有限。

这三个失效场景不是靠"加强流程管理"能解决的,本质问题是精力分配和开口成本。自动化工具有办法把第一层过滤做掉,把人的精力留给真正重要的讨论。

1.2 open-code-review 的定位:兜底、提速、降低开口门槛

open-code-review 给自己的定位是三件事:兜底、提速、降低张嘴门槛。

兜底指的是机器会逐行扫 diff,把明显的代码问题——空指针隐患、错误处理缺失、调试代码漏删、魔法数硬编码、明显的性能浪费——在第一时间指出来,不让它们流到人工 review 阶段。

提速是回应时间。webhook 触发后,平均响应在一分钟左右,作者不用再等两天才知道自己写的东西有没有问题。改得越早,成本越低,这是铁律。

降低开口门槛是个偏软的收益。机器人的评论是客观的、非情绪化的,把那些"这行是不是有点问题"的基础问题说完之后,人类 reviewer 反而更容易就真正的设计问题开口。我们观察到,机器人介入后,人工评论的深度明显增加了,大家不再把精力耗在低级 nit 上。

1.3 边界画清楚:它不能替你做什么

我也得泼点冷水。开始做这个项目之前,一定要把边界想清楚,否则期望管理会出大问题。

open-code-review 能审的是 diff 层面的问题——上下文相关的问题,比如"这个变量名是不是有歧义""这个异常吞了要不要处理""这段逻辑和另外某个函数是不是重复"。它目前做不到的是跨 PR 的架构级判断——比如"这次改动会不会影响整体的模块划分""这个接口设计是不是应该拆成两个"。这不是模型能力不够,而是单次审查拿不到完整仓库的历史脉络和团队约定,硬让它做架构评审只会产生一堆泛泛而谈的意见。

还有一类东西我也不建议让它审:业务逻辑正确性。机器人可以告诉你"这段代码没有判空",但它没办法告诉你"这段业务应该发邮件而不是发短信"。业务校验要靠测试用例和业务负责人,别指望自动化工具。

2. 拆开 open-code-review 的内部链路:一次 PR 的完整旅程

搞清楚了边界,就可以看内部了。我设计这套链路时有一个核心原则:每个环节都必须是可观测、可干预、可降级的。自动化审查最大的风险不是"审错了",而是"审错了你还不知道为什么错、改不了、停不掉"。

2.1 从 webhook 到 diff 快照:入口处的三次校验

整个流程从 GitHub 的 pull_request webhook 开始。收到事件后,第一步不是拉代码,是三次校验。

第一次校验是权限。请求头里带过来的签名需要用 GitHub App 的私钥哈希验证,防伪造。我吃过亏——最初没有验签就处理请求,结果有人拿裸 HTTP 请求把服务器打满了,一个晚上跑掉几百万 token。

第二次校验是事件类型。只有 opened、synchronize、reopened 三种事件才需要触发审查,其它如 closed、assigned、labeled 都直接忽略。这里有个小坑:每次 push 新 commit 都会触发 synchronize,如果一个 PR 被频繁推送,机器人就会反复审查,惹作者烦。我的做法是在这个环节加一层"节流"——距上次审查不足十分钟的新事件直接丢弃,除非改动超过五十行。

第三次校验是配置项。仓库根目录有没有 .opencodereview.yml?文件里有没有被 enable?审查开关是不是开着的?这些都要在入口确认,不能等拉完 diff 才发现不需要审,白白消耗 token。

三次校验通过后,程序调 GitHub API 拿 PR 的完整 diff。这里要注意分页,一个大 PR 的 diff 可能几百 KB,需要循环拉取直到拿到全部内容。我建议在内存里把 diff 转成一个结构化对象,包含变更文件列表、每个文件的增删行、以及每行对应的旧行号和新行号——后面做评论回写时需要精确的行号定位。

2.2 按文件切分审查单元:避免把整个 PR 塞给模型

这是整个项目里性价比最高的一次设计。绝大多数自动审查工具直接把整个 diff 丢给模型,让模型一次性输出所有意见,效果很差——上下文太长容易丢失细节,一次输出太多评论也会让审查结果变成一锅粥,作者根本没法逐条处理。

我的做法是"按文件切分审查单元"。一个 PR 里可能改了十几个文件,我把每个文件单独作为一个审查任务提交给模型。每个文件内部的 diff 再按 hunk 做第二轮切分,但也不是死板地切,而是按逻辑块合并。具体规则是这样的:

  • 改动行数小于 200 行的文件,整体作为一个审查单元;
  • 改动行数超过 200 行的,按文件内具体的函数或类划分,尽量保证每个审查单元是一个完整逻辑;
  • 纯自动生成的 lock 文件、package-lock.json、go.sum 这类直接跳过;
  • 文档类改动 (.md、.txt) 默认不审,除非配置里显式开启。

切分的收益非常明显。模型专注于一个文件的一段逻辑时,给出的意见明显更具体、更有针对性,而不是泛泛的"请确保处理错误"。另外,按文件切分还有一个隐藏好处:单个文件审查失败不会拖垮整个 PR 的审查任务,可以做部分成功、部分重试的降级处理。

来看一下核心调度代码,这段逻辑我用 Python 写的:

def split_diff_into_units(diff_text, config): files = parse_diff(diff_text) units = [] for file in files: if should_skip(file.path, config): logger.info("skip file: %s", file.path) continue extension = get_extension(file.path) if file.total_lines <= config.max_whole_file_lines: units.append(ReviewUnit(file=file, content=file.full_diff())) elif extension in config.logical_chunk_extensions: units.extend(chunk_by_logical_blocks(file)) else: units.append(ReviewUnit(file=file, content=file.truncated_diff( max_lines=config.max_chunk_lines ))) return units

chunk_by_logical_blocks 这个函数会尝试通过缩进和函数声明做启发式切分。语言不同启发式也不同,Python 看顶层 class/def 的缩进,JavaScript 看顶层 const/function/class 声明,Go 看 func 声明。切分结果虽然做不到 100% 准确,但覆盖了大多数情况,而且因为每块都在可接受的大小内,即使切错了位置,模型还是能根据上下文判断出大致的逻辑边界。

2.3 审查 prompt 的分层设计:任务指令、规则库、项目上下文

prompt 是审查质量的核心,但很多人对 prompt 的理解仅限于"写一段好的对话模板"。我在项目里把 prompt 设计成了三层:

第一层是任务指令。固定的一段文本,告诉模型"你是一个代码审查助手,需要基于 diff 找出问题,按严重程度分级输出"。这一层要写得非常死板,防止模型发挥。我遇到过模型开始夸代码写得好的情况——我不是要它夸我,我要它挑刺。

第二层是规则库。这是一个可配置的部分,来自仓库目录下的 .opencodereview/rules/*.md 文件。团队可以在规则库里写自己的约定,比如"禁止在 controller 层写业务逻辑""所有金额计算必须用 Decimal""错误信息必须包含上下文标识"。模型每次审查时会把规则库内容一起塞进 prompt,相当于让机器人对齐团队自己的规范。

第三层是项目上下文。包括当前分支名、PR 标题、变更文件的路径、以及上一次审查结果中已经被作者解决掉的意见列表。项目上下文不追求大而全,只要能让模型大概知道这次改动处于什么位置。

三层放一起的效果是:模型的建议逐渐从"通用的编码建议"变成"贴合你们团队习惯的编码建议",这个变化需要迭代几周才能稳定下来,但一旦稳定,团队对新工具的抵触会小很多。

2.4 结果聚合与去重:单文件多轮的评论怎么合并

模型返回的审查结果是一段 JSON 结构,包含若干条意见。每条意见需要映射回具体的文件、行号。这里有个关键技术点:模型的意见里通常没有精确的行号,只有"在 xxx 附近"或者"第 120 行附近",需要做模糊匹配。

我的做法是取意见里的行号,向上下各扩展 10 行,找到最近的可评论行(新变化行),把评论钉在那里。如果实在找不到就返回整个文件的顶部——但这种情况极少。

聚合的时候还要去重。一次改动如果横跨多个 hunk,模型可能在不同 hunk 中对同一个问题给出类似意见,需要做一次简单的文本相似度去重。我用的是最简单的方案:意见的文本归一化后计算 Jaccard 相似度,超过 0.75 就认为是同一条,保留严重程度高的一条。

最后,把所有意见一次性通过 GitHub 的 review API 贴回 PR,而不是逐条地发普通评论。使用 review API 的好处是:意见会聚合在一个 review 里,作者可以一次性看到全部;状态标记为 REQUEST_CHANGES 可以让 CI 流程感知;后续新的 commit push 上来时,这条 review 会自动标记为 outdated,有清晰的变更轨迹。

这里给出简化版的聚合提交逻辑:

async function submitReview(github, repo, prNumber, findings) { const comments = findings.map(f => ({ path: f.filePath, line: f.lineNumber, side: 'RIGHT', body: formatComment(f.severity, f.category, f.message, f.suggestion) })); const event = findings.some(f => f.severity === 'error') ? 'REQUEST_CHANGES' : 'COMMENT'; await github.pulls.createReview({ owner: repo.owner, repo: repo.name, pull_number: prNumber, event, comments, body: `🤖 open-code-review 自动审查完成\n\n本次审查发现 ${findings.length} 个问题,其中 ${findings.filter(f => f.severity === 'error').length} 个严重问题。` }); }

这已经可以跑通了,真正让这个项目从"能跑"到"敢上生产",是部署和调优阶段的事。

3. 部署与接入:从仓库克隆到机器人开口说话

很多开源项目死在"仓库能跑"和"生产能用"之间的那条沟里。open-code-review 也一样——代码逻辑处理好之后,部署形态、鉴权配置、成本控制、异常降级,这些才是真正磨人的地方。

3.1 两种运行模式的取舍:Docker 常驻与 CLI 批跑

我提供了两种运行模式,覆盖不同的使用场景。

第一种是 Docker 常驻模式,适合团队内部署。它启动一个 HTTP 服务,接收 GitHub webhook 推送,内部用队列管理审查任务。为什么用队列?因为 webhook 的到达不是均匀的,可能一分钟内连推五个 PR,也可能一小时一个都没有,直接同步处理容易把模型接口打满。队列可以把任务平滑地摊开,配合并发数控制在 RAID 限流范围内。

第二种是 CLI 批跑模式,适合本地调试和个人开源项目。命令很简单:

open-code-review --repo owner/name --pr 123 --provider openai --model gpt-4o-mini

它会主动拉指定的 PR,审查完把结果输出到 stdout 或写进本地文件。这种模式不需要公网服务暴露、不需要配置 webhook,一次性跑完就退出,很适合接在 CI 里手动触发。

两种模式共用一个 core 库,审查逻辑完全一致,只差在外部交互方式上。这种设计带来一个好处:本地调试时用 CLI 模式十分钟跑完一个 PR,不用反复推 webhook。

3.2 GitHub 接入配置:App 权限、Webhook 密钥与重试语义

接入 GitHub 时最容易踩坑的是权限配置。我强烈建议用 GitHub App 而不是 Personal Access Token。GitHub App 有三个好处:权限可以收得很窄、有独立的 rate limit 配额、密钥可以轮换。

App 需要的权限如下:

权限级别原因
Pull requestsRead & write读取 PR diff、写 review
ChecksRead & write创建 check run 展示状态
ContentsRead-only读取规则库文件
MetadataRead-only基础元数据访问,必选

Webhook 配置时注意 Secret 一定要填,这是签名验签的基础。我在前文提到过不验签的惨痛教训,这里再重复一遍:不验签等于把服务器裸奔在公网上,任何人都可以伪造事件把你的 token 烧光。

重试语义也要处理。open-code-review 对外部依赖(GitHub API 和模型 API)都要做重试,但重试策略完全不同。GitHub API 失败通常是限流,返回 429 或 403,这时应该等待 Retry-After 头指定的时间再试,而不是用固定间隔。模型 API 失败通常是 5xx 临时错误或者超时,可以用指数退避:第一次 1 秒、第二次 2 秒、第三次 4 秒,最多五次。超过重试上限的任务进入死信队列,由管理员手动处置。

3.3 模型选型与 token 预算:先算清楚这笔账再看效果

选型不能只看"哪个模型审得更准",先算账,再看效果。我提供一个估算公式:

单次审查 token 消耗 ≈ 固定 prompt 开销 + Σ(每个文件的 diff token + 规则库 token)

固定 prompt 开销大约 1500 token(三层 prompt 的基础部分)。每个文件的 diff 大约每行 15-25 token。一个典型的中型 PR,改动 15 个文件、平均每个文件 60 行 diff,加一个 500 token 的规则库,总消耗大约:

1500 + 15 × (60 × 20 + 500) = 1500 + 15 × 1700 = 27000 token

按 100 个 PR 每月、每百万 token 5 美元级别的定价算,一个月大约 13.5 美元。这个成本对绝大多数团队来说完全可以接受。

模型选型上,我跑了大半年实验后的结论是:速度比绝对准确率重要。审查机器人最重要的是反馈及时,哪怕意见 80% 是对的,只要十分钟内给出,作者就会觉得这个工具有用。如果意见 95% 准确但要跑四十分钟,作者的上下文已经断了,工具的价值会大幅下降。所以生产环境我默认用中档推理模型,真正重度的架构审查才考虑换高级推理模式。

3.4 踩坑实录:队列堆积、乱序回复与超大 diff

跑生产之后,最先遇到的坑是队列堆积。GitHub App 创建时默认只有 5 个并发在跑模型请求,有一次下午突然推上来十多个 PR,队列长度瞬间飙到 30,后续任务等了快二十分钟才被处理。作者们开始抱怨:机器人还不如人审得快。

排查后才发现问题不在模型接口,而在 GitHub App 的并发配额。修复很快:把并发数调到 10,加了一个简单的监控,队列深度超过阈值就报警。顺带加了按 PR 大小的路由策略——小 PR 走快速通道,大 PR 走慢速通道,避免一个大 PR 占掉所有并发导致一堆小 PR 饿死。

第二个坑是回复乱序。同一批审查任务并发执行,模型返回时间不一致,后台是哪个任务先结束就立刻把对应文件的 review 提交上去。结果作者在 PR 里看到的是"文件 A 的意见先到,过了两分钟文件 C 的意见才到",评论顺序被拆散,阅读体验很差。

修复方案是加"批次收集":一次 webhook 触发的所有文件审查任务都完成后,统一汇总提交一轮 review。代价是整体响应时间变慢了一点点,但阅读体验好非常多。作者打开 PR 看到的是一份完整的审查报告,不是一个零散的心电图。

第三个坑是超大 diff。曾经有个重构 PR 一次性改了 4 个文件、将近两千行,如果整包塞给模型,token 消耗直逼五万,结果吐回来的全是大而化之的意见。后来加了"截断策略":单文件超过 300 行的改动不再完整审查,只审关键节点比如函数声明处、TODO、调试代码标记,其余部分留给人工。这不是偷懒,是实用主义——超大 PR 本身就需要拆分成多个小 PR 来审,机器人的职责是帮你把"这个大 PR 里明显的问题"扫出来,而不是替你做全量 review。

4. 审查质量调优:把"AI 味评论"压到最低

部署通了只是开始,真正让人头疼的是"审得准不准、有没有在说废话"。我花在调优上的时间比写核心逻辑的时间还多,这套调优方法论值得单独拿出来讲。

4.1 误报与漏报的平衡:置信度阈值不是越高越好

一开始我以为误报控制得越严格越好,于是给模型加了一条指令:不确定的问题不要输出。结果误报确实变少了,但漏报多到团队开始觉得这工具没用——真正有问题的地方机器人都没说话。

后来我改成按严重程度分级区分处理:error 级别的意见要求模型必须给出足够证据,低置信度直接丢弃;warning 级别允许"疑似问题"存在,但必须在意见里说明为什么觉得有问题;suggestion 级别更宽松,主要用于风格类建议,但也必须落在 diff 的具体行上。

这么分级之后,误报和漏报找到了一种平衡。作者看 error 级意见时会非常认真,因为那些确实大概率是问题;suggestion 级的意见会因为语气是建议性的,读者心理防御比较低,反而愿意接受。

4.2 让机器人学会团队规范:rules 文件的设计

这是让团队更容易接受机器的关键。每次工具给出和团队习惯相悖的建议,成员就会觉得"这玩意不懂我们"。与其解释,不如把规范喂给它。

rules 文件设计成纯 Markdown,放在 .opencodereview/rules/ 目录下,文件名即分类名。例如:

# error-handling.md ## 规则一 所有外部 API 调用必须设置超时,默认不超过 5 秒。 ## 规则二 错误信息必须包含上下文标识,例如订单号、用户 ID。 ## 规则三 禁止裸捕获。捕获到的异常必须记录日志或者向上抛出,不可静默吞掉。

规则写的越具体、越有例子,模型遵循得就越准确。为了避免规则库无限膨胀导致 prompt 过长,我建议每份规则文件不超过 20 条,总库不超过 10 份。模型不是 vector store,塞太多规则反而让核心任务失真。

一条经验:规则不是一次写好的,是"摩擦驱动"的。机器人给出了让你不舒服的建议,你就把它写进规则里;机器人漏掉了你们团队认为非常重要的问题,你也要写进规则里。每两周花十分钟过一次 rules 文件夹,删掉已经不用的、合并重复的、补上新约定的。这套动态更新机制非常管用。

4.3 换模型不如换 prompt:三组对比实验的结论

我做了三组对比实验,想验证"换个更强的模型是不是能让审查质量翻倍"。

第一组:同一份代码,用基础模型和高级推理模型分别审。高级模型在复杂逻辑的解析上确实更有优势,意见的"宏观感"更强,但在低级问题的查找上并没有显著差异。

第二组:给同一模型换不同风格的 prompt。从"请审查以下代码"到"请以资深 reviewer 的身份,带着找出五个真实问题的目的审查代码",问题识别的数量翻了将近一倍——但误报也同步上涨。这说明 prompt 引导对输出的影响,不亚于模型本身的能力差异。

第三组:在 prompt 中加入规则库后,意见和团队实际的代码风格匹配度立刻上升,模型给出的建议直接可采纳的比例从 30% 提高到了 55% 以上。

三组结论合并成一个:先调 prompt,再调规则,最后才考虑换模型。换模型是性价比最低的选择,因为模型的差异可以通过调度策略和温度参数来缩小,而 prompt 和规则库才是把"通用的模型能力"转成"你的项目的专属能力"的部分。

5. 实践两个月后,团队协作方式悄悄变了

工具上线到现在快三个月,团队里的协作氛围发生了不少变化,有几个变化是我当初没想到的。

5.1 新 PR 时间线:机器人先审,人再审

现在的 PR 流程变成这样:作者提交 PR 后,机器人十分钟内给出一份完整的自动审查报告。那些不涉及设计判断的问题,作者通常在机器人给出报告后的半小时内就改完了。等人类 reviewer 真正上手时,PR 已经是"通过机器人初筛且作者自测过"的状态,人工 review 的焦点就自然地落在了更值得讨论的模块设计、边界情况这些高价值问题上。

有一次我统计过,一个典型的中型 PR,从提交到拿到第一个有效人工评论的时间,从平均两天多缩短到了四个小时左右。这不是因为人变勤快了,而是因为"人需要做的事情变少了"——人只处理机器审不了的题,自然愿意早点开始。

5.2 作者与"没有感情的审查者"的相处方式

团队对机器人的态度经历了一个"反感—接受—依赖"的曲线。初始阶段,有人觉得"给代码提建议的又不是人,改了有啥意义",还有人对某些误报直接不回。转折点出现在一次线上事故——根因是调用了第三方的 API 没做超时控制,而这问题机器人其实在 PR 阶段就发现过,被作者以"先上线再说"为由忽略了。

那次之后,团队定了个不成文的规矩:error 级意见必须给出不修复的明确理由才能忽略。机器人的意见从"仅供参考"变成了"默认要处理"。这个变化不是靠增加批评力度实现的,靠的是"回答写得好不好,先看提问质量高不高"——机器人只要提出来的问题足够多打在痛点上,团队的信任度就会慢慢建立起来。

5.3 我们追踪的三个指标和真实变化

这三个月我们一直在追踪三个指标,数据是最有说服力的。

第一个指标是"首次审查响应时间",从 48 小时以上降到了 15 分钟以内。这个指标直接决定了作者愿意不愿意及时修复问题,反馈越快改得越勤。

第二个指标是"PR 合并前平均 review 轮次",从原来的 2.8 轮降到了 1.6 轮。因为有了一台 24 小时在线的"粗筛机",人都盯着真正值得讨论的问题去了,讨论效率是实打实上来的。

第三个指标是"缺陷逃逸率"——合并后一周内被测试或用户发现的功能性缺陷数量,同期对比下降了大约 35%。这个数据样本还不大,但趋势是明显的。

顺带一提,团队新人上手陌生代码库也有了新路径:提交一个 PR 到不熟悉的模块,机器人的意见就是一份"这个模块哪部分容易出错"的活地图。新人不再需要翻遍整个目录才知道该注意什么。

跑了几个月下来,我的总结是:open-code-review 不会替代任何人的代码审查能力,也不应该替代。它做的只是把最辛苦、最重复、最不该占用人的精力的那层活接过来,然后让人类 reviewer 去做真正需要经验、判断和沟通的事情。如果你也想在团队里搭一套类似的机制,我最大的建议是:从规则库开始维护,从小范围试点开始跑,先让十几个人用起来,再慢慢铺开——直接把一个"什么都会提两句意见的机器人"丢给全团队,大概率会被众人表决撤下去。机器人的信任感像代码一样,是一行一行挣出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询