☰
open-code-review:基于CI的自动化代码审查工作流设计与实践
2026/10/19 9:17:30 网站建设 项目流程

两年前,我们线上出过一次特别低级的故障。一个同事在条件分支里把>=写成了>,导致用户积分到临界值时不更新。代码 review 了三轮,四个人都看过,没人发现。那次之后我开始认真琢磨 open-code-review:代码审查这件事,有没有可能让机器先把明显问题拦下来,让人的精力集中在真正需要判断的设计和逻辑上。后来我们团队把想法落地成了一个开源项目,就叫 open-code-review,一套可以跑在 CI 里的自动代码审查工作流。这篇文章我会把整个落地过程、规则设计思路、踩过的坑和团队数据都摊开讲,适合正在优化代码审查流程的技术负责人,也适合想从零搭建自动审查工具的开发者参考。

1. 为什么我会在团队里推动 open-code-review

1.1 一次线上事故背后的审查盲区

先说回那次故障。背景是一个积分商城系统,用户每次消费后需要根据当前积分等级发放对应的权益。逻辑很简单:用户积分达到某个阈值时,等级提升,积分展示也要刷新。问题出在更新积分的条件上,代码写的是“当新增积分大于 100 时触发”,但实际业务要求是“当新增积分大于等于 100 时触发”。少了一个等号,边界用户永远无法升级。

这个 bug 为什么会通过三轮人工审查?我当时拉取了那次的 MR 记录,发现 diff 只有 21 行,但团队里四位参与 review 的人,主要精力都花在讨论一个 Redis 缓存更新的实现上。等到有人注意到积分配额的边界条件,已经是上了生产、收到用户反馈之后。这不是责任心的问题,而是人的注意力天然存在瓶颈。尤其是当 diff 里同时包含“值得讨论的设计”和“一眼望不到底的业务判断”,人在压力下很容易漏掉那些看起来不起眼的边界。

正是这次事故,让我意识到代码审查不能只靠“多找几个人看”。人数增加只能提高覆盖概率,却不能保证覆盖质量。我们需要一套机制,先把确定性问题从人的负担里卸下来,让人去做更高级的判断。open-code-review 的第一版需求就这么来的:不是让人不看代码,而是让人在进入 review 之前,机器已经把明显问题标注在对应代码行上。

1.2 人工审查的三个软肋

做了这个项目之后,我花了很多时间观察团队的 MR 审查习惯,也把过去半年所有的 review 意见做了分类。最后发现人工审查的问题集中在三个方面。

第一,标准不一致。同一个“事务里不要做远程调用”的问题,A 同事会在 review 里指出,B 同事觉得无所谓,C 同事根本没注意。每个人脑子里都有一套自己的“好代码标准”,但它们从来没有被写下来。第二,反馈不及时。如果代码提交之后要等半天才有人 review,开发者的上下文早就切换走了。等审查意见回来,修改成本已经翻了好几倍。第三,疲劳导致的沉默。一个超过 300 行的 MR,大概率后半部分没有人真正一行一行看,但几乎没有人会在 MR 下面明说“这个 diff 太大了我看不动”。结果就是,没有人反对,代码就合了。

这三种问题不是通过“加强流程”就能解决的。你越是要求“每个人必须逐行评论”,效果越差,因为大家会把 review 当成任务完成,而不是当成一次有效的质量屏障。所以我设想的 open-code-review 不是去替代人工审查,而是在人工审查之前先做一道“确定性问题过滤”:代码风格、明显错误、老生常谈的坏味道,机器先拦掉;人只负责审那些机器给不出确定性结论的部分。

为了更直观地理解两类审查的区别,我整理了一个对比表格,这是我们内部培训时用的材料:

维度传统人工审查open-code-review 补位后的审查
标准统一性依赖个人经验,标准漂移规则沉淀到仓库,全局统一
反馈速度取决于 reviewer 什么时候有空PR/MR 创建后几分钟内出结果
审查深度典型 diff 后半段关注度下降对每个改动行做全量规则匹配
误报处理靠人现场沟通通过白名单/规则等级持续调优
核心人力消耗花大量时间看“不该犯的错”只讨论设计、扩展性与边界

