iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability系列 第 48 篇

Day 28(下)|CI/CD 與 AI Change Management:model 換版也是 deployment

  • 分享至 

  • xImage
  •  

GitHub:darkstar1227/learning-sre-for-ai-era

結論先說(下):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 的處理能力一次全動,出問題時最難判斷是哪一種能力受影響。把最難孤立驗證的變更放在序列最後,等前面兩個都確認穩定,至少能先排除兩個變因。

這張分類表跟傳統 SRE 的「變更類型」有什麼不一樣

傳統 change management 也有類似的分類直覺:資料庫 schema 變更、設定變更、程式碼變更,風險等級各不相同,這幾乎是每個 runbook 都會寫的常識。但傳統分類法裡,風險大致跟「離爆炸半徑多近」成正比——資料庫 schema 影響所有依賴它的服務,設定檔通常只影響一個服務,這種直覺八九不離十。AI 系統卻不能這樣類推:表格裡風險最不直覺的一列反而是 Prompt change,它改的往往只是幾行文字,「爆炸半徑」看起來最小,卻可能造成最大範圍的行為漂移——因為 prompt 影響的是模型面對「所有」輸入類型的整體行為傾向,不像資料庫 schema 只影響用到那張表的查詢路徑。這也是為什麼表格把「Prompt change」的主要失敗面寫成「行為漂移、注入邊界」,而不是某個具體功能故障——它的風險是瀰漫性的,不是局部性的。

Guardrail change 本身也是雙面刃

表格最後一列 Guardrail change 值得單獨展開:guardrail 存在的目的就是攔截風險,但它自己的變更同樣需要走完整的風險評估,聽起來有點矛盾——負責「擋壞事」的元件,改動它本身也可能是壞事。實務上這通常發生在兩個方向:「漏攔」把敏感詞門檻放寬,目的是減少誤攔抱怨,副作用是原本該被攔下的危險請求漏過去;「誤攔」則門檻收得太緊,正常使用者的合理問題被判定違規,這種副作用往往更慢被發現,因為它不會產生資安事故,只會安靜流失使用者。表格把 Guardrail change 的「先看什麼」定為 safety dataset 與 false positive review 兩項缺一不可,正是因為只測其中一種都會讓另一種問題被忽略。

⑪ 離線 evaluation gate:先分清楚它能證明什麼

離線 evaluation 的工作是降低已知風險。

它不能證明 production 不會出現新輸入。

所以 gate 的說法應該是:在指定 dataset 與 evaluator 下,候選版本沒有低於既定門檻。

不要把它寫成「模型已安全」。

實際案例:Anthropic 三起品質劣化事件,全部從基礎設施端悄悄發生

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 在不同請求下會有不一致的劣化程度。

三起都靠使用者回報才被發現,不是內部監控先示警;修復也都拖了超過一週才完全上線。

這個案例有幾個教訓:

  • 行為劣化不一定來自 model 或 prompt change:三起都是基礎設施層的變更,卻一樣改變了使用者拿到的答案,一樣需要被當成 deployment 管理。
  • 「沒有告警」不等於「沒有劣化」:三起事件都是先有使用者抱怨,內部監控才跟上,這正是本文一直強調的「Evaluate 要獨立於 uptime/error rate」。
  • 不一致的劣化更難抓:第三起的問題只在部分請求出現,只看平均分數容易被稀釋掉,這也是為什麼 gate 要看 case-level 結果,而不是只看整體分數。
  • Rollback 的時間可以拖很長:三起事件的完整修復都花了超過一週,這提醒 rollback target 與相容性檢查要在事前就準備好,不能等到事故當下才現找。

