代码评审这件事,我做了快十年。从最早的互相口头“过一遍”,到后来用 Gerrit 走流程,再到 GitLab MR 加人工 review,回头看最大的感受是:review 本身不复杂,复杂的是让团队里的人真的愿意 review、会 review,并且能从 review 里获得正反馈。我最近把团队这套流程彻底开源重构了一版,项目名就叫 open-code-review,核心思路特别简单——用开源工具链把代码审查从“事后检查”往前推成“事中约束”,再配合自动化规则和清晰的人工审查清单,把整个评审过程变成一套可量化、可复现、能持续改进的机制。
这篇文章我想完整聊聊这套方案的选型逻辑、核心配置、实际落地过程,以及我在推进过程中踩过的一些坑。内容偏实战,适合正在做团队代码规范建设、或者想把手动 review 流程自动化一半的技术负责人和一线开发,看完基本可以直接参照搭一套自己的方案。
1. 内容整体设计与思路拆解
1.1 传统代码审查为什么越来越不好用
先泼一盆冷水:很多团队的代码评审其实是“仪式感大于效果”。我见过不少团队,MR 里贴了一堆人,结果真正把改动完整过一遍的没几个。原因不完全是态度问题,而是流程设计本身就有缺陷。
第一,评审时机太晚。很多团队只在 MR 阶段才做 review,但这时候代码已经写完了,设计上的问题早就在实现过程中固化下来了。reviewer 能提的只剩代码风格、命名这类表层问题,真正影响架构的反馈基本不可能出现了。
第二,评审范围太大。一个大 MR 动辄几千行变更,没有人能在合理时间内高质量看完。实测下来,超过 400 行的 MR,review 密度会断崖式下降,reviewer 基本只挑函数名和注释看,核心逻辑全靠“相信作者”。
第三,反馈无法沉淀。今天 review 出来的问题,明天换个人又写一遍同样的。因为没有把规则固化到工具层,review 完全依赖个人经验,团队整体的代码水平就被少数人的经验天花板锁死了。
所以我做 open-code-review 的第一个原则非常明确:把能在机器层面解决的检查全部自动化,人工只做机器做不了的事。代码审查不该是“人肉找茬”,而应该是“人机分工协作”。
1.2 为什么坚持全部用开源方案
这块我认真纠结过。市面上商业级代码审查平台很多,功能确实全,但有几个问题我接受不了。
一是代码仓库的代码安全和合规问题。代码审查意味着把全量代码交给第三方平台做分析,很多公司尤其是金融、医疗行业根本过不了合规这关。我们自己部署一套开源方案,数据完全在自己手里,这个是底线问题。
二是定制能力。商业产品的规则体系再怎么灵活,也是别人定义好的。但团队自己的技术栈、历史包袱、开发习惯都不一样,一套开箱即用的规则根本满足不了真实需求。开源工具链的好处是每个环节都能改,规则写错了能自己修,不像商业产品只能提工单等版本更新。
三是成本可控。GitHub/GitLab 自带的审查能力加上一组开源分析工具,部署成本基本上是零。就算把 CI 执行时间算进去,也远低于商业产品的按人头授权费。
整个方案的技术栈我尽量控制在主流范围内:代码托管用 GitLab,CI 走 GitLab CI 或者 GitHub Actions 都行,静态分析用 ESLint + SonarQube 这类成熟工具,增量扫描用 Reviewdog 做 diff 过滤,安全和依赖检查用 Trivy、OSV-Scanner,最后再用一个轻量级的 Node.js 服务把结果汇总成统一报告。每个组件都尽量选社区活跃、文档完善的项目,避免方案搭完没人维护变成新的技术债。
2. 核心细节解析与实操要点
2.1 审查流程整体架构:把“人机分工”铺成流水线
open-code-review 这套流程我把它拆成了四个阶段,每个阶段干固定的事,机器能做的绝不让人来做。
提交前阶段:开发者在本地跑 lint 和单元测试,通过 pre-commit 钩子把低级错误挡在提交之前。这一层不追求全量检查,只要求快和直接。
CI 静态检查阶段:代码推到远端、发起 MR 之后,CI 自动跑一轮完整的静态分析。包括语法错误、未使用变量、复杂度超标、潜在空指针、日志泄漏这些“一眼就能看出来”的问题。这个阶段的目标是把 review 报告里所有“低级术语”清零。
AI 辅助审查阶段:把 MR 的 diff 抽出来,交给大模型做一轮代码审查,重点看设计合理性、边界处理、安全隐患、资源泄漏这类需要理解语义的问题。AI 给出的问题建议以“疑似问题”的方式呈现,标记在 MR 评论区,供人类 reviewer 二次确认。
人工审查阶段:把前面所有自动化结果汇总成一份清单,reviewer 只需要看清单上“机器拿不准”的部分,以及真正的业务逻辑变更。相当于机器已经把地扫干净了,人只需要处理剩下的 20% 核心环节。
这套流程设计的关键在于“每层只解决一个问题”。本地钩子只管格式,CI 只管静态指标,AI 只管语义分析,人工只管业务和架构。职责不重叠的好处是,某一层出了问题能快速定位,不会出现“一堆机器检查报错但没人知道该谁修”的混乱局面。
2.2 我把关键检查项配成了这些硬性指标
自动化检查如果规则太松,等于摆设;太严,团队会怨声载道。经过几轮调整,我目前这套配置在“严格”和“可接受”之间找到了一个平衡点。
// .eslintrc.json 中的核心规则(重点摘录) { "rules": { "complexity": ["error", { "max": 10 }], "max-depth": ["error", { "max": 4 }], "max-lines-per-function": ["error", { "max": 80 }], "no-console": "warn", "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "eqeqeq": ["error", "always"], "no-await-in-loop": "warn", "no-implicit-coercion": "error" } }这几个规则我解释一下为什么这么设。
complexity循环复杂度设成 10,超过说明函数逻辑分支太多,应该拆解。max-depth设 4,嵌套超过四层,代码基本没法读。max-lines-per-function设 80 行,强制函数保持单一职责。
no-await-in-loop我特意设成 warn 而不是 error。在 Node.js 里没有Promise.all的情况下,循环内 await 会造成严重的串行性能问题,但有些场景下业务逻辑确实要求顺序执行,所以先警告,让 review 阶段人工判断。
SonarQube 那侧我主要开了代码异味和覆盖率相关规则,其中新增代码覆盖率低于 80% 直接让流水线失败。这招非常有效,逼着开发者在写新功能的时候同步补测试,而不是攒到最后一次性糊弄。代价是 CI 会慢一点,但换来的是测试质量。
2.3 AI 审查的提示词怎么设计才靠谱
AI 审查这一段,很多人以为把 diff 丢给大模型让它“找 bug”就行了。实际效果很差,模型会吐一堆泛泛而谈的建议:“建议增加边界检查”“请确保异常被正确处理”——这种反馈没有任何价值,reviewer 看完只会更烦。
我自己调了几版提示词,目前比较稳定可用的结构是这样的:
你是一名资深代码审查专家。请审查以下 MR 的代码变更(patch 格式), 重点分析以下问题: 1. 边界条件:是否存在数组越界、空值未判断、数值溢出等风险 2. 资源管理:打开的资源(连接、流、文件句柄)是否都存在释放路径 3. 并发安全:共享状态是否有竞态风险,锁范围是否合理 4. 错误处理:异常是否被正确传递,是否会吞掉关键错误 5. 安全性:是否存在注入、敏感信息硬编码、不安全的反序列化 6. 性能:是否存在明显的重复计算或不必要的全量扫描 对每个问题,请给出: - 严重程度(P0 阻断 / P1 需修复 / P2 建议优化) - 对应代码位置(文件:行号) - 问题描述(为什么这是问题) - 修复建议(给出示意代码,不要完整重写) 如果不确定,请不要勉强给出结论。只说真正有把握的问题。关键点在于限定问题维度和输出格式。限定维度能防止模型思维发散到无关方向,限定输出格式能保证后续自动化处理,直接把 AI 结论挂到 MR 评论对应位置。还有个细节:我会在审查前明确告诉模型“不确定就不要说”,这个约束让误报率下降了好几个档次。
3. 实操过程与核心环节实现
3.1 从零搭建这套审查体系的完整步骤
下面把整个搭建过程按顺序捋一遍,我是以 GitLab + GitLab CI 为例,如果用 GitHub 的话,理念完全一致,把流水线配置迁到 GitHub Actions 即可。
第一步,安装核心组件。静态分析我用的是 ESLint 加 SonarQube,安全扫描用 Trivy,diff 过滤用 Reviewdog。SonarQube 我直接跑的 Docker 版,单机部署非常省事:
docker run -d --name sonarqube \ -p 9000:9000 \ -v sonarqube_data:/opt/sonarqube/data \ -v sonarqube_logs:/opt/sonarqube/logs \ sonarqube:community-versionSonarQube 第一次启动之后需要初始化数据库,时间比较久,等着就行。然后在管理界面创建项目,拿到一个SONAR_TOKEN,这个 token 后面要配置到 CI 变量里。
第二步,配置 CI 流水线。这是整个方案的骨架。GitLab CI 我按三个阶段来组织:
stages: - validate - scan - security lint: stage: validate image: node:20 script: - npm ci - npm run lint only: - mr - merge_requests sonar-scan: stage: scan image: sonarsource/sonar-scanner-cli:latest variables: SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar/cache" script: - sonar-scanner -Dsonar.projectKey=${SONAR_PROJECT_KEY} -Dsonar.projectName=${SONAR_PROJECT_NAME} -Dsonar.sources=src -Dsonar.host.url=${SONAR_HOST_URL} -Dsonar.token=${SONAR_TOKEN} -Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID} -Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} only: - merge_requests trivy-scan: stage: security image: aquasec/trivy:latest script: - trivy fs --severity HIGH,CRITICAL --exit-code 1 --no-progress . only: - merge_requests三个任务分工明确:lint 管代码风格和低级错误,sonar-scan 管深层静态分析,trivy 管依赖和容器镜像的安全漏洞扫描。有一个很关键的点:only: merge_requests必须加对。如果不加,每次 push 都会跑全量任务,既浪费资源,又把流水线时间拖得很长,开发体验会非常差。
第三步,接 Reviewdog 做增量检查。这是整个方案里体验提升最大的一环。Reviewdog 的核心理念是“只报告与当前 MR 变更相关的部分”,它会把多行错误定位到 diff 中的具体行并打注释,交互体验类似同行 review。
以 ESLint 为例:
reviewdog -reporter=gitlab-mr-discussion \ -f=eslint \ -level=error \ -filter-mode=added \ -fail-on-error=true-filter-mode=added这个参数很关键,它让 Reviewdog 只关心本次新增的行,老代码里历史遗留的问题不会刷屏,避免了你打开 MR 评论区以为自己犯了什么大错、结果全是历史债的尴尬局面。
第四步,接入 AI 审查服务。我写了一个轻量级的 Node.js 服务,监听 Merge Request 事件,拿到 diff 后发给大模型接口,再以 bot 评论的方式把结果贴回 MR 讨论区。关键代码逻辑不算复杂:
// ai-review/index.js(关键逻辑摘录) const { exec } = require('child_process'); const { promisify } = require('util'); const execP = promisify(exec); async function getMergeRequestDiff(projectId, iid, token) { const api = `https://gitlab.example.com/api/v4/projects/${projectId}/merge_requests/${iid}/changes`; const res = await fetch(api, { headers: { 'PRIVATE-TOKEN': token } }); const data = await res.json(); return data.changes || []; } async function runAiReview(projectId, iid, token) { const changes = await getMergeRequestDiff(projectId, iid, token); const patchText = changes.map(c => c.diff).join('\n'); const response = await fetch('https://api.example-llm.com/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'code-review-model', messages: [ { role: 'system', content: AI_REVIEW_PROMPT }, { role: 'user', content: patchText } ], temperature: 0.1, max_tokens: 2000 }) }); const json = await response.json(); const reviewComments = json.choices[0].message.content; // 把审查结果以评论形式贴回 MR await createMrComment(projectId, iid, token, reviewComments); }实际部署时我用 GitLab Webhook 监听Merge Request Hook,触发时向腾讯云函数或自建轻量服务器转发请求,服务器异步处理完再通过 GitLab API 回写评论。整个链路不用常驻队列,按次触发就行,成本很低。
温度参数设 0.1 是刻意为之。代码审查要的是稳定和保守,不是创意和发散,温度调高会让模型产生一些莫名其妙的“建议”,纯粹浪费人工 review 的时间。另外 max_tokens 2000 也够用,MR 的 diff 我们做了截断处理,超过 10000 token 的部分不参与 AI 审查,避免 API 开销过大。
3.2 审查反馈闭环:问题一定要推到“写代码的人”面前
自动化检查和 AI 审查如果只是“在 CI 里打一条 pending 状态”,效果会打折扣。我发现反馈闭环做得好不好,直接决定这套方案团队用不用得起来。
我目前的做法是“三层触达”。
第一层,Reviewdog 以 MR 讨论串形式把问题钉在对应代码行上。reviewer 打开 MR,第一眼就能看到哪些行有问题,不用到处翻日志。
第二层,SonarQube 的质量门禁在 MR 合并前强制卡住。红色状态不允许 merge,这个不能靠自觉,要配置成硬约束,谁来了都得走这个过程。
第三层,机器人通知通过企业微信/钉钉群把审查结果摘要推给开发者和相关负责人。这条主要针对严重问题(P0/P1),确保不是合并了才从日志里翻出来。
三层触达下来,每个问题都像是“当场点名”——开发者会在收到提醒的第一时间看到问题在哪、为什么是问题、怎么改,而不是等别人 review 时才发现自己写的东西经不起细看。
3.3 流水线细节优化:跑得快,怨言少
配置完一套流程之后,团队最常见的一个抱怨是“CI 怎么又慢了”。代码审查是为了提效,如果流水线本身成为效率瓶颈,那方案肯定是失败的。
我把流水线时间从平均 15 分钟压到 6 分钟以内,主要做了三件事。
一是 lint 分级处理。本地 pre-commit 钩子跑一遍全部规则,推到远端 CI 的 lint 任务只跑--diff模式,只检查本次 MR 涉及的文件。全量 lint 放到 nightly 定时任务里兜底,防止本地钩子被跳过的情况。这一项直接砍掉了一大半 lint 耗时。
二是依赖缓存。GitLab CI 支持 cache 配置,我把 node_modules 和 SonarQube 的缓存目录都设置成按分支名加锁的缓。开发分支的重复构建直接从缓存拉依赖,不用每次重新下载。实测单个任务能省下 1-2 分钟。
三是并行化。lint、SonarQube、Trivy、AI 审查之间没有依赖关系,全部并行跑,而不是串行等待。GitLab CI 里不同 job 默认是并发执行的,只要 runner 足够,这条基本是白捡的收益。
四是控制资源占用。AI 审查服务如果一个 MR 打开就触发,再加上团队同时改十几个 MR,API 调用量会非常惊人。我做了两级节流:每个 MR 最多只审查前 3 次提交版本,超过之后只审查最后一次;另外每个开发者同时只允许运行一个 AI 审查任务,多余的直接排队等前面结束。
4. 常见问题与排查技巧实录
4.1 误报太多,机器人被拉黑
这套方案刚上线两周,最大的问题不是审查功能不行,而是机器人被开发者“拉黑”了。原因很直接——Reviewdog 在 ESLint 规则不完善的时候,会输出大量错误级别的问题。有些是历史代码,有些是规则本身没配好导致的误判,开发者打开 MR 一看满屏红点,直接在群里抱怨“这工具疯了”。
排查之后我发现是 ESLint 配置里no-undef和 TypeScript 类型检查冲突导致的误报,比如在某些自动生成的声明文件里,ESLint 不认识自定义的全局类型。解决办法是给对应的文件加 ignore 注释,并在 ESLint 配置里关掉与 TypeScript 编译冲突的规则(统一交给@typescript-eslint插件处理)。
经验:引入自动化审查一定要先从警告级别开始跑两周,让团队日常开发先适应噪音,等规则稳定之后再逐步升级为 error。一开始就全网铺 error,结果大概率是规则被集体屏蔽。
4.2 SonarQube 新增代码覆盖率卡不住人
SonarQube 的覆盖率质量门禁,我设的是新增代码覆盖率低于 80% 就失败。但上线之后发现,很多人为了过门禁,写了大量只为了“跑通覆盖率”的假测试,断言都不加,就是 execute 一下不报错,覆盖率数字很漂亮,回到业务逻辑能不能跑根本没人知道。
这个问题防呆的办法有两个。一是生命周期检查,我在扫描配置里额外加了 mutation testing 的步骤,用 Stryker 做变体测试,检测测试用例是否有真实断言能力。二是靠代码审查阶段的人工观察,reviewer 重点盯那些覆盖率很高但测试文件都是空壳的 MR,抽查几个用例,确认里面真的有实际断言。
4.3 多环境规则不一致,同一段代码两套结论
生产环境用的 SonarQube 和本地 IDE 集成的 SonarQube 插件版本不一样,导致同一段代码本地看是 A 类问题,CI 里却是 B 类问题。开发者会困惑,到底以哪个为准。
我的解决办法是固定工具链版本。CI 镜像里锁死 SonarQube Scanner 的版本,同时要求团队本地必须安装相同版本的插件。这个同步用 .tool-versions 文件统一管理,插件升级必须走 MR 流程,不允许个人私自升。另外,规则文件也全部纳入版本库,SonarQube 里的规则变更必须同步到项目,从源头杜绝“本地和线上不一致”。
4.4 快速定位问题:审查系统自检清单
如果发现审查结果异常,我会按照下面这个顺序排查:
- 先看 CI 日志有没有报错。SonarQube、Reviewdog 这类工具执行失败时经常伴随后续阶段不执行,先确认日志全绿。
- 再验证 token 和权限。SonarQube token 过期、GitLab token 对应的用户被移出项目成员,都会导致 API 调用失败,但 CI 里显示正常。每次配置变更后,我都会手工跑一次 API 调用确认权限没问题。
- 检查规则集是否更新成功。SonarQube 的质量配置更新之后,如果项目没有重新绑定,扫描出来的结果可能还是旧规则。
- 确认 Reviewdog 过滤模式。有时候觉得“报告不准确”,其实是
filter-mode用错了,added是看新增的,diff_context是看 context 范围的,用混了会感觉报告跳行。
这几条排查下来,绝大多数问题都能在 15 分钟内定位。说实话,这套系统上线初期一大半时间不是在写规则,而是在排查“为什么机器又说不对了”。但渡过这段阵痛期之后,稳定性会迅速上升,因为核心链路的组件都很成熟。
5. 团队落地经验:好工具也得有人用
5.1 上线初期做的三件“软性”铺垫
技术方案好不好,最终要看团队愿不愿意用。open-code-review 上线时我做对了几件小事,大家可以参考。
第一,先带小团队试点,不要一下铺全员。我选了业务代码积累最重、历史债务最多的后端组先跑两周,把他们 MR 里的实际问题一条条整理出来,分类统计,用真实数据说服其他组。别的组看到后端组 MR 里的问题是同行真实反馈,而不是管理者在讲道理,接受度完全不一样。
第二,建立“审查反馈帮助文档”。不是写一堆测试报告,而是把团队里出现过的典型问题整理成一个个“场景 - 代码示例 - 正确写法”的条目。比如“不要在循环里 await 数据库查询,改成批量查询”,配一段改前改后的对比。这样当机器人评论里提到某个规则时,开发者点开文档就能看到这个规则为什么存在、怎么修。
第三,把会 review当成一种能力。我安排固定时间做内部 review 演练,让大家轮流当 reviewer,评审一段经过精心准备的“有坑代码”,然后对照机器审查结果复盘讨论。很多人第一次意识到,review 不是挑刺,而是互相学习、确保团队代码整体不拖后腿的手段。
5.2 规则演进:让审查体系跟着业务走
代码审查工具最怕一成不变。业务变化、技术栈升级、团队风格演变,审查规则如果不动,很快就变成“历史遗留约束”,不再有实际价值。
我在 open-code-review 里设计了一套规则演进机制。每个季度做一轮规则复盘,收集开发者的反馈,对警告数量过多、误报率高的规则降低优先级或调整参数;对影响代码质量的规则升级为 error。这个流程不是管理者拍脑袋,而是每季度末在团队里收集数据,以“实际 MR 中该规则命中后,人工确认它是否是有效问题”为基准来调整。
有一个比较成功的例子:上线初期no-console规则设的是 warn,不少开发者为了方便调试写了很多console.log。两个月后,团队出现了一次因为日志输出导致的信息泄漏风险(log 里不小心带了 token),我立刻把它升级为 error,同时补充了统一的日志工具库。调整之后,生产环境再也没出现过日志泄漏问题。
5.3 一点忠告:自动化审查替代不了人
最后说点掏心窝的话。这套系统的目的从来不是“取代人工审查”,而是把人工从繁琐的低级问题里解放出来。
我见过一些团队,上了自动化检查之后,人力 review 变成了象征性的“看一眼,没问题,过”。这是完全反方向的用法。自动化把“低级问题”清零之后,人类 review 应该把 100% 的精力集中在更高维度:业务逻辑是否正确、接口设计是否合理、异常分支是否处理干净、这个改动是否符合团队整体技术规划。这些恰恰是机器目前做不好的。
在实际项目里,我依然坚持人工 review 是必须的环节,甚至会刻意让 AI 审查的结果也只作为“建议”,不让它直接决定合并状态,因为 AI 看不懂业务上下文,它只能基于通用模式给建议。在一个业务属性极强的系统里,判断“这样写对不对”的最终决策权必须在人手里。
就我个人体会,这套方案上线半年多,团队 review 的密度和有效性问题数量明显增加,大家在提 MR 前会更愿意自己先把机器人检查跑一遍,而不是提交一个“等 review 来告诉我哪里错了”的半成品。这才是代码审查该有的样子——它不是流程上的负担,而是帮助大家把代码写得更舒服、更放心的工具。