当然,这个表格不是说要完全取代人工。真实情况是,我们会把人工 review 的时间从“找小问题”里释放出来,用在“看大问题”上。

1.3 为什么我们不直接买商业工具

项目启动前,我们也评估过市面上的商业代码审查平台和各类 SAST 工具。结论是可以买,但对团队来说存在几个绕不开的问题。

第一是私网问题。很多商业产品需要把代码上传到对方服务器,或者至少要允许对方的 Agent 访问我们的私有 GitLab。对于中小团队可能无感,但在一些对代码仓库访问权限控制较高的公司里,这一步很难推进。第二是规则可定制性不够。我们需要的不是“别人总结出的最佳实践”,而是“自己团队的坏味道沉淀”。例如“禁止在事务中调用外部 HTTP 接口”“禁止新代码使用 Date 类处理时间”“日志必须带 requestId”,这类规则非常特殊,商业工具里通常没有现成配置,就算有,改起来也依赖厂商排期。第三是成本。按人计费的许可模型对小团队是负担,而且审查量增上来之后,很多平台还会针对“更多并发扫描”单独收费。

所以最终我们决定自己做一套轻量级方案。核心不是要变成一个多么强大的静态分析平台,而是做到三件事:能把规则配置文件放进仓库、能在 CI 里跑起来、能把结果以评论形式回到 MR。这三点搭起来并不复杂,复杂的是后续的规则积累和误报调优。这也是我把项目命名为 open-code-review 的原因——审查规则应该是开放、可演进、属于团队自己的。

2. open-code-review 的定位与工作边界

2.1 它解决什么,不解决什么

刚开始设计模块时,大家很容易把功能范围越画越大。有人建议直接做实时 IDE 插件,有人建议接入机器学习模型做跨行逻辑推理。我都踩过刹车。open-code-review 的定位必须非常克制:它解决的是“代码变更时的确定性检查”,不解决“设计是否合理”的问题,也不解决“业务逻辑是否满足需求”的问题。

具体来说,它能做这些事:

  • 检查新增代码是否命中团队维护的规则,比如禁止var、禁止遗留 TODO、禁止直接使用裸 IP 连接数据库。
  • 基于 AST 做轻量级结构分析,检测括号匹配、分支遗漏、重复代码等模式。
  • 在 MR 的 diff 上逐行输出评论,让开发者在看到代码时立刻知道哪里有问题。
  • 通过 webhook 通知到企业 IM,减少“等你上线再看”的滞后感。

它不解决的事,我在文档里用加粗标了出来:不替代 Code Review 中关于“为什么这么设计”“有没有更好的方案”“对扩展性有什么影响”的讨论。机器说不好“设计感”,但它能把“低级问题”从设计讨论里剥离出来。

还有一个边界要特别说明:open-code-review 不做自动合并或强制拦截。团队可以配置某个级别的规则如果出现,CI 就失败,但这需要人工决定,而不是工具替你决定。我们内部默认把“error”级规则设为阻塞项,但“warning”级只做提示。这样做的好处是,开发者不会因为一个 console.log 就没办法合代码,负反馈不会过于强烈。

2.2 核心模块拆解

从工程实现角度,open-code-review 分为三个核心模块:触发层、分析层、反馈层。

