没人愿意谈论的数字
非正式代码审查能够发现被审代码中 15% 到 30% 的缺陷。这不是观点,而是在四十年、多家公司、数十项研究中反复验证的结论。
迈克尔·费根于 1976 年在 IBM 记录了这一数据。1987 年 AT&T 贝尔实验室的研究发现了 20%。1996 年惠普的研究发现了 25%。2013 年微软研究院关于现代代码审查的论文也发现了大致相同的范围。工具从打孔卡演进到 GitHub,但人的表现曲线并未移动。
如果你的团队认为 Pull Request 审查是质量的最后防线,那么数据告诉你:你们大约只发现了四分之一的缺陷,其余四分之三已经发布上线。
数字的来源
费根最初的方法论简单而残酷。他向代码中注入已知缺陷,运行审查流程,统计审查者发现了多少。然后将其与团队最终通过测试、生产事故和客户报告发现的总缺陷数进行比较。审查中发现数与总缺陷数的比率就是去除率(removal rate)。
关键洞察在于分母很重要。一场发现十个缺陷的审查听起来不错,直到你知道文件里其实有五十个。费根测量了完整的分母,而大多数团队今天并不这么做。
后续研究采用了类似的设计。研究人员植入缺陷、比较审查类型,或从生产环境反向追踪缺陷,看它们本可以在哪里被捕获。结果高度集中:
| 审查类型 | 缺陷去除率 | 关键研究 |
|---|---|---|
| 无审查 | 0% | 基线 |
| 非正式 / PR 审查 | 15-30% | Fagan 1976, Porter 1995, Microsoft 2013 |
| 结构化走查 | 30-50% | Yourdon 1979, Weller 1993 |
| 费根审查 | 60-90% | Fagan 1976, Russell 1991, NASA 2002 |
非正式审查与结构化审查之间的差距并不小。缺陷逃逸率相差 3 倍。
为什么 PR 审查表现如此糟糕
问题不在于审查者工作不力。问题在于 Pull Request 审查的设计目的就不是发现缺陷,而是让两个人同意代码可以合并。
以下是典型的 PR 审查中实际发生的事。审查者打开 diff,阅读摘要,扫描新增内容,检查测试是否通过,寻找明显错误。这花费五到十五分钟。然后点击批准。
整个流程为速度而非彻底性优化。没有准备时间。审查者没有阅读周边代码,没有追踪数据流,没有建立变更的心智模型。他们只是对屏幕上的 diff 做出反应,而人类大脑极不擅长在这种格式中发现缺陷。
2015 年 Bacchelli 和 Bird 在微软的研究发现,最常见的审查评论类别与缺陷完全无关。排名靠前的类别是关于意图的提问、澄清请求和改进建议。真正的缺陷发现只是评论中的少数。这个工具充当的是沟通渠道,而非质量门禁。
如果你知道工具在做什么,这没问题。如果你以为它在干别的,就很危险。
结构化审查的不同之处
费根审查和其他结构化审查方法通过改变流程设计而非更换人员,实现了更高的去除率。
最大的杠杆是个人准备。在费根审查中,每位审查者在任何小组讨论之前都要先花时间独立研读材料。费根发现,有准备的审查者发现的缺陷数大约是未经准备者的两倍。而 PR 模式默认就是未经准备直接上场。
第二个杠杆是节奏。费根建议每小时审查 100 到 125 行代码。大多数 PR 审查者的处理速度是这个的十倍。速度扼杀发现。大脑会用预期模式填补空白,而不是真正阅读眼前内容。
第三个杠杆是专注。费根会议只有一个目的:记录缺陷。没有设计辩论,没有解决方案提议,没有风格讨论。PR 讨论串 routinely 偏离到架构意见上,消耗了本可以用来发现空指针解引用(null dereference)的相同认知预算。
第四个杠杆是读者角色。让未参与编写代码的人大声复述代码,会迫使整个团队以理解速度而非浏览速度进行加工。屏幕上的 diff 会让眼睛跳过无聊部分,但说话的人不会跳过。
成本论是反的
反对结构化审查的常见理由是成本。一次费根审查要消耗四到六人时来审几百行代码。一次 PR 审查只消耗一个人十五分钟。这笔账看起来很明显。
它确实明显,但它也是错的。
费根还测量了在不同阶段发现缺陷的成本。审查阶段发现的缺陷成本大约是测试阶段发现的十分之一。一旦逃逸到生产环境,这个比率会膨胀到二十或三十倍。2002 年 NASA 的研究发现,审查中每投入一小时,平均可避免后续 33 小时的维护工作。
这些节省是看不见的。你无法度量被你预防的缺陷。会议的成本是即时而显眼的。这就是为什么组织会优化可见的速度而非不可见的质量,即使数据表明最终代价更高。
数据驱动的中间路线
你不需要 IBM 的会议文化就能获得大部分收益。你只需要借用在数据上真正有效的流程特征,抛弃无效的部分。
数据认为以下因素重要:
-
准备时间。 要求审查者在发表评论前先花时间研读代码。哪怕十分钟专注阅读也胜过匆匆扫过。
-
节奏限制。 对关键文件强制最大审查速度。如果审查者在五分钟内批准了 500 行的变更,那是数据,不是尽职。
-
仅聚焦缺陷。 将风格和架构反馈与缺陷 hunt 分离。前者用自动格式化工具处理,后者留给人类注意力。
-
检查清单。 费根发现,使用检查清单的审查者能发现凭直觉审查者遗漏的缺陷。检查清单的存在正是因为专业经验会造成盲点。
以下是一个轻量级脚本,用于从 Git 历史中测量审查深度。它估算一次审查是否有足够时间做到彻底:
#!/usr/bin/env python3
"""Estimate review depth from git history."""
import subprocess
import sys
from datetime import datetime, timezone
def get_commit_info(commit_hash: str) -> dict:
"""Return author, committer, and timestamps for a commit."""
fmt = "%H|%an|%cn|%ad|%cd"
result = subprocess.run(
["git", "log", "-1", f"--format={fmt}", commit_hash],
capture_output=True,
text=True,
check=True,
)
parts = result.stdout.strip().split("|")
return {
"hash": parts[0],
"author": parts[1],
"committer": parts[2],
"author_date": datetime.strptime(parts[3], "%a %b %d %H:%M:%S %Y %z"),
"commit_date": datetime.strptime(parts[4], "%a %b %d %H:%M:%S %Y %z"),
}
def get_lines_changed(commit_hash: str) -> int:
"""Count total lines added + deleted in a commit."""
result = subprocess.run(
["git", "diff", f"{commit_hash}^", commit_hash, "--stat"],
capture_output=True,
text=True,
check=True,
)
# Last line of --stat contains totals like "3 files changed, 42 insertions(+), 7 deletions(-)"
for line in reversed(result.stdout.strip().splitlines()):
line = line.strip()
if "insertions" in line or "deletions" in line:
# Extract numbers roughly
parts = line.split(",")
total = 0
for part in parts:
digits = "".join(ch for ch in part if ch.isdigit())
if digits:
total += int(digits)
return total
return 0
def estimate_review_depth(commit_hash: str) -> dict:
"""Estimate whether a commit had time for thorough review.
Returns lines changed, time between author and commit dates
(a rough proxy for review duration), and a verdict.
"""
info = get_commit_info(commit_hash)
lines = get_lines_changed(commit_hash)
# Time between author date and commit date is a proxy for review time
# In many workflows, commit date reflects when the merge happened
review_seconds = (info["commit_date"] - info["author_date"]).total_seconds()
review_hours = review_seconds / 3600
# Fagan recommended 100-125 lines/hour for thorough review
fagan_rate = 125
needed_hours = lines / fagan_rate if lines else 0
verdict = "insufficient"
if review_hours >= needed_hours:
verdict = "adequate"
if review_hours >= needed_hours * 2:
verdict = "thorough"
return {
"hash": commit_hash[:8],
"lines": lines,
"review_hours": round(review_hours, 2),
"needed_hours": round(needed_hours, 2),
"verdict": verdict,
}
if __name__ == "__main__":
commit = sys.argv[1] if len(sys.argv) > 1 else "HEAD"
result = estimate_review_depth(commit)
print(f"Commit: {result['hash']}")
print(f"Lines: {result['lines']}")
print(f"Review time: {result['review_hours']} hours")
print(f"Fagan time: {result['needed_hours']} hours")
print(f"Verdict: {result['verdict']}")
保存为 review_depth.py 并运行:
python review_depth.py abc1234
输出会告诉你该提交是否拥有足够的审查时间以达到费根标准下的彻底程度。大多数提交会显示 insufficient。这就是重点。数据几十年来一直在告诉我们这件事,而我们却一直在构建更快的流水线,而非更好的流水线。
这对你的团队意味着什么
Pull Request 审查并非无用。它构建共享上下文、传播知识、捕获明显错误。但数据对其做不到什么非常清楚:它无法捕获大多数缺陷。
如果你的质量策略依赖 PR 审查作为主要过滤手段,那你就是在用筛子过滤。15-30% 的去除率不是审查者的失败,而是流程本身的属性。
超越这一数字的团队并非雇用了更聪明的人。他们改变了流程。增加准备时间、强制节奏限制、将缺陷发现与设计讨论分离、使用检查清单将注意力引导至直觉会遗漏的地方。
你不需要对每个 diff 都做完整的费根审查。你需要知道当前流程实际达到了什么效果,并停止假装它达到了更多。
FAQ
数据对 Pull Request 审查有效性说明了什么?
IBM、AT&T、HP 和 Microsoft 的多项研究一致发现,非正式代码审查能发现代码中存在的 15-30% 的缺陷。这一范围从 1970 年代到基于 GitHub 工作流的现代研究都保持稳定。
为什么 Pull Request 审查发现的缺陷如此之少?
PR 审查为速度和合并批准而优化,而非系统性的缺陷检测。审查者通常每次只花 5-15 分钟,没有准备时间,以研究推荐速度的十倍处理代码,且 diff 格式鼓励浏览而非深入分析。
像费根审查这样的结构化审查好多少?
费根审查一致报告 60-90% 的缺陷去除率,大约比非正式审查好 3-4 倍。差异来自个人准备、强制节奏限制、角色分离、检查清单驱动的专注,以及记录缺陷而非争论解决方案的会议。
提升 PR 审查效果最廉价的方法是什么?
要求评论前进行个人准备、使用针对团队常见缺陷类型定制的检查清单、将风格反馈与缺陷 hunt 分离、对关键文件限制审查速度。即使对准备和专注做微小调整,也能在不增加会议开销的情况下显著改善数字。
如何度量团队的真实缺陷去除率?
追踪审查中发现的缺陷与后续在测试或生产环境中发现的缺陷。早期捕获数与总发现数的比率就是你的去除率。大多数团队不追踪这个,所以他们高估了审查效果。开始记录每个缺陷是在哪里发现的,然后按月计算比率。