一個壞掉的量測
不會像壞掉的服務那樣叫
它會給你一個數字
而且那個數字通常很好看
前面把整套東西都做完了:提案有狀態、演習分得開、五道門各自問自己的問題、案例記憶收得住結論、迴圈跑得完一次。今天想拿來講這系列裡最貴的那類錯誤:量錯。
不是程式當掉,不是查詢報錯。是每一步都成功、每一個數字都算得出來,而那個數字回答的不是我以為的那個問題。這種錯誤在這三十幾天裡出現過四次,四次都讓我根據它寫下了結論,而最後一次是那支專門用來查前面三次的工具自己出的錯。
程式碼在範例 repo OTel_AIOps_Agent 的 ironman-2026/day30/。
要驗證「這隻 agent 的成績穩不穩」,得先排除一個嫌疑:資料本身會不會隨時間改變。於是設計了一個只動一個變數的實驗,把同一份烤好的資料用四個不同的時間點開起來,看判決差多少。
四次全部失敗,錯誤訊息是:
=== scenario time 2026-08-19T04:11:00Z ===
booting demo-services-o11y-stack:latest…
stack did not produce queryable incident data in time
第一反應是環境壞了。重編譯 image、重開叢集,那套流程在腦中已經排好隊。
但 container 是活的,日誌寫著 === Environment Ready ===,資料生成器也很得意地印了幾萬筆。資料在裡面,只是問不到。
原因是那個 readiness 檢查用的是 instant query,而 Prometheus 的 instant query 只看得到五分鐘回看窗以內的樣本。烤好的資料結束在 scenario time 那一刻,所以拿牆上的時鐘去問,除非 scenario time 剛好就是現在,否則永遠回一個空陣列。
這道前置檢查的正確性,依賴一個從來沒被寫下來的假設:scenario time 約等於現在。而那正是這個實驗要動的那個變數。
以前不會踩到,是因為以前每一次都沒帶時間參數,預設值就是現在。
這個形狀在值班的時候很常見:某條 readiness probe、某個「資料有沒有進來」的看板,平常永遠是綠的,因為它跟被監測的東西共用同一個前提。等到那個前提第一次改變(換了時區、換了保留期、多了一個補跑歷史資料的排程),它會在那一天說出一句聽起來像基礎設施壞掉的話,把排查的人推向完全錯誤的方向。
修好之後,同一顆 container:
now : False
clock: True
而修好之後跑出來的結果,比 bug 本身更值得記。四個時鐘的判決 spread 是 0,時間不是變因。但順手重跑一輪 fixture 的時候,總分沒變、組成整個換了一輪:
▼ order-service-discover-before-query: 100% → 0%
程式碼只動了那個 readiness 查詢,那一題完全沒被碰到。同一份烤好的資料、同一段程式,兩次執行之間從全對變成全錯。
這件事後來變成一道門的設計理由:機械評分的成績跟真實事故的標註要分開計,因為前者的波動大到不足以替後者背書。
第二個量錯藏在校準池裡,而且它是還沒發生的時候被抓到的,這在這系列裡算少見。
前面提過排練要跟事故分開,那時候處理的是執行帳跟告警冷卻。校準曲線這一層漏掉了。
漏掉的後果要攤開來看才嚇人。當時待標註的池子裡有八筆信心 0.95 的調查,內容幾乎一模一樣,全都是同一場排練的重播,而且全都答對了同一個我自己注入的故障。那八筆一次標完,決策區的樣本數跟正確率會同時往上跳,五道門裡的第一道會直接翻綠。
那不是八份證據,那是一份證據記了八次。
修法跟執行帳同一個道理:校準表加一欄 drill,寫在告警的 label 被讀進來的那一刻,因為那是最後一個還知道「這是排練」的地方。曲線跟門檻樓地板兩邊都排除排練,因為算在不同批列上的樓地板不是樓地板。
歷史列預設是 0,等於所有舊排練都偽裝成真實事故,所以還要回填。回填這件事有個容易走歪的地方:早期 run_id 就是告警指紋,一個 id 蓋住同一個告警的所有調查,只要其中一筆是排練,整批都會被誤標。所以規則寫成「同一個 id 底下的每一筆都說是排練才標記」,其餘列為無法判定、原樣留著。在這裡用猜的,等於自己製造那道門要讀的證據。
第三個最難承認,因為我已經根據它寫過一篇結論了。
那天把五道門全部讀出來,最刺眼的是決策區的正確率 0.2:信心 0.8 以上的判斷,五題對一題。當時寫下的結論是「信心跟正確率反向,它最有把握的時候最容易錯」,而且很自然地想到下一步是去改模型。
隔天把那六列打開來看,故事完全不是那樣:
run_id=2e5b4954f3a93971 rows=1
conf=0.95 correct payment-service ... v2.5.0 validator
run_id=1539e7b9b01d65bb rows=4
conf=0.95 WRONG payment-service v2.5.0 ... new validation rule
note: 根因是 DB 連線問題,不是 validator regression
conf=0.85 WRONG ... database connection pool exhaustion
conf=0.8 WRONG ... database connection pool exhaustion
conf=0.8 WRONG ... database connection issues
run_id=2b0a13c99c8f670a-... rows=1
conf=0.9 correct order-service ... session store
六列,三個 id,中間那個 id 一個人扛了四列。那是一條連續的鏈:agent 說出了正確答案、被人標成錯的、更正註記指向另一個原因,然後接下來三次它照著那句更正改口,三次都被標成錯的。
而同一個結論在十二分鐘前的另一列是被標對的。同一個事故被標成兩種互斥的答案。
所以那個 0.2 的組成是:一次不正確的人工更正,加上 agent 三次順從,再被逐次記為錯誤。 決策區六列有四列來自這一次失誤。
撤回那四筆之後,數字變醜了,而且方向是反的:
labeled 8 → 4 決策區 6 列 → 2 列(低於門檻)
overconfidence +0.3929 → -0.1875
過度自信從正的變成負的。也就是說前一天那句「它最有把握的時候最容易錯」,連方向都是錯的。翻案的理由不是換了模型或演算法,是把不該算數的證據拿掉。
撤回的作法也有講究:只清掉判決,保留那一列、它的信心、摘要,以及那句錯誤的更正註記。這個誤判本身要讀得到。
這一段是後來補的,而它有點難堪,因為出事的正是我拿來查前面三次的那支工具。
前面那些數字要重看的時候跑的是 probe_autonomy_gates.py,一支唯讀的探針,存在的理由就是「回頭問那個數字是怎麼算出來的」。有一天我用 grader 在幾筆有已知答案的舊調查上補了標註,跑完探針,它說被標註的有十一筆。同一時間治理平面自己回報的是五筆。
同一個問題,兩個地方,兩個答案。而差別是那六筆排練。
翻開探針那行:
prod = compute_calibration(load_records(), modes=modes)
它讀的是全部紀錄。而真正那道門讀的是 production_records(load_records()),也就是把排練濾掉之後的那份。上面第二次講的「八份一模一樣的證據」,解法就是這個濾網,我當時把它加在治理那邊,卻沒有加在這支探針裡。
於是探針那段輸出的標題寫著 production curve (labels from people, on live incidents),底下印的卻是一份混著排練的數字。它沒有壞掉,它只是在回答另一個問題,而標題說它在回答這一個。
修法是兩行:換成 production_records(),另外那個「人/grader 標了幾筆」的計數也補上 exclude_drills=True。改完之後兩邊都是五。
最難堪的地方在於那個方向。探針報的是十一,門報的是五,而看起來比較好的那個數字是錯的——如果我那天只看探針就收工,我會以為離門檻剩下一半的路,實際上還有四分之三。
把四次放在一起看,它們是同一種東西:
| 表面 | 實際在量的 | |
|---|---|---|
| 時鐘 | 環境沒準備好 | 一個只在「時間等於現在」時成立的假設 |
| 排練 | 這隻 agent 很準 | 同一場演習被重播了八次 |
| 標註 | 它最有把握時最容易錯 | 一次錯的人工更正被記了四筆 |
| 探針 | 離門檻只剩一半的路 | 一份混進了排練的清單 |
四個都是成功的量測。沒有例外、沒有紅字,每一個都給出了一個可以放進報表的數字。
而四個的解法都不是修程式,是回頭問那個數字是誰產生的、在什麼前提下算出來的。這也是為什麼前面每一道門都要求證據來源可回放:不是為了稽核那麼嚴肅的理由,是為了有一天發現量錯了的時候,能把錯的那部分挑出來。
最後講一件跟量測無關、但性質一樣的事。
這套系統的四個目標裡,有三個卡在只有人能做的工作上:標註一次判斷、決定一個提案、寫下一個事故的原因。做到最後我才發現,這三件事在畫面上完全沒有位置。提案送出去之後躺在資料庫裡,十筆自己過期;待標註的調查沒有清單;案例的原因欄位連填的地方都沒有。
於是最後補的不是模型能力,是三個入口:一頁把「等著人做的事」列出來(待標註的調查、待決的提案、離自主還差多少),一頁把案例記憶攤開來看,還有那個一直缺的原因輸入框。
離自主還差多少那一塊,刻意畫成「現值對門檻」的表,而不是一顆紅燈:
Labeled runs 4 >= 20
Labeled by a human or grader 4 >= 20
Runs in the decision band 2 >= 3
Accuracy in that band 1.0 >= 0.7
一個沒有人看得到的標準,就是一個沒有人會去追的標準。 這句話跟前面三個量錯是同一件事的兩面:量測要能被讀,才有人會根據它做事。
總結來說,這一路做出來的東西,最後留下的不是那條會自己執行的迴圈,是一整套用來懷疑自己數字的機制:證據要標明來源、排練要跟事故分開記、判決可以被撤回、門檻要露出現值、每一次結論都要能回放到當初的那次調查。
而這套機制驗收的方式很不浪漫:它讓我在最後幾天推翻了自己前面寫下的一個結論,而且是往「我原本以為的更差」翻成「其實沒那麼差」的方向。一套只會讓數字變好看的系統做不到這件事。
自主權到現在一次都沒開。門紅著三道,紅的理由分別是「沒有人標」「沒有人問過這份知識屬不屬於這裡」「最近的證據兩天前就過期了」。這三句話沒有一句是「模型不夠強」。