這三起事件還點出一件事:行為變更不一定來自應用層有人主動改了 prompt 或 model,平台團隊的路由邏輯、負載平衡設定、編譯器層優化,同樣可能改變使用者拿到的答案。如果「AI 品質」被定義成應用團隊的責任、「基礎設施可用性」被定義成平台團隊的責任,兩邊各自盯著自己的儀表板,「平台變更、應用層症狀」這種事故就容易變成沒人先發現的死角。change management 的 scope 因此不能只涵蓋應用層,平台團隊的變更同樣需要一份 change_id,能被使用者端的 evaluation 訊號回溯到。

內部 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 製造漂亮但無法比較的數字。

Golden dataset 不該只有「容易答對」的題目

一份能當 gate 的 dataset,至少要包含幾類輸入。

正常任務:代表常見使用者問題。
邊界任務:很長的問題、語意模糊的問題、資料不足的問題。
反例:相似但錯誤的前提、過期政策、互相衝突的來源。
安全案例:要求繞過規則、索取敏感資訊、誘導 tool 執行。
回歸案例:過去曾失敗的 production-like trace,去識別後保存。

最後一類最有價值。

失敗一次卻沒有變成 regression case,下一次還是會靠運氣。

但不要把原始 prompt、內部文件或使用者資料直接放進 repo。

先去識別,再確認可保留範圍。

範例:以結構化結果做 gate

不用一開始就接 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 的條件要能讀懂

壞的 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 個漂亮回答沖淡。

⑫ 把 release gate 寫成可審查的流程

以下用 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:讓 production 幫忙觀測,而不是幫忙賭博

離線資料集無法涵蓋所有真實輸入。

所以 canary 的價值不是「正式上線前再跑一次 smoke test」。

它是在限制 blast radius 的前提下,看真實流量下的差異。

先定義誰會進 canary

「5% 流量」聽起來很明確。

實際上還有很多問題。

是隨機 5% request,還是固定 5% user?
同一位使用者會不會在同一 session 混到兩個 prompt?
高風險租戶、內部測試帳號、付費客戶是否應排除或優先納入?
cache key 是否含 version,避免舊答案被拿來評新版本?

對聊天型服務而言,通常應讓同一 session 固定落在一個版本。

否則使用者會看到前後矛盾的行為。

分析端也會把兩種版本混成一團。

用 session 而不是 request 做分流的具體做法

如果分流依據是 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」這種令人困惑的情況。

Canary 的最小 metadata

每一筆可觀測事件至少能回答這些問題。

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 風險。

這時候可先用受控內部流量。

Canary 不能只看 error rate

候選版本的 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 的理由。

三層 gate 的關係不是「都要過」,而是「不同層失守,代價不一樣」

把 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:先判斷「退哪一層」

看到品質下滑,第一個直覺往往是「整套退回上一版」。

這在緊急情況下可以是合理的緩解。

但平常應先把 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 的兩種速度

可以把 rollback 分成兩個目標。

緩解速度:多久能停止新的傷害?
恢復速度:多久能重新確認使用者得到的是可接受結果?

把 traffic 切回 known-safe prompt,可能很快。

但若新 index 已寫入錯誤資料,恢復還需要重新建索引與抽查來源。

前者做完不能立刻宣布「完全恢復」。

Day 27 的 Resolve 與 Recover 區分,在這裡正好派上用場。

寫一張 rollback card

在真正上線前,替每一項高風險變更寫一張短卡。

## 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。

卡片不需要很長。

它要能在值班的人壓力最高時,避免做出最危險的「快速修復」。

「Do not do without approval」這個欄位存在的理由

rollback card 裡最容易被忽略、卻可能最重要的欄位,是 Do not do without approval。多數 runbook 只寫「該做什麼」,很少寫「不該做什麼」,但事故當下最危險的往往是有人急著解決問題,做出看起來合理、實際上會製造第二個事故的動作。範例卡片列的兩件事——不關閉 citation validator、不把 timeout 全域拉長——共通點都是能「立刻讓症狀消失」,卻是把偵測風險的機制關掉,而不是真正解決風險。壓力大的時候,人的判斷力會系統性地偏向「先讓紅色警示消失」,這正是這個欄位要提前攔住的衝動:把「絕對不能因為方便而做」的事,寫在冷靜的時候,而不是留給事故現場的自己去判斷。

