Anthropic 一句話否決的 900 行 PR:效能最佳化要付多少維護成本

Anthropic 在 2026 年 9 月 23 日的工程文章〈How we made claude.ai 3x faster in two weeks〉裡提到一份約 900 行的 PR,能讓每次送出訊息少花約 2 ms,代價是多一個 build plugin 要維護。原文引了否決時的那句回覆:「going to gavel that 2ms per send is not worth the complexity of maintaining this build plugin.」意思是每次送出省下的 2 ms,抵不過維護這個 build plugin 的複雜度。原文沒有交代這個 2 ms 怎麼量,也看不出有沒有人質疑數字,只確定這句回覆認為維護成本不值得。

效能改動合併前,得先確認量到的改善,再算留下來要付多少維護成本。這句回覆寫在原文談工作調度的那段,團隊同時開著超過 150 條範圍很窄的 Slack thread(討論串),各自盯一個 benchmark 或一段使用流程找改進,工程師則決定先做哪裡,遇到 thread 互相干擾或收益漸少時,再調整或結束工作。AI 可以短時間交出好幾個改動,原本排不進時程的最佳化也有機會先量過;合併之後,相容性和後續修改仍是團隊的工作。原文沒說那份 PR 是誰寫的,收不收仍要另看維護成本。

3.1 秒到 0.55 秒量的是什麼

同一篇文章說,全新載入 claude.ai 之後,頁面可以開始輸入的 p75 時間從 3.1 秒降到 0.55 秒。這量的是頁面何時能打字,那時訊息還沒送出,模型回答多快不在這個數字裡。其他操作各量各的:Claude Code 開新 session 從 0.8 秒降到 0.3 秒,Cowork 雲端 session 載入從 2.6 秒降到 0.73 秒,幅度都不一樣。

Google 定 Core Web Vitals 門檻時選 75th percentile,要多數造訪達標,同時不讓少數極端值把結果拉歪。p75 改善只表示四分之三的載入落在這個時間內,其餘仍可能更慢;p95、p99 有沒有一起改善,原文沒有給,也不能從 p75 推出來。

先定義量測的起點與終點

開始修改前,先把「網站很慢」縮小到一次操作。畫面出現和輸入框能打字是不同時間點;按下送出後,等待可能發生在請求送出前、後端處理中,或資料回來之後。Anthropic 的做法是先補量測,直到每個基準都能互相比較:每次量測從使用者互動開始,到結果渲染完成為止,並分開 client 和伺服器各花了多少時間。

假設後端少花了 100 ms,前端仍要多等 500 ms,整段操作還是要等,使用者能不能感覺到改善,要看整段耗時怎麼變。反過來,讓輸入框提早能打字,也要確認使用者在那段時間打的字能完整送出,否則量到的數字變好,體驗反而變差。

測試條件跟數字一起留下來

固定資料量和同時請求數,記下測試裝置,冷快取和熱快取的結果分開看,修改前後用同一套可重跑的測試,才知道差異來自程式,還是條件換了。Anthropic 文中一張圖標示了 4 vCPU container、headless Chrome、不限制 CPU、每個數值跑 2 到 3 次,這麼少的次數看得出很大的差異,變異有多寬就估不準。純 JS 的 hot path 則用 Valgrind 搭配 node --predictable 數指令,跟 commit 進 repo 的基準比較,再和實際耗時(wall-clock time)互相對照;原文的說法是「one run, no statistics needed」,意思是在他們固定的測試條件下,指令數可以重現,不必靠多次取樣估計。

上線後還要看實際流量,確認改善出現在原本要解決的操作上。他們比較的是 8 月 13 日與 8 月 27 日的真實使用者 p75,兩天都是星期四,依平台和產品分開看。每次部署後,再依 build 和平台讀實際使用者的資料。比較時要留意,兩天量到的未必是同一群使用者,裝置和地區的比例變化也可能影響 p75,不能把差異都算到程式改動上;兩天之間合併了 3,000 多個變更,單看這兩個數字,也分不出哪一個改動貢獻多少。如果只有測試環境變快,就回頭找環境差異,不能因為程式已經寫完就放寬驗收標準。

上線方式也影響能不能退回:他們把近 200 個 feature flag 分成 kill switch 和逐步開放兩類,高風險改動先給員工,再給 1% 使用者,最後才全部開放,照原文的說法,3,000 多個變更合併後,沒有使用者看得到的事故,也沒有 rollback。

把行為條件寫進 AI 的任務

以快取為例,查詢時間可能立刻下降,但資料更新後多久讀得到新值、權限改了以後舊結果是否還讀得到、快取失效或後端故障時請求怎麼走,都要一起驗證。