触发层解决的问题是“什么时候跑”。最简单的方式是接入 GitLab CI / GitHub Actions,当 MR 创建或更新时触发流水线。触发条件里我特别加了两个限制:只跑opened和synchronize事件;只跑目标分支为main或release/*的事件。这样开发者中途反复提交代码时,不会每次都阻塞流程,而是等停下后再汇总新的审查意见。

分析层是核心。它先读取仓库根目录下的.open-code-review.yml配置文件,拿到规则列表,再通过git diff --unified=0获取当前分支相对于目标分支的文件变更列表。这里最关键的一步是“只分析变更行”,避免把历史存量代码的问题全部翻出来。技术栈上我们使用了 tree-sitter 做多语言解析,这样比纯正则匹配更可靠,能处理嵌套结构、字符串转义等问题。

反馈层负责把结果送回 MR。我们调用 GitLab API 的discussions接口,按“文件:行号”维度创建评论。如果同一个文件有多个问题,就合并成一条多行评论,而不是刷屏式地每个问题发一条。这样既保证了可见性,又不会让人打开 MR 时看到 20 条评论产生心理负担。

三个模块之间用简单的异步队列连接,扫描过程本身不阻塞主流程。我们最开始犯过一个错误:把扫描结果直接同步写在 CI 日志里,开发者根本不会去看 CI 日志,问题还是被漏过去了。后来改成评论回 MR 才真正起作用。反馈渠道和反馈形式,有时候比规则本身更重要。

2.3 一次审查请求的完整流转

用一个具体例子把流程串起来。假设开发者提交了一个 Java 文件,里面不小心写了一行BigDecimal b = new BigDecimal(0.1d),这行代码的问题是通过浮点初始化 BigDecimal,会丢失精度。团队规则里有avoid-bigdecimal-double-constructor这条 error 级规则。那么整体流程是这样:

开发者把分支推送到 GitLab,创建 Merge Request。GitLab CI 检测到 MR 事件,启动open-code-review任务。工具拉取源分支与目标分支的代码,计算 diff。然后解析被修改的 Java 文件,AST 层面找到BigDecimal的构造调用,检查参数类型是否 double。命中规则后,构造一条评论,定位到具体行,输出错误原因、修复建议和文档链接。CI 流程结束,如果存在 error 级别问题,流水线标记失败;MR 页面在对应代码行出现一条评论。

整个过程从推送代码到收到自动评论,实测平均耗时 40 秒到 1 分钟,取决于仓库大小和 runner 资源。当 diff 很大时,我们会自动切到“增量模式”,只检查最近一次提交涉及的行。这样处理之后,即使一个 MR 改了几十个文件,扫描时间也控制在 3 分钟以内。我见过很多团队在这个环节会陷入“工具跑太久”的抱怨,其实解法很简单:不要全量跑,只跑 diff。

3. 从零接入:仓库配置与规则集设计

3.1 最小可运行的接入步骤

如果你也想在团队里搭一套类似的轻量审查工具,最便宜的方式不是先写大平台,而是先写一个命令行工具加一条 CI 任务。open-code-review 第一版大概只有不到 1500 行代码,就实现了完整的“读取配置、扫描 diff、评论 MR”闭环。

最小可运行版本分四步走。第一步,在仓库根目录放一个.open-code-review.yml,里面先写一条最简单的规则试试水。我先拿 TypeScript 仓库举例:

rules: - id: no-console-log level: warning file_pattern: "**/*.ts" message: "新增代码里不要保留 console.log,请使用 logger.info 替代。" regex: "console\\.log"

第二步,把 open-code-review 装进 CI。GitLab CI 配置如下:

open-code-review: stage: test image: registry.example.com/open-code-review:latest script: - open-code-review scan --base=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --head=$CI_COMMIT_SHA rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

第三步,在 GitLab 上配置一个 Access Token,给项目或群组设置api权限。这里的权限只用于读取 MR 信息和创建评论。第四步,跑一个 MR,看到评论,再把规则模型从正则模式切换到 AST 模式,支持更多复杂检查。

这套最小版本跑通之后,再逐步演进规则库。不要一上来就追求几百条规则,先把团队在最近几次事故里总结出来的痛点规则写进去,每一条规则都要对应一个真实的问题。宁可规则少,也要每条都被人信服。

3.2 规则集怎么写才不会被开发者讨厌

这是整个 open-code-review 项目里我最想分享的部分。工具推不下去,绝大多数不是因为技术不行,而是因为规则集惹人烦。我见过一个团队配置了 200 条规则,结果上线第一周,每天产生 300 条警告评论,开发者基本不看了,直接把 MR 里评论区域最小化。这种状态下,规则再正确也没有价值。

我们的策略是“少而准”。第一版只放了 6 条规则,全是过去三个月内实际出现过的线上问题。比如禁止在事务中调用外部 HTTP、禁止在核心链路上使用同步 IO、禁止BigDecimal的 double 构造器。每条规则都有明确的案件背景,在配置里还可以写why字段,指向内部的 wiki 页面。这样开发者在看到报错时不只会觉得“工具在找茬”,还能知道“工具是在提醒我上次踩过的坑”。

一个可供参考的规则配置格式:

rules: - id: no-sync-io-in-controller level: error file_pattern: "**/controller/**/*.java" message: "Controller 中不允许同步调用远程服务,避免线程阻塞。" why: "https://wiki.example.com/performance/no-sync-io-in-controller" # 这里使用 tree-sitter 解析调用链,message/why 都会输出到评论里

还有一个重要设计是“只对新改动生效”。老代码里如果有 100 处console.log,不要在审查里报出来,因为开发者看都不会看。我们通过git diff过滤,只把“改动行”上的问题暴露出来。这一点如果不做,工具会因为历史存量问题被开发者差评。

3.3 规则的设计逻辑:error / warning / info 三级

规则不能全在一个等级上,否则就失去了优先级。我们把规则大概分成三类。

error 级规则对应“如果不改,线上事故概率很高”。这类规则直接阻塞 CI 合并流程,比如禁止使用Math.random生成优惠券编码、禁止在数据库查询里使用SELECT *且无LIMIT。对这类规则,团队没有任何讨论空间,出现就一定得改。

warning 级规则对应“最好优化,但短期不阻塞”。比如代码里出现了魔法数字、未使用的 import、变量命名不符合规范。这些不影响正确性,但会提高后续维护成本。warning 级评论会展示,但不会让 CI 失败。

info 级规则主要用于信息提示,比如“这段新增代码覆盖了两个边界场景,建议补充单元测试”。这类规则通常结合可测试性分析,是代码风格之外更偏“人性化”的提示。

等级设计的关键意义是给开发者一个心理预期。如果每个警告都导致 merge 被阻塞,反馈会变得让人厌恶。我们内部有一条经验法则:error 级规则的数量永远不要超过 warning 的一半,否则团队每天盯着的都是“你不许这样写”,而不是“你可以这样写得更好”。负反馈比重过大的工具,最后一定会被绕过。

4. 接入 CI 后遇到的那些“意外”

4.1 误报与噪声:如何调参

工具上线后第一个星期,我们就遇到了“规则被大面积忽略”的危机。原因很简单:误报太多。有一次扫描结果里出现了一个错误提示,说新增代码里有非法的数据库连接泄漏,但实际上那是单元测试里 mock 的DataSource,根本没有真实连接。问题出在规则条件写得太宽,没有排除@Mock、@InjectMocks标注的测试类。

处理误报不能靠“删规则”,要靠“精确化规则”。我们花了两个星期专门做噪声治理,流程是:每天从 MR 评论里收集被开发者标记为“误报”的反馈,人工确认后,要么在规则中加入排除条件,要么调整文件匹配范围。这个过程非常烦琐,但非常值。经过两周后,工具的准确率从第一天的 67% 提到了 94%,“可以关掉评论”的负面反馈也基本消失。

具体调参手段有这么几类:增加skip_files和skip_patterns,排除测试目录、生成的 SDK 代码;针对同一规则在类型/函数声明层面做 AST 上的上下文判断,比如只有真正开启事务的方法才触发事务相关规则;提供comments里的suggestion字段,让报错同时给出修复片段,开发者在没有理解规则时也可以照着建议改。调参的目标不是追求 0 误报,而是把误报率压到开发者愿意读规则说明的程度。

4.2 结果被开发者忽略?把评论写得“像人”

技术问题解决之后,新的问题变成了“评论没人看”。GitLab 的 MR 页面里确实有评论,但在 MR 列表里并不会直接显示,开发者经常合并代码之后微信群里问“刚才那个自动评论什么意思”。

我们的解法是给 open-code-review 增加了一个“评论聚合通知”的机制:当一次扫描完成时,不是发 10 条评论,而是把本次发现的所有问题聚合成一条 MR 级总结,再通过企业 IM 机器人推给提交者。总结里包含问题总数、error 数量和最严重的三个问题。开发者被通知后,再回到 MR 页面看逐行评论。

除了发送渠道,评论内容本身也很重要。不要写“不符合规范”这种空话,要写“这一行可能导致什么后果,建议怎么改”。我们参考了 Code Review 社区里的人情味表达。例如:BigDecimal的 double 构造器会有精度问题,建议改为BigDecimal.valueOf(0.1d)。如果你想保留原始 double 值,也可以使用new BigDecimal(String.valueOf(0.1))。拉长这个文案看起来技术含量不高,但它能把“错误-原因-修改方案”一次说清,开发者不用再去搜索引擎。