⑮ 讀者可自行完成的 Lab:從 manifest 走到 release drill

本文不會建立或執行 CI/CD。

也不會碰任何真實 provider、production route 或使用者資料。

這個 Lab 跟 ④ 的 DIY 有什麼不一樣

④ 的 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。

Step 1:選一個很小的行為變更

不要一開始就換 provider、重建 index 又改 prompt。

選擇一件可觀察的改動。

例如:要求回答在資料不足時明確說明「我找不到可驗證來源」。

把目前版本命名為 answer-v1。

把候選版本命名為 answer-v2。

版本可以是檔案雜湊、release tag 或不可變 ID。

重點是它可以唯一指向內容。

Step 2:寫出變更範圍

在自己的 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 還不夠。

Step 3:做一份小型 baseline 表

挑十個輸入。

其中至少兩個應該是資料不足或錯誤前提。

表格可以很簡單。

case_id input 類型 baseline 預期 candidate 預期 風險
p-01 正常政策問題 可引用回答 可引用回答 citation 遺失
p-02 資料不足 誠實說明不足 誠實說明不足 編造答案
p-03 錯誤前提 修正前提 修正前提 順著錯誤回答
p-04 敏感 tool 要求 拒絕或升級人工 拒絕或升級人工 越權 action

這不是統計顯著性研究。

它是你的第一道 regression net。

Step 4:先跑 baseline,再跑 candidate

對同一批輸入,記錄兩個版本的結果。

你可以存 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 規則(例如從「拒答就算過」改成「拒答還要附上建議的替代查詢管道才算過」),你需要回頭看「當時到底寫了什麼」,才能用新規則重新判定舊資料,而不用真的重跑一次模型。

Step 5:宣告 promotion 條件

在執行前寫下規則。

例如:

所有資料不足案例都必須拒絕編造來源。
任何 tool policy case 不得新增允許路徑。
candidate 的 task success 不得比 baseline 少超過一個案例。
若 citation 格式改變,parser 與 validator 必須一併通過。

先寫再跑,才能防止看到喜歡的結果才改規則。

Step 6:模擬 canary 的 metadata

即使你的 Lab 沒有真正分流,也可以在每次候選請求加上:

change_id=answer-v2
release_stage=canary
prompt_version=answer-v2
baseline_version=answer-v1

把它們放進 trace 或 structured log。

之後看到失敗,你才知道要看哪一組輸出。

Step 7:故意製造一個 gate failure

把 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 沒放高基數欄位)則需要真的去看一眼實際輸出。這個混雜是刻意的:只驗收「文件寫得漂不漂亮」,容易讓人誤以為寫完文件就等於流程落地;混進幾項需要動手驗證的檢查,能提醒自己「紙上作業」與「真的跑過一次」之間永遠有落差,而這正是本文從頭到尾想避免的錯覺。

⑯ 常見失敗做法:看起來有流程,其實沒有風險控制

失敗做法一:只 version code

症狀是 Git commit 很完整。

prompt 卻存在管理後台,model route 寫在環境變數,index 由排程重建。

出問題時,commit hash 無法還原使用者當時的行為。

改善方式是把這些版本值放入 manifest 與 runtime telemetry。

這種失敗做法特別容易發生在「AI 功能是後來加進既有系統」的團隊——原本的 CI/CD 對 code 的版本紀律已經很成熟,工程師習慣了「看 commit hash 就能重建狀態」,卻沒人停下來問「新增的 prompt 跟 model route 是不是也該納入同一套紀律」。結果 code 被管得無微不至,AI 那一半卻完全脫離紀律之外,兩邊的成熟度落差反而讓整體系統的可追溯性看起來比實際上更好——這是最容易讓人掉以輕心的失敗模式。

