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(filefile, contentfile.full_diff())) elif extension in config.logical_chunk_extensions: units.extend(chunk_by_logical_blocks(file)) else: units.append(ReviewUnit(filefile, contentfile.truncated_diff( max_linesconfig.max_chunk_lines ))) return unitschunk_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、写 reviewChecksRead 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 去做真正需要经验、判断和沟通的事情。如果你也想在团队里搭一套类似的机制我最大的建议是从规则库开始维护从小范围试点开始跑先让十几个人用起来再慢慢铺开——直接把一个什么都会提两句意见的机器人丢给全团队大概率会被众人表决撤下去。机器人的信任感像代码一样是一行一行挣出来的。
