昨天留下的三個洞裡最實際的是最後那個:非決定性讓 CI 很痛。軌跡評估的算式再漂亮,一旦掛進 pull request 的紅綠燈,同一份 commit 連跑三次就會給出三個分數,於是團隊要嘛把門檻降到毫無鑑別力、要嘛把整套 eval 移出 CI 眼不見為淨。今天處理這件事的工程解:非決定性從哪裡來(不是 temperature)、哪一部分能用錄放關在門外、剩下的怎麼用統計量而非精確比對來設門檻、成本上限如何決定哪些留在 CI 哪些退到 nightly,以及一個能立刻抄的 regression gate。

📖 學

temperature=0 不是旋鈕:非決定性的真正來源是 batch 不變性

Thinking Machines Lab 的 Horace He 在 2025-09-10 那篇〈Defeating Nondeterminism in LLM Inference〉把這件事拆到底。實驗設定:Qwen/Qwen3-235B-A22B-Instruct-2507,非思考模式,prompt 是「Tell me about Richard Feynman」,temperature 設 0,各生 1000 token,取樣 1000 次。結果是 80 個不同的 completion,其中最常出現的那一個出現 78 次。分歧點很精確:前 102 個 token 全部一模一樣,第 103 個 token 開始岔開——所有輸出都生出「Feynman was born on May 11, 1918, in」,然後 992 次接「Queens, New York」,8 次接「New York City」。

一般的解釋是「GPU 浮點不可結合 + 併發執行」,這在 LLM 推論上是錯的,值得更正。浮點不可結合確實是根因,但 atomic add 才是併發造成 run-to-run 不決定的機制,而一個 LLM 的 forward pass 裡通常連一個 atomic add 都沒有——batch 維度本身就有足夠平行度,不需要沿 reduction 維度切;同一份權重同一份輸入重複跑 torch.mm,結果 bitwise 相同。

真兇是 kernel 缺乏 batch invariance(批次不變性):矩陣乘法在數學上應該對 batch 中每個元素獨立,實作上不是(文中反例 [2048, 4096] × [4096, 4096],把第一列單獨算與整批算完再取第一列,最大絕對差 1669.25)。而 batch size 取決於伺服器當下的負載,對單一使用者完全不可觀測。把「kernel 對某個性質不變」與「那個性質本身不決定」組合起來,就得到一個不決定的系統。

要修就得讓 RMSNorm、matmul、attention 三個含 reduction 的運算都變成批次不變,其中 attention 最麻煩。代價可量測:批次不變的 matmul 比 cuBLAS 慢約 20%;單卡 Qwen3-8B 跑 1000 條序列,vLLM 預設 26 秒、未優化的決定性模式 55 秒、改良 attention kernel 後 42 秒。

工程結論很硬:如果你打的是別人的 API,bitwise 重現這件事你買不到。seed 與 temperature=0 只消掉取樣的隨機性,消不掉數值上的隨機性,而後者的來源在你控制範圍之外。整套 CI 策略必須建立在「同一份輸入會得到不同輸出」這個前提上,而不是試圖消滅它。

錄放與環境隔離:能關在門外的那一半

VCR 那套「錄一次互動、之後重播」的老招搬到 agent 上分兩種切法:langchain-replay模型的決策,重播時吐出錄好的決策但工具照樣真的執行;agent-vcr傳輸層,把 MCP 的 JSON-RPC 2.0 互動存成 .vcr cassette。凍住模型決策之後你測的是 harness 的接線而不是判斷品質,那是單元測試不是 eval,而且改 prompt 或換模型就會讓 cassette 全數失效。

還有一層 flakiness 跟錄放無關但同樣致命。Anthropic 在 2026-01-09 的〈Demystifying evals for AI agents〉裡寫成硬要求:每個 trial 必須從乾淨環境開始,殘留檔案、快取、資源耗盡這類共享狀態會造成相關失敗(correlated failure),讓多次 trial 不再獨立。自家反例是某些內部 eval 中 Claude 靠讀取前一次 trial 留下的 git history 取得不當優勢——共享狀態不只讓分數變吵,還會讓分數虛高,而虛高難發現得多。