這些條件應該在交給 AI 之前寫進任務。沒寫的話,就算它交回更快的 benchmark,團隊還是得自己確認原有行為,補查權限與錯誤回應。任務可以先限定一條查詢路徑,保留修改前的數字,把權限檢查和錯誤回應的測試補齊,再比較修改後的延遲和資源消耗。一次只動一條查詢路徑,改善從哪裡來才分得清,不對就整段退回。Anthropic 每條 thread 只盯一個 benchmark 或一段流程,範圍也是這樣切。

合併前算維護成本,採用後留成回歸檢查

合併前先算維護成本。Knuth 1974 年談過早最佳化時,也提醒讀者別放過關鍵的 3% 機會。原文對這份 PR 的否決理由著重在維護成本,改動越容易產出,這筆合併後的成本越容易漏算。刪掉不必要的查詢,往後少一段工作,也少一層要讀的程式;新增一層快取,還要搭配背景工作同步資料,往後除錯就得多查幾個地方。2 ms 在高頻率的迴圈裡可能很重要,偶爾執行一次的操作收益就小得多,依 Amdahl’s law,局部加速對整體的幫助,受限於那段原本占了多少時間。原文沒給每次送出的總耗時,2 ms 占多少比例,外人算不出來。

Anthropic 把兩條路徑的指令數改善寫成 CI 的 ratchet(只能往下調的門檻):之後任何讓這兩條路徑指令數上升的 PR 都會讓 CI 失敗,每天另有一個 job 在指令數下降時把上限往下調。這種不留容差、只准下修的規則,用在有雜訊的實際耗時上容易誤判:上限追著每天的最小值走,最小值落在雜訊偏低的那一側,之後沒改任何東西的 PR 大多也會超過上限。下面用模擬試一下:程式完全不變,只改雜訊大小。模擬假設雜訊彼此獨立、門檻沒有容差,每日 job 把上限下修到當天所有 PR 量到的最小值:

import random

def false_fail_rate(noise_sd: float, days: int = 60, prs_per_day: int = 10) -> float:
    rng = random.Random(20261104)
    true_cost = 1000.0                      # 程式沒變,真實成本固定
    ceiling = true_cost + rng.gauss(0, noise_sd)
    fails = 0
    for _ in range(days):
        today = [true_cost + rng.gauss(0, noise_sd) for _ in range(prs_per_day)]
        fails += sum(x > ceiling for x in today)
        ceiling = min(ceiling, min(today))  # 每日 job:量到更低就下修上限
    return fails / (days * prs_per_day)

for sd in (0.0, 5.0, 20.0):
    print(f"noise_sd={sd:>4}: {false_fail_rate(sd):.1%} 的 PR 被誤判變慢")
noise_sd= 0.0: 0.0% 的 PR 被誤判變慢
noise_sd= 5.0: 98.7% 的 PR 被誤判變慢
noise_sd=20.0: 98.7% 的 PR 被誤判變慢

在這個模型裡,雜訊只要不是零,誤判率就接近百分之百,通過的只有低於或等於當天門檻的少數讀值,而且跟雜訊大小無關:上限追著最小值走,比的只是排序,雜訊放大幾倍,排序都一樣。這個結果只適用於上面的模型,實際 CI 的誤判率仍取決於取樣方式和門檻規則。實際耗時要當門檻,得像〈Monte Carlo 測試的容差怎麼定:標準誤、誤拒率與位元重現〉那樣先估變異、留容差,基準也不能每次都追著最低值下修,代價是小幅退步會被放過。

下一次讓 AI 改效能,可以先挑一條使用者常抱怨的流程,建立基準並補好行為測試,採用之後就把量測留成回歸檢查。若使用指令數,先確認它能反映實際耗時,後續同一個操作變慢時,團隊才有資料可以追。

補充筆記

  • Anthropic 用一句話否決了每次送出省 2 ms 的 900 行 PR,理由是省下的時間抵不過維護這個 build plugin 的複雜度。
  • 3.1 秒到 0.55 秒是全新載入到頁面可以輸入的 p75,不代表模型回答速度,也看不出 p95、p99 的變化。
  • 先定義一次操作的起點與終點,分開 client 和伺服器的時間,才知道該改哪一段。
  • 兩個日期的 p75 未必來自同一群使用者,流量組成也可能影響數字,要依平台和產品分開看。
  • 交給 AI 之前先寫明行為條件,例如快取的資料新鮮度、權限與故障處理。
  • 2 ms 值不值得要看它占整體多少比例;CI ratchet 若用實際耗時,又不留容差、只追最低值,就容易把未變慢的 PR 判成退步。

延伸閱讀

想法與技術判斷出自 Sheng,和 Claude 一起起草 · 案例數字取自 Anthropic 2026-09-23 原文,模擬在 Python 3.13.18 實跑。