上篇談了 incident response 的六段流程(Detect→Triage→Mitigate→Resolve→Recover→Review)、AI 系統多出來的版本座標與 evaluation 覆蓋問題,並示範了一份能在壓力下使用的 runbook 該長什麼樣子。這篇接著處理事故發生時該怎麼記時間線、怎麼用可重跑的程式碼把版本追蹤變成系統習慣,以及 postmortem 寫完之後,這個行業真正在做的復盤,長什麼樣子。
結論先說:postmortem 有沒有用,不是看寫得夠不夠詳細,而是看 Action Item 有沒有真的被兌現——這也是本篇要反覆回來檢驗的一條主線。
傳統 Web 服務的事故時間線,常從 error rate 開始寫。
AI workflow 需要多幾條軸線,否則「200 但錯答」會在時間線裡消失。
以下是假想演練,不是 production 事件紀錄。
09:00 prompt-v42 完成 canary;技術 smoke test 通過。
09:08 answer API 的 HTTP 2xx 維持正常;P95 latency 微幅上升。
09:12 feedback 中「引用不相關」標記增加;quality evaluator 開始失敗。
09:16 on-call 以 trace 比對發現 retrieval span 正常,prompt 要求的引用格式被改壞。
09:19 incident commander 暫停 canary,將 10% 流量退回 prompt-v41。
09:25 新進請求的 citation-valid rate 回到基線;舊 session 的快取結果仍存在。
09:34 清楚標記受影響期間與版本;開始收集 regression samples。
09:48 citation-valid rate 連續 15 分鐘達恢復條件;宣告服務恢復。
隔日 討論為何 release gate 沒有涵蓋這種格式漂移。
這條時間線同時記錄了 technical、workflow 與 quality 狀態。
它避免把「API 沒壞」誤寫成「服務沒壞」。
把這條時間線跟傳統系統的事故時間線並排看,差異會更明顯:
傳統系統的時間線 AI workflow 的時間線
───────────────── ─────────────────
09:00 error rate 開始上升 09:00 prompt-v42 完成 canary
09:05 on-call 收到告警 09:08 HTTP 2xx 正常,P95 微升
09:10 確認是資料庫連線問題 09:12 quality evaluator 開始失敗
09:20 重啟連線池 09:16 發現 citation format 劣化
09:25 error rate 回穩,宣告恢復 09:19 暫停 canary
09:25 新流量恢復,舊快取仍殘留
09:48 連續 15 分鐘達恢復門檻
傳統時間線的每一步都圍繞著同一個訊號——error rate;AI workflow 的時間線裡,最關鍵的那一步(09:12 quality evaluator 開始失敗)發生在 HTTP 層完全正常之後。如果這次事故的記錄者只盯著 HTTP 狀態碼,這條時間線會直接跳過 09:08 到 09:16 之間真正發生的事,變成一份「看起來什麼都沒發生,卻莫名其妙暫停了 canary」的迷惑紀錄——這正是 ①、③ 段反覆強調「API 200 不等於服務沒壞」在時間線紀錄上的具體落地方式。
事故期間的 chat 訊息很快會被淹沒。
每個關鍵判斷應該留下可回看的連結或識別字。
| 時間 | 判斷 | 證據 | 決策者 | 下一步 |
| --- | --- | --- | --- | --- |
| 09:12 | 不是 HTTP availability 問題 | dashboard panel URL、quality evaluator query | IC | 查 release diff |
| 09:16 | retrieval 正常,citation format 劣化 | trace_id、prompt-v42 diff | app owner | 暫停 canary |
| 09:25 | 新流量已恢復,快取仍含舊回答 | cache key sample、SLI query | IC | 清理與公告 |
請避免把使用者 prompt、內部文件片段、完整模型輸出直接貼在公開 incident channel。
記 trace_id、受控儀表板連結與脫敏摘要即可。
實務上判斷「這句話能不能貼進 incident channel」,可以用一個簡單的自我檢查:如果這句話裡出現了使用者可能輸入的完整原文、或模型可能輸出的完整原文,就先假設它不能貼;換成描述加連結——「使用者詢問了關於帳戶設定的問題,詳見 trace_id: abc123」,而不是把那句提問原封不動貼上去。這條界線不需要每次都靠人臨場判斷,多數 incident 工具都能設定訊息長度或格式的簡單提示,在貼上一大段疑似原始輸出的文字時跳出提醒——這種輕量級的提醒,比事後才發現敏感內容已經在 channel 裡被幾十個人看過要便宜得多。
需要原始內容的人,應走既有的存取控制與稽核流程。
這個表格式的紀錄方式,解決的是一個具體又常見的問題:incident 現場的 chat 訊息量往往極大,一場 45 分鐘的事故可能累積上百則訊息,裡面混雜著閒聊、猜測、確認過又被推翻的假設、還有真正重要的判斷。三天後回頭寫 postmortem 時,沒有人有時間重新爬梳這上百則訊息去拼湊「到底是誰在什麼時候基於什麼證據做了什麼決定」。表格式的關鍵判斷紀錄,本質上是在事故發生的當下就先做一次篩選——只留下「判斷、證據、決策者、下一步」四個欄位,把敘事的重建工作分攤到事故進行的過程中,而不是全部留到復盤時才臨時拼湊。
至於「保留連結而非保留內容本身」這條規則,背後的考量不只是隱私。它同時也是一種「證據新鮮度」的設計——一個指向 dashboard panel 或 trace_id 的連結,永遠指向當下最新、最完整的資料;而貼進 chat 的一段截圖或文字貼上,只是那個時間點的一張快照,容易過時、容易被裁切失真,也容易在事後被誤讀成「完整的原始證據」。
這裡有一個常見誤解值得單獨拆開講:很多團隊以為「事故都在 incident channel 裡討論過,訊息都還在,等於證據已經留住了」。這句話忽略了 chat 平台的兩個結構性弱點。第一,搜尋成本會隨時間指數上升——三天後想找「那則說 retrieval span 正常的訊息」,得在上百則夾雜閒聊與已被推翻的假設裡人工翻找,這件事本身就是一種摩擦,摩擦足夠大,postmortem 撰寫者往往會選擇跳過查證、直接憑印象寫。第二,chat 訊息沒有結構,一則「應該是 prompt 的問題吧」跟一則「trace 顯示 citation format 壞了,附連結」在 chat 介面裡長得一模一樣,都只是一則普通訊息,沒有欄位區分「這是猜測」還是「這是有證據支撐的判斷」。上面那張表格式紀錄,本質上就是在事故進行的當下,用四個固定欄位(時間、判斷、證據、決策者)把這兩個弱點都繞過去——它不取代 chat,但它是 chat 之外,唯一保證三天後還能被快速讀懂的一份紀錄。
一次調查至少要能回答下列版本問題:
程式碼 revision 是什麼?
prompt label 與 prompt hash 是什麼?
model provider、model name、route 與 fallback 是什麼?
embedding、chunking、index、reranker 的版本是什麼?
tool schema 與 policy 的版本是什麼?
evaluator、golden dataset 與 threshold 的版本是什麼?
其中任何一個留白,調查就會變成「大家記得昨天有改什麼嗎」。
這不是記憶力測驗。
把「版本」講得再具體一點,一份能直接回答上面六個問題的 diff,通常長得像這樣——這才是調查時真正需要比對的東西,而不是一句「prompt 改了」:
--- prompt-v41.txt
+++ prompt-v42.txt
@@ 輸出格式指示 @@
- 回答結尾請附上參考來源。
+ 回答結尾請以自然語言總結參考來源,避免使用制式格式。
這段 diff 看起來像是一次無害的文字潤飾——把「附上參考來源」改成語氣更自然的「總結參考來源」,甚至還加了一句「避免使用制式格式」,聽起來像是在改善使用者體驗。但正是這句「避免使用制式格式」,讓模型不再穩定輸出 [source:...] 這種可被 parser 辨識的固定格式,而 citation parser 卻仍然假設輸出會符合舊格式。這個例子想說明的是:版本 diff 上看起來「越沒有風險」的變更,越需要被記錄與追蹤——真正危險的往往不是大幅改寫的版本,而是這種讀起來像優化、實際上悄悄打破了下游某個隱性假設的小改動。如果沒有把這份 diff 存下來、對應到 prompt_version 欄位,調查時就只能靠翻聊天記錄猜「上次改了什麼」。
值得注意的是,這六個版本問題彼此的變動頻率天差地遠。程式碼 revision 可能一週動一次;prompt 可能一天動好幾次;retrieval index 可能每小時重建;evaluator 的 threshold 可能幾個月才調整一次。正因為這六條軸線的節奏完全不同步,「故障發生的那一刻,這六個座標各自是什麼」不會自動被任何單一系統記下來——除非有人刻意設計一個機制,在每次請求發生時,把當下所有版本座標一起打包記錄下來。這正是下面 ⑩ 段 IncidentContext 這個資料結構想解決的問題:與其事後拼湊六條各自變動的軸線,不如讓每個請求誕生的那一刻,就自帶一份完整的座標快照。
這六個版本座標各自屬於不同團隊維護,事故發生時「誰該提供哪一段紀錄」如果沒有先講清楚,調查現場常常變成大家互相等對方先開口。一個實務上能提前建立的簡單對照,是把六個座標各自的擁有者與查詢入口列成一張表,平常就放在 runbook 旁邊:
座標 擁有團隊 查詢入口
──────── ──────── ────────
code revision app team git log / deploy 記錄
prompt version app team prompt 版控 repo
model/route platform team model gateway 設定
retrieval index data team index 建置管線 log
tool schema platform team tool registry
evaluator/threshold eval team evaluation config repo
這張表不是要取代人的判斷,而是把「事故當下該去問誰」這件事,變成不需要臨場現想的資訊——凌晨三點被叫醒的值班者,不必先花時間搞清楚「retrieval index 出問題該找誰」,這張表已經先回答了。它也順帶回答了另一個常被忽略的問題:一次故障如果同時牽涉兩個以上座標(例如 prompt 版本跟 retrieval index 同時在事故窗口內變動),意味著調查需要同時拉進兩個團隊,而不是等第一個團隊查完、發現「不是我們的問題」之後才輪到下一個——這種循序排查在 AI 系統的多維度故障座標下特別浪費時間,並行拉人比循序等待快得多。
Day 28 會把這些資料收斂成 change manifest 與 release decision。
這一節仍然只提供讀者自行執行的步驟。
本文沒有建立、安裝、啟動或驗證你的 DIY 專案。
請在隔離的練習環境操作,並用 mock provider 或固定回應;不要故意讓真實的模型帳單、使用者資料或 production 流量承受演練。
下面七個步驟不是隨意排列的程式碼片段,它們刻意對應到本篇前面幾段各自談的一個具體論點——讀者不需要真的跑起這個 DIY 專案,也能透過每一步「為什麼這樣寫」的說明,看懂它在驗證什麼:
步驟一 → 驗證 ①「故障要能被解釋,不是靠記憶」
步驟二 → 驗證 ③「版本追蹤要自動預填,不能靠事後回想」
步驟三 → 驗證 ①③「HTTP 200 不等於服務沒壞」
步驟四 → 驗證 ③「敏感資料要脫敏後才進事故紀錄」
步驟五 → 驗證 ②「六段流程要能在不完整資訊下做決策」
步驟六 → 驗證 ⑦「已知故障要回填成可重複驗證的資產」
步驟七 → 驗證 ⑦「版本欄位要長在請求路徑裡,不是留給演練現場」
如果你手邊沒有時間真的跑一次,至少讀完每一步的「預期觀測」與「常見卡點」,你會發現這些坑不是抽象的警語——它們是任何一個團隊第一次認真做版本追蹤或事件記錄時,幾乎必然會撞到的東西。
開始前,準備下列資訊:
- 一條能呼叫 `/ask` 或等效 endpoint 的本機服務。
- 可查詢的 metrics、structured logs 與 traces;沒有完整平台也可先保留 JSON 範例。
- 一個可以安全切換的 prompt 或 mock response 版本。
- 一位演練主持人與一位記錄者;同一人也可以兼任,但要明寫。
- 一個停損規則:任何碰到真實資料或外部 provider 的情況立即停止。
不要一開始選「模型胡說八道」這種難以復現的情境。
選擇一個版本與輸入都能固定的故障,例如 citation parser 對缺少方括號的輸出判定失敗。
def parse_citations(answer: str) -> list[str]:
# 演練用:故意要求 [source:...] 格式。
# 真實服務請使用自己的 parser 與測試資料。
return [line for line in answer.splitlines() if line.startswith("[source:")]
answer = "遠端工作須經主管核准。\n來源:Remote Work Policy"
citations = parse_citations(answer)
assert citations == []
這段程式碼的價值不在 parser 寫得多漂亮,而在於把故障縮小成可重跑的輸入、規則與預期——把 answer、parser revision 與觸發時間記下來,才有材料討論為什麼線上 gate 漏掉它。
重點:選一個 deterministic、輸入輸出都固定的故障(而不是「模型偶爾講錯話」這種難以復現的情境),演練焦點才能放在「流程」而非「猜測模型行為」——這正是①段「故障要能被解釋」的具體示範:answer、parser 版本與判定結果,合起來就是任何人事後都能重跑驗證的證據,不需仰賴誰「記得」發生過什麼。
常見坑:assert citations == [] 應直接通過,這才是重點,故障在這裡是「確定會發生」的,不是碰運氣。真正的坑是討論階段忍不住想「順手修掉」這個簡單 bug,跳過了記錄、分類、桌上演練的順序——這正是 Mitigate 不等於 Resolve 這個順序的縮影演練。
每次演練請求至少帶出一組可跨 signals 查詢的識別字。
from dataclasses import dataclass
from uuid import uuid4
@dataclass(frozen=True)
class IncidentContext:
request_id: str
trace_id: str
task_id: str
prompt_version: str
deployment_id: str
context = IncidentContext(
request_id=str(uuid4()),
trace_id=str(uuid4()),
task_id="policy-answer",
prompt_version="v42-drill",
deployment_id="local-drill-001",
)
print(context)
預期觀測不是一串隨機 UUID。
你應能以同一個 request_id 找到結構化 log,以 trace_id 找到 span,並在 incident note 中指向它們。
不要把 request_id 當 Prometheus label。
它的高基數會讓 metrics 系統失去效率;細節留給 logs 與 traces。
重點:IncidentContext 用 frozen=True 的 dataclass 而非可變 dict,是刻意的訊息——這組識別字一旦在請求開始時建立,就不該再被任何後續程式碼修改,更像是這次請求的「出生證明」。五個欄位在請求最早的地方就一起建立,正是 ③ 段「版本欄位要由部署管線自動寫入,而不是留給 on-call 事後手動填表」最直接的程式碼實踐——prompt_version 與 deployment_id 不是事後才去問「上次是誰改的」,而是誕生時就已經跟著記下來。
常見坑:第一次導入時,團隊常常只補了 request_id(因為它多半已存在於既有 tracing 基礎設施裡),卻漏了需要額外查詢部署系統才拿得到的 prompt_version 或 deployment_id;也容易把這些欄位當成「日後有空再補」的優化項,而不是上線前必須具備的基本配置。跳過這一步的代價很具體:三天後某個回答品質下降,手上只剩一句「昨天下午好像有人調了 prompt」,沒有 trace_id、prompt_version、deployment_id 可查,調查第一步變成「先問誰記得昨天改了什麼」——這正是 ③ 段「故障的知識只留在參與者短期記憶裡」的具體重演。
同樣的 HTTP 200,可能代表完全不同的用戶結果。
演練時請把 technical、workflow、quality、safety 分開記錄。
from enum import StrEnum
class TechnicalStatus(StrEnum):
OK = "ok"
ERROR = "error"
class WorkflowStatus(StrEnum):
COMPLETED = "completed"
DEGRADED = "degraded"
FAILED = "failed"
class QualityStatus(StrEnum):
PASS = "pass"
FAIL = "fail"
NOT_EVALUATED = "not_evaluated"
class SafetyStatus(StrEnum):
PASS = "pass"
BLOCKED = "blocked"
REVIEW = "review"
測試時可故意建立下列案例:
HTTP 200 + completed + fail + pass
代表服務完整回覆,卻沒有通過來源驗證。
HTTP 200 + degraded + pass + pass
代表 fallback 成功完成;不該跟完全正常混在一起。
HTTP 503 + failed + not_evaluated + pass
代表技術故障,品質沒有被測到,不是品質通過。
HTTP 200 + completed + pass + blocked
代表產生了內容,但安全規則阻擋最終交付。
這些組合會直接影響 incident 的影響範圍敘述。
重點:四個 StrEnum 分開定義,而不是合併成一個 overall_status 這種單一欄位,是刻意的架構決定——單一狀態欄位必須假設所有維度同時變好或變壞,但 AI workflow 的現實正相反:技術層完全正常、品質層卻失敗,是最常見也最容易被忽略的組合。這是①、③ 段「HTTP 200 不等於服務沒壞」的可執行版本,逼你在每次事件明確回答四個問題,而不是用籠統的「有沒有出事」帶過。
常見坑:completed + fail(跑完了但結果不對)與 degraded + pass(備援跑完、結果對,但不該混同完全正常)這兩種組合遠比想像中常見。團隊導入初期習慣把所有東西塞進最熟悉的 technical_status,quality 與 safety 這兩個新維度反而在忙起來時最容易被漏填或填成敷衍值——而這正是它們最需要獨立存在的時刻。
把內容縮成一筆可檢視的 JSON event。
{
"event": "quality_gate_failed",
"request_id": "<uuid>",
"trace_id": "<uuid>",
"task_id": "policy-answer",
"prompt_version": "v42-drill",
"deployment_id": "local-drill-001",
"technical_status": "ok",
"workflow_status": "completed",
"quality_status": "fail",
"safety_status": "pass",
"failure_reason": "citation_format_missing"
}
不要把原始問題、完整答案或 retrieved document 無差別塞進 event。
如果 incident 調查確實需要內容樣本,先做脫敏、最小化收集與權限控管。
重點:這筆 JSON event 只留識別字、版本座標、四個分類狀態與一個簡短的 failure_reason 字串,完全不含使用者原始輸入或模型完整輸出——incident 事件紀錄的目的是讓事後調查能重建發生了什麼、能被跨系統關聯查詢,不是保存完整內容證據,後者該由另一套有存取控制的系統負責。這是 ③、⑨ 段「事件紀錄要脫敏、公開版本與限制版本分開」最具體的落地。
常見坑:第一次寫這種事件紀錄的工程師,常忍不住多塞一個 raw_answer 欄位「以防萬一以後要用」——這個看似貼心的舉動,恰恰是敏感資料外洩最常見的起點,因為一旦欄位允許裝下完整內容,遲早會被填進真實使用者資料,而事件紀錄通常會轉送到比原始服務更多人能存取的追蹤系統或公開頻道。
主持人按照時間推進,不要先告訴參與者根因。
第 0 分鐘:收到 citation-valid rate 降低的 alert。
第 5 分鐘:提供 dashboard 與兩筆 trace_id。
第 10 分鐘:提供 prompt version 的變更資訊。
第 15 分鐘:要求決定 rollback、fallback 或繼續觀察。
第 20 分鐘:提供 rollback 後的新指標。
第 25 分鐘:要求提出恢復判定與待追查項目。
第 30 分鐘:停止演練,開始整理事實與卡點。
演練真正要測的不是猜中根因。
它要測你是否能在不完整資訊下,做出可逆、可解釋、以用戶風險為中心的決策。
重點:主持人分階段釋放資訊,刻意模擬真實事故裡「資訊逐步浮現」的現實,呼應 Etsy 的 Debriefing Facilitation Guide(見 ⑫、⑭ 段)的原則——問「當時你觀察到什麼」而非「為什麼沒有早點發現」,讓參與者描述真實決策脈絡而不是事後諸葛式的辯護。第 15 分鐘要求在資訊不完整時決定 rollback/fallback/繼續觀察,正對應 ② 段「Mitigate 的目標是讓 SLI 回穩,不是完全理解根因」;如果先給完整根因再演練,就失去訓練這個核心能力的意義。
常見坑:主持人要求決定時,參與者常下意識拖延——「能不能再給我們兩分鐘查一下 trace」。這個反應本身就值得記錄,它暴露了「在資訊不完整時做決定」有多違反工程師直覺;主持人應溫和但堅定地卡住時間,把「卡在這裡多久」記成一項卡點,這通常是整場演練最有價值的紀錄。
如果問題能以固定輸入重現,就不要只把它留在 postmortem PDF 裡。
把它放進 evaluation dataset 或單元測試的代表案例。
def citation_gate(answer: str) -> bool:
return "[source:" in answer
def test_missing_citation_is_not_a_success() -> None:
answer = "遠端工作須經主管核准。\n來源:Remote Work Policy"
assert citation_gate(answer) is False
這個測試不證明回答內容真實。
它只保護這次已知的格式失敗,不會讓未來的 prompt 悄悄放回相同缺口。
重點:citation_gate 跟步驟一的 parse_citations 邏輯幾乎一樣,這是刻意的重複——步驟一的版本用來在演練現場重現故障,這裡則被包裝成一段會被 CI 持續執行的測試,正是把「一次性事故經驗」轉成「系統長期記憶」的關鍵動作,也是 ⑦ 段「把已知案例回填到測試、dataset、runbook 與 release gate」最具體的示範。
常見坑:這個測試理論上永遠通過,價值在於未來 prompt 不小心引入同樣缺陷時能擋在合併或部署之前。常見的坑是:postmortem 的 Action Item 寫成「新增 regression test」,卻沒人真的寫這段程式碼並確認它進了 CI——一個沒被真正加進 CI 的 regression test,跟從沒寫過的一樣,都防不住故障重演,這正是 ⑤、⑥ 段「Action Item 兌現率」的具體場景。
前六步分別驗證了六個獨立論點,但演練結束後最常見的下場,是這六段程式碼各自留在不同的檔案裡,沒有人回頭把它們接成系統裡真的會執行的一段程式。步驟七要做的,是把 IncidentContext(步驟二)、四維度狀態(步驟三)與脫敏事件紀錄(步驟四)串成一個能掛在 FastAPI 應用上的最小 middleware:
import json
import logging
import time
from uuid import uuid4
logger = logging.getLogger("incident_events")
async def incident_context_middleware(request, call_next):
context = IncidentContext(
request_id=str(uuid4()),
trace_id=request.headers.get("traceparent", str(uuid4())),
task_id=request.url.path,
prompt_version="v42-drill",
deployment_id="local-drill-001",
)
start = time.monotonic()
response = await call_next(request)
duration_ms = (time.monotonic() - start) * 1000
event = {
"event": "request_completed",
"request_id": context.request_id,
"trace_id": context.trace_id,
"task_id": context.task_id,
"prompt_version": context.prompt_version,
"deployment_id": context.deployment_id,
"technical_status": "ok" if response.status_code < 500 else "error",
"duration_ms": round(duration_ms, 1),
}
logger.info(json.dumps(event))
return response
重點:prompt_version 與 deployment_id 刻意寫死,是為了聚焦示範「這五個欄位該長在請求路徑的哪一層」,而不是「部署查詢怎麼做完善」——那是另一個獨立工程任務。這一步把步驟一到六各自獨立可跑的程式碼,串成 ⑦ 段證據鏈圖最左邊的起點:沒有一個 middleware 在每個請求進來時就記錄這些欄位,「同一次使用者請求的完整快照」就只是一張畫在紙上的示意圖。
常見坑:接上後,服務 stdout(沿用 Day2 的 JSON structured logging,由 Alloy 收集轉送 Loki)應能看到每個請求都帶著完整五個 IncidentContext 欄位。最常見的坑是把它當成「演練專用、之後要拔掉」的臨時程式碼——其實該反過來想,這正是 ③ 段「版本欄位要由部署管線自動預填」在程式碼層級最小的落地,拔掉它就等於把系統送回「事後靠記憶拼湊版本」的起點。
演練結束後,請把觀測結果放進下列判讀表。
| 看到的訊號 | 可能解釋 | 先做的檢查 | 可以採取的低風險動作 | 不該直接下的結論 |
|---|---|---|---|---|
| API P95 上升、LLM span 正常 | queue、retrieval、tool 或應用資源有問題 | 比較各 span、worker saturation、deploy diff | 暫停 rollout、保護低優先級流量 | 模型 provider 一定壞了 |
| HTTP 2xx 正常、quality fail 上升 | prompt、retrieval、evaluator 或資料版本改變 | 查 version、抽樣 trace、比較 golden case | 暫停該行為變更、切回已知版本 | 使用者沒有受到影響 |
| safety block 大增 | 攻擊流量、policy 變更或分類誤判 | 比較輸入類型與 policy version | 限縮高風險 route、通知 security owner | 安全系統壞掉了 |
| retry 增加、error rate 仍低 | 依賴變慢或重試掩蓋問題 | 看 attempt count、queue depth、成本 | 降低非必要工作、啟用既定 fallback | 服務健康,因為 5xx 很少 |
| evaluator 突然全 pass | evaluator 失效、資料缺失或 route 被跳過 | 檢查 evaluation coverage 與樣本量 | 暫停自動 promotion | 品質一定變好了 |
表格不是取代追查。
它讓第一輪判斷不必靠最大聲的人決定。
這張表存在的理由,跟 ⑧ 段 runbook 要有「停下來的條件」是同一個道理:高壓下的第一反應,往往是抓住最熟悉的那個解釋,而不是最符合證據的那個解釋。一個工程師如果過去處理過三次「P95 上升」都是 provider 端的問題,下一次看到同樣的訊號,直覺會直接往同個方向查——即使這次真正的原因是自家新上線的 retrieval 服務吃滿了連線池。這張表格的每一欄「不該直接下的結論」,就是在提前攔截這種「用舊經驗套新案例」的捷徑。
值得特別注意最後一列:evaluator 突然全部通過(pass),表面上看起來是好消息,實際上往往是三種壞消息之一——evaluator 本身掛了、資料管線把某段流量漏接了、或是某個判斷分支被意外跳過。這個反直覺的訊號,呼應了 ③ 段一路強調的「evaluator 本身的可信度」問題:一個從不失敗的品質關卡,通常不是因為品質真的完美,而是因為這個關卡已經停止在真正檢查任何東西。把「evaluator 全 pass」自動觸發一次覆蓋率稽核,而不是直接放行 promotion,是這張表格轉譯成日常告警規則時最容易被忽略、但也最重要的一條。
假設 incident 前後指標如下:
平均 latency:900ms → 950ms
P95 latency:1.4s → 8.1s
低延遲 request:仍佔 96%
高延遲 request:集中在 tool-using workflow
只看平均值,服務看起來幾乎沒變。
但受影響的那 4% 可能正是付費客戶執行的重要工作。
恢復條件應反映 user journey 與受影響的 workflow,而不是方便的單一平均數。
同理,quality score 的平均值也可能掩蓋某個政策領域完全失敗。
請按 task type、model route、prompt version 或安全決策切片;切太細則會失去統計意義,所以要先寫清楚採樣與最小樣本量。
平均值掩蓋問題的另一種型態,發生在多租戶系統:
全體租戶的 quality pass rate:97.8% → 97.1%
(看起來只是小幅波動,不足以觸發任何門檻)
但拆開來看:
租戶 A(占流量 60%):pass rate 98.5% → 98.3%(幾乎沒變)
租戶 B(占流量 3%,企業合約客戶):pass rate 96% → 12%(幾乎全滅)
全體聚合出來的 0.7 個百分點下滑,不會觸發任何合理設定的告警門檻;但對租戶 B 而言,這是一場幾乎完全故障的事故。這種訊號被稀釋的原因很直接:租戶 B 的流量占比太小,它的災難性下滑被其他 97% 流量的正常表現平均掉了。如果 postmortem 或告警系統只看全租戶聚合的 SLI,這類「小客戶、大影響」的故障會系統性地被漏掉,而往往正是流量占比小、卻是高價值合約客戶的那些租戶,最需要被個別看見。
這個反例跟前面「用平均值宣布恢復」的差異在於切片的維度不同——前者切的是 latency 分布(P95 vs 平均),這裡切的是租戶維度。兩者共通的教訓是同一句話:任何一個匯總指標,都在用某種方式犧牲了子群體的可見度;復盤時該問的不是「這個指標有沒有異常」,而是「這個指標的匯總方式,可能藏住了哪個子群體的異常」。
復盤不是把每個格子填滿的競賽。
當證據不足時,直接記錄 unknown。
### Unknowns
- 09:00-09:08 的部分 trace 因 sampling 未保留,無法確認最早異常的比例。
- 當天的人工 feedback 有選擇偏差,不可直接推估全量使用者滿意度。
- rollback 後 quality 指標回升,但尚未證明 prompt-v42 是唯一根因。
這樣做不會削弱報告。
它能防止後續讀者把推測誤當成事實,並讓 Action Item 對準真正缺少的觀測。
一份好的 postmortem 不需要寫得像法庭辯論書。
但它要讓沒參與 incident 的人分得出來:什麼已證實、什麼合理推論、什麼仍未知。
以下模板可直接複製到自己的事故文件。
# Incident: citation validation regression
## 摘要
- 影響:09:08-09:25 UTC,部分 policy Q&A 的 citation validation 失敗。
- 使用者結果:API 多為 200;受影響回答不應被視為已通過品質門檻。
- 緩解:停止 prompt-v42 canary,切回 prompt-v41。
## 已證實事實
- prompt-v42 在 09:00 UTC 進入 10% canary。
- `quality_status=fail` 在 09:08 UTC 後高於基線。
- rollback 後,citation-valid rate 在連續 15 分鐘回到既定門檻。
## 推論
- prompt 的輸出格式變更可能讓 parser 漏掉來源列。
- 此推論需要以固定輸入的 regression test 再確認。
## 偵測落差分析
- MTTD:約 8 分鐘(09:00 canary 上線 → 09:08 quality evaluator 開始回報 fail)。
- 使用者第一次公開回報:無(rollback 已在使用者投訴前完成)。
- 為何不是 0 分鐘:release gate 目前僅檢查回應延遲與 schema 正確性,未檢查 citation 格式;quality evaluator 屬於非同步批次評分,天生比同步的 gate check 慢一拍。
- 可縮短空間:若把 citation 格式檢查移進同步 release gate(見 Day 28),MTTD 可望壓到 canary 上線前,即 0 分鐘。
## Unknowns
- 受影響使用者的精確數量;feedback 並非全量蒐集。
- 是否有其他 prompt-v42 的行為改變未被目前 evaluator 覆蓋。
## 防護表現
- 有效:quality evaluator 讓問題在人工回報前被發現。
- 無效:release gate 沒有包含缺少方括號的 citation format。
## Action items
- [ ] 新增缺少格式標記的 regression case;owner:AI application;期限:日期。
- [ ] 在 manifest 加入 parser version;owner:platform;期限:日期。
- [ ] 檢查 sampling 對高風險 workflow 的 coverage;owner:observability;期限:日期。
注意 Action Item 不是「改善品質」或「加強監控」。
那種句子沒有 owner、沒有完成條件,也無法驗證。
一個可追蹤 Action Item 應有具體產物、負責人、期限與驗收方式。
「偵測落差分析」這個章節,是把 ② 段 MTTD/MTTM/MTTR 的量化概念,接進 postmortem 模板裡最具體的位置。多數團隊寫 postmortem 時只會記錄「我們什麼時候發現的」,卻很少往下多問一句「為什麼不是更早」——這一句追問,才是真正能導向系統改進的問題,因為它會逼你去檢視偵測機制本身的設計,而不是停在事件本身的敘述。上面範例裡「為何不是 0 分鐘」這一行看似瑣碎,實際上是整份 postmortem 裡最容易轉換成具體 Action Item 的一句話——它直接指向一個可執行的修復方向(把檢查移進同步 gate),而不是一句空泛的「加強監控」。
一份 postmortem 從撰寫完成到被歸檔,往往只經歷過一次認真的閱讀——就是撰寫完成、發給團隊審閱的那一次。三個月後,如果不是又發生了類似的故障、有人搜尋關鍵字才意外翻到它,這份文件幾乎不會再被任何人打開。這造成一個容易被忽略的後果:postmortem 裡「已證實事實」跟「推論」的區分,只在寫作的當下有意義;一旦這份文件被歸檔,讀它的人(尤其是幾個月後的新同事)很容易把「推論」段落裡的內容,直接當成跟「已證實事實」同等級的定論來引用——因為對一個沒有參與過事故現場的人來說,白紙黑字寫在文件裡的東西,看起來都一樣可信。
一個簡單但很少被採用的補強方式,是替每份 postmortem 加一個「回顧」欄位,跟前面 ⑧ 段 runbook 的 last_drilled 是同一種設計精神——不是寫完就算數,而是定期回頭檢查當初的推論有沒有被證實:
## 回顧(三個月後由非當事人補寫)
- 「prompt 輸出格式變更讓 parser 漏掉來源列」這條推論,
已由 2026-07 的 regression test 結果證實。
- 「是否有其他 prompt-v42 行為改變未被目前 evaluator 覆蓋」這條 unknown,
截至本次回顧仍未被排查,建議轉為正式 Action Item。
這個欄位刻意要求「由非當事人補寫」,理由跟前面 ⑤ 段強調追蹤 Action Item 兌現率是同一個邏輯:讓原始撰寫者自己回頭確認自己的推論,容易出現確認偏誤——傾向於相信「我當初猜的應該是對的」;而換一個沒有涉入情緒與立場的人,重新核對「這條推論後來到底有沒有被證實」,才更有機會誠實地把還沒解決的 unknown 轉成新的 Action Item,而不是讓它安靜地被遺忘在一份沒有人再打開的文件裡。
上面這份模板把「已證實事實」跟「推論」分成兩欄,看起來只是排版問題,但真正難的不是排版,而是在會議現場,怎麼問問題才能讓大家說出事實,而不是說出事後合理化的解釋。
Etsy 公開的 Debriefing Facilitation Guide 對這件事給出了一套具體、可操作的方法,值得完整搬過來講。這份指南的核心區分,是「描述」(description)跟「解釋」(explanation)的不同:描述是「當時你看到什麼、聽到什麼、基於什麼做出那個判斷」;解釋是事後才重建出來的因果鏈,通常帶著馬後炮式的合理化。指南原文直接點出這個風險:「explanations tend to be reductive or defensive and usually scare the learning away」——解釋傾向於簡化或防禦性,而這通常會把真正的學習機會嚇跑。
具體到問問題的方式,指南建議用「怎麼」取代「為什麼」:
好問題(追問描述):
「當時是什麼讓你覺得不對勁?」
「那個當下你看到 / 聽到什麼?」
「是什麼讓你做出那個決定?」
「當時還有沒有其他可以選的做法?」
壞問題(誘發解釋與防禦):
「你為什麼會這樣做?」(隱含要求對方辯護)
「為什麼沒有更早發現?」(隱含指責)
「是什麼導致系統故障的?」(假設因果已經確定,跳過描述)
這個差異在演練現場很容易感受到:被問「你為什麼沒有更早發現」的人,第一反應通常是解釋或防禦(「因為 dashboard 上看起來還好」);被問「當時 dashboard 上你看到什麼」的人,反而會誠實地把當下的觀察攤開來——而正是這些攤開來的觀察,才是拼湊「系統條件如何一路演變成故障」的真正材料。
指南也給出一套具體的會議節奏,值得對照本篇 ④ 段的 30-45 分鐘演練設計:一場 60 分鐘的復盤會議,建議用 35-40 分鐘重建時間線(這是主要工作,逐步確認每個判斷點的事實與證據),剩下的時間才留給提煉學習重點與蒐集補救構想。更值得注意的是一個常被忽略的細節:指南建議把補救構想(remediation ideas)先蒐集起來,不要在會議當下立刻拍板優先順序,而是讓這些構想「醞釀」至少兩天以上,再回頭定案成正式的 Action Item。這個「先發散、晚收斂」的設計,是為了避免會議室裡最大聲或最資深的人當場拍板,蓋過其他可能更貼近根因的方案——這跟前面 ⑤ 段強調的「Action Item 兌現率」互相呼應:一個倉促拍板、沒有充分討論過可行性的 Action Item,正是最容易變成「寫了但沒人真的做」的那種。
會議開始前,指南還建議主持人先跟可能感到緊張或擔心被究責的當事人做一次一對一的前置溝通,明確提醒對方:「事後諸葛的認知,正是讓他們當初的決策或行動事後看起來很丟臉的原因」——這句話直接點破無責復盤最容易失敗的地方:不是流程設計不好,而是當事人在會議現場因為擔心被評判,而下意識地隱藏了關鍵細節。
「某人沒有檢查 prompt」通常描述的是最後一個動作,不是能修的系統原因。
比較有用的追問是:
為什麼變更可繞過既有 review?
為什麼 golden dataset 沒有這個 case?
為什麼 canary 的 quality signal 沒有自動停下 rollout?
為什麼 on-call 無法在五分鐘內找到 parser version?
這些答案才會導向權限、流程、資料集、告警與可觀測性上的改進。
它們也比較不會在下一個值班者身上重演。
完成本文的練習後,逐條檢查:
[ ] 能用一句話描述 alert 對使用者旅程的風險,而非只報 metric 名稱。
[ ] Runbook 的每個第一步都包含輸入、預期觀測、分支與停止條件。
[ ] 已區分低風險 mitigation 與需要額外核准的高風險動作。
[ ] 能用 request_id、trace_id 與版本欄位關聯同一次 workflow。
[ ] 沒有把高基數 request_id 寫成 metrics label。
[ ] technical、workflow、quality、safety 狀態在事件筆記中分開記錄。
[ ] 已做一次 30 分鐘桌上演練,且沒有接觸真實使用者資料或 production 流量。
[ ] 演練輸出包含時間線、決策紀錄、未知項目與至少一個可追蹤 Action Item。
[ ] 已把一個固定 failure mode 轉成 regression case 或 evaluation dataset 項目。
[ ] 恢復條件以受影響 workflow 的 SLI 表達,不只看 HTTP 2xx 或平均 latency。
[ ] 已明確揭露尚未驗證的假設與 sampling 限制。
[ ] 復盤討論時用「怎麼」而不是「為什麼」追問當事人的觀察,而非要求對方解釋或辯護。
[ ] 補救構想有經過至少一天的醞釀期,才收斂成正式 Action Item,而非會議現場立刻拍板。
[ ] 根因分析至少追問了三層「為什麼」,沒有停在「某人沒注意到」這種無法系統化修復的結論。
[ ] Action Item 有明確的 owner、due date 與可驗收的完成定義,而不是一句無法檢驗的口號。
[ ] 已明確記錄 Resolve 與 Recover 分別發生在哪個時間點,兩者不是同一格。
若其中任何一項做不到,這不是失敗。
那一項就是下一次演練最值得補的觀測或流程缺口。
清單裡有幾條特別容易被輕忽,值得多說一句「為什麼這條也要打勾」:
「沒有把高基數 request_id 寫成 metrics label」這條看起來跟復盤沒什麼關係,但它其實是本篇⑨、⑩兩段反覆強調的「識別字要能跨 signals 關聯」的反面提醒——request_id 該進 logs 與 traces,一旦被誤放進 metrics label,Day 2 SRE Lab 建立的 Prometheus 就會因為 label 基數爆炸而效能劣化,下一次事故反而失去可用的 metrics 儀表板,等於用這次演練替下一次埋了一個新坑。
「根因分析至少追問了三層為什麼,沒有停在『某人沒注意到』」這條,呼應的正是 ⑭ 段 Richard Cook 的核心論點——一份停在「工程師沒仔細看」的復盤,不是寫得不夠用心,而是提問方式從根本上就問錯了問題,答案自然也就沒有辦法系統化落地成任何可執行的改進。
這份清單本身也會過時。三個月後回頭看,若每一項都輕鬆打勾,代表門檻該往上調了——加入更嚴苛的情境(例如同時發生兩個獨立故障、或 on-call 換手途中發生 incident),逼團隊的流程繼續往前演化,而不是停在「能通過驗收」就滿足。
Google 的 postmortem 文化強調:「writing a postmortem is not punishment—it is a learning opportunity for the entire company」。核心是假設所有人都出於好意,問題在於系統條件、防線或流程。
Google 透過「Wheel of Misfortune」災難角色扮演進行常態化訓練:工程師在安全環境中模擬故障決策,事後討論「如果再來一次,我們會怎麼做不同」。這種操演勝過任何文檔,實際的時間壓力與不確定性無法在會議室完全模擬,但反覆練習能建立團隊對 incident response 的肌肉記憶。
這個機制的具體玩法,其實跟本篇 ④ 段的桌上演練設計非常接近:一名主持人扮演「系統」,手上握著一份預先寫好的故障劇本(哪個服務會在第幾分鐘出現什麼徵兆),其他人扮演 on-call 或 IC,只能根據主持人逐步釋出的訊號做決策——不能翻書、不能查已知答案。差別在於 Google 的做法會刻意加入意外轉折:原本以為是資料庫故障,主持人在演練進行到一半時追加一句「這時你發現資料庫其實是好的,但下游有另一個服務同時在重啟」,逼參與者當場修正原本的假設。這個設計呼應的正是 ⑫ 段強調的「不要太早鎖定單一根因」——真實故障很少乾淨到只有一條因果鏈,Wheel of Misfortune 存在的目的,就是讓工程師習慣在假設被推翻時如何調整,而不是在真正的深夜 incident 裡才第一次經歷這種措手不及。
Netflix 走得更遠:他們分析「差點故障但防護攔截了」的 near-miss 事件。例如,某個依賴服務延遲上升,自動故障轉移被正確觸發,用戶完全未感受到問題,這樣的「成功防護」也會進入復盤。
這改變了復盤的基調:不再是「我們出了什麼問題」,而是「我們的防線是否在工作、哪些防線需要強化」。Netflix 的 postmortem 模板明確要求「What Went Well」章節,慶祝有效的回應。
near-miss 分析之所以值得投入,是因為它捕捉到一種傳統 SRE 容易忽略的訊號:防線「差一點失效」跟「完全沒有防線」在事後看起來很像(用戶都沒受影響),但風險完全不同。一個自動故障轉移在延遲逼近 timeout 邊界前 200 毫秒才觸發,跟在延遲剛超過門檻的瞬間就觸發,兩者的「安全邊際」天差地遠——前者代表這條防線隨時可能因為些微的負載變化而失守,值得列進復盤的觀察名單,即使這次它「成功了」。
⑫ 段已引用過 Etsy 的 Debriefing Facilitation Guide 與其核心區分(描述 vs 解釋、後見之明偏誤如何污染事後敘事)。這裡把它列為第三個深度案例,是因為它補上 Google 與 Netflix 案例都沒有直接回答的問題:Google 強調「不究責」、Netflix 強調「記錄防線表現」,但兩者都假設與會者願意誠實說出當時的判斷過程——Etsy 的指南處理的正是「怎麼讓人願意誠實說」這個更底層的問題,把它拆解成一套可訓練、可重複的引導技巧,而非依賴主持人的個人魅力。
這套技巧能落地成三件具體的事,在你自己團隊的下一場復盤裡就能直接套用:
1. 主持人角色要跟 incident commander 分開
→ 引導討論需要保持中立,親自參與救火的人很難同時客觀主持。
2. 每個關鍵決策點,先問觀察、再問判斷、最後才問行動
→ 「你看到什麼」→「這讓你覺得可能是什麼問題」→「所以你做了什麼」
三段式拆解,逼問題聚焦在事實而非事後包裝過的因果鏈。
3. 遇到防禦性回應("我當時沒辦法知道...")
→ 不要反駁或說服,而是換句話重新問一次觀察面向的問題。
防禦反應本身就是訊號:代表這個人感覺被評判了,該先降溫而不是繼續追問。
前面三個案例談的都是「業界怎麼做」——Google 的流程與訓練、Netflix 的模板設計、Etsy 的引導話術。但有一個問題,這三個案例都沒有正面回答:為什麼「不要問 why、要問 how」「不要找單一根因」這些做法是對的,而不只是一種比較溫和的說法?
答案來自一篇比這三個案例都早、卻很少被工程部落格直接引用出處的短文:Richard Cook(芝加哥大學認知科技實驗室,醫師背景)在 1998 年寫的《How Complex Systems Fail》。這篇文章原本談的是醫療系統的意外,但它的論點幾乎可以逐字套用到今天的 incident postmortem,這也不是巧合——Google SRE Book 自己在談 postmortem 文化時就承認,「無責復盤」這套哲學「源自醫療與航空業」。
Cook 的核心主張可以濃縮成三句話,每一句都直接反駁一個工程團隊很容易掉入的直覺:
直覺:重大事故一定有一個根本原因,只要找得夠深就會找到。
Cook:「no isolation of the root cause of an accident is possible」——
重大事故永遠是多個各自都不足以致命的小缺陷同時疊加造成的,
「唯一根因」滿足的是社會心理上「需要有個交代」的需求,
不是技術上真實存在的單一因果鏈。
直覺:事後回頭看,當時的警訊「明明」很清楚,怎麼會沒人發現。
Cook:「the outcome knowledge poisons the ability of after-accident
observers to recreate the view of practitioners before the accident」——
結果已知這件事,會系統性地讓事後的你高估「當時應該看得出來」的程度,
這些線索在事故發生前,對當事人來說根本不顯眼。
直覺:只要工程師更小心一點、更遵守流程一點,這種事就不會發生。
Cook:第一線的人隨時都在生產壓力與風險之間做權衡,每個決定都是
資訊不完整下的「賭注」;賭輸的那次被貼上「error」標籤,
但賭贏的無數次也是同樣性質的賭注,只是沒有暴露出來而已。
這三句話合起來,其實是在說同一件事的三個側面:「這次事故是誰的錯、根本原因是什麼」這整套提問方式,從一開始就假設了一個不存在的東西——一條乾淨、單一、可以被「找到」的因果鏈。真實的系統失效,更像是好幾層原本各自都撐得住的防護,湊巧在同一個時間點都出現了縫隙,縫隙疊在一起才讓故障穿過去。Day27 ⑫ 段一路強調的「根因分析別停在 human error,要往系統條件追問」,Cook 這篇文章給的正是這個立場最原始、也最徹底的理論依據——它不是說「找個人負責比較不厚道」,而是說「找一個人負責」在技術上根本答不出真正的問題。
這也解釋了為什麼 Etsy 的引導技巧要刻意用「怎麼」取代「為什麼」:「為什麼」這個提問形式,天生就在邀請對方說出一條事後合理化的因果故事;「怎麼」這個提問形式,逼對方回到「當時你觀察到什麼」這個更誠實、也更貼近 Cook 所說「當事人視角」的位置。兩篇文獻雖然一篇談的是醫療意外、一篇談的是工程復盤引導技巧,指向的其實是同一個機制。
把「人是在賭,不是在犯錯」這個框架,套回 Day27 前面幾段談過的內容,會發現它其實一路都在:⑥ 段「Resolve 不是 Recover」這個容易被跳過的落差,本質上是值班者在「觸發因素已消除」跟「使用者已無事」之間,用當下有限的訊號做了一次合理但不完美的賭注;⑧ 段「不要把高風險捷徑列成第一招」裡,工程師選擇拉長 timeout 或關閉 validator,也不是疏忽,而是在「圖表立刻變綠」跟「未知的下層風險」之間,選擇了當下看起來勝率更高的那一注。承認這些都是賭注,而不是可以靠「下次更小心」解決的錯誤,才會真正把改進的力氣放到系統條件上——加一道獨立的確定性檢查、把停止條件寫進 runbook、讓版本欄位自動預填——而不是放到「提醒大家要注意」這種對下一次任何具體情境都沒有幫助的口頭呼籲上。
一、恢復與根因分析是兩個不同的活動,不應該混在一起。
在 incident 發生的第一小時內,目標是「讓服務回穩」,不是「完全理解故障」。團隊經常被迫在高壓下做出「修復還是回滾」的決定,此時搞清楚完整的根本原因會拖累決策速度。更好的做法是:(1) Mitigate 時,相信既有的 runbook 與 fallback;(2) Recover 後,再進行詳細的根因分析。
二、復盤應該記錄系統條件,而不是人的行為。
「工程師在凌晨 3 點沒有注意到日誌警告」與「系統沒有自動轉移故障流量」是兩個層級的問題。前者無法修復(永遠會有人犯錯),後者可以系統化改進(加自動故障轉移邏輯)。無責復盤的核心就是這個區別。
這裡有一個常被誤解的地方,值得單獨澄清:「無責」不等於「沒有後果」。
Blameless ≠ 沒有後果
→ 復盤不追究個人責任、不在文件裡寫誰的名字,
但如果同一個人反覆做出偏離既定流程的判斷,
這仍然是需要被正視的訊號——只是正視的方式
不是在 postmortem 裡點名,而是回到訓練、輔導、
或流程設計本身去問「為什麼這件事一直沒有被
系統性地擋下來」。
Blameless = 不預設個人是問題的終點
→ 復盤把注意力放在系統條件,是因為系統條件是
「可以被下一個人驗證、可以被改進」的東西;
個人層級的輔導與教練,走的是完全不同的管道,
不該混進同一份要被公開分享的文件裡。
這個區分很重要,因為「無責復盤」如果被簡化理解成「反正寫了也不會怎樣」,反而會侵蝕它原本要保護的東西——誠實。Google 與 Etsy 的做法都清楚劃出這條界線:postmortem 文件本身永遠不記名、不究責,但一個團隊如果觀察到某類型的判斷模式反覆出現問題,這個觀察值得被處理,只是處理的場域是一對一的輔導對話,不是拿著上一份 postmortem 當呈堂證供。
三、AI 系統的復盤需要新的維度:版本、評估、安全檢查。
傳統系統的 postmortem 主要關注「API 層、資料庫層、網路層」。AI 系統則需要額外追蹤「這個模型版本的輸出為什麼是錯的?evaluation 為什麼沒有在 release 時捕捉?」。更困難的是,evaluation 系統本身也會有盲點,Meta 的案例就是提醒:即使是精心設計的身份驗證,也可能被欺騙。
即刻行動(下一次 incident 發生時)
本週的建立工作
下季度的文化改進
第四點「量化進步」看起來最直觀,卻是最容易做錯的一項,因為「同類故障」這四個字本身就需要一個明確的判準,而不是靠印象比對。判斷兩次故障算不算「同一類」,可以問一個具體的問題:如果上一次的 Action Item 真的被完整執行了,這一次的故障還會發生嗎? 如果答案是「不會」,代表這是真正意義上的重演,Action Item 兌現率的問題比根因分析的問題更值得優先處理;如果答案是「會,因為這次是完全不同的觸發路徑」,即使表面症狀相似(例如都是 latency 上升),也不該被算成同一類重演,硬要湊在一起計數,反而會讓「重演頻率」這個指標本身失去意義——變成只是在比較「症狀」,而不是在比較「根因有沒有真的被消除」。
Day 27 把事故後的證據與學習收好:timeline、runbook 更新、postmortem 與 action item。
這些證據不是寫完就結束的文件,它們是下一篇要用的原始材料。一份寫清楚「哪個版本欄位當時沒被記錄」的 postmortem,會直接變成 Day 28 的變更管理系統要補的欄位;一份寫清楚「evaluator 判斷標準本身有誤判」的 Unknowns,會直接變成 Day 28 的 release gate 要新增的檢查項。復盤做得紮實,下一篇要處理的問題清單幾乎是現成的。
Day 28 要往前推一步,處理事故發生之前的問題:prompt、model、retrieval index 與 tool schema 的變更,要怎麼像 code 一樣走版本、評估、漸進發布與 rollback,讓下一次故障在還沒發生前就少一個觸發點。
下一篇會接著處理「CI/CD & AI Change Management」。
這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.