用不變量取代精確比對

斷言的 flakiness 不是一個連續光譜,而是幾個離散的等級,取決於斷言的定義域裡有沒有模型的自由度。

斷言型別具體例子變異進 PR gate?
狀態檢查(outcome / state check)DB 裡存在該筆退款、測試由 fail 轉 pass幾乎零該,最強的一類
schema 符合性合法 JSON、欄位齊全、型別正確幾乎零
結構上界最多五條就不能回六條、回合數 ≤ 10
precedence 硬約束verify_identity 必須在 process_refund 之前
工具集合 precision / recall呼叫過的工具與參考集合比對只當 tracked metric
軌跡 exact match工具與順序完全相同極高,幾乎恆為 0不該,只當煙霧警報
judge 絕對分數rubric 打 0–1高,需與人標校準不該,退 nightly

Anthropic 那篇的建議明確站在表格上半部:優先用 outcome / state 檢查——測試通過、檔案正確改動、退款記錄存在、目標頁面狀態變成要求的樣子。而且他們把話說得更重:有一種常見直覺是去檢查 agent 是否照特定步驟走(例如一串順序正確的工具呼叫),這個做法太僵硬、會產出過度脆弱的測試,因為 agent 經常找到 eval 設計者沒預期到的有效路徑;為了不懲罰創造力,通常比較好的是評分 agent 產出了什麼,而不是它走的路徑。

這跟昨天整篇軌跡評估的立場正面衝突,值得攤開來講。 我的和解方式是分開「診斷」與「把關」。軌跡評估的價值在於定位——per-agent 指標照不到交接縫,只有軌跡層能告訴你 0.59 掉在哪一段;但定位工具不適合當紅綠燈,因為它的分數對合法的路徑變化極度敏感。所以:軌跡指標進 nightly 儀表板與人工排查,不進 PR 的紅綠燈;PR 上只留確定性的硬約束,也就是 precedence 斷言與副作用檢查。它們是同一套資料的兩種排程,不衝突。昨天結尾抱怨的「邊界貴」,一大半其實是排程問題而不是方法問題。

統計門檻:跑幾次、線畫在哪

兩個指標必須先分清楚。pass@k 量 k 次嘗試裡至少一次成功的機率,無偏估計式是 1 − C(n−c, k) / C(n, k)(n 為總嘗試數、c 為成功數)。pass^k 量 k 次嘗試全部成功的機率:(c/n)^k。k=1 時兩者相等;k 變大時 pass@k 往 100% 爬、pass^k 往 0 掉。(Anthropic 那篇把 pass@k 的出處連到一篇 2019 年 NeurIPS 論文、pass^k 連到 τ-bench 的 arXiv:2406.12045;pass@k 更早的譜系我沒親自確認,已標存疑。)

Philipp Schmid 的實算很直白:單次成功率 70% 的訂票改期 agent,取 n=100、c=70、k=3,pass@3 = 1 − C(30,3)/C(100,3) = 1 − 4060/161700 ≈ 97%,pass^3 = 0.7³ = 34.3%——同一個 agent、同一批資料,兩個數字差 62.7 個百分點。

pass^k 的出處 τ-bench(arXiv:2406.12045)給了真實數字:GPT-4o 在 τ-retail 的 pass^1 是 61.2%、在 τ-airline 約 35%,而 retail 的 pass^8 掉到 25% 以下。這裡有一個只要動筆算就會發現的東西:0.612^8 ≈ 1.97%,而實測 pass^8 約 25%,兩者差了十倍以上。差距的來源是 pass^k 是逐題算完再對題目取平均,不是「平均成功率的 k 次方」。也就是說,多數題目是穩定成功或穩定失敗,只有一部分題目在中間搖擺。這個差距本身就是最好用的診斷量:

