代码审查这件事,说起来简单,做起来全是细节。团队里总有那么一阵子,上线前靠人肉盯代码,盯到后面发现根本没时间看完,合并请求一多,review 就变成“已读不回”或“只是路过”。我做了一个叫 open-code-review 的开源工具,就是为了解决这个我自己也受够了的困境。它不追求替代人工评审,而是把机器能干的、重复性高的那部分脏活先接住,然后再把人的注意力逼到真正需要判断的地方。这篇文章不聊虚的,直接讲清楚这个工具要解决什么问题、核心是怎么设计的、实际怎么跑起来,以及我在落地过程中踩过的那些坑。
1. 为什么需要 open-code-review:代码审查的普遍痛点
1.1 人工评审不是不行,是效率根本跟不上
很多团队一开始都靠发 Merge Request、让同事手动看代码来保证质量。这种方式在项目早期、人员少、提交不频繁的时候,确实能顶住。但一旦代码量上去、团队人数到五六个以上,问题就全来了。
我见过最常见的场景是这样的:上午十点,一堆 review 请求同时弹出来,每个人手上还有自己的开发任务。你看一眼变更文件,几百行代码,真正的改动可能就十几行,但你还是得从头扒一遍才能确认哪些地方有风险。稍微认真点的人,看一个 MR 要花半小时;不认真的,直接点个 approve,风险就留给后面的人。
更要命的是,人工审查的标准不统一。同一个问题,有人觉得必须改,有人觉得无所谓,还有人干脆没注意到。代码规范如果只是写在文档里,等于没写。人脑不是机器,不可能每次都能记住规则里的每一条。我就是在连续被几个低级错误坑了以后,才下定决心,一定要做一个能在合并前自动拦截问题的工具。
1.2 现有工具不少,为什么还是要自己写
提到代码审查自动化,很多人第一反应是:不是有现成的吗?确实,有一些商业平台和开源工具都能做静态检查、代码规范扫描,但实际用起来的时候,我觉得它们普遍有两类问题。
一类是规则太死板。内置规则库看着很全,但应用到具体业务代码时,老是会误报。比如项目里有一些特殊的数据处理逻辑,本来写法就是有意的,工具却每次都不分青红皂白地标红。团队被这种误报骚扰几次以后,干脆对审查结果麻木了,最后连正常的警告也不看。
另一类是接入成本高。有的东西看起来很强大,但是要配数据库、配服务发现、配权限系统,小型团队根本没有精力去维护这么一套东西。我要的是那种可以轻量部署、能直接跟现有工作流嵌在一起的工具,而不是再引入一个需要专人照顾的新系统。所以 open-code-review 的定位从一开始就很明确:轻量、灵活、能快速落地,规则可以由每个团队自己定,而不是被内置规则绑架。
1.3 open-code-review 的设计初衷与适用人群
这个工具说到底,是把“代码评审”这个动作拆成两部分:一部分是绝对客观的检查,比如格式、语法错误、明显的坏味道;另一部分是需要人来判断的设计和架构问题。open-code-review 的目标就是稳稳接住第一部分,并且为人做第二部分提供清晰线索。
适合谁来用?我觉得至少有三类团队会很需要:
- 没有专职测试和架构师的创业团队,代码质量主要靠开发自觉。
- 有多条业务线并行,review 人力严重不足的中型团队。
- 准备从零建设代码质量体系的组织,希望在一个可持续扩展的框架上做增量。
它不是银弹,也不是要让 code review 这件事消失,而是想让大家终于可以把时间花在真正需要思考的问题上。
2. 核心设计思路:open-code-review 的整体架构与工作流程
2.1 极简又解耦的模块划分
在动手写第一行代码之前,我先想清楚了一件事:这个工具不能是一个庞大的单体应用。一旦什么功能都揉在一起,后面就没法灵活扩展了。最后我把整个项目拆成了几个互相独立的模块,每个模块只做一件事,彼此之间通过标准的数据结构通信。
- 拉取模块:负责从代码仓库拉取变更信息,比如本次提交涉及了哪些文件、具体 diff 是什么。
- 分析模块:真正干活的引擎,支持插件化的规则,跑完以后输出“问题列表”。
- 报告模块:把问题列表转成人能读懂的格式,可以是控制台文本、Markdown,也可以是 JSON。
- 通知模块:把报告推送到下游系统,比如 GitLab 评论、企业微信机器人、邮件等。
这个拆法有个很直接的好处:如果你只想用分析能力,可以单独把分析模块抽出去;如果你已经用自己的脚本拿到了 diff,也可以完全跳过拉取模块,直接喂给分析模块。每个模块都暴露简单的接口,组合起来就是一套流程,拆开用也完全没问题。
2.2 工作原理:从 diff 到问题列表
open-code-review 的核心处理链路,简单来说是这样的:
- 先确定这次审查的对象。默认是对比当前分支和目标分支之间的差异,也可以指定任意两个 commit。
- 拉取 diff 后,按文件拆开,再按行号定位变更位置。这一步很关键,因为后面报问题的时候,必须看清楚问题出现在“新增代码”还是“旧代码”里。
- 分析模块拿到变更代码后,运行规则引擎。每条规则可以是一个函数、一个正则模式,也可以是调用外部 CLI 工具的包装器。
- 结果统一收集,经过一个后处理阶段,过滤掉被排除的路径、被忽略的告警,然后生成最终的问题列表。
这个流程看起来不复杂,但真正实现的时候,细节比想象中要多。尤其是 diff 的解析,不同代码托管平台的 diff 格式有细微差别,如果处理不好,行号会错位,后面所有规则都会跟着乱。
2.3 规则引擎:为什么用简单脚本而不是固定规则库
第一版 open-code-review 用的是内置规则,简单是简单,但马上发现不对。不同项目的风格差异太大了,内置规则没法适应所有情况。后来我干脆把规则引擎重写了一遍:每一条规则,本质上就是一个可执行脚本,输入是文件内容和行号信息,输出是“问题级别 + 描述 + 建议”。
用脚本做规则,最大的好处是灵活性。规则可以是一个能跨语言使用的通用 Shell 脚本,也可以是针对某些特定技术栈单独写的 Python、Node.js 脚本。团队完全可以根据自己的技术栈和痛点,随时加一条新规则,而不需要等待主项目发版。
注意:规则越多不代表越好。我见过有人一下配了上百条规则,结果跑一次要几分钟,误报还一堆。我的经验是,规则的启动范围宁小勿大。先把最有把握、收益最高的 5 到 10 条规则跑起来,跑顺了再慢慢加。
3. 落地实操:把 open-code-review 接入真实项目
3.1 本地环境准备与初始化
先说环境。open-code-review 本身是用 Go 写的,编译完是一个单个二进制文件,部署非常省事。你只需要在本地装好 Go 1.20 以上版本,然后克隆仓库,构建即可。
git clone https://github.com/yourname/open-code-review.git cd open-code-review make build构建完成后,会生成一个ocr命令。先跑一下版本信息,确认安装成功:
./ocr version初始化配置也很直接,执行 init 命令会在当前目录生成一份默认配置open-code-review.yaml,我跟团队开玩笑说,这一个命令顶过去写十页流程文档。
./ocr init生成的配置文件内容大概是下面这样,里面把规则路径、排除目录、输出格式都集中在一起管理:
# open-code-review.yaml repo: remote: origin base_branch: main analysis: rules_path: ./rules exclude_paths: - "vendor/" - "dist/" - "build/" severity_treshold: warning output: format: table comments_enabled: true这里有两个字段值得重点解释。rules_path指向规则脚本所在的目录,你自定义的规则可以全部丢进去,加载器会递归读取。severity_treshold是输出阈值,比如设置成warning,那低级信息就不显示在最终报告里,免得刷屏。
3.2 写一条自定义规则,能让新人秒懂的范例
光看默认规则不过瘾,我举个例子,带大家写一条非常实用的自定义规则:禁止在 TypeScript 项目里使用隐式any。
这里我会用 Node.js 脚本写规则,逻辑很直接:读取变更的.ts文件,查找any类型关键字,如果发现就报出 warning。规则脚本需要实现一个标准的输入输出协议。输入从标准输入读取一个 JSON 对象,里面包含文件路径、文件内容、变更行号列表。处理完以后,把结果写到标准输出,同样是一行 JSON。
#!/usr/bin/env node // rules/no-explicit-any.js const fs = require('fs'); function main() { const input = JSON.parse(fs.readFileSync(0, 'utf-8')); const { filePath, content, changedLines } = input; const findings = []; const lines = content.split('\n'); changedLines.forEach((lineNum) => { const line = lines[lineNum - 1] || ''; if (line.includes(': any') || line.includes('as any')) { findings.push({ file: filePath, line: lineNum, severity: 'warning', message: '避免使用显式 any,建议定义具体类型或使用 unknown', }); } }); console.log(JSON.stringify(findings)); } main();把这个文件放在rules目录下,执行./ocr analyze,工具会自动发现这条规则并运行。整个过程不需要改主程序代码,也不需要重新编译,对团队里负责维护规则的人来说非常友好。
3.3 与 Git 仓库的联动:一次完整的本地审查演练
初始化完配置,也加了一条自定义规则,我们来跑一次真实审查。假设我在feature/payment分支上改动了一个文件,现在想看这个分支相对于main是否存在问题。
./ocr analyze --base main --head feature/payment命令背后的逻辑是:先去找到feature/payment相对main的差异,提取变更文件列表和对应行号,再送给规则引擎,最后在终端渲染成表格。
一次典型的输出长这样:
文件 行号 级别 说明 src/payment/order.ts 42 warning 避免使用显式 any,建议定义具体类型 src/payment/order.ts 57 error 调用了未定义的方法 getUserId看到结果以后,我可以选择直接修代码,也可以先调用生成报告命令,把这个结果导成 Markdown,方便贴到 Merge Request 的描述里。
./ocr analyze --format markdown > review-report.md整个流程完全没有打开浏览器,也没有登录任何外部平台,在命令行里就把一次初步审查做完了。这对很多习惯终端操作的开发者来说,体验是相当顺滑的。
3.4 把 open-code-review 接到 GitLab CI
本地跑通以后,下一步就是把审查自动化,推到代码托管平台。我团队用的是 GitLab,下面给一个最简配置,适用于 GitLab CI。核心思路是利用 Merge Request 事件触发任务,跑完以后把报告作为评论发布到对应 MR。
# .gitlab-ci.yml code-review: stage: test image: golang:1.22 only: - merge_requests script: - apt-get update && apt-get install -y git - git fetch origin main - go install github.com/yourname/open-code-review/cmd/ocr@latest - ocr analyze --base main --head $CI_COMMIT_SHA --format markdown > ocr-report.md - ocr post-comment --file ocr-report.md这里有个小技巧:GitLab CI 的环境变量CI_COMMIT_SHA指向当前流水线的 commit,main是目标分支,通过这两个参数就能算出合并请求的差异。ocr post-comment命令是专门用来把报告发到 GitLab 的 MR 评论区的,内部会通过 GitLab API 创建评论,前提是在 CI 变量里配置一个带api权限的 token。
在 GitHub 环境里,思路是类似的,只是把 post-comment 换成 GitHub 的 issue 评论接口即可。项目的文档里已经包含了 GitHub Actions 的样例,不需要另外写额外插件。
3.5 几个加速本地排查的辅助命令
日常使用中,我不太想每次都跑全量流程,所以工具里还提供了一些辅助命令。比如只列出当前工作区的可疑文件,不做完整规则分析,节省时间:
ocr diff --staged还有一个命令是快速诊断规则配置是否正常,适合调试刚写的规则时用:
ocr check-rules它会加载全部规则,并输出每条规则的名称、适用语言、是否启用等状态。我是真心建议大家在写完一条新规则后先跑一遍 check-rules,而不是直接丢到测试里,因为规则加载出错时的报错信息通常没有你想的那么直观。
4. 实战中的误报治理、性能优化与团队推广问题
4.1 误报太多,团队失去信任怎么办
工具上线一开始,最大的问题不是没有报告,而是报告太多,里面还有不少是误报。每天打开 MR,评论区被工具塞了一堆噪音,开发人员自然就开始习惯性忽略。信任一旦崩塌,再好的工具都白搭。
解决误报,我从三个方向入手。
第一,规则分级,严格区分 block 级别和提示级别。会把 CI 卡住的规则,只留给那些几乎零误报、确实会造成线上问题的判断。其他的全部降级为提示,不阻塞合并。
第二,提供内建排除机制。比如某一行是特殊情况,开发者可以在代码里加一个注释,工具读取到以后就跳过这行检查。
// ocr-ignore: no-explicit-any const data = JSON.parse(raw) as any;第三,重视反馈回路。每次有人工确认误报的情况,都要及时在规则脚本里增加上下文判断,从源头减少重复报错。三个月下来,我们规则的误报率大概降了一半,剩下的报告基本都是有效信息。
4.2 仓库很大,跑一次审查要几分钟怎么办
随着仓库历史越来越长,每次拉取全量 diff 再分析,确实会慢。尤其在 monorepo 结构下,一个 MR 可能涉及几十个包,如果每次都对全仓库做分析,CI 等待时间会让人抓狂。
我的优化策略是分层处理。第一步,先按变更文件的后缀名过滤,只分析与已配置规则相关的文件。第二步,增量分析,利用 git 的 object hash 做缓存,如果某个文件的某个版本之前已经分析过,且这次没有变更,就直接复用上次结果。第三步,并行执行规则。
./ocr analyze --parallel 4 --cache-dir .ocr-cache这样改完以后,我们一个中等规模仓库的审查时间从 3 分半降到了 50 秒左右,至少大家不会在等待的时候切走去刷网页了。如果时间还是很长,那就需要检查是不是有规则脚本写得特别低效,比如在规则里循环了所有文件行,却没有按变更行号过滤。
4.3 报告出来了,团队还是没人看怎么办
工具把报告生成出来,只是第一步。报告给谁看、在哪个环节看,是另一步。如果报告最后只是躺在 CI 日志里,那等于没有做。
我的做法是让报告出现在两个地方。第一,MR 评论区。只要打开 MR 就能看到,不用点进流水线翻日志。第二,按模块责任人的维度拆分,把问题直接 @ 给对应模块的 owner。这样不是“大家的事”,而是“谁的代码谁负责”。
另外,我把报告分成三层:第一层是阻塞性问题,合并前必须处理;第二层是建议,默认三天内处理;第三层是信息,随缘。这种明确的分级反而比一视同仁更有推动力,因为开发人员面对少量必须看的问题时,更愿意认真对待。
4.4 team 推广时常见的几个坑
工具落地,技术难点其实不难,难的是让团队接受。我踩过不少坑,分享几个印象最深的。
- 一开始别拿历史代码开刀。如果第一次跑就报出一堆旧代码的问题,很容易引发情绪抵触。最好只检查新增代码,让规则从“今天”开始生效,历史问题专门开一次清扫任务慢慢消化。
- 别让工具直接否掉合并。自动检查结果可以作为提醒,但不能上来就禁止 MR 合并,否则开发会有一种被机器管着的感觉。要先跑两周,建立信任以后再逐步提高阻塞阈值。
- 规则不是越多越好,要有人负责。每条规则都应该有一个 owner,当有人对这条规则有异议时,能立刻找到做决定的人,而不是互相踢皮球。
第 4.4 节原本是我最想省略的部分,但想想还是写下来。因为一个工具是否能在一个团队里扎下根,到最后百分之八十的问题都不是因为代码写得不够好,而是流程设计和人际沟通的问题。
最后再分享一个小技巧。open-code-review 这个项目从最初只有我一个人在用,到现在被周围几个团队在日常流程里推荐,最关键的转折不是我加了多少规则,而是我开始把规则也当代码一样维护。每一条规则都有注释,说明它要解决的问题、适用范围和责任人。这样就算某一天维护者离开了,下一个人也能很快搞懂一条规则的来龙去脉,而不是面对一堆神秘的匹配正则发呆。代码审查这件事本身就是一种工程文化,工具只是帮我们把文化里的纪律部分自动化了。