前段时间,我把团队 PR 的评审流程重新捋了一遍,最后在 CI 里给每个改动都加了一道由 open-code-review 负责的自动评审。说实话,这个决定刚开始被同事当成偷懒:"机器看一眼怎么够?" 但三个月跑下来,数据反而支撑住了这个方向:首轮人工评审时长从平均 38 分钟降到了 20 分钟以内,漏在代码里的低级错误也少了大半。
这篇文章不是来吹"AI 会取代 code review"的。恰恰相反,我想聊的是 open-code-review 这类开源工具在真实评审流程里到底应该站在哪个位置、怎么接、怎么配置、会踩哪些坑,以及它和人工评审之间的边界到底在哪。
如果你正要给团队搭一套开源 code review 辅助流程,或者你只是一个人维护项目但希望 MR 被打开之前先有人"把一眼能看出来的问题拦下来",这篇文章应该能让你少走不少弯路。
1. 先说结论:open-code-review 解决的是评审里的"常识过滤层"
1.1 人工评审的低效区间不在"看不懂代码",而在"找一遍常识问题"
我带过的评审里,真正消耗时间的从来不是架构讨论,而是这类问题:
- 某处把生产环境的密钥直接写进了代码;
- 一个
debugger语句被留在了主干; - 变量名拼错了,但整个团队看惯了所以没人提;
- 新接的 HTTP 客户端忘了设置超时时间;
- 一个改动同时改了格式、重构、业务逻辑,reviewer 根本看不出重点。
这些问题有个共同点:它们不需要理解业务上下文,只需要"按规则对一遍"。但人工做这件事非常不划算。评审者打开 diff 之后,前二十分钟很可能都在机械地找这类低级问题,等真正需要动脑子的业务逻辑讨论时,注意力已经消耗得差不多了。
这是 open-code-review 这类工具最值得站的位置:它不应该替你做架构评审,也不应该基于历史代码生成一堆"建议优化性能"的废话。它应该先帮人把"常识错误"这一层过滤掉,让人的注意力留给真正需要人的部分。
1.2 我理解的 open-code-review 不是"机器人评审官"
之前我也试过几个商业代码评审服务,用下来最别扭的地方是:它们通常需要你把整个仓库推送到第三方服务器,然后给你返回一大段"看起来很有道理"的评论。这听着很酷,但实际落地时有两个问题。
第一,代码安全。很多团队没办法接受完整仓库被外部服务过一遍,哪怕是有保密协议的商业服务,审批流程也很麻烦。
第二,反馈太"重"。大模型特别喜欢把代码里所有可疑点全列出来,50 行 diff 能给你提 30 条意见。你本来只想要人帮你看一眼有没有低级错误,结果收到一篇论文。
所以当我决定上开源方案时,我对 open-code-review 的定位想得很清楚:本地可执行、规则可配置、输出必须简短。它不是一个替你写代码的 AI,它是一个把"一眼就能看出来的问题"自动标注出来的过滤器。人工评审仍然存在,只是评审者不再需要从零开始。
2. open-code-review 的核心工作流:一条命令背后的四个模块
2.1 先看一条命令
我在本地跑的最基础命令长这样:
ocr check \ --base main \ --head feature/xxx \ --config .open-code-review.yml它会解析main到feature/xxx之间的 diff,对每个变更块跑一遍内置规则,然后把命中结果输出成结构化文本:
[ { "rule": "no-debugger", "severity": "warning", "file": "src/api/client.go", "line": 88, "message": "debugger 语句被提交到主干" } ]如果什么都没命中,输出就是空。这个设计我很喜欢,因为它很容易接进 CI:命令退出码、JSON 输出、Markdown 报告都可以作为后续流程的判断依据。
2.2 四个核心模块拆开看
open-code-review 能跑通,靠的不是一个巨大的"检查器",而是下面这四块相互独立的逻辑:
| 模块 | 负责的事 | 常见问题 |
|---|---|---|
| Diff 解析器 | 从 Git 历史中提取变更文件、变更块、新增行/删除行 | 上下文不足导致规则误判 |
| 规则引擎 | 按配置逐条执行检查,决定命中还是放过 | 规则写得过宽或过窄 |
| 评论生成器 | 把命中结果整理为固定格式的评审意见 | 格式不统一,后续难以自动处理 |
| 上下文聚合器 | 收集本次改动涉及的函数、引用、配置文件关系 | 只做文件级检查时会漏跨文件问题 |
我把这四个模块单独拎出来说,是因为很多刚开始用的人会在"规则引擎"上花太多时间,却忽略了 Diff 解析器和上下文聚合器才是决定准确率的关键。
比如一个规则要判断"新增代码里是否调用了dangerouslySetInnerHTML",如果 Diff 解析器只给规则一行一行的字符串,规则根本不知道这行代码是不是在注释里、是不是在测试目录下。open-code-review 的做法是给每条规则传一个change对象,里面包含文件路径、变更类型、原始代码块、当前代码块、行号等结构化信息。规则要判断的是"变更内容",而不是"整份文件"。
2.3 为什么这套逻辑适合以开源方式落地
当初选型的时候,我也纠结过到底是自己写一套脚本还是直接用现成工具。自己写的优势是完全定制,劣势是维护成本极高。代码评审规则不是写一次就完事的,你会不断发现新坑:某类正则误报太多、某个目录应该排除、某个内部框架有特殊写法。这些迭代如果都堆在一个人的脚本里,三个月后就没人敢动了。
open-code-review 这类开源项目的好处是,它的核心框架是稳定的,规则和配置是分离的。你不需要懂 Git 对象模型,不需要自己解析 diff,不需要处理不同平台的换行符差异,只需要维护一份规则配置。框架升级你跟着升就行,规则逻辑是你的资产,不会被锁死在某个商业平台里。
我当时还特意确认了一件事:这工具是不是真的能在没有外网的情况下运行。答案是能。它不强制联网,没有遥测,所有检查都在本地进程里完成。这一点对很多内网开发环境来说很重要。
3. 本地接入的完整过程:从 clone 到第一次跑通
3.1 安装:别一上来就想跑满配
open-code-review 的安装方式很简单,我用了一个简单的二进制文件:
# 示例:Go 安装方式 go install github.com/<your-org>/open-code-review@v1.2.0 # 验证 ocr --version如果你是给别人做分享,也可以直接从 release 页面拉对应平台的压缩包,比如 Linux amd64 就是:
curl -sL \ "https://github.com/<your-org>/open-code-review/releases/download/v1.2.0/ocr_linux_amd64.tar.gz" \ | tar xz这里我给一个非常个人的建议:第一次装的时候不要配任何自定义规则,直接用默认配置跑一遍再说。原因很简单,你连工具默认的行为边界都没摸清,上来就写 20 条自定义规则,后面只会分不清哪些误报是规则写错了、哪些是工具本身的问题。
3.2 最小配置:先把"不想被管"的地方写清楚
我建议的最小配置不是一堆规则,而是一份ignore清单和两条规则。一份合理的最小配置大概长这样:
version: 1 ignore: paths: - "vendor/**" - "dist/**" - "node_modules/**" - "*.lock" rules: no-debugger: pattern: "debugger" hardcoded-secret: pattern: "(?i)(password|token|api_key)\\s*[:=]\\s*\"[^\"]+\"" severity: error这份配置里的规则数量只有两条,但已经能挡住不少最常见的低级问题。重要的是,我一开始就把vendor和dist目录排除掉了。如果忽略清单不写清楚,后续所有规则的误报率都会被人为放大,因为你会在生成的构建产物里反复命中一堆无意义的检查结果。
3.3 我第一次跑完的真实感受:准确率可以,但"噪音"要人工消
第一次在真实仓库上跑,默认规则命中了 70 多处。如果只看数量,你会觉得"这也太吵了"。但仔细看下来,真正值得处理的其实只有 20 多条,剩下 40 多条全是需要排除目录、排除注释、排除测试代码的命中。
这个比例我没觉得意外。代码检查工具第一天跑都是这个状态,关键是你要有一套"消化噪音"的流程,而不是因为噪音多就放弃。
我当时的处理顺序是:
- 把明显不需要管的目录加进
ignore.paths; - 把命中场景太宽的规则降级,从
error改成warning; - 给部分规则加上
exclude条件,比如测试目录内忽略某些规则; - 把剩下真正有价值的命中逐条提交给对应负责人修复。
这个过程花了一个下午。但之后的效果是持续的,第二周开始,新提的 PR 几乎不会再出现密钥或 debugger 这类问题。因为人可能会忘记检查,但配置不会。
4. 接到 CI:让每个 PR 自动拿到第一轮意见
本地跑通只是第一步,真正让 open-code-review 产生价值的地方是 CI。我的目标很简单:每次有人提 PR,工具自动跑一遍,在 PR 下面给出评论。如果发现error级别的问题,直接让 CI 失败。
4.1 一个最小可用的 GitHub Actions 工作流
以 GitHub Actions 为例,我用的工作流文件大概长这样:
name: open-code-review on: pull_request: types: [opened, synchronize] jobs: review: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run open-code-review id: ocr run: | curl -sL "https://github.com/<your-org>/open-code-review/releases/download/v1.2.0/ocr_linux_amd64.tar.gz" | tar xz ./ocr check \ --base origin/${{ github.event.pull_request.base.ref }} \ --head HEAD \ --config .open-code-review.yml \ --format markdown \ --output ocr-report.md echo "exit=$?" >> "$GITHUB_OUTPUT" - name: Comment on PR if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); const body = fs.readFileSync('ocr-report.md', 'utf8'); if (body.trim().length > 0) { await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); }这里有一个关键点:fetch-depth: 0。如果不用fetch-depth: 0,GitHub Actions 默认只拉取最后一次提交的代码,ocr check --base origin/main根本拿不到完整历史,diff 解析会直接失败或者给出一个不完整的对比结果。
还有一个小细节我是后来才注意到的:评论脚本里我用了if: always(),因为它必须保证即使ocr返回非 0 退出码,也能把报告写到 PR 评论里,而不是只让 CI 静默失败。
4.2 评论格式为什么非要用 Conventional Comments
open-code-review 默认输出的是 Conventional Comments 格式,这是它最开始吸引我的点之一。普通工具会直接输出一句"这里有问题",而它输出的是带标签的结构化意见:
:warning: [issue] 硬编码密钥可能被提交到仓库当然,我实际用的是 Markdown 版本,核心区别在于每条评论会带上类型标签:
| 类型 | 含义 | |---|---| | `suggestion` | 建议修改,不强阻塞 | | `issue` | 必须处理,阻塞合并 | | `nitpick` | 非功能性细节,供参考 |为什么这个重要?因为它让评论可以被程序化处理。我的 CI 里并没有把所有评论都当成阻塞项,只有issue级别的规则命中才会让ocr check返回非 0。suggestion和nitpick只出现在 PR 评论里,给人看,不卡流程。
如果没有统一的评论格式,后面无论接机器人还是人工筛选,都要重新做一轮文本解析,成本就上去了。
4.3 多平台接入时的差异
如果你的代码库不在 GitHub,而是在 GitLab 或者 Gitea 上,流程大体一致,但有两个坑要注意。
第一个是 base 分支的获取方式。在 GitLab CI 里,合并请求的ref和 GitHub 不太一样,你需要在工作流里显式地去 la 取目标分支:
git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME第二个是评论接口。GitLab/Gitea 的 API 字段、权限模型各不相同,你通常不能在 CI 里直接用github-script,得换成对应平台的 action 或者用 API token 自己调。这时候 open-code-review 的"输出标准化"就派上用场了:我只需要把同一个 JSON 报告转换一下,就能适配不同平台。
5. 规则设计:决定工具值不值得留用的关键
5.1 高价值规则长什么样
跑了三个月后,我发现真正值得保留的规则其实不是那种"大而全"的规则,而是下面这四类:
| 类别 | 示例 | 为什么高价值 |
|---|---|---|
| 安全底线 | 硬编码密钥、http 明文链接、任意eval | 出了就是事故,必须机器拦 |
| 调试残留 | debugger、console.log、TODO 未清 | 根源不是逻辑,是流程遗漏 |
| 依赖卫生 | 锁文件未提交、依赖版本未固定 | 改一次环境崩溃一片,必须卡 |
| 性能反模式 | 循环内同步请求、N+1 查询模式 | 人工看 diff 很难发现,机器可识别 |
以"硬编码密钥"为例,规则的匹配模式不能太天真。你不能只搜password,否则会在文档、测试用例、示例代码里疯狂误报。后来我调整成:只检查新增行,且只检查赋值给变量的字符串字面量,并排除测试目录和文档目录。这样误报率从 15% 降到了 2% 左右。
5.2 我删掉的三类规则
有保留必然有放弃。以下三类规则是我在真实使用后陆续删掉的:
第一类是纯代码风格规则,比如单引号还是双引号、缩进用 2 空格还是 4 空格。这类问题完全应该交给 formatter 或 linter,放进 code review 工具里只会制造噪音。
第二类是"没有上下文就必误报"的规则。我一开始写了一条规则,要求所有 HTTP 请求必须显式设置超时时间。结果内部有很多封装好的 SDK,它们已经统一设置了超时,每条未设置超时的调用其实都走的是封装层。工具不知道这层封装关系,于是每 10 个命中里有 8 个是误报。
第三类是结果无法被执行的规则。比如"函数参数不要超过 5 个"这种度量类规则。它可能真的是个问题,但修复成本很高,而且常常牵涉到重构,不适合由机器在 PR 阶段直接拦住。这种规则更适合定期汇总成报告,由团队决定要不要处理,而不是阻塞合并。
5.3 自己写规则的最小闭环
open-code-review 的规则并不只是"写个正则"。它允许你写一个接收change对象、返回评论数组的脚本规则。我写了一个最小例子:
export function audit(ctx) { const comments = []; for (const change of ctx.changes) { if (!change.file.endsWith('.js')) continue; if (!change.content.includes('eval(')) continue; comments.push({ rule: 'no-eval', severity: 'error', file: change.file, line: change.line, message: '不建议直接调用 eval,建议改用 new Function 或白名单匹配', }); } return comments; }这个规则只扫描变更里的新增行,不会因为历史代码里的eval命中。这就避免了"每次跑都报一堆历史问题"的尴尬。
我在写自定义规则时养成了一个习惯:每写一条规则,至少准备两个测试用例,一个是必须命中的正例,一个是不能命中的负例。比如"不应该使用dangerouslySetInnerHTML"这条规则,正例是一行div.innerHTML = userInput,负例是文件里出现了这个字符串但它是注释或者它在测试用例里。测试负例的次数要占一半以上,因为代码检查工具经常不是规则不够,而是"太能管了"。
6. 三个月下来的数据、边界与后续想做的事
6.1 一组真实的变化数据
把 open-code-review 接进流程三个月,我记录了几个比较粗糙的数字:
| 指标 | 接入前 | 接入三个月后 |
|---|---|---|
| PR 首轮人工评审平均时长 | 约 38 分钟 | 约 19 分钟 |
| 每周因低级错误被打回的 PR 数 | 3-4 个 | 0-1 个 |
| 每条 PR 平均机器评论条数 | 无 | 12 条左右 |
| 机器评论中实际被人工确认有效的比例 | 无 | 约 85% |
12 条看起来不少,但拆开看,大部分是suggestion级别的建议,真正需要阻塞的issue平均只有 2-3 条。人工评审省下来的时间,主要不是因为机器把意见全提了,而是因为打开 PR 时,常见的坑已经被标好了,评审者可以直接进入"这些意见有没有道理"和"业务逻辑有没有问题"这两个更高价值的环节。
6.2 它解决不了的事
我必须强调,open-code-review 解决不了以下这些问题:
它不理解业务语义。一个改动是不是符合需求、数据库字段命名和业务名词是否一致、统计逻辑有没有把用户群体算错,这些它完全无能为力。
它不适合做架构评审。比如"这里是不是应该用事件驱动而不是直接调用内部 API"这种问题,工具只会看到表面的调用关系,看不到团队对系统边界的约定。
它也会产生新问题:如果把规则写得过于严格,会让团队形成"反正 CI 会拦,我提交前不看一眼"的心态。这是流程设计上的风险,需要用规则的分级和人工评审兜底来平衡。
还有一个很实际的问题:工具的规则覆盖不到那些"你还没意识到是问题"的问题。它只能按照已经定义好的规则去匹配,所以它不是创新工具,而是执行工具。真正的评审智慧仍然来自人。
6.3 后续我想做的扩展
接下来我想做两件事。
第一是把规则按业务域拆成单独的文件。现在所有规则都堆在一个.open-code-review.yml里,已经有点长了。我计划按security.yml、performance.yml、style.yml、team-practice.yml拆分,然后在主配置里引入,这样不同团队可以共用基础规则,再各自叠加自己的团队实践。
第二是做一份"误报反馈周报"。open-code-review 支持把未命中和误报的记录导出成 JSON,我打算每周收集一次,看看哪些规则在过去一周误报率最高,判断它们要不要删除或优化。这比凭感觉调规则靠谱得多。
如果你也在考虑给团队铺一套开源 code review 辅助流程,我的建议很直接:不要一开始就追求"自动发现所有问题",先把自己最近一个月在评审里提得最多的三类问题固化成规则,跑起来,再慢慢迭代。工具的意义不是替你思考,而是把从思考到执行的重复劳动省下来,让你更愿意把每一次评审都当成一次有价值的交流。