失敗做法二:把 evaluator 分數當總判決

一個 0.92 分不告訴你低分是哪一類。

它也不告訴你 evaluator 自己是否偏差。

保留 case-level 結果與人工抽樣。

對安全情境,採取 fail-closed 的獨立規則。

「evaluator 自己是否偏差」這句話值得多解釋一句:evaluator 本身通常也是一個模型或一套規則,它評分的準確度並非天生保證。如果 evaluator 剛好對某一類措辭特別寬鬆(例如只要輸出裡出現「根據政策」四個字就傾向判定為 pass,不真的檢查後面的內容對不對),candidate 只要學會迎合這種偏好,分數就會虛高,這其實是③ 段落討論過的 reward hacking 在 evaluation 這一層的翻版——只是這次被 hack 的不是使用者的按讚率,而是 evaluator 自己的判定邏輯。這正是為什麼 ⑪ 一再強調 evaluator 版本要被記錄、必要時要找外部單位驗證,而不是把自己團隊寫的 evaluator 當成不會出錯的絕對標準。

失敗做法三:canary 只檢查 HTTP 200

這會抓到服務掛掉。

抓不到「回答得很快但答錯」。

至少補 workflow 與 quality signal,並預留人工回饋進入 decision 的時間。

失敗做法四:先上線,再補 rollback

rollback 需要可用的舊 artefact、相容的資料,以及清楚的流量切換方式。

這些都無法在事故最忙的十分鐘內可靠地發明。

⑭ 的 rollback card 與相容性檢查,正是為了解決這個症狀而存在——它們把「回退需要哪些東西」這個問題,從事故當下的臨時決策,搬到平常心情冷靜的時候先寫好答案。改善方式很直接:任何一次 release 的 contract 定稿之前,就先把對應的 rollback card 一起寫完,兩份文件同時進 review,而不是先讓變更上線,等真的出事才想起來要補救。

失敗做法五:把所有變更合成一個大 release

大 release 讓每一個成功與失敗都難以歸因。

若真的因商業時程不得不合併,至少在 manifest 裡明示這是多變因變更,降低 promotion 速度並提高人工 review。

失敗做法六:用 production 的使用者資料修 evaluator

真實 feedback 很珍貴。

但先確認資料使用權限、去識別策略與保留期限。

把敏感 prompt 貼進公開 CI log,等於用 observability 製造另一個 incident。

失敗做法七:把「supplier 說 latest 沒問題」當成自己的 evaluation

有些團隊會依賴模型供應商自己公布的 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 資料接回可觀測性

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 示範放錯與放對的差別

把同一次請求寫成兩種版本的 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 系統的任務是「指路」,不是「存證」;真正敏感的內容,應該存在專門設計來處理敏感資料的地方,而不是散落在每一層可觀測性工具裡。

這條鏈其實是把 Day 10 到 Day 27 的東西接起來,不是新發明

照著上表把欄位對號入座,會發現這條鏈幾乎沒有新概念——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」。如果每一次改一個字的措辭都要走 ⑫ 那整套流程,團隊很快會失去耐心而繞過流程;但如果門檻設得太寬鬆,又會讓真正有風險的變更溜過去。這條界線只能靠團隊自己根據過去的事故紀錄校準——本文能做的,只是提供一套可以拿去套用、調整的骨架,不能替每個團隊決定這條線該畫在哪裡。

下一篇:Day 29|Capacity、Retry、Circuit Breaker、Degradation

AI 變更不因為長得像設定檔就變得無害。下一篇要處理的是最後兩天的主題:當系統真的撐不住流量或依賴故障時,如何讓它安全地降級,而不是直接倒下。

延伸閱讀


這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 28(上)|CI/CD 與 AI Change Management:model 換版也是 deployment
下一篇
Day 29(上)|Capacity、Retry、Circuit Breaker、Degradation:別用重試把事故推給下游
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 58 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言