我在两年前把团队内部的代码评审机制重新整理了一遍,仓库名就叫open-code-review。这个名字起得很直白,目标是想让代码评审从“两个人关起门来看一眼”变成“所有人都能看见、都能评论、事后还能复盘”的开放过程。当时团队正处在从八个人扩张到三十个人的阶段,PR 越来越多,光靠组长逐个 review 已经明显跟不上了。试行open-code-review之后,评审平均耗时从 3.2 天降到了 1.1 天,更重要的是,新人通过看别人 MR 里的争论,学习效率比读文档高了不少。
如果你也在带团队,或者你正在维护一个多人协作的开源代码仓库,只是想知道“什么样的评审流程才不算形式主义”,这篇文章值得往下看。它不依赖某个特定工具,GitLab、GitHub、Gitea 都能落地,核心是一套可执行、可度量的规则。
1. 从我踩过的坑说起:为什么需要 open-code-review
1.1 封闭式评审的问题并不在“人”,在于流程
先说说之前的评审长什么样:
- 开发者提 MR,在群里 @ 两个人:“帮忙看下。”
- 被 @ 的两个人点开 MR,看到一大坨代码不知道从哪看起,索性先标记为已读。
- 如果真的看了,也很少在代码行上评论,更多是私聊作者:“这里好像有点问题。”
- 合并之后,评审记录消失在聊天记录里。
这种模式把信任建立在工作记忆上,问题就出在这里。团队十个人的时候,每个人写什么模块,组长心里有数;团队三十个人的时候,光靠记忆根本转录不过来。私聊里的一句“这里好像有问题”,没有沉淀在 MR 上,就会造成两个麻烦:作者修完以后,别人不知道他为什么改;后来人看这段代码,也看不到当初的取舍。
还有更隐蔽的问题:只有被 @ 的人有资格评论,其他人想说话会被默认成“多管闲事”。这会让一些真正了解公共模块的老同事选择闭嘴。一次我印象特别深,一个后端同事私下跟我讲,他在某次评审里发现了一个数据竞争问题,但碍于“不是被指定的人”,一直没好意思开口,直到测试环境出了故障才暴露出来。这种事情反复出现之后,我开始认真思考:问题可能不在人的积极性,而在评审机制本身就是封闭的。
1.2 open-code-review 的四个核心词
我理解的open-code-review,不是简单把权限放开、谁都能评论,而是四个关键词组成的一套规则:公开、结构化、可度量、有闭环。
- 公开:仓库内所有成员都能查看 MR、都能在代码行上评论。评论不需要被分配,也不需要经过组长同意。只有公开的讨论才能形成知识沉淀,私聊里的评审意见本质上是一次性消耗品。
- 结构化:每条评论必须能归到某类问题:逻辑错误、安全问题、性能隐患、代码风格、测试遗漏。没有结构的评审,最后会变成“我觉得这里的代码不太优雅”这种模糊对话。
- 可度量:评审时长、第一次评论时间、阻塞性评论数量、参与评审人数,这些都应该有数据记录。没有数据,团队就无法判断评审到底卡在哪一环。
- 有闭环:每一条评论都要有明确的结果:修改、确认忽略、转移到待办。所有阻塞性问题都解决了,代码才能合并。不能一边在评论里吵着,一边有人手动点了“合并”。
这四个词合在一起,才是open-code-review的核心。它解决的不是“谁来看”,而是“看的过程有没有留下可追溯的、对团队有用的信息”。
2. 把 open-code-review 落地成一套流程
2.1 从提交到合入:open-code-review 的六步流程
不要一上来就整很复杂的流程,我推荐从最基础的六步开始。以下表格是我在团队里实际使用的流程版本,你可以直接复制过去改。
| 阶段 | 关键动作 | 时限 | 负责人 |
|---|---|---|---|
| 提交前 | 本地跑 lint 和单测,写清楚 MR 描述 | 无 | 作者 |
| 发布 | 创建草稿 MR,标注改动模块和风险点 | 无 | 作者 |
| 分配 | 机器人或维护者至少指派 1 名评审人 | 发布后 2 小时内 | 维护者 |
| 评审 | 评审人在行级评论,必须标注评论级别 | 24 小时内首次评论 | 评审人 |
| 修改 | 作者解决所有 Blocking 评论并回复 | 48 小时内 | 作者 |
| 合入 | CI 全绿、至少 1 个 Approve、无未解决 Blocking | 无 | 维护者 |
每个环节背后都有一个设计意图,不是随便定的。
提交前跑 lint 和单测,是为了把“代码能不能跑”这件事从评审中彻底拿掉。评审人的精力应该花在“这么做对不对”“以后好不好维护”上,而不是纠正缩进和拼写错误。
创建草稿 MR 的意义在于让评审人知道什么时候可以开始看。很多团队的问题是,代码刚 push 上来还在改,评审人就被 @ 到了,结果他评论完之后作者又推了一版完全不同的。草稿 MR 是一个很好的信号:这里还没准备好,你可以先围观,但请不要正式评审。
分配评审人这一步尤其重要。我见过太多团队用“在群里喊一嗓子”来代替指派,最后没人觉得自己有责任看。必须有一个确定的负责人,谁没参与一眼就能看出来。
2.2 角色定义:谁必须评、谁可以评、谁来兜底
open-code-review 强调开放,但开放不等于没有责任人。我在流程里定义了四种角色:
- 作者:负责提交代码、回复评论、在 MR 描述里写清楚背景。
- 评审人:被正式指派的人,有验收责任。他的
approve是合入的硬性条件之一。 - 维护者:拥有合并权限的人,通常是技术负责人或模块 owner。他负责兜底,处理评审人之间的分歧。
- 围观者:任何能看到仓库的人都可以评论,但评论不进入验收计数。
这里有个细节值得注意:围观者的评论是否要强制解决?我的答案是,不需要。如果强制要求作者必须解决所有围观者的意见,那评论区会变得越来越冷清,因为大家怕给作者添麻烦。正确的做法是:把围观者的评论当成“提问”,作者可以选择回答“这不会发生,原因是 XXX”并关闭,没有人会因为关闭了评论就被记一笔。
这个设计的优点是既保住了开放的评论入口,又不会让作者陷入无休止的争论。我记得有一次,一个来自 QA 的同事在某个前端 MR 里提了一个关于边界状态的疑问,虽然不是评审人,但他看得非常准。作者解释了自己的方案,顺便把边界情况加了一个测试。如果我们的流程不允许围观者评论,这个测试大概率就不会出现。
2.3 评审检查单:不要只凭感觉给意见
很多评审人看完一个几百行的 MR 之后只会写“LGTM”(Looks Good To Me),不是因为他真的觉得没问题,而是他不知道自己该看什么。解决这个问题最好的办法,就是给评审人一张明确的检查单。
以下是我整理的一份精简版,每条都尽量是“可验证”的,而不是“请检查代码质量”这种废话。
| 维度 | 检查项 | 判断方法 |
|---|---|---|
| 正确性 | 边界条件有没有处理? | 看空值、越界、初始状态、并发场景 |
| 安全性 | 外部输入是否校验? | 看用户输入、文件上传、权限判断 |
| 性能 | 有没有明显 N+1 查询或循环内调用? | 看数据库查询和数据量变化 |
| 可测试性 | 新逻辑有没有对应测试? | 搜索关联 test 文件 |
| 可维护性 | 命名是否能直接看出意图? | 不看实现,只看函数名和变量名 |
| 可扩展性 | 后续需求变更时,这段代码需要推倒重来吗? | 只做合理推测,这里不要过度设计 |
检查单不需要太长,十二到十五条顶天了。太长会让评审人产生“完成检查单”的心理,反而把真正的问题漏掉。
我在团队里把这份检查单放在 MR 模板中,并在 CI 里生成一个“评审回执”,让评审人在批复 approve 之前勾一遍。老实说,一开始很多人嫌麻烦,但坚持一个月之后,大家对于“什么叫完成了评审”这件事有了共同的预期。后来把检查单本身也开源到了团队公共仓库里,形成了open-code-review项目的一部分。
3. 用工具把评审规则固化成“硬约束”
3.1 提交前:用 Git 钩子和 commit 规范挡掉低级问题
流程是软的,工具才是硬的。如果你想在没有代码托管平台插件的情况下,尽量把流程固定下来,Git 钩子是最低成本的方案。
我们在仓库根目录维护了一份.git/hooks/commit-msg脚本,用来统一 commit message 格式。这个脚本很简单,核心逻辑就是正则匹配:
#!/bin/bash # .git/hooks/commit-msg msg_file=$1 if ! grep -qE '^(feat|fix|refactor|docs|test|chore)(\(.+\))?: ' "$msg_file"; then echo "commit message 不符合规范,示例:fix(user): 修复登录后闪退问题" >&2 exit 1 fi有人会觉得这很形式化,但我实际体验下来,commit message 统一之后,很多自动化工具才能跑起来。比如我们之后的版本发布脚本,就是靠git log --grep='^feat'来生成 changelog。如果每个人写的格式都不一样,这个脚本就完全废了。
除了 commit-msg,我还会在pre-push钩子里跑一次快速 lint:
#!/bin/bash # .git/hooks/pre-push echo "pre-push: 运行代码检查..." npm run lint if [ $? -ne 0 ]; then echo "lint 未通过,禁止推送" >&2 exit 1 fi这里的关键点是:钩子必须放在本地,并且靠约定来同步。因为 Git 钩子不会随仓库自动克隆到开发者本地,所以我会在package.json里加一个prepare脚本,让开发者在npm install时自动安装 hooks。如果你用 Python,也可以在pyproject.toml里配置类似动作。
这样做的好处是,即使团队里有人不主动跑测试,提交时也会被迫过一遍检查,大大减少了评审人需要处理的“低级问题”。我们的切身体会是,引入这个机制之后,评审压力减小了很多。
3.2 评审中:把 MR 描述和评论模板化
如果你不想自己开发工具,最简单的办法是在代码托管平台里配置 MR 描述模板。我们用的 GitLab,在项目根目录创建.gitlab/merge_request_templates/default.md:
## 为什么改 <!-- 写清楚这次需求或 bug 的背景,以及为什么采用当前做法 --> ## 改动点 <!-- 列出主要改动的文件和模块,不要写 git diff 里能看到的东西 --> ## 自测结果 <!-- 本地测试、单元测试、手动验证的结论 --> ## 需要评审者重点看什么 <!-- 如果有不确定的设计决策,请在这里详细说明 -->模板的价值不是逼着作者填表,而是迫使作者在提交代码之前把上下文梳理一遍。很多评审问题其实是上下文缺失导致的。评审人看到一段没头没尾的 diff,本能反应就是“感觉不对”,但说不清哪里不对。模板让作者先把自己的思路摆出来,评审人就容易给出有针对性的反馈。
评论模板同样重要。我们在团队里统一了评论前缀的规范,所有行级评论都必须带下面三种标记之一:
[Blocking]:必须修复才能合入。通常用于逻辑错误、安全漏洞、导致线上事故的问题。[Nit]:非阻塞的小建议。可以忽略,但建议作者看一眼。[Question]:作者需要给出解释,不一定改代码。
这套机制看起来简单,但它做了一件事:把“作者和评审人之间的地位博弈”从评审中剥离了。以前大家不好意思指出问题,是怕态度太强硬伤了和气;现在有了前缀,[Blocking]不是说“你代码写得差”,而是“这里如果不改会有实际风险”。评论的对抗性大幅下降。
我也要求作者在回复时用Acknowledged作为确认词。对于[Nit]类评论,作者只要回复Acknowledged表示收到即可,不需要额外声明“我不改”。这样可以避免每个 nit 都来回拉扯两轮。
3.3 数据化:用 API 统计评审指标,周一例会不用拍脑袋
规则有了,工具也有了,但还不够。我相信一句话:没有数据支撑的流程改进,都是靠感觉。所以open-code-review里还有一个核心模块,是用代码托管平台的 API 拉取评审数据,生成每周报告。
下面是一个简化版的 Python 脚本,用来统计一个 GitLab 项目中已合并 MR 的平均评审耗时和评论数。实际项目中我会加上多项目汇总、Excel 导出等功能,但核心就是这样:
import requests from datetime import datetime, timezone TOKEN = "glpat-xxx" BASE = "https://gitlab.example.com/api/v4" PROJECT_ID = 42 headers = {"PRIVATE-TOKEN": TOKEN} mrs = requests.get( f"{BASE}/projects/{PROJECT_ID}/merge_requests", headers=headers, params={"state": "merged", "per_page": 100}, ).json() total_duration = 0 total_comments = 0 count = 0 for mr in mrs: created = datetime.fromisoformat(mr["created_at"].replace("Z", "+00:00")) merged = datetime.fromisoformat(mr["merged_at"].replace("Z", "+00:00")) # 评论数通过另一个接口获取,这里简化成列表 notes = requests.get( f"{BASE}/projects/{PROJECT_ID}/merge_requests/{mr['iid']}/notes", headers=headers, ).json() duration_hours = (merged - created).total_seconds() / 3600 total_duration += duration_hours total_comments += len(notes) count += 1 if count > 0: print(f"本周合并 MR 数: {count}") print(f"平均评审耗时: {total_duration / count:.1f} 小时") print(f"平均评论数: {total_comments / count:.1f}")这类数据在我们团队里主要用来发现流程瓶颈。比如平均评审耗时异常高,可能是某个模块缺 owner,或者某个人长期被指派却从来不参与。平均评论数如果低于 1,说明大部分人只是在走审批流程,根本没真的看代码。
这里有个容易踩的坑:API token 权限不要搞太大,只需要read_api权限就够了。而且脚本不要用团队的公共 token,最好用某个定时任务的专用机器人账号。我也见过有人把 token 直接 commit 到仓库,这个风险极大,强烈不推荐。
另一个要注意的是,单纯的“耗时”指标会被大 MR 拉高。所以我会额外看一条评审效率 = 总改动行数 / 评审时长,虽然很粗糙,但能帮团队发现超大 MR 对整个流程的拖累。后来我们把 MR 拆小,限制单次改动尽量不超过 400 行,评审效率和评论质量明显上升。
4. 常见问题排查实录:评审推不下去怎么办
4.1 没人评论,或者评论永远是 LGTM
推行open-code-review初期最典型的问题是,MR 挂了两天,除了机器人没有任何人说话。后来有人终于点了 approve,评论只有一个单词:LGTM。
我复盘时发现,问题不在于同事敷衍,而在于被指派的评审人不知道“该怎么评”。尤其是刚毕业的工程师,让他 review 一个资深开发的 MR,他压力很大,既怕说错,又怕说太多得罪人。最后只能回一个 LGTM 保平安。
解决办法是拆成两步:
第一步,给评审人非常具体的任务。在 MR 描述里,作者必须写清楚“需要评审者重点看什么”。比如“重点看登录态过期后,并发请求的处理是否正确”,评审人就有了支点。
第二步,把“评论质量”放进周会。每周挑一条最有价值的[Blocking]评论,展示它如何发现了一个潜在线上问题。这样大家会逐渐明白:指出问题不是找茬,而是在帮助团队避免事故。我们坚持了几个星期之后,LGTM 的比例从 70% 降到了 20% 左右。
4.2 评审变成“找茬现场”,人情味没了
开放评审到一定程度,会遇到另一个反面问题:评论区变成了辩论赛。两个人在一个缩进问题上来回争论十几次,最后还要拉维护者来裁判。
本质上是因为评论没有权重,所有意见看起来都同等重要。解决思路是强化[Blocking]和[Nit]的分级。我明确规定:只有[Blocking]才能阻止合入,[Nit]和Question类评论,作者可以选择不处理。这样一来,风格、命名、行数这类主观问题都归入 nit,根本吵不起来。
如果有人非要用[Blocking]来提风格建议,维护者会介入,告诉他这不是 blocking 级别的问题。明确规则之后大概一个月,评论区的语气就恢复正常了。大家会把精力放在真实性问题上,而不是个人偏好上。
4.3 跨时区异步评审,上下文丢失怎么办
团队分散在不同城市之后,我遇到最大的问题不是没人评,而是“看了跟没看一样”。评审人打开 MR,看到 300 行 diff,却不知道这是什么功能,只能从代码里硬猜,评论的质量自然上不去。
这个问题的根源是上下文没有传达到位。异步评审不像线下黑板上可以指着说,它必须靠作者把上下文写清楚。
我们用的措施有两招。第一招,要求作者把 MR 描述里的“为什么改”写到不少于三句话,并且包括一个完整场景例子。比如共享组件库的 MR 中,我会要求写出“现在登录后调用 getUserInfo,如果 token 过期会抛 401,本次改动让请求队列自动重试”,这样一来,评审人不需要先读完整业务代码,也能理解改动意图。
第二招,把大 MR 拆小。这是我反复强调的,超过 400 行的 MR,评审人很难在异步对话中保持完整的上下文。拆成多个小 MR 之后,每个 MR 的问题域聚焦,评审人只需要理解一小块逻辑。异步评审的最大敌人是“上下文爆炸”,而不是时差。
4.4 合入门禁被绕过
流程设计得再完善,也会有人绕过。最常见的是维护者觉得“这个改动太简单了”,或者“客户在等这个修复”,于是手动点了合并,跳过了门禁。一两次可以理解,但形成习惯后,评审流程就会变成废纸。
我在 GitLab 里把主分支设置成“保护分支”,在这个模式下,不是所有人都能直接 push 和 merge。合入必须满足三个条件:
- CI pipeline 通过
- 至少 1 个指定评审人 approve
- 所有
[Blocking]评论已 resolve
这三条是平台的硬约束,不是靠自觉。如果有人真的需要绕过,那必须经过维护者和技术负责人双重同意,并且在 MR 里记录绕过原因。我个人经验是,一旦把流程固化成平台规则,成员反而不会觉得麻烦,因为“规则不是我定的,是系统强制要求的”,这是心理上很微妙的一层。
5. 把 open-code-review 变成团队知识沉淀
5.1 将高频评审问题转化为自动化规则
评审每个月都会产生几十条评论,如果这些评论只停留在 MR 里,价值就被浪费了。我习惯在每个迭代结束后,把评审评论导出来做一次关键词聚合,看看哪些问题反复出现。
常见的重复问题有:空指针没有判空、时间边界差了 1 秒、日志里没有打印关键请求 ID、异常被吞掉导致排障困难。这些问题每条都很小,单独看不会影响产品,但频繁出现会消耗大量评审精力。
解决方式是把高频问题变成自动化检查。比如日志规范,用一个简单的eslint规则或者grep脚本就能排查;错误处理问题,可以在 CI 里对指定目录做静态检查。当我们把open-code-review跑了一个季度之后,线下的评审评论数量反而下降了,因为一半问题都在 CI 阶段被挡掉了。评审终于可以把精力放在真正的架构问题上。
5.2 用评审数据反哺个人绩效与技术分享
评审数据还有一个容易被忽视的作用,就是反哺团队建设。每隔两周,我会拉一次评审统计数据,但目的不是考核,而是发现哪类技术讨论值得全员分享。
举一个实际例子:有几次评审里,几个同事都在争论 Python 的可变默认参数问题,然后各自用不同方式绕开了。这件事说明团队在这个知识点上存在系统性的认知缺口。我把典型 MR 里的讨论整理成一次 30 分钟的技术分享,讲了为什么def foo(items=[])会踩坑,以及怎么在代码评审中快速识别这类问题。分享之后,同类评论明显减少。
open-code-review的价值也在这里:它产生的不只是“代码能不能合并”的结论,更是一份团队能力地图。通过分析评审中反复出现的问题,你总能找到下一步要补的短板。
5.3 一个值得抄作业的最小启动包
如果让我给一个还完全没有评审流程的团队列最小启动包,我会写这三行:
- 每个 MR 至少指派 1 名评审人,评审人必须在一个工作日内留下第一条评论。
- MR 描述必须包含“为什么改”和“需要评审者重点看什么”,模板直接套用 3.2 节。
- 合入前必须完成所有
[Blocking]评论,这个用保护分支强制。
这三条已经能覆盖 80% 的收益。我自己踩过最大的坑,就是一上来把流程做得太大,又是指标又是机器人,结果团队成员一星期就放弃了一半。后来老老实实从最小闭环开始,跑了三个月,再把统计脚本和自动化规则加进去,反而稳妥得多。
如果你正在整理自己团队的评审流程,建议把仓库名直接命名为open-code-review,把模板、脚本、流程图都放进去。后续你甚至可以把它做成一个可复用项目,让其他团队也能通过复制仓库快速启动。代码评审这件事,本质上不是约束人的,是用规则让信息流动起来,让每个人都能看到、能提问、能改进。从最小的指派和模板开始,比什么都重要。