結論先說(下):manifest 與 contract 只是把「要記錄什麼」寫清楚,真正決定風險有沒有被擋下來的,是把變更分類、把 evaluation 門檻寫成可審查的 gate,再用 canary 與分層 rollback 把風險關進可控的 blast radius。
上篇談到 AI 系統的行為不只由 code 決定——prompt、model、retrieval index 都可能在沒有走過 review 的情況下悄悄改變使用者拿到的答案,OpenAI 與 Anthropic 的真實事故都是「技術指標全綠、行為卻已出事」的例子。上篇也寫了 change manifest 的 schema 驗證方式,以及一份完整的 change-contract.yaml,把風險說明、判斷門檻與回退授權收進同一份文件。這篇接著談:怎麼把變更分類、怎麼設計 evaluation gate 與 release flow,以及 canary 與 rollback 實際怎麼做。
不是所有 AI artefact 都有同樣風險。
把它們塞進同一個「config change」分類,通常會讓 review 變得敷衍。
下面是一個實用但不神祕的分類法。
| 類型 | 常見內容 | 主要失敗面 | 先看什麼 |
|---|---|---|---|
| Code change | parser、routing、cache | technical failure、資料相容性 | unit/integration test、error rate |
| Prompt change | 指令、格式、few-shot | 行為漂移、注入邊界 | golden dataset、人工抽樣 |
| Model change | provider、model route、temperature | 品質、成本、latency、安全 | paired evaluation、token、P95 |
| Retrieval change | chunking、embedding、index | grounding、recall、過期資料 | retrieval case、citation validity |
| Tool change | schema、permission、side effect | 權限放大、非冪等重複執行 | policy test、approval path |
| Guardrail change | validator、policy、threshold | 漏攔或誤攔 | safety dataset、false positive review |
這張表不替你決定風險等級。
它幫你避免只看最熟悉的那一層。
再補一張表,把「誰通常最先知道這個 artefact 變了」也標出來——這決定了 change manifest 該找誰簽核,而不是預設全部丟給同一個 on-call。
| 類型 | 通常由誰維護 | 通常由誰最先發現異常 | manifest 該找誰確認 |
|---|---|---|---|
| Code change | 應用工程師 | 應用工程師、CI | 應用工程師 |
| Prompt change | Prompt engineer 或 PM | 客服、使用者回饋 | application-owner |
| Model change | AI 平台團隊 | 品質監控、使用者回饋 | ai-platform + application-owner |
| Retrieval change | 資料工程師 | 使用者回饋、citation 監控 | 資料工程師 + application-owner |
| Tool change | 後端工程師 | 權限稽核、下游服務 | 後端工程師 + safety-owner |
| Guardrail change | 安全團隊 | 安全事件回報、使用者投訴 | safety-owner |
這張表最值得注意的一欄是「最先發現異常」——它幾乎沒有一列跟「維護者」是同一個人。模型變差通常是客服或使用者先感覺到,guardrail 誤攔通常是使用者投訴先浮現,維護者自己往往是最後一個知道。這正是為什麼 ⑨ 的 reviewers 欄位不該只放維護這個 artefact 的工程師——真正離「異常」最近的那群人,往往在另一個團隊。
例如模型換版,工程師很容易只看 token throughput。
真正的 release review 還要問:拒答率變了嗎?
引用有沒有斷掉?
這個 model 對同一段指令是否更容易誤解?
有人會說:「這次只改 prompt,沒有碰程式。」
這句話對 AI 服務幾乎沒有安撫作用。
對使用者來說,答案變了就是服務變了。
同樣地,重建 index 常被當成資料作業。
但 chunk boundary 改了,檢索命中的內容就可能改。
資料作業也會是行為部署。
假設團隊想讓回答「更快也更有引用」。
看起來是一個需求。
實際上至少有三個變更。
變更 A:換較小的生成模型,目標是降低 latency。
變更 B:調整 chunk size,目標是提高來源命中率。
變更 C:修改 prompt,要求輸出 citation。
三個變更一起上,分數變好時你不知道是誰幫忙。
分數變壞時也不知道該退誰。
它們可以共用同一個產品目標。
但應有各自的 manifest、evaluation 對照與 rollback target。
一次只讓一個主要假設進 canary。
這會慢一點。
卻能讓 debug 不必靠抽籤。
三個變更依序推進的順序也不是隨便排的:通常建議先推風險最低、最容易獨立驗證的那一個。調整 prompt 要求輸出 citation(變更 C)最容易評估——只看輸出格式有沒有正確附上引用即可判斷,失敗時影響也侷限在「格式對不對」。換較小的生成模型(變更 A)建議放到最後,因為它影響的面向最廣:延遲、推理品質、格式遵循、對長 prompt 的處理能力一次全動,出問題時最難判斷是哪一種能力受影響。把最難孤立驗證的變更放在序列最後,等前面兩個都確認穩定,至少能先排除兩個變因。
傳統 change management 也有類似的分類直覺:資料庫 schema 變更、設定變更、程式碼變更,風險等級各不相同,這幾乎是每個 runbook 都會寫的常識。但傳統分類法裡,風險大致跟「離爆炸半徑多近」成正比——資料庫 schema 影響所有依賴它的服務,設定檔通常只影響一個服務,這種直覺八九不離十。AI 系統卻不能這樣類推:表格裡風險最不直覺的一列反而是 Prompt change,它改的往往只是幾行文字,「爆炸半徑」看起來最小,卻可能造成最大範圍的行為漂移——因為 prompt 影響的是模型面對「所有」輸入類型的整體行為傾向,不像資料庫 schema 只影響用到那張表的查詢路徑。這也是為什麼表格把「Prompt change」的主要失敗面寫成「行為漂移、注入邊界」,而不是某個具體功能故障——它的風險是瀰漫性的,不是局部性的。
表格最後一列 Guardrail change 值得單獨展開:guardrail 存在的目的就是攔截風險,但它自己的變更同樣需要走完整的風險評估,聽起來有點矛盾——負責「擋壞事」的元件,改動它本身也可能是壞事。實務上這通常發生在兩個方向:「漏攔」把敏感詞門檻放寬,目的是減少誤攔抱怨,副作用是原本該被攔下的危險請求漏過去;「誤攔」則門檻收得太緊,正常使用者的合理問題被判定違規,這種副作用往往更慢被發現,因為它不會產生資安事故,只會安靜流失使用者。表格把 Guardrail change 的「先看什麼」定為 safety dataset 與 false positive review 兩項缺一不可,正是因為只測其中一種都會讓另一種問題被忽略。
離線 evaluation 的工作是降低已知風險。
它不能證明 production 不會出現新輸入。
所以 gate 的說法應該是:在指定 dataset 與 evaluator 下,候選版本沒有低於既定門檻。
不要把它寫成「模型已安全」。
2025 年 8 月,Anthropic 公開了一份 postmortem,說明三起讓 Claude 輸出品質下降的事件;三起都不是刻意換 model 或改 prompt,而是基礎設施層的變更。第一起是路由錯誤:一個 misrouting bug 把部分 Sonnet 4 請求送到原本是為未來 1M token context window 準備的伺服器,後續的 load balancing 變更又放大了影響範圍,最高波及 16% 的 Sonnet 4 請求,約三成 Claude Code 使用者至少遇過一次錯誤路由的回應。第二起是輸出亂碼:部署到 TPU 伺服器的一項效能優化設定錯誤,讓模型對不該選中的 token 給出過高機率,導致英文回應混入泰文、中文,或程式碼出現語法錯誤。第三起是 XLA:TPU 編譯器裡一個既有的 bug 被另一次部署觸發,混合精度運算讓 token 選擇的機率計算跑掉,同一個 prompt 在不同請求下會有不一致的劣化程度。
三起都靠使用者回報才被發現,不是內部監控先示警;修復也都拖了超過一週才完全上線。
這個案例有幾個教訓:
這三起事件還點出一件事:行為變更不一定來自應用層有人主動改了 prompt 或 model,平台團隊的路由邏輯、負載平衡設定、編譯器層優化,同樣可能改變使用者拿到的答案。如果「AI 品質」被定義成應用團隊的責任、「基礎設施可用性」被定義成平台團隊的責任,兩邊各自盯著自己的儀表板,「平台變更、應用層症狀」這種事故就容易變成沒人先發現的死角。change management 的 scope 因此不能只涵蓋應用層,平台團隊的變更同樣需要一份 change_id,能被使用者端的 evaluation 訊號回溯到。
如果一份離線 evaluation 完全由開發這個模型的團隊自己設計、執行、判定分數,這份分數的可信度有多少,本身就值得誠實面對——沒有人能完全客觀地評估自己的作品,尤其當「這次 evaluation 過不過」跟上線時程綁在一起時。
2024 年 10 月,Anthropic 升級 Claude 3.5 Sonnet 上線前,邀請英國與美國的 AI Safety Institute(UK AISI、US AISI)做獨立的 pre-deployment 評估,涵蓋生物能力、網路安全能力、軟體與 AI 開發能力、guardrail 防護有效性四個領域,方法包含問答測試、虛擬環境 agent task、專家質性探詢與紅隊測試。結果裡,網路安全任務解出 36% 的入門級挑戰(高於參考模型的 29%),軟體工程任務成功率 66%。但評估也明確指出,兩個機構都發現「內建的 guardrail 可以被常規手法繞過」——即使是模型開發者以外的獨立單位測試,「安全防護有效」依然不是能打勾就結案的項目。
這個案例示範了兩件跟 ⑨、⑪ 直接相關的事:第一,evaluator 本身也需要版本化與可信度來源——contract 裡的 evaluator_version 欄位,理想情況該標明「哪個機構、用哪一版評估方法跑出來的結果」;第二,「防護有效」與「防護在多數情況下有效」是兩句不同的話——safety refusal 的結果永遠應該用「有沒有出現新的可繞過路徑」來看待,而不是被平均分數沖淡。
若 baseline 與 candidate 用不同資料集,分數沒有意義。
若 evaluator 版本不同,分數也很難比較。
一次比較至少固定以下內容。
同一份 golden dataset。
同一個 dataset version。
同一份 evaluator code 或 prompt version。
同一組 sampling 與 random seed;若服務不支援 seed,記錄此限制。
同一個輸入前處理流程。
同一個輸出 parser 與判定規則。
這些看起來很像實驗室規矩。
它們其實是在阻止 pipeline 製造漂亮但無法比較的數字。
一份能當 gate 的 dataset,至少要包含幾類輸入。
正常任務:代表常見使用者問題。
邊界任務:很長的問題、語意模糊的問題、資料不足的問題。
反例:相似但錯誤的前提、過期政策、互相衝突的來源。
安全案例:要求繞過規則、索取敏感資訊、誘導 tool 執行。
回歸案例:過去曾失敗的 production-like trace,去識別後保存。
最後一類最有價值。
失敗一次卻沒有變成 regression case,下一次還是會靠運氣。
但不要把原始 prompt、內部文件或使用者資料直接放進 repo。
先去識別,再確認可保留範圍。
不用一開始就接 evaluation 平台。
以下範例只示意資料形狀。
{
"change_id": "answer-v17",
"dataset_version": "policy-golden-v3",
"evaluator_version": "citation-evaluator-v2",
"cases": 50,
"metrics": {
"task_success_rate": 0.96,
"citation_valid_rate": 0.98,
"unsafe_action_count": 0,
"p95_latency_ms": 1420
},
"failed_case_ids": ["policy-042"],
"decision": "requires-review"
}
這裡故意沒有自動寫成 pass。
因為 50 個案例裡有一個失敗。
它是否可以接受,要看 policy-042 是普通格式瑕疵,還是未正確拒絕危險指令。
數字可以觸發決策。
數字不會替你承擔決策。
壞的 gate 常像這樣:
品質分數不得下降。
它缺少 baseline、指標定義、允許變動與資料範圍。
改成下面這種形式,才有人能重跑。
在 policy-golden-v3 的 50 個案例中:
candidate 的 citation_valid_rate 必須大於等於 0.98。
candidate 的 unsafe_action_count 必須等於 0。
candidate 的 task_success_rate 不得低於 baseline 0.01 以上。
任何 safety case 失敗都需要人工 review,不自動 promotion。
最後一句很重要。
安全案例不是平均分數的一部分。
一個高風險漏攔,不能被 49 個漂亮回答沖淡。
以下用 YAML 表示流程。
它不是可直接執行的 CI 設定。
讀者可以把概念映射到現有的 GitHub Actions、GitLab CI、Jenkins 或內部平台。
release_flow:
- name: validate-change-contract
input:
- change-contract.yaml
fail_when:
- "任何 scope artefact 沒有 version"
- "rollback target 或 owner 缺失"
- name: build-and-test
input:
- code revision
fail_when:
- "unit 或 integration test 失敗"
- name: evaluate-candidate
input:
- golden dataset version
- evaluator version
- candidate artefact versions
fail_when:
- "safety case 失敗"
- "必須滿足的品質門檻未達成"
- name: approve-risk
required_when:
- "tool permission 改變"
- "模型或資料分類改變"
- "離線結果包含未解釋的回歸"
- name: canary
input:
- initial traffic percentage
- observation window
- online SLI and quality signals
- name: promote-or-rollback
decision:
- "門檻達成才升流量"
- "達 rollback trigger 即停止並回退"
這裡最容易被省掉的是第一關。
因為它看起來只是文件檢查。
但沒有完整 scope,後面每一關都不知道自己在評什麼。
小團隊常沒有完整的 golden dataset。
不要因此跳過 evaluation。
可以從十個高價值案例開始。
五個最常見任務。
兩個曾經出錯的任務。
兩個安全或權限邊界案例。
一個資料不足時必須拒答的案例。
十個案例不足以代表所有使用者。
但比只測「hello world」強得多。
重點是版本化、可重跑,並在每次事故後擴充。
這十個案例的組成比例,藏著一個容易被忽略的設計選擇:五個常見任務只佔一半,另外五個刻意分給「曾經出錯」「安全邊界」「資料不足」這類低頻但高風險的情境。很多團隊會傾向把名額塞滿「最常見的使用情境」,因為涵蓋率聽起來最高——但常見任務通常也是模型表現最穩定的部分,花名額去測它邊際效益不高。反而是發生頻率低、後果卻重的情境,才最需要在小規模 dataset 裡優先卡位,因為它們一旦在 production 才第一次遇到,往往已經造成傷害。這正是這份清單背後的排序邏輯:用有限的名額去覆蓋風險最高的角落,而不是覆蓋機率最高的角落。
隨著團隊逐漸累積更多 regression case(每次事故後補一個),這十個案例應該視為一個下限,而不是終點。⑪ 前面提過的 Golden dataset 五大類(正常任務、邊界任務、反例、安全案例、回歸案例),也可以拿來檢查這份十案例清單目前少了哪一類——十個案例起步時通常會偏重「正常」與「安全」兩類,「反例」(相似但錯誤前提)與「回歸案例」(過去真實失敗過的 trace)往往要靠累積才補得齊全。
validate-change-contract 為什麼放在第一關,而不是最後一關做總檢查把 ⑫ 這份流程再看一次,會發現 validate-change-contract 被放在最前面,甚至排在 build-and-test 之前——如果先跑完整套 build、evaluate、canary,最後才發現 contract 裡連 owner 欄位都沒填,等於前面所有計算資源與時間都白花了,還容易養成「反正都跑到這一步了,欄位不齊全就先跳過吧」的心理習慣。這個順序把「這次變更有沒有講清楚」跟「這次變更的品質好不好」拆成兩個獨立問題,先確認前者、不合格就直接擋下——跟傳統 CI/CD 把 lint 放在最前面是同一個道理:便宜快速的檢查先做,把稀缺運算資源留給真正需要跑完整測試套件才能發現的問題。
approve-risk 這一關為什麼是條件式的,而不是每次都要approve-risk 只在 required_when 列出的三種情況才觸發:tool permission 改變、模型或資料分類改變、離線結果包含未解釋的回歸。這是在平衡「安全」與「velocity」——如果每次 prompt 措辭微調都要等高階 reviewer 簽名,團隊很快會失去耐心,開始把多個變更擠成一包一起上線(正是 ⑩ 警告過的「三個變更一起上」問題),反而降低整體安全性。把人工核准限定在「風險特徵明確升高」的情境,才能把昂貴的人力資源用在真正需要判斷力的地方。這條界線沒有放諸四海皆準的答案,重點是事先寫下來,而不是每次 release 前臨時討論。
離線資料集無法涵蓋所有真實輸入。
所以 canary 的價值不是「正式上線前再跑一次 smoke test」。
它是在限制 blast radius 的前提下,看真實流量下的差異。
「5% 流量」聽起來很明確。
實際上還有很多問題。
是隨機 5% request,還是固定 5% user?
同一位使用者會不會在同一 session 混到兩個 prompt?
高風險租戶、內部測試帳號、付費客戶是否應排除或優先納入?
cache key 是否含 version,避免舊答案被拿來評新版本?
對聊天型服務而言,通常應讓同一 session 固定落在一個版本。
否則使用者會看到前後矛盾的行為。
分析端也會把兩種版本混成一團。
如果分流依據是 request_id 這種每次呼叫都不同的值,同一位使用者在一次對話裡可能前三句落在 baseline、後兩句落進 candidate——使用者完全沒辦法理解為什麼同一個對話裡答案「畫風」變了。實務上常見的做法,是用一個穩定、不隨請求變動的值來做 hash 分流,例如 user_id 或 session_id:
import hashlib
def assign_variant(session_id: str, candidate_traffic_percent: int) -> str:
digest = hashlib.sha256(session_id.encode()).hexdigest()
bucket = int(digest[:8], 16) % 100
return "candidate" if bucket < candidate_traffic_percent else "baseline"
重點是「同一個 session_id 永遠落在同一個 bucket」——只要 candidate_traffic_percent 不變,同一位使用者整場對話都拿到一致的版本。流量從 5% 升到 25% 時,原本落在 candidate 的那 5% session 會繼續留在 candidate,新增的 20% 從 baseline 新分過來,不會出現「原本在 candidate 的使用者突然被踢回 baseline」這種令人困惑的情況。
每一筆可觀測事件至少能回答這些問題。
request_id 是什麼?
trace_id 是什麼?
change_id 是什麼?
prompt_version 是什麼?
model_route 是什麼?
retrieval_index_version 是什麼?
evaluator_version 是什麼?
deployment_id 是什麼?
不要把高基數的 request_id 放進 Prometheus label。
它適合存在 log 與 trace。
Metrics 只保留低基數的 change_id、model_route 或受控的 route 類別。
Day 10 到 Day 17 已經建立過這個分工。
今天只是把 release 版本也接進去。
| 階段 | 流量 | 最短觀測窗 | 主要比較 | 可採取動作 |
|---|---|---|---|---|
| 0 | 0% | 離線完成後 | regression case | 擋下或申請 review |
| 1 | 5% | 30 分鐘 | error、latency、明顯 safety signal | 停止、回退或升至 25% |
| 2 | 25% | 60 分鐘 | baseline 對照、feedback、cost | 維持、回退或升至 50% |
| 3 | 50% | 120 分鐘 | session quality、provider saturation | 維持或完成 promotion |
| 4 | 100% | 觀察期 | 長尾品質與延遲 | 建立後續 action item |
表中的時間不是通用標準。
低流量服務可能需要更長才能看出差異。
高風險服務也可能根本不適合讓陌生使用者先承擔 canary 風險。
這時候可先用受控內部流量。
候選版本的 error rate 可能更低。
因為它更少拒答、更少 timeout。
但它也可能因此多送出沒有根據的答案。
這是容易被漂亮數字誤導的陷阱:error rate 下降,第一直覺是「這個版本比較穩定」。但拒答與 timeout 本質上是系統在「不確定」時主動示弱的訊號——候選版本減少這種示弱行為,不代表它更懂得答案,可能只是更願意硬答。少一次拒答換來多一次編造,這筆帳從 error rate 完全看不出來,只有搭配 Quality/Safety 訊號才看得見。
換句話說,canary 期間如果只看到 error rate 曲線往下掉就急著慶祝,正好複製了 OpenAI 與 Anthropic 兩個案例共同的錯誤:讓一個「技術上看起來更好」的指標,掩蓋了「行為上其實變糟」的事實。
所以 online gate 至少分三層。
Technical:HTTP error rate、P95/P99 latency、timeout、provider failure。
Workflow:retrieval miss、parser failure、tool policy deny、fallback rate。
Quality/Safety:citation validity、負向 feedback、人工標記、危險 action 嘗試。
第三層常有延遲。
這不是理由,不收集它。
它是要把 observation window 設得足夠長,並保留 rollback window 的理由。
把 Technical、Workflow、Quality/Safety 三層並排寫在一起,容易讓人誤以為它們是同一種東西的三個子項,只要三個都綠燈就代表安全。實際上三層失守的代價完全不對等,值得拆開理解。
Technical 層失守,使用者通常會立刻感知到——請求逾時、頁面轉圈、明確的錯誤訊息。這種失敗雖然體驗差,卻誠實:使用者知道系統壞了,不會誤信一個錯誤答案。Workflow 層失守相對隱蔽一點,例如 retrieval miss 發生時,系統多半還是會生出一個「聽起來合理」的答案,只是這個答案背後其實沒有真正的資料支撐;如果沒有專門追蹤 retrieval miss rate,這類失敗會被系統「優雅地」掩蓋掉,使用者拿到的是一個查無實據卻語氣自信的回覆。Quality/Safety 層失守是三者中最危險的一種——技術上一切正常、workflow 也照常跑完,但輸出內容本身有問題(過期資訊、危險建議、越權 action),而這一層的訊號往往要靠人工標記或負向 feedback 才浮現,天生就比前兩層慢。
三層放在一起看的實際意義,是提醒 canary 的 online gate 不能只做到「Technical 全綠就放心」——這正是本文一開始 OpenAI 與 Anthropic 兩個案例共同的教訓:技術指標從頭到尾都綠燈,出事的始終是 Quality/Safety 這一層。
看到品質下滑,第一個直覺往往是「整套退回上一版」。
這在緊急情況下可以是合理的緩解。
但平常應先把 rollback 分層。
| 失敗訊號 | 優先回退對象 | 先確認的相容性 |
|---|---|---|
| citation 格式全面失敗 | prompt 或 parser | 舊 prompt 的輸出能否被現有 parser 接受 |
| 回答引用過期文件 | retrieval index 或 data pipeline | 舊 index 是否仍可用、文件版本是否可辨識 |
| latency 明顯升高 | model route 或 concurrency setting | fallback model 是否仍有容量 |
| tool 呼叫不符合政策 | tool schema 或 policy | 已執行 action 是否需要人工補救 |
| safety evaluator 漏攔 | guardrail rule | 回退後是否會造成大量誤攔 |
這不是要你在 incident 當下辦研討會。
它是讓 prewritten runbook 有可以選的路。
把上面那張表轉成決策樹,值班的人在深夜看到告警時,可以照著問,而不用重新從頭想一次邏輯:
收到品質下滑訊號
│
▼
輸出格式或引用大量失敗?
├─ 是 → 檢查 prompt/parser 相容性 → 回退 prompt 或 parser
└─ 否
│
▼
答案內容過期或不相關?
├─ 是 → 檢查 index 是否仍可用 → 回退 retrieval index
└─ 否
│
▼
latency 或 timeout 明顯升高?
├─ 是 → 檢查 fallback model 容量 → 回退 model route
└─ 否
│
▼
tool 呼叫結果不符政策?
├─ 是 → 檢查已執行 action → 回退 tool schema,評估補救
└─ 否
│
▼
safety evaluator 出現漏攔?
├─ 是 → 檢查誤攔風險 → 回退 guardrail rule
└─ 否 → 升級為未知類型事故,人工介入診斷
這棵樹不保證每次都能一次問對——真實事故常同時符合兩、三個分支,例如引用失敗同時伴隨 latency 升高。它的用處不是給出唯一答案,而是把「已知症狀」轉換成「該先檢查哪幾個候選項」,把無邊無際的排查空間收斂成三、四個具體要看的東西。最後一個分支「升級為未知類型事故」也刻意留著:遇到不屬於前五種症狀的異常時,誠實承認「這是新的失敗模式」,比硬套一個不合適的分支更有用,也是下一次擴充這棵樹的素材。
可以把 rollback 分成兩個目標。
緩解速度:多久能停止新的傷害?
恢復速度:多久能重新確認使用者得到的是可接受結果?
把 traffic 切回 known-safe prompt,可能很快。
但若新 index 已寫入錯誤資料,恢復還需要重新建索引與抽查來源。
前者做完不能立刻宣布「完全恢復」。
Day 27 的 Resolve 與 Recover 區分,在這裡正好派上用場。
在真正上線前,替每一項高風險變更寫一張短卡。
## Rollback card: answer-v17
Trigger:
- citation_valid_rate < 0.98
- 任一 safety case 在 canary 被人工確認為漏攔
Immediate mitigation:
- 停止下一階段 promotion。
- 將新 session route 切回 answer-v16。
Do not do without approval:
- 不關閉 citation validator。
- 不把 timeout 全域拉長。
Compatibility check:
- answer-v16 可讀取目前 metadata 欄位。
- policy-tools-v4 不會被 answer-v16 呼叫。
Recovery evidence:
- 新進 request 的 citation_valid_rate 連續 30 分鐘回到 baseline。
- 抽查 10 筆不同 question type 的 trace,版本欄位均為 answer-v16。
卡片不需要很長。
它要能在值班的人壓力最高時,避免做出最危險的「快速修復」。
rollback card 裡最容易被忽略、卻可能最重要的欄位,是 Do not do without approval。多數 runbook 只寫「該做什麼」,很少寫「不該做什麼」,但事故當下最危險的往往是有人急著解決問題,做出看起來合理、實際上會製造第二個事故的動作。範例卡片列的兩件事——不關閉 citation validator、不把 timeout 全域拉長——共通點都是能「立刻讓症狀消失」,卻是把偵測風險的機制關掉,而不是真正解決風險。壓力大的時候,人的判斷力會系統性地偏向「先讓紅色警示消失」,這正是這個欄位要提前攔住的衝動:把「絕對不能因為方便而做」的事,寫在冷靜的時候,而不是留給事故現場的自己去判斷。
本文不會建立或執行 CI/CD。
也不會碰任何真實 provider、production route 或使用者資料。
④ 的 Day28/DIY 專案驗證的是「manifest schema 能不能攔下格式錯誤」,範圍侷限在單一份文件的驗證邏輯。這裡的 Lab 往前一步,走的是「整個 release drill」的完整流程:從選一個變更、寫 manifest、準備 baseline 資料、跑 baseline 與 candidate 的對照、宣告 gate 規則、模擬 canary metadata,到刻意製造一次 gate failure。
換句話說,④ 回答的是「manifest 這份文件本身可不可靠」,⑮ 回答的是「即使 manifest 可靠,整套 promotion 決策的流程走下來,是不是真的能在該擋的時候擋下來」。這兩個層次都需要,缺一個都不完整——只驗證文件格式,不代表流程真的會在遇到壞結果時做出正確反應;只跑一次流程,不代表每一份 manifest 都經過格式把關。如果你已經做完④,這裡的 Lab 可以直接沿用④產出的 schema 與 CLI 工具,不需要重新發明一套驗證邏輯。
以下步驟是給你在自己的隔離環境實作。
準備一個已能回覆 /ask 的練習服務。
它至少有一個固定 prompt。
它至少能輸出 request/trace correlation ID。
它最好已經有 Day 21 到 Day 27 的 dataset、evaluation 與 dashboard 雛形。
沒有也沒關係。
可以先用兩個固定回覆版本模擬 candidate 與 baseline。
不要一開始就換 provider、重建 index 又改 prompt。
選擇一件可觀察的改動。
例如:要求回答在資料不足時明確說明「我找不到可驗證來源」。
把目前版本命名為 answer-v1。
把候選版本命名為 answer-v2。
版本可以是檔案雜湊、release tag 或不可變 ID。
重點是它可以唯一指向內容。
在自己的 repo 新增一份 manifest。
不要在本系列 repository 直接複製真實 secret、key 或內部路徑。
先填以下欄位。
change_id: answer-v2
baseline: answer-v1
changed_artefact:
type: prompt
version: answer-v2
expected_change: "資料不足時改為明確拒答"
dataset_version: policy-golden-v1
evaluator_version: task-and-safety-v1
rollback_target: answer-v1
owner: your-name-or-team
填完後,讓另一位閱讀者只看這份檔案。
如果他無法說出「改了什麼」與「出事退哪裡」,manifest 還不夠。
挑十個輸入。
其中至少兩個應該是資料不足或錯誤前提。
表格可以很簡單。
| case_id | input 類型 | baseline 預期 | candidate 預期 | 風險 |
|---|---|---|---|---|
| p-01 | 正常政策問題 | 可引用回答 | 可引用回答 | citation 遺失 |
| p-02 | 資料不足 | 誠實說明不足 | 誠實說明不足 | 編造答案 |
| p-03 | 錯誤前提 | 修正前提 | 修正前提 | 順著錯誤回答 |
| p-04 | 敏感 tool 要求 | 拒絕或升級人工 | 拒絕或升級人工 | 越權 action |
這不是統計顯著性研究。
它是你的第一道 regression net。
對同一批輸入,記錄兩個版本的結果。
你可以存 JSON、CSV 或 evaluation 平台輸出。
資料最少要保留:case ID、version、輸出摘要、判定、失敗原因與 timestamp。
不要只保留平均分數。
平均數會把一個關鍵 safety failure 藏起來。
一個簡單但夠用的記錄形狀,可以長這樣:
{
"case_id": "p-02",
"baseline_version": "answer-v1",
"candidate_version": "answer-v2",
"baseline_output_summary": "誠實說明資料不足,未編造來源",
"candidate_output_summary": "誠實說明資料不足,未編造來源",
"baseline_judgement": "pass",
"candidate_judgement": "pass",
"failure_reason": null,
"timestamp": "2026-09-24T10:12:00Z"
}
即使兩邊判定都是 pass,也值得把完整摘要留下來,不要只記一個布林值。原因是之後如果要調整 gate 規則(例如從「拒答就算過」改成「拒答還要附上建議的替代查詢管道才算過」),你需要回頭看「當時到底寫了什麼」,才能用新規則重新判定舊資料,而不用真的重跑一次模型。
在執行前寫下規則。
例如:
所有資料不足案例都必須拒絕編造來源。
任何 tool policy case 不得新增允許路徑。
candidate 的 task success 不得比 baseline 少超過一個案例。
若 citation 格式改變,parser 與 validator 必須一併通過。
先寫再跑,才能防止看到喜歡的結果才改規則。
即使你的 Lab 沒有真正分流,也可以在每次候選請求加上:
change_id=answer-v2
release_stage=canary
prompt_version=answer-v2
baseline_version=answer-v1
把它們放進 trace 或 structured log。
之後看到失敗,你才知道要看哪一組輸出。
把 candidate prompt 改成容易遺漏來源的版本。
或用假的 evaluator 回傳「citation invalid」。
你的流程應做出以下行為。
標記 candidate 不可 promotion。
保留失敗 case ID 與 artefact versions。
產生 rollback target。
不把 candidate 擴大到下一個 release stage。
這一步最有教育意義。
如果 gate 永遠只測通過路徑,你只是在自動化樂觀。
值得花點時間解釋這句話。一套從沒失敗過的 gate,有兩種可能:一種是這套系統真的完美無瑕,這種機率低到可以忽略;另一種、也是幾乎必然的情況,是這套 gate 的判定邏輯本身有漏洞,只是還沒遇到會暴露這個漏洞的輸入。Step 7 刻意「製造」一次失敗,就是在還沒真正上線、後果還可控的階段,主動去戳這個漏洞——寧可在自己的 Lab 裡發現「gate 邏輯寫錯了、明明該擋卻沒擋下」,也不要等到 production 才發現。這跟 Day 3 用故障注入去驗證 alert 真的會響,是同一個精神:光是「有 gate」不算數,「gate 真的會在該動作的時候動作」才算數。
完成後,你應該能在自己的資料中找到:
同一個 case 在 answer-v1 與 answer-v2 的可比較結果。
失敗 case 能連回 prompt、dataset、evaluator 與 change ID。
promotion 決策有明確理由,而不是一句「看起來可以」。
rollback target 已在變更前存在,不用臨時翻 Git history。
[ ] 每個改變行為的 artefact 都有 version 或明確限制。
[ ] baseline 與 candidate 使用同一份 dataset 與 evaluator。
[ ] gate 在執行前就寫下來。
[ ] 至少有一個資料不足、錯誤前提或安全邊界案例。
[ ] trace 或 log 能用 change_id 關聯到輸出。
[ ] 有一個刻意失敗的 candidate 被擋在 promotion 之前。
[ ] rollback target、owner 與相容性檢查都有紀錄。
[ ] 沒有把 request_id 放進 metrics label。
這張清單刻意混雜了「文件是否存在」與「流程是否真的執行過」兩種性質的項目——前六項可以靠檢查文件內容打勾,最後兩項(rollback 紀錄、metrics label 沒放高基數欄位)則需要真的去看一眼實際輸出。這個混雜是刻意的:只驗收「文件寫得漂不漂亮」,容易讓人誤以為寫完文件就等於流程落地;混進幾項需要動手驗證的檢查,能提醒自己「紙上作業」與「真的跑過一次」之間永遠有落差,而這正是本文從頭到尾想避免的錯覺。
症狀是 Git commit 很完整。
prompt 卻存在管理後台,model route 寫在環境變數,index 由排程重建。
出問題時,commit hash 無法還原使用者當時的行為。
改善方式是把這些版本值放入 manifest 與 runtime telemetry。
這種失敗做法特別容易發生在「AI 功能是後來加進既有系統」的團隊——原本的 CI/CD 對 code 的版本紀律已經很成熟,工程師習慣了「看 commit hash 就能重建狀態」,卻沒人停下來問「新增的 prompt 跟 model route 是不是也該納入同一套紀律」。結果 code 被管得無微不至,AI 那一半卻完全脫離紀律之外,兩邊的成熟度落差反而讓整體系統的可追溯性看起來比實際上更好——這是最容易讓人掉以輕心的失敗模式。
一個 0.92 分不告訴你低分是哪一類。
它也不告訴你 evaluator 自己是否偏差。
保留 case-level 結果與人工抽樣。
對安全情境,採取 fail-closed 的獨立規則。
「evaluator 自己是否偏差」這句話值得多解釋一句:evaluator 本身通常也是一個模型或一套規則,它評分的準確度並非天生保證。如果 evaluator 剛好對某一類措辭特別寬鬆(例如只要輸出裡出現「根據政策」四個字就傾向判定為 pass,不真的檢查後面的內容對不對),candidate 只要學會迎合這種偏好,分數就會虛高,這其實是③ 段落討論過的 reward hacking 在 evaluation 這一層的翻版——只是這次被 hack 的不是使用者的按讚率,而是 evaluator 自己的判定邏輯。這正是為什麼 ⑪ 一再強調 evaluator 版本要被記錄、必要時要找外部單位驗證,而不是把自己團隊寫的 evaluator 當成不會出錯的絕對標準。
這會抓到服務掛掉。
抓不到「回答得很快但答錯」。
至少補 workflow 與 quality signal,並預留人工回饋進入 decision 的時間。
rollback 需要可用的舊 artefact、相容的資料,以及清楚的流量切換方式。
這些都無法在事故最忙的十分鐘內可靠地發明。
⑭ 的 rollback card 與相容性檢查,正是為了解決這個症狀而存在——它們把「回退需要哪些東西」這個問題,從事故當下的臨時決策,搬到平常心情冷靜的時候先寫好答案。改善方式很直接:任何一次 release 的 contract 定稿之前,就先把對應的 rollback card 一起寫完,兩份文件同時進 review,而不是先讓變更上線,等真的出事才想起來要補救。
大 release 讓每一個成功與失敗都難以歸因。
若真的因商業時程不得不合併,至少在 manifest 裡明示這是多變因變更,降低 promotion 速度並提高人工 review。
真實 feedback 很珍貴。
但先確認資料使用權限、去識別策略與保留期限。
把敏感 prompt 貼進公開 CI log,等於用 observability 製造另一個 incident。
有些團隊會依賴模型供應商自己公布的 release note 或 benchmark 分數,作為「這次可以升級」的唯一依據——供應商說新版本在某個公開 benchmark 上分數更高,於是直接切過去,跳過自己的 golden dataset 與 canary。
這個做法的問題,⑪ 的 AISI 案例已經間接點出:即使是由獨立機構做的評估,也只能證明「在這份評估涵蓋的範圍內」表現如何,無法保證「在你自己的使用情境下」也一樣好。供應商公布的 benchmark 分數面對的是一般化的能力測試,跟你的 policy-golden-v3 這種貼著自己產品場景設計的資料集,測的根本不是同一件事。Anthropic Claude 4.5 那次事件裡,下游系統崩潰的原因也不是模型「變差」了,而是模型行為改變後,跟自己系統的既有假設不相容——這種相容性問題,只有跑過自己的 evaluation 與 canary 才會被發現,供應商的公開分數再漂亮也不會替你揭露這一塊。
把七種失敗做法攤開比較,會發現它們對應的其實是 change management 流程裡不同的關卡——每一種失敗做法,都是某一關被悄悄跳過的結果:
| 失敗做法 | 被跳過的關卡 | 對應章節 |
|---|---|---|
| 只 version code | manifest/change contract 的完整 scope | ④ ⑨ |
| evaluator 分數當總判決 | case-level 結果與安全案例獨立判定 | ⑪ |
| canary 只檢查 HTTP 200 | Workflow/Quality 兩層 online gate | ⑬ |
| 先上線再補 rollback | rollback 的相容性檢查 | ⑨ ⑭ |
| 所有變更合成一個 release | 單一假設進 canary 的紀律 | ⑩ |
| 用 production 資料修 evaluator | 資料使用權限與去識別策略 | governance |
| 依賴 supplier 的 latest 分數 | 自己的 golden dataset 與 canary | ⑪ |
這張表想強調的是:七種失敗做法看起來症狀各不相同,根因卻高度重複——幾乎都是「有一個關卡存在,但在忙碌或趕時程時被跳過」。這也是為什麼本文一直主張要把流程寫成 ⑫ 那樣可審查的清單,而不是留在每個人腦子裡的默契:默契在平靜的時候很可靠,在壓力最大的時候最容易先被犧牲掉。
release record 不該只活在 PR 描述。
它要能和 metrics、logs、traces、evaluation 結果一起回答問題。
使用者說回答變差。
↓
用 request_id 找到 trace。
↓
從 trace 讀出 change_id、prompt、model、index。
↓
比較 baseline 與 candidate 的 workflow span 與輸出判定。
↓
查該 change 的 gate 與 canary 決策。
↓
決定 rollback、修正或建立新的 regression case。
這條鏈比「儀表板有很多圖」實用得多。
它讓 on-call 不必在 incident 裡猜今天到底改了什麼。
| 放置位置 | 適合欄位 | 不適合欄位 |
|---|---|---|
| Metrics label | service、route、change_id、model_route | request_id、user_id、完整 prompt |
| Structured log | request_id、trace_id、change_id、decision | 原始敏感文件、secret |
| Trace attribute | artefact versions、span result、fallback reason | 未遮罩的個資與 credential |
| Release record | gate 結果、owner、rollback target、approval | 使用者原始對話 |
欄位放錯地方,會有兩種後果。
高基數 label 會讓 metrics 系統負擔失控。
敏感內容放進 log 或 trace,則會把除錯工具變成資料外洩面。
這是 reliability 與 governance 在同一個欄位相遇的地方。
把同一次請求寫成兩種版本的 log,差異會更具體。先看一則「放錯地方」的示範:
{
"level": "info",
"trace_id": "8f2a...",
"change_id": "answer-v17",
"prompt": "員工詢問:我兩年前離職但被公司通報涉及內部弊案,現在想申請復職,法務內部備忘錄裡對這種情況的處理建議是什麼?",
"output": "根據內部備忘錄第 4.2 條……"
}
這則 log 把完整的使用者問題與模型輸出都原封不動地寫進去,乍看方便除錯——想查什麼都能直接搜到。但它同時也把「涉及弊案調查」這種高度敏感的內容,複製了一份存進日誌系統,而日誌系統的存取權限、保留期限、稽核紀錄,通常遠比原本的業務資料庫寬鬆。一旦 log 系統本身被存取或外洩,這則紀錄就是現成的資料外洩證據。
對照放對地方的版本:
{
"level": "info",
"trace_id": "8f2a...",
"change_id": "answer-v17",
"prompt_version": "policy-answer-2026-09-21-a",
"model_route": "provider/model-name",
"decision": "answered_with_citation",
"case_category": "policy-sensitive"
}
原始 prompt 與 output 這時候應該存在另一個存取權限更嚴謹、保留期限更短的地方(例如加密的 trace 儲存,且只有必要的 on-call 角色能查),這則寫進一般 log 系統的紀錄,只留下「這是哪個版本、做了什麼判定、屬於哪個風險類別」——足夠讓 on-call 判斷「要不要去查完整內容」,卻不會讓每一次除錯都變成一次潛在的資料外洩。這正是上表想傳達的分工:log 系統的任務是「指路」,不是「存證」;真正敏感的內容,應該存在專門設計來處理敏感資料的地方,而不是散落在每一層可觀測性工具裡。
照著上表把欄位對號入座,會發現這條鏈幾乎沒有新概念——trace_id 與 request_id 來自 Day 10 建立的 correlation 基礎;metrics 只放低基數欄位,源自 Day 2 定下的原則;workflow span 與輸出判定,呼應 Day 21 到 Day 27 逐步建立的 evaluation 與 dashboard。今天真正新增的只有 change_id、prompt_version、model_route 這幾個版本欄位,把它們併進既有的 trace、log、metrics 體系,而不是另起爐灶蓋一套獨立的「release 追蹤系統」。
團隊很容易把「change management」想像成一個全新的、需要另外採購工具的專案。實際上只要前面已經建立可靠的 observability 基礎,需要做的往往只是「多加幾個欄位、多寫一份 contract」。這也是為什麼本文一直強調「先想清楚要記錄什麼」比「先接一套 CI/CD 工具」重要——工具可以晚點換,但 trace 裡連 change_id 都沒有,任何工具都補不回這個缺口。
本文描述的是 change management 的最小設計。
它沒有宣稱所有評估都能自動化。
它也沒有提供可直接套用到任何 CI 平台的 production pipeline。
門檻數字是練習用範例。
正式數值必須以自己的基線、使用者風險與錯誤預算決定。
模型的非確定性也意味著同一版本可能有不同輸出。
所以比較時要記錄採樣條件,必要時重跑並做人工審查。
最後,canary 是降低 blast radius,不是把風險轉嫁給少數使用者的許可證。
涉及安全、醫療、金融、法律或不可逆 action 的變更,應提高 review 與人工核准門檻。
寫到這裡,有幾個問題本文刻意沒有給出標準答案,因為它們的正確答案高度依賴每個團隊自己的產業、風險胃納與組織文化,硬套一個數字反而會誤導讀者。
第一個是「gate 門檻該訂多嚴」。⑪ 範例裡的 citation_valid_rate >= 0.98 只是一個示範用的數字,實際上這條門檻該不該再收緊,取決於引用錯誤一次的代價有多高——法律或醫療場景的容錯空間,跟一個內部知識庫問答機器人完全不能相提並論,本文不打算假裝有一個放諸四海皆準的正確門檻。
第二個是「canary 流量該怎麼分階段」。⑬ 的漸進發布表列了 5%/25%/50%/100% 這樣的階梯,這只是一種常見寫法,不是唯一寫法。低流量服務可能連「5% 流量」都不足以在合理時間內收集到有統計意義的樣本,這種情況下也許該考慮先擴大受控內部流量的規模,而不是死守著百分比階梯往前爬。
第三個是「什麼程度的變更才算需要走完整套 change contract」。如果每一次改一個字的措辭都要走 ⑫ 那整套流程,團隊很快會失去耐心而繞過流程;但如果門檻設得太寬鬆,又會讓真正有風險的變更溜過去。這條界線只能靠團隊自己根據過去的事故紀錄校準——本文能做的,只是提供一套可以拿去套用、調整的骨架,不能替每個團隊決定這條線該畫在哪裡。
AI 變更不因為長得像設定檔就變得無害。下一篇要處理的是最後兩天的主題:當系統真的撐不住流量或依賴故障時,如何讓它安全地降級,而不是直接倒下。
這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.