我当初发起 open-code-review 这个项目,纯粹是被团队里的 Code Review 逼出来的。
那会儿我们组代码审查已经名存实亡:PR 挂着三四天没人理,偶尔有人点个 approve 也是"LGTM"三个字母走人,合并进去的代码照样出线上事故。我一度以为是团队态度问题,后来想明白了,真正的问题在于我们把 Code Review 做成了纯粹的负担。人看代码的时间和精力本来就有限,工具和流程不帮忙,光靠"认真一点"解决不了问题。
所以我就开始折腾一套更开放的代码审查体系,也就是后来开源的 open-code-review。这篇文章把我从设计思路到具体配置、再到踩坑复盘的全过程都整理出来,希望对于正在被审查流程折磨的团队有点参考价值。
1. 为什么多数团队的 Code Review 最后都成了摆设
1.1 三个典型症状,我猜你团队里至少中一个
在动手改流程之前,我先花了两周时间观察我们团队的真实状态,总结下来其实就是三个典型症状。
第一个是"approve 机器人"。打开一个 PR,reviewer 名字签了一排,但点进去看评论记录,除了"好像没问题""试过了OK"这类话,没有一条实质性反馈。代码合并得飞快,出问题的时候没人记得自己看过。
第二个是"reviewer 摆设化"。PR 指派的评审人往往是团队里资历最浅的那个人,或者干脆是随便点的。资深工程师永远在开会、写方案、处理线上问题,根本挤不出时间看代码。安排给他 review,他就等着别人先给意见,最后批个 approve 走流程。
第三个是"合并窗口焦虑"。一到周五下午,大家开始疯狂合代码,因为怕带着半成品过周末。这种焦虑驱动的合并,code review 必然走形式——谁还有心思逐行看逻辑,能编译过就万事大吉。
这些症状的本质,是把 Code Review 当成"流程关卡",而不是"质量机制"。关卡思维下,所有人的目标都是"赶紧过关",至于审查本身有没有起作用,根本没人关心。
1.2 根子不在态度,在审查成本太高
有一次我专门统计了一下组内平均一个 PR 的处理时长。不算评审等待时间,光算人肉逐行阅读有效代码的时间,一个 300 行左右的 PR 大概要花掉 40 到 60 分钟。如果 PR 里还夹着格式化改动、依赖升级、文件权限变动这些噪音,实际真正需要人脑判断的核心逻辑可能只有 100 行。
我更想说的是成本结构的问题。人做一次认真评审,真正贵的地方在"进入状态"和"上下文切换"。你得先读懂这个 PR 的背景,搞明白改动跟现有模块的关系,再逐段推理边界条件和异常分支。这个过程极其烧脑,而且很容易被钉钉消息打断。一旦被打断,重新回到深度评审状态又是 10 分钟。
所以很多有经验的工程师下意识地选择"跳过细节,只看大方向"。不是不想负责任,是制度设计没有帮他们降低这种认知负担。工具该干的活让人干了,审查自然就成了走形式。
1.3 我最初的失败尝试:光靠制定规则根本没用
意识到问题之后,我的第一反应是写规范文档。洋洋洒洒三千字,《Code Review 红线清单》《评审人职责说明》《PR 描述规范手册》,发到群里,还专门开会宣贯了一轮。结果呢?两周之后打回原形。
规则没有被内嵌到工作流里,它就只是一份没人看的文档。开发者的注意力天然被需求、排期、bug 占据,让他在提交 PR 的时候还记得翻规范文档,根本不现实。
这次失败让我下决心换思路:与其花力气教育人,不如把流程规则全部工程化、自动化,设计一套"即使大家不完全自觉,也能跑起来"的机制。这就是 open-code-review 最早的雏形。
2. 一套可持续的审查机制该怎么搭
2.1 从作者自查开始:提交 PR 之前先过一道机器卡口
我设计的这套体系,最前面一道关不在 reviewer,而在提交者本人,只是这个"本人"被机器强制拉回来了。
open-code-review 在 CI 里嵌了一个"提交自检"阶段,干三件事。第一件,校验 PR 标题和描述格式,要求必须写清楚背景、改动内容、影响范围。第二件,检查 PR 的 diff 规模。如果一次 PR 改动超过 500 行,CI 直接提示拆分,不允许合并。第三件,跑变更文件的 lint 和静态检查,有问题直接在 PR 页面上标注出来,而不是等 reviewer 去翻日志。
这里面最关键的设计是"机器先说话"。人看到机器人已经提了一堆意见,心理上就不会觉得"这个 PR 还没人管",而且作者在提交前就被引导着把描述写清楚、把改动控制在小范围,reviewer 真正看到的是一个有上下文、有边界的变更,而不是一坨没法下手的 diff。
2.2 工具链选型:哪些该交给机器,哪些必须留给人
在选型阶段,我定了一个分工原则:凡是规则明确、可重复、不需要设计判断的事,全部交给机器;凡是涉及架构权衡、业务语义、长期维护性问题,全部留给人。
按这个原则,我选了四个核心组件:
| 组件 | 定位 | 负责内容 |
|---|---|---|
| 代码托管平台原生保护 | 流程闸门 | 分支保护、必填状态检查、CODEOWNERS 审批 |
| ReviewDog | 注释引擎 | 把 lint / 静态检查结果直接写到 PR 行级别 |
| Semgrep / ESLint 规则集 | 检测能力 | 安全风险、业务规范、反模式识别 |
| open-code-review 脚本 | 流程编排 | 自检报告生成、审查清单核对、AI 辅助意见汇总 |
你可能注意到我没有选那些"全自动合并审查"的大而全商业工具。因为我觉得代码审查这件事,最终应该引导人去做判断,而不是让工具替团队背锅。商业工具没做不好,只是它们把重点放在"帮你看完代码"上,而我觉得当务之急是"帮你把看代码的环境理清楚"。
2.3 为什么我把这些东西开源出去
做一个内部工具,其实没必要开源。我之所以把 open-code-review 做成开源项目,原因还挺实在的。
首先是团队维护成本。代码审查相关的规则和脚本,如果只躺在公司内网仓库里,就得自己长期维护。开源之后,社区能帮忙提问题、补规则、适配新框架,消耗的是公共的智力资源。
其次是规则透明。审查规则直接写在仓库里,任何人都能提 PR 修改。这比在内部文档里改一版又一版要健康得多。规则对所有人可见、可讨论、可回溯,本身就是一种开放文化。
最后是行业积累。代码审查这件事,每个团队都在做,但大家踩的坑高度相似。与其各自闭门造车,不如把通用部分提取出来,让前端、后端、移动端团队都能直接在 open-code-review 基础上二次开发。我觉得这才是开源该有的样子。
3. CI 流水线里卡审查:七组关键配置直接抄作业
3.1 整体流程:从提交到合并的完整链路
我直接给出 open-code-review 项目的参考流水线配置。这套配置在 GitHub Actions 上实现,GitLab CI 的逻辑也是一样的,改改语法就能迁移。
整个流程分为 submit、analyze、approve、merge 四个阶段:
- submit 阶段:作者提交 PR,触发标题规范校验和描述模板检查。
- analyze 阶段:静态检查、lint、AI 辅助意见并行执行,结果回写到 PR 评论。
- approve 阶段:检查 CODEOWNERS 指派的评审人是否已 approve,以及所有状态检查是否通过。
- merge 阶段:再次校验最新分支是否通过全部检查,然后自动合并。
这个顺序很关键,看起来是常识,但很多团队就是在这里栽跟头,后面我会详细说。
3.2 提交自检:PR 标题与描述模板校验
第一个配置是标题规范。open-code-review 采用 Conventional Commits 规范的一个子集,要求 PR 标题匹配^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .{10,}$。这个正则要求:必须写清楚变更类型、可选的影响模块,而且描述不能短于 10 个字符。
name: pr-check on: pull_request: types: [opened, edited, synchronize, reopened] jobs: check: runs-on: ubuntu-latest steps: - name: Check PR title uses: actions/github-script@v7 with: script: | const title = context.payload.pull_request.title const pattern = /^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .{10,}$/ if (!pattern.test(title)) { core.setFailed('PR title must follow: type(scope): description (>=10 chars)') }标题规则会被直接卡住,但这其实是最不重要的一条,它只是为了让后续的变更记录和发布日志更好生成。真正有价值的是描述模板。
3.3 静态检查结果直接变成 PR 行级注释
光有静态检查还不够,关键是把检查结果变成开发者最容易看到的形态:PR 讨论页里 diff 行级别的小气泡。这个用 ReviewDog 实现。
- name: Run reviewdog uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review level: warning filter_mode: diff_context fail_on_error: true这里有两个参数值得说明。reporter: github-pr-review意味着每条 lint 错误都会直接发表在 PR 对应代码行的评论区,作者收到通知后顺着对话就能定位问题,不需要跑去 CI 日志里翻。
filter_mode: diff_context表示只报告当前 diff 上下文里的问题。这是个克制但重要的选择,如果整个项目历史遗留问题很多,全量报告会产生大量噪音,直接磨掉大家看评论的耐心。
3.4 强制 codeowner 审批:谁来把关落实到文件
GitHub 的 CODEOWNERS 机制已经存在很多年,但我发现很多团队压根没用过。在 open-code-review 的参考仓库里,我会放一份这样的配置:
# CODEOWNERS # 核心模块必须由模块负责人审批 /src/core/ @core-maintainer # 数据存储相关改动 /src/storage/ @infra-maintainer # 全仓库兜底 * @tech-lead配合分支保护规则,要求"至少 1 个 codeowner 的 review"才能合并。这个机制把审批责任精确落到代码对应模块的负责人头上。比如我改了 storage 模块的接口,只有 infra-maintainer 点的 approve 才算数,其他人批得再多,状态检查也不会变绿。
选 CODEOWNERS 而不是"团队成员都行",目的是对抗"approve 分摊制"。当任何一个人都可以批准任何模块时,review 就变成了同事之间互相掩护的社交行为。责任落到具体人头之后,每个人都得对自己签过字的改动负责,审查质量会明显改善。
3.5 分支保护与合并队列:避免"检查通过后代码又变了"的概率
很多团队的 CI 有这样一个 bug:检查在 PR 创建时触发,等到合并前分支已经被 push 了新代码,但状态检查还是绿的。GitHub 的分支保护设置里有一个选项叫"Require branches to be up to date before merging",即要求 PR 分支始终基于最新目标分支。
我建议打开这个选项,同时开启 GitHub 的 merge queue。前者在原理上保证 CI 结果对当前代码有效,后者解决多个人同时点合并时互相覆盖的问题。
# .github/mergequeue.yml 示例 merge_queue: strategy: method: merge check_successful: true合并队列本质上是一个串行化的合并闸口。所有满足合并条件的 PR 进入队列,系统按顺序逐一把最新的目标分支合入 PR 分支,重新跑一遍 CI,通过后再合并。这样能彻底消除"上午检查通过,下午合并出问题"的经典事故。
3.6 变更范围安全网:敏感路径必须额外审批
在常规的代码审查之外,我还加了一层"变更范围安全网"。有些文件,谁都不会仔细 review:依赖锁文件、CI 配置、部署脚本、权限配置。但恰恰是这些"没人看"的文件,出问题的时候影响面最大。
open-code-review 里内置了一个 path-filter 规则,识别以下情况:
package-lock.json/yarn.lock/poetry.lock等依赖锁文件的变更;.github/workflows/下的 CI 流程改动;- Dockerfile、docker-compose.yml、Kubernetes 部署清单;
- 任何权限相关的配置文件。
遇到这些路径的变更,CI 会自动在 PR 上追加标签safe-guard-review,并且要求额外的 codeowner 审批。这不是说普通工程师不能改这些文件,而是希望这些变更必须有更资深的同事把一眼。实际效果是:依赖升级和 CI 改动被讨论的频率显著提升,因为标签给了 reviewer 一个明确的"这里要小心"信号。
3.7 自动生成审查报告:让每次 review 都有迹可循
最后一个配置是审查报告的自动生成。每个 PR 关闭之后,机器人会把以下信息整理成一条评论:变更行数、文件数、reviewer 人数、讨论条数、从提交到合并的总耗时、静态检查发现问题数。
这些数据会逐步沉淀成团队的过程指标。不用等季度复盘,随时打开任意一个 PR 都能看到"这个改动经历了多少次真讨论才合进去",也是后续绩效考核的一个客观参考维度。
- name: Generate review report uses: actions/github-script@v7 with: script: | const { owner, repo } = context.issue const { data: reviews } = await github.rest.pulls.listReviews({ owner, repo, pull_number: context.issue.number }) const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: context.issue.number }) // 汇总数据,发布到 PR 评论区4. 用 AI 辅助 Review,让人把精力留给真正的设计问题
4.1 为什么我坚持 AI 只做辅助,不做最终裁决
人工智能辅助写代码现在不算新鲜事,很多工具都能直接帮你看 diff。我在 open-code-review 里也接了一层 AI 辅助审查,但定位非常克制:AI 输出的每条意见都标注为建议,且不参与合并关卡,更不会因为 AI 认为有问题就阻止合并。
原因很简单:AI 对业务语义的理解仍然不可靠,用它做硬卡点,会引来大量误报和优先级错乱。让 AI 负责"指出疑点",让人负责"做判断",才是当前阶段最现实的人机分工。
4.2 open-code-review 里的 AI 审查工作流长什么样
具体操作流程是这样的。PR 触发后,机器人会把该 PR 的标题、描述、diff 摘要、相关文件路径整理成一个 Prompt,发送给大模型接口,然后要求模型输出结构化意见:按文件+行号、严重级别、一句话描述、建议拆分的格式。
我在项目里给了一块专门的 Prompt 模板,核心约束有三条:
- 只关注变更本身,不要对没改动的代码提意见;
- 区分"确定性 bug"与"风格偏好",后者必须明确标注为建议;
- 发现疑似 bug 时,必须给出推理链路,不允许只贴结论。
这套约束背后的逻辑是让 AI 的输出可读、可验证。如果模型直接给出"这里有空指针风险,请修复",reviewer 还得自己去推一遍,反而更浪费。但如果它给出"当 userId 为空时,这行会收到 null 然后直接调用 .name,请确认上游是否总是传入非空值",这种带推理链的意见就是真能帮上忙的。
4.3 实测下来 AI 真正有效的是这三类场景
用了一段时间之后,我总结出 AI 辅助审查实际效果最好的三类场景。
第一类是"跨函数上下文补全"。人类 reviewer 第一次打开一个 PR,很难快速搞清改动函数的上下游调用链。AI 可以从仓库里搜索相关函数定义,直接把"这个改动影响到了哪些外部调用方"补充出来,省去了 reviewer 大量跳转时间。
第二类是"重复模式识别"。当团队里多个 PR 在同时改相似逻辑时,AI 能发现这些模式并提示"这块逻辑在另一个模块出现过,可能存在复制粘贴式实现",方便 reviewer 决定要不要提取公共函数。
第三类是"测试盲区提示"。AI 看完 diff 之后,会列出它认为该补但没补的测试用例。人只需要判断这些用例是否是关键路径,不需要从头设计。
我始终强调的观点是:AI 不能替你做架构判断,也不会懂你们业务的成败细节,但它能帮你排除掉大量的低水平噪音,让你把宝贵的注意力留给真正需要智慧的地方。
5. 踩坑实录:三个差点让这套体系崩掉的细节
5.1 机器人注释噪音导致的"审查疲劳"
这套体系上线第二周,我就发现一个危险的迹象:机器人评论的数量开始超过人类评论,很多 PR 评论区整页都是结构化警告。开发者为了清掉这些警告,开始用// eslint-disable-next-line这种注释绕开规则,或者干脆把规则调低。
这其实让整个项目面临"狼来了"的处境。审查工具的使命是帮人聚焦问题,结果自己变成了最大的噪音源。
后来我做了一次规则收敛。把 reviewdog 的报告级别拆成两档:error级别继续卡 CI,warning级别不阻塞,只是提示。同时把 filter 策略从"diff 里所有问题"改成了"仅对每个 hunk 最多报 3 条",宁可从多个角度报同一条,也不让每行都挂一个机器人意见。
规则收敛之后,PR 页面的讨论质量明显回升,开发者不再把机器人当成必须强行清零的牛皮癣。
5.2 分支保护顺序配置错误引发的"合并死锁"
这是最让我崩溃的一个 bug。某个模块的 PR 提交之后,CI 状态一直是 pending,但明明所有步骤都跑完了。排查了很久才发现,问题出在"分支保护 + 必填状态检查 + 合并队列"组合配置的顺序上。
具体现象是这样的:由于我开启了"Require branches to be up to date before merging",PR 分支必须包含最新目标分支才能进入合并队列。但同时 merge queue 要求的是"队列任务跑完"才算通过,而任务跑起来之后又会往 PR 分支推一个合并 commit,重新触发检查。这两者配置在同一个分支保护规则里时,会出现一种互相等待的状态:分支检查等合并队列,合并队列等分支检查,最终所有 PR 卡死,谁也合不进去。
解决方法是理清次序:分支保护里只设置"必填状态检查 = CI 通过",合并队列单独管理"重跑基线"。避免在分支保护规则里同时勾选"up to date"和 merge queue 的重跑功能。现在这套配置已经稳定运行很久,但我第一次遇到的时候确实怀疑人生。
5.3 只卡了代码却漏了环境,文档和配置成了后门
还有一次事故让我彻底改了开发流程。某个服务改了环境变量配置,CI 全绿、review 通过、合并上线,结果线上启动直接爆配置缺失。原因就是环境变量数值只存在于部署机上的.env文件,代码仓库里的配置模板压根没人更新。
复盘之后,我在 open-code-review 里加了一条新检查:如果代码改动涉及配置读取逻辑,PR 必须同时包含对应的配置模板或文档变更。实现方式是搞一个"配置影响检查"脚本,扫描 diff 中是否引用了仓库里的配置键,并和文档中的环境变量清单比对,缺失就报错。
从这次开始,我把"代码审查"的范围从纯代码扩大到了"代码 + 配置 + 文档 + 流程"的完整制品。一个改动如果只改了代码但没改配套文档,我会让它卡在审查阶段。很多团队以为文档是可有可无的东西,但代码审查体系一旦把文档纳入范围,这些细枝末节的债务就不至于越滚越大。
6. 从 Code Review 到工程文化的下一步
6.1 把审查沉淀成团队的经验库
open-code-review 跑顺之后,我发现这套体系最有价值的副产品,是长期积累的审查讨论记录。
每个 PR 上的评论、批注、讨论,本质上都是团队经验的高度浓缩。新人加入项目时,与其让他读一堆文档,不如让他看几条"真实出过问题"的审查讨论记录,理解速度要快得多。我现在已经形成了一个习惯:每隔一段时间,把最近一个月的高质量 review 讨论整理成团队的"反模式手册",补充到 open-code-review 的 wiki 里。
6.2 从代码审查演进到架构审查和事前设计 review
另一个方向是把这套机制从"审查代码"升级到"审查设计"。当一个功能涉及到跨模块的接口变更、数据模型调整、新依赖引入时,光靠 PR 页面的 diff 讨论已经不够了。open-code-review 里我增加了一个"设计申报"约定,要求这类改动必须先提交一份设计说明文档,内容包括:当前的系统状态、改动的背景、核心方案、备选方案、影响评估。这个文档先进入审查流程,通过后再进入编码阶段。
这个做法很有效,因为它把讨论的节点从"写完之后"提前到了"动工之前"。事前讨论改的是文档,成本低;事后审查改的是代码,成本高。这一步调整,让整个团队从"review 代码"走向"review 设计",这个变化比任何工具带来的提升都大。
6.3 给准备尝试的团队最后几句实在话
如果你也想把 Code Review 真正落地,我的建议是先别急着搞大而全的工具链。先把一条最简单的分支保护配好,要求 PR 必须有人 approve 才能合并。等这条跑顺了,再加上 title 规范和 CODEOWNERS,再逐步引入静态检查和 AI 辅助。
开头的节奏越慢越好。审查机制本质上是在改变团队的协作习惯,而习惯的改变需要时间,不是一次发布会、一份文档就能完成的。open-code-review 项目里我写了详细的快速开始文档,照着跑一遍 Demo 就能看到 CI 对 PR 的完整检查流程。如果你想在自己的仓库里落地,直接 fork 一份改改路径参数就行,不用从零开始。
最后分享一个我个人的体会:代码审查最大的受益者并不是被审查的人,而是审查者自己。只要你认真去读别人的代码、试图理解别人的设计思路、找出潜在的边界问题,你自己的编码能力提升速度会远超写业务代码本身。open-code-review 做成的实质,是把这种"阅读-思考-反馈"的循环制度化、日常化了。我一直觉得,代码审查不是一种负担,它其实就是开发者的日常训练场。