效果对比很明显。改版之前,自动评论的“回复并解决”比例不到 20%,多数情况下开发者直接无视。改版后,接近 70% 的评论会在 MR 被合并前得到提交者的确认或修改。评论被认真对待,规则才能真正起到作用。

4.3 大仓库扫描超时的三种解法

上线一个月后,开始有大型服务接入。一个 Java 仓库包含 80 多个微服务模块,单次git diff就可能涉及几千个文件。最开始的实现是扫描所有变更文件,然后在每个文件上跑 AST 解析和规则匹配。结果一次扫描跑了 12 分钟,直接超过了 CI 任务的 10 分钟超时限制。

第一次优化是“增量扫描”。既然 diff 已经给出了变更行,我们可以只解析这些行所在的代码块,而不是解析整个文件。对于 Java 这类语言,可以先用git diff --unified=10提取上下文,然后使用 tree-sitter 对单个函数体做局部解析。这样虽然丢失了跨文件的类型信息,但对大多数风格和局部逻辑规则来说完全够用。

第二次优化是“并发限制”。发现问题出在同时启动了 16 个扫描进程,每个进程都要加载语言解析器,导致运行 scan 的 runner 内存暴涨,最后被 OOM。我们改成固定 4 个并发 worker,并使用进程池复用已有的 tree-sitter 语言对象,扫描时间稳定在 3 分钟左右。

第三次优化是“规则预编译”。配置文件在启动时会被解析成 Python 正则或 AST query 数组,这个动作本应在初始化阶段完成。我们最初写得太随意,在每次规则检查时读配置文件,代码里还带 IO 操作,性能极差。改成内存中预编译一次以后,单个文件的扫描时间直接降了一个数量级。如果你们的扫描也慢,先查一下是不是在循环里反复读取配置文件了。

5. 实测效果与团队落地经验

5.1 一组来自真实仓库的数据

接入 open-code-review 三个月后,我们从 GitLab 后台导出数据做了前后对比,对象是团队里最活跃的 6 个服务仓库,共 47 名开发者的合入数据。

指标接入前三个月接入后三个月
平均 MR 审查时长(从创建到首条人工评论)6.2 小时2.8 小时
单 MR 人工评论条数4.15.3
需要二次提交的 MR 占比38%52%
线上缺陷中“边界/低级错误”占比31%9%

数据上最明显的变化是,人工评论条数并没有减少,反而增加了。原因是开发者把原本花在“补风格规范”上的时间用在了“讨论设计”上。MR 的二次提交率上升,也说明大家在合并前更愿意处理问题。线上缺陷里那些边界和低级错误占比从 31% 降到 9%,效果最直接。

还有一点变化发生在流程之外:新入职的同事通过看自动审查评论,快速了解了团队里“哪些写法是被禁止的”,而不需要翻厚厚的开发规范文档。这算是工具带来的隐性收益。

5.2 推动团队接受代码审查的几个小技巧

作为一个内部工具项目,open-code-review 真正难的不是写代码,而是让团队愿意用它,并且持续贡献规则。

我总结下来最有效的几个技巧:先找两个配合度高的核心仓库做试点,跑通后再扩展到边缘仓库,不要一开始就在全部仓库上开启阻塞模式。规则配置从“最痛的问题”开始,每个季度让团队投票选出“最想自动拦截的坏味道”,把规则库变成团队共识的沉淀。自动审查结果要尽量和团队已有的 Code Review 流程结合,比如规定“error 级规则必须由提交者确认并注释原因才能解决”,而不是让机器人一个人说了算。定期导出规则命中率,公开讨论哪些规则误报多、哪些规则应该去掉,保持规则库是活的。

最后再分享一个我的个人体会:代码审查工具最重要的不是“审查能力”本身,而是“让人愿意读它的评论”。刚开始我们花了很多时间讨论技术选型、扫描性能、规则引擎,最后发现真正决定成败的是那一段段写给开发者看的评论文字。把评论写得清楚、具体、有人情味,open-code-review 才真正在团队里活了下来。如果你的团队也在为“有审查流程但效果不好”头疼,不妨先从小规则的自动评论做起,让机器帮人把人最容易漏掉的部分拦住。

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

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

立即咨询