沒人願意談論的數字

非正式程式碼審查能夠發現被審程式碼中 15% 到 30% 的缺陷。這不是觀點,而是在四十年、多家公司、數十項研究中反覆驗證的結論。

麥可·費根於 1976 年在 IBM 記錄了這一資料。1987 年 AT&T 貝爾實驗室的研究發現了 20%。1996 年惠普的研究發現了 25%。2013 年微軟研究院關於現代程式碼審查的論文也發現了大致相同的範圍。工具從打孔卡演進到 GitHub,但人的表現曲線並未移動。

如果你的團隊認為 Pull Request 審查是品質的最後防線,那麼資料告訴你:你們大約只發現了四分之一的缺陷,其餘四分之三已經發布上線。

數字的來源

費根最初的方法論簡單而殘酷。他向程式碼中注入已知缺陷,執行審查流程,統計審查者發現了多少。然後將其與團隊最終透過測試、生產事故和客戶報告發現的總缺陷數進行比較。審查中發現數與總缺陷數的比率就是去除率(removal rate)。

關鍵洞察在於分母很重要。一場發現十個缺陷的審查聽起來不錯,直到你知道檔案裡其實有五十個。費根測量了完整的分母,而大多數團隊今天並不這麼做。

後續研究採用了類似的設計。研究人員植入缺陷、比較審查類型,或從生產環境反向追蹤缺陷,看它們本可以在哪裡被擷取。結果高度集中:

審查類型缺陷去除率關鍵研究
無審查0%baseline
非正式 / 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 的會議文化就能獲得大部分收益。你只需要借用在資料上真正有效的流程特徵,拋棄無效的部分。

資料認為以下因素重要:

  1. 準備時間。 要求審查者在發表評論前先花時間研讀程式碼。哪怕十分鐘專注閱讀也勝過匆匆掃過。

  2. 節奏限制。 對關鍵檔案強制最大審查速度。如果審查者在五分鐘內核准了 500 行的變更,那是資料,不是盡職。

  3. 僅聚焦缺陷。 將風格和架構回饋與缺陷 hunt 分離。前者用自動格式化工具處理,後者留給人類注意力。

  4. 檢查清單。 費根發現,使用檢查清單的審查者能發現憑直覺審查者遺漏的缺陷。檢查清單的存在正是因為專業經驗會造成盲點。

以下是一個輕量級指令碼,用於從 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 分離、對關鍵檔案限制審查速度。即使對準備和專注做微小調整,也能在不增加會議開銷的情況下顯著改善數字。

如何度量團隊的真實缺陷去除率?

追蹤審查中發現的缺陷與後續在測試或生產環境中發現的缺陷。早期擷取數與總發現數的比率就是你的去除率。大多數團隊不追蹤這個,所以他們高估了審查效果。開始記錄每個缺陷是在哪裡發現的,然後按月計算比率。