(平均成功率)^k 遠低於實測 pass^k,代表失敗集中在「難題」,那些題目穩定地做不到,是能力問題,改 prompt 或換模型才有用;實測 pass^k 接近 (平均成功率)^k,代表失敗散在所有題目上,這是不穩定性問題,加 retry、加自我檢查、縮小決策空間才有用。兩種病的處方完全不同,而只看單次成功率的儀表板分不出來。選用原則是 Anthropic 給的:pass@k 用在「一次成功就夠、部署前你檢查得出來」的場景(有單元測試的 coding);pass^k 用在「一次失敗就很貴、agent 又沒辦法自我檢查」的場景(客服)。

接下來是 CI 門檻的核心:雜訊地板。題目成功率為 p、suite 有 n 題各跑一次時,通過率的標準誤約為 sqrt(p(1−p)/n)。代 p=0.9、n=50:SE = sqrt(0.09/50) = sqrt(0.0018) ≈ 0.0424,也就是一個標準誤 4.2 個百分點,95% 區間約 ±8.3pp。在一個 50 題的 suite 上看到 90% 掉到 84%,統計上可能什麼事都沒發生。 這一條直接否決了最常見的門檻寫法「不得低於上次分數減 1pp」——那條線完全落在雜訊裡,你買到的只有假警報。想把區間縮一半就得把題數變四倍:n=200 時 SE ≈ 2.1pp。

Evan Miller 的〈Adding Error Bars to Evals〉(arXiv:2411.00640,2024-11-01,14 頁,stat.AP)把這件事寫成方法論——eval 本質上是實驗,卻長年被排除在實驗設計的文獻之外——具體建議包括每題生成多個答案降低評分隨機性、用 paired difference(配對差異) 比較兩個模型、用檢定力分析反推需要幾題、題目有分群結構時改用 cluster-adjusted standard error。

CI 不該問「這次分數是否 ≥ 0.85」,該問「這次相對 baseline 的逐題配對差異,其信賴區間是否包含 0」。 配對消掉了「題目難度」這個共同變異來源,同樣的題數能偵測到小得多的效果——這就是為什麼三家工具都把 baseline 對齊做成一級功能。

三套主流工具都把重複執行做成一級參數,可以直接抄。promptfoo 用 repeat: Nrepeat-min-pass: M(每題 N 次至少過 M 次,須 M ≤ N),並有 --pass-rate-threshold 對整個 suite 設最低通過率,CI 端接 promptfoo-action@v1。LangSmith 用 num_repetitions=N(target 與所有 evaluator 都重跑),並以 @pytest.mark.langsmith 把 pytest test case 同步成 dataset example,PR 或 nightly 都能跑。Braintrust 用 trial_count / trialCount,把同 input 的 case 自動分桶算摘要統計,同時給你 aggregate 與 variance;選定 baseline 之後對齊 test case、逐列標 delta,改善綠、退步紅。

repeat: 3repeat-min-pass: 2 是最務實的預設值,但代價要算清楚:單題真實成功率 0.9 時,三次至少兩次過的機率是 0.9³ + 3×0.9²×0.1 = 0.972,仍有 2.8% 機率被誤判為失敗,50 題的 suite 平均會有 1.4 題無辜變紅。min-pass 解決單題 flakiness,不解決 suite 層的假警報。

成本預算決定 CI 與 nightly 的切線

慕尼黑工業大學與 CQSE 的〈Cost of Flaky Tests in Continuous Integration〉在一個約 30 名開發者、約 100 萬行程式碼的商業專案上分析五年開發史,得到一組值得記住的數字:處理 flaky test 佔掉至少 2.5% 的生產性開發時間(調查可疑失敗 1.1%、修復 1.3%、開發監控工具 0.1%)。最反直覺的一條是:自動重跑一個測試的成本是 0.02 美分(5.67,差約 2.8 萬倍,所以該專案把力氣從「調查與修復」轉向「自動重跑」。文中另引 Microsoft 與 Google 的報告:4%–16% 的測試涉及 flakiness、1.5% 的 CI 測試執行是 flaky 的。

這個結論不能直接搬到 agent eval 上,而不能搬的原因剛好就是切線所在:傳統測試的重跑成本趨近零,agent eval 的重跑成本是 token 費用乘以 k。算一次帳(以下單價與 token 量都是我設的假設,不是任何廠商的實際定價):一條任務平均 60K 輸入 token(含工具結果回填)與 8K 輸出 token,模型每百萬輸入 15,單次 trial 成本 = 60 × 0.003 + 8 × 0.015 = $0.30。於是:

  • 每個 PR 跑 50 題 × 3 trial = 150 次 = **900/日、約 $2.7 萬/月。
  • 每個 PR 跑 12 題 smoke × 1 trial = **72/日、約 $2160/月。

差了 12.5 倍,多數團隊只容得下後者。所以分層是被成本逼出來的,不是品味問題:PR 上跑零成本的確定性斷言加一個 10–15 題的 smoke(單 trial,只看硬失敗);nightly 跑完整 suite × 5 trial(50 × 5 × 75/日)並算 pass^k 與配對差異;LLM-as-judge 與軌跡指標放週報,配人工讀 transcript。 Anthropic 的定位也是這樣,並用瑞士乳酪模型(Swiss cheese model)比喻:沒有任何單層能抓到全部問題,穿過一層的失誤由下一層攔住。

regression gate 怎麼設計,以及紅燈的第一假設

Anthropic 對 eval 分兩類的區分是這一節的地基。capability eval(能力 eval) 問「這個 agent 能做好什麼」,應該從低通過率起跑,給團隊一座山爬。regression eval(回歸 eval) 問「以前會做的它還會不會」,通過率應該接近 100%,掉了就代表壞了。飽和的 capability eval 可以「畢業」成 regression suite——曾經量「我們做不做得到」的題目,之後量「我們還能不能穩定做到」。

推論很直接:紅綠燈只該掛在 regression suite 上。 很多團隊抱怨 agent eval 在 CI 裡無法使用,實際情況是他們把一個還停在 60% 的 capability eval 拿來當 gate——真實通過率 0.6 的門檻在統計上永遠會閃,這不是非決定性的錯。

紅燈的第一假設應該是「我的 eval 壞了」,不是「模型退步了」。 Anthropic 的判準是用前沿模型跑、多次 trial 都得到 0% 通過率(0% pass@100)時,最常見的原因是任務壞了而不是 agent 無能。兩個實例講得很痛:

Opus 4.5 在 CORE-Bench 上一開始只有 42%,一位 Anthropic 研究員追查後找出多個問題——僵硬的評分把答 96.12、期望 96.124991… 判為錯,任務規格有歧義,還有一些隨機性任務根本無法精確重現。修完 bug 並換上限制較少的 scaffold 之後分數跳到 95%,53 個百分點全部來自 eval 本身。另一個是 METR 的 time horizon benchmark:數個題目要求 agent「最佳化到某個分數門檻」,評分邏輯卻要求「超過」該門檻,結果照指示做的模型被罰分。原則是 grader 檢查的每一件事都必須在任務描述裡講清楚

把上面收成一個可以抄的 gate 骨架(以下 YAML 是我寫的示意結構,不對應任何工具的實際 schema):

pr_gate:                        # 確定性、零 LLM 成本、違反即紅
  deterministic_assertions:
    - state_check:     {refunds: {status: processed}}
    - schema:          {output: json, required: [order_id, amount, reason]}
    - precedence:      ["verify_identity BEFORE process_refund",
                        "search_policy BEFORE issue_credit"]
    - static_analysis: [ruff, mypy, bandit]
    - bounds:          {max_turns: 10, max_toolcalls: 12}
  smoke_suite:
    tasks: 12                   # 從 regression suite 抽最具代表性的
    repeat: 1
    fail_on: hard_failure_only  # 只看崩潰、schema 違反、precedence 違反
  tracked_metrics: [n_turns, n_toolcalls, n_total_tokens, cost_usd, ttft_ms]
  cost_ceiling_usd: 5
 
nightly:                        # 統計性、有 LLM 成本、看區間不看單點
  regression_suite:
    tasks: 50
    repeat: 5
    per_task_rule: {min_pass: 4}
    suite_rule: {paired_vs_baseline: {ci95_excludes_zero: true,
                                      direction: worse}}
    report: [pass_1, pass_k, mean_rate_pow_k, gap]   # gap 分難題 vs 不穩定
  capability_suite: {tasks: 30, repeat: 3, gate: false}   # 只上儀表板

骨架裡最容易被忽略的是 tracked_metrics:成本與 token 用量幾乎沒有 flakiness,卻最早反映 harness 退化——prompt 改壞了通常是回合數先漲、正確率後掉。

judge 若非得進 CI,Anthropic 的兩個限制值得照抄:給模型一條退路(資訊不足時回「Unknown」而不是硬猜),以及每個維度用獨立的 judge 打分。

🧠 記

  • temperature=0 只消掉取樣隨機性,消不掉數值隨機性;真正的來源是 kernel 缺乏 batch invariance,而 batch size 由伺服器負載決定——打別人的 API 就買不到 bitwise 重現。實測:1000 次 temperature=0 取樣得 80 個不同輸出,第 103 個 token 開始分歧。
  • pass^k 是逐題算完再平均,不是「平均成功率的 k 次方」;τ-bench 上 GPT-4o retail pass^1 = 61.2%、pass^8 ≈ 25%,而 0.612^8 ≈ 1.97%——兩者的差距就是「難題」與「不穩定的題」的分界,處方完全不同。
  • 雜訊地板決定門檻:p=0.9、n=50 時 SE ≈ 4.2pp、95% 區間 ±8.3pp,「不得低於上次減 1pp」買到的只有假警報,該改問配對差異的區間是否包含 0。
  • 分層是成本逼出來的:單次 trial 若 45、跑 12 題 smoke 是 $3.6,差 12.5 倍。紅綠燈只該掛在 regression suite 上。
  • 紅燈的第一假設是「eval 壞了」:Opus 4.5 在 CORE-Bench 上從 42% 修到 95%,53pp 全來自僵硬評分、規格歧義與無法重現的隨機任務。

✍️ 實踐

今天的動作:量出你自己的雜訊地板,然後照它重畫門檻。 這件事不需要新工具,只需要一份現有的 eval suite 與 25 分鐘。

  1. 抽樣並各跑 5 次(8 分鐘,含等待)。從 eval suite 挑 10 題,涵蓋一題最簡單的、一題最常壞的、一題有副作用的。用 promptfoo repeat: 5、LangSmith num_repetitions=5 或 Braintrust trial_count=5;沒用框架就寫 for loop。每題記下 5 個 0/1 結果,同時記下這 50 次的總 token 數與總費用

  2. 算雜訊地板(5 分鐘)。逐題算 p_i = 成功次數 / 5,再算 10 題的平均 pSE = sqrt(p(1−p)/10),寫下 2 × SE——這就是「小於它的變化都不算證據」的那條線。

  3. 分三堆(5 分鐘)p_i = 1.0 → regression 候選,可以 gate。0 < p_i < 1 → 這是你的 flakiness 來源,數一數佔幾成。p_i = 0先當壞題目查,讀 transcript 確認是 agent 真做不到,還是 grader 太僵硬、規格有歧義、環境不給它成功的機會。

  4. 算兩個 pass^k 比較(4 分鐘)。逐題算 p_i³ 取平均得逐題版 pass^3,再算 ,相除:比值大代表失敗集中在難題(改能力),接近 1 代表失敗散在所有題目(改穩定性)。

  5. 改 config(3 分鐘)。第一堆 repeat: 3 / min_pass: 3 留 PR,第二堆 repeat: 5 / min_pass: 3 搬 nightly,第三堆標 quarantine;suite 門檻改成「配對跌幅需大於 2 × SE」。

自我檢查(全部要能打勾)

  • 我寫得出這個 suite 的 2 × SE 是幾個百分點,而且它比我原本設的門檻大。
  • 我知道 0 < p_i < 1 的題目佔幾成——超過三成,問題就是穩定性不是能力;而且每一題 p_i = 0 的我都讀過 transcript,能說出是模型的錯還是 eval 的錯。
  • 我算得出「跑一次完整 gate」的單價,乘上每日 PR 數之後付得起;PR gate 裡沒有 judge 絕對分數、沒有 capability eval,至少有兩條 precedence 斷言。

🔗 延伸學習

💬 問 AI

我要把一套非決定性的 agent eval 放進 CI,但不想製造 flaky 地獄。
請用我的實際數字算,不要給通用建議;需要我補資料才能算的,直接列出來要我補。
 
【我的情況】
- agent 型態;自架推論還是外部 API(若是 API,寫服務商與模型):____
- eval suite 題數,其中幾題是 regression、幾題是 capability:____
- 每題的單次成功率?有就貼分佈,沒有就直說沒有:____
- 一條任務平均輸入/輸出 token、回合數、工具呼叫數;模型單價:____
- 每天幾個 PR、CI 一次能忍受跑多久、每月 eval 預算上限:____
- 目前 CI 裡的 eval 門檻怎麼寫的(原文貼出來):____
- 最近一次 CI 紅燈,最後查出是模型退步、eval 壞了、還是雜訊:____
- 用的框架/平台;工具清單並標出哪些有副作用;有無真實外部服務被打進 eval:____
 
【請依序做】
1. 先算我的雜訊地板。用 SE = sqrt(p(1−p)/n) 代入我的題數與平均成功率,給出 1×SE
   與 95% 區間,直接判斷我現在的門檻是否落在雜訊裡。若題數不足以偵測我在意的
   效果量,反推我需要幾題,並說明「加題」與「加 trial」哪一個更划算。
 
2. 把我的斷言分兩堆:可進 PR gate 的確定性斷言(狀態檢查/schema/結構上界/
   precedence/靜態分析),與只能進 nightly 的統計性指標;逐條說明我現有的門檻該
   搬去哪一堆、判定條件要改寫成什麼。另外用我的工具清單推出 precedence 硬約束,
   寫成 X BEFORE Y 並標明違反時的後果(賠錢/資料錯/不可逆),給可貼進 config
   的形式。
 
3. 用我的 token 量與單價算三種排程的月成本(每 PR 跑完整 suite × 3 trial/每 PR 跑
   N 題 smoke × 1 trial 加 nightly 完整 suite × 5 trial/只有 nightly),各自給出月
   費用與「能偵測到多小的退步」,推薦一個並明說理由是成本還是靈敏度。
 
4. 算「難題 vs 不穩定的題」的診斷比值(逐題 p_i^k 的平均除以 (平均 p)^k),告訴我
   失敗主要是能力還是穩定性問題,以及下一步該改 prompt/換模型/加 retry/縮小
   決策空間中的哪一個。
 
5. 針對我最近那次紅燈,用「先假設 eval 壞了」的順序給排查清單(grader 是否僵硬/
   規格是否有歧義/harness 是否限制了模型/環境是否不給它成功的機會),每項標明
   怎麼在 10 分鐘內驗證。然後產出兩份可貼上的 config(PR gate 與 nightly),標明
   每個門檻數字是怎麼從第 1 步的雜訊地板推出來的;我沒給資料的地方留 TODO 並寫
   清楚我要量什麼才能填。
 
【限制】
- 不要建議我「把 temperature 設 0」或「固定 seed」來解決非決定性,除非我寫的是
  自架推論且你能說明我要改哪些 kernel。
- 不要把 judge 的絕對分數放進 PR gate,也不要把 capability eval 當紅綠燈。
- 不要給我任何沒有算式支撐的門檻數字;預算撐不起就直接說,並給降級版本。
- 明確告訴我:以我的規模,有哪一項其實不值得做。