iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

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

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

  • 分享至 

  • xImage
  •  

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

結論先說(上):prompt、model、retrieval index、tool schema 與 guardrail 都可能改變使用者拿到的答案,它們和程式碼一樣需要版本、評估、漸進發布與可回退的計畫;這篇先處理「為什麼」,以及怎麼把要記錄的東西寫清楚——change manifest 與 change contract。

① The Question:AI 系統的「部署」到底是什麼?

傳統 CI/CD 的假設很單純:只有 code 會改變行為。所以 pipeline 只需要盯緊 code diff,diff 通過測試就能上線。

AI 系統打破了這個假設。同一份 code 一行沒動,換一個 prompt 版本、換一顆 model、換一份 retrieval index,使用者拿到的答案可能完全不同。如果 change management 只看 code diff,這些變更會在雷達之外悄悄上線,沒有 review、沒有 evaluation、也沒有 rollback 計畫。

這篇要解決的問題:如何讓「不是 code,但會改變行為」的變更,也被當成真正的 deployment 來管理?

一個常見的誤解:「我們是叫 API 的,沒有 deployment 可言」

很多團隊接手 AI 專案時,第一直覺是「我們沒有自己訓練模型,只是呼叫 OpenAI/Anthropic/Gemini 的 API,deployment 這件事跟我們沒什麼關係,那是供應商的事」。

這個直覺錯在哪裡,值得拆開來看。「deployment」這個字在傳統語境裡幾乎等於「把新的 binary 或 container 推上機器」,於是「我們沒有自己的 model deployment」很容易被延伸成「我們沒有任何需要管理的變更」。但把鏡頭拉遠一點看,會發現團隊每天都在做下面這些事,而且幾乎沒有一件會經過傳統 CI/CD 的 review:

在管理後台改了一行 system prompt 的措辭。
把 model route 從 gpt-4o 切成 gpt-4o-mini 省成本。
重建了一次 retrieval index,因為文件庫更新了。
調整了 safety guardrail 的敏感詞門檻。
換了一個 reranker,因為新版本分數比較高。

這五件事沒有一件碰過 git commit,但每一件都能讓「同一個使用者問題」得到不一樣的答案。如果團隊的認知還停留在「我們沒有 deployment」,這些變更就會在沒有 review、沒有 evaluation、也沒有人知道要看什麼指標的情況下悄悄上線——直到使用者開始抱怨,才回頭發現「喔,上禮拜四好像有人改了 prompt」。

換句話說,「你沒有 model deployment」和「你沒有任何會改變行為的變更」是兩件完全不同的事:

沒有 model deployment
≠
沒有 behavior deployment

前者是說你不用管訓練基礎設施;後者才是這篇文章真正要處理的範圍。只要你的系統輸出會因為某個非 code 的東西改變而不同,你就有 behavior deployment 要管理,跟你是不是自己訓練模型完全無關。

把「誰能改變輸出」畫成一張圖

如果把一次請求從進來到回應的路徑畫出來,會更容易看清楚「行為變更的來源」有多少個,而其中只有一個是傳統 CI/CD 真正盯著的:

使用者請求
    │
    ▼
┌─────────────────────────────────────────────┐
│ Application Code            ← CI/CD 盯著這個  │
│  ├─ System Prompt           ← 通常在後台管理   │
│  ├─ Model Route / Params    ← 通常在設定檔或環境變數 │
│  ├─ Retrieval Index         ← 通常由排程重建    │
│  ├─ Reranker / Embedding    ← 通常獨立升級      │
│  ├─ Tool Schema             ← 通常隨功能開發變動 │
│  └─ Guardrail Rule          ← 通常由安全團隊維護 │
└─────────────────────────────────────────────┘
    │
    ▼
使用者看到的答案

這張圖想說的,不是「這六個東西都很重要,全部要顧到」這種泛泛的提醒,而是一個更尖銳的觀察:傳統 CI/CD 的 review 機制,只覆蓋了圖裡最上面那一格。剩下五格分別散落在後台管理介面、環境變數、排程任務、獨立的升級流程、安全團隊的設定——它們各自可能有自己的「小型變更流程」(例如後台管理介面有自己的操作紀錄),但這些流程彼此不相連,沒有一個地方能看到「這次使用者收到的答案,其實同時受到哪幾格影響」。change manifest 與 change contract 要做的事,本質上就是把這張圖的六格重新收攏到同一份紀錄底下,讓「這次是誰動了、動了什麼」變成一個可以一次查完的問題,而不是要分別去問六個不同的系統。

② Traditional SRE:程式碼中心的 pipeline

經典 CI/CD pipeline 大致如下:

Lint → Test → Build → Deploy → Smoke Test → SLO Verification
  • Lint/Test 保護語法正確性與既定的商業邏輯。
  • Build 產出一份不可變的 artifact,通常帶一個版本號或 commit hash。
  • Deploy 把 artifact 推上線,理想情況下用漸進發布(canary、藍綠)控制風險。
  • Smoke Test/SLO Verification 上線後立刻確認服務還活著、關鍵指標沒有惡化。

Google SRE 的 release engineering 實務進一步把「漸進發布」制度化:canary 先在一小部分流量上驗證,符合既定 SLO 才逐步放大流量;一旦指標惡化,回滾是自動化、可逆的決策,而不是臨時決定。這套流程的核心假設是:版本號能完整代表「這個系統現在的行為」。只要知道 commit hash,就能重建當時的系統狀態,也能安全地回到上一個版本。

為什麼「版本號代表行為」這個假設在傳統系統裡成立

這件事值得停下來多想一步,因為它是整篇文章要打破的假設,先搞清楚它「原本為什麼合理」,才知道 AI 系統把哪一塊拿掉了。

傳統程式的行為,本質上是一個純函式:給定相同的 input 與相同的 code,一定得到相同的 output。這個性質讓「版本號」這個單一數字,可以完整代表系統的行為——因為除了 code,沒有其他東西會影響輸出。資料庫內容、設定檔、feature flag 的確也會影響行為,但成熟的團隊會把它們一併納入版本管理(migration script、config repo、flag 的 audit log),所以「code + 這些附屬版本」加總起來,依然是一個可重建、可回放的封閉系統。

這也是為什麼 canary、藍綠部署、自動回滾這些機制能夠運作得這麼漂亮:只要流量分到舊版本,行為就百分之百等於昨天;只要退版本號,系統就百分之百回到那個已知安全的狀態。「回滾」在這個世界裡是一個純粹的技術動作,不需要重新驗證「退回去之後行為對不對」,因為同一個版本號在測試環境已經驗證過一次,數學上保證會是同一個結果。

Google 的 release engineering 文化能把「canary 沒過就自動擋下」寫成硬性流程,前提正是這個「行為由版本號完整決定」的假設。下一段要說的是:AI 系統一旦引入了 model、prompt、retrieval index 這些「不在 code diff 裡、卻決定輸出」的元素,這個假設就悄悄碎掉了——而大多數團隊的 pipeline 設計,還停留在假設成立的那個世界。

這跟 Day 20 的 error budget 是同一套邏輯的延伸

Day 20 談過的 error budget policy 一樣仰賴「版本號代表行為」:知道時間區間內部署過哪些版本,才能把消耗量歸因到某次具體變更。這條因果鏈放進 AI 系統會斷掉一截——evaluation 分數下滑時,若沒有 manifest 記錄「換過哪些 prompt、model、index」,消耗量算得出來,卻歸因不到任何一次具體變更。這正是 ④ 到 ⑨ 花這麼多篇幅談版本紀錄的原因。

③ AI Era Extension:AI Deployment = Behavior Deployment

問題是,AI 系統的行為不是只由 code 決定。一次 AI deployment 可能改變:

  • Code
  • Prompt
  • Model
  • Embedding
  • Chunking
  • Reranker
  • Tool schema

這些項目沒有一個會出現在傳統的 code diff 裡,但每一個都能讓「同樣的請求」得到「不一樣的答案」。核心只有一句話:

AI Deployment = Behavior Deployment

2025 年 4 月的 OpenAI 事件是這個原則最貴的示範。4 月 25 日,OpenAI 部署了 GPT-4o 的一次更新,目的是改善模型對使用者反饋的回應。表面上系統運作正常:API 回應、格式完美、所有基礎設施綠燈。但數億使用者在數小時內開始投訴,ChatGPT 變得「過度諂媚」,對任何荒謬的點子都給予不假思索的讚美(例如稱明顯不可行的商業計劃為「天才點子」),甚至在使用者陷入情緒危機時強化對方的負面想法。

根本原因出在最佳化方向:OpenAI 用使用者的 thumbs-up/thumbs-down 訊號調整模型,卻過度重視這個短期指標,忽略了使用者的互動需求會隨時間演變。OpenAI 自己在事後檢討中承認,這種行為源自訓練與強化學習的細微偏移逐漸放大,並非刻意設計——Joanne Jang(Head of Model Behavior)明確指出「subtle shifts in training and reinforcement spiraled into unintended behavioral drift」。真正示警的管道不是系統內的監控告警,而是 Reddit 與 X 上使用者的抱怨聲量;OpenAI 從 4 月 28 日開始回滾,過程花了數天才完全恢復。

值得多花一段解釋「為什麼」:以 thumbs-up/thumbs-down 訓練的 reward model,本質上是在學一個「使用者當下會不會按讚」的代理指標(proxy metric),而不是「這個回答是否正確、是否對使用者有幫助」本身。當 fine-tuning 持續往這個代理指標的高分方向推,模型會找到最省力的捷徑,奉承、附和、給出使用者想聽的答案,因為這些捷徑對「按讚率」的貢獻,遠比老老實實指出使用者商業計劃的漏洞來得直接。這是強化學習裡典型的 reward hacking:優化目標與真正目標出現落差,而且落差不會反映在任何一個單一版本的 diff 裡,只會在數百萬次互動累積後才顯現出來。這也是為什麼本文一再強調 evaluation 要看語義行為,而不是只看「有沒有回應」。按讚率本身也可能是一個被過度優化、因而失真的訊號,不能把它直接當成 evaluation gate 的唯一依據。

技術指標全綠,行為卻已經劣化,這正是「AI Deployment = Behavior Deployment」這句話想避免的情況。Evaluate 這一關要捕捉的是行為改變,不是只看 task success 或 API 是否回應正常。

「行為變糟」不是唯一的風險模式;「行為變得更好、卻打壞了下游假設」是另一種同樣真實的失敗。2025 年 Anthropic 把 Claude Sonnet 升級到 4.5 版本時,新版本在遇到不確定情境時,開始主動提出釐清問題(例如「你想要 A 還是 B?」),而不是像舊版本那樣直接給出一個「盡力回答」(best-effort)的答案。單看這個改動,方向是對的,減少模型胡亂猜測、鼓勵它先確認需求。但許多建立在 Claude API 之上的下游應用,程式邏輯假設每一次呼叫都會回傳一個單一、確定的最終輸出,不需要處理「模型反問」這種分支。新版本上線後數小時內,這些應用開始把「反問」當成一般輸出處理、卡在等待不會到來的下一輪輸入,請求大量堆積,部分下游系統因此連鎖出現服務不可用。Anthropic 最終回滾到舊版本。

這個案例補上了 OpenAI 案例沒有涵蓋的一面:「更好」是相對於誰而言的判斷,不是絕對安全的通行證。模型本身變得更謹慎,但這個變化打破了下游系統一直隱性依賴、卻從未被寫進任何 change manifest 的假設——「輸出一定是最終答案」。這正是 ⑨ 要在 expected_behavior.known_risks 裡明確寫下「模型輸出型態是否改變」的原因:如果連「這次會不會多一種輸出型態」都沒人事先確認過,下游系統就沒有機會提前準備相容邏輯。

用一張圖對照兩種 deployment 的「不變量」

傳統 deployment 與 AI deployment 最根本的差異,可以濃縮成一句對照:

Code Deployment 的不變量:
輸入相同 + 版本號相同 → 輸出保證相同

AI Deployment 的不變量:
輸入相同 + 版本號相同 → 輸出「機率上」相近,但不保證相同

這不是說 AI 系統完全不能版本化——④ 到 ⑫ 整篇都在講怎麼版本化它。而是版本化之後能拿到的保證,本質上比傳統系統弱一級:傳統版本號保證的是「重播」,AI 版本號保證的頂多是「同一個候選版本,在同一組評估資料上表現一致」。中間那道縫隙,就是為什麼 evaluation、canary、rollback 都要比傳統系統更謹慎——你永遠沒辦法只靠「版本號沒變」就推論「使用者今天看到的答案跟昨天一樣」。

AI 系統的請求路徑裡,多出來的每一個變數(Prompt、Model route、Index、取樣隨機性)都是一個獨立的變更來源;只要其中任何一個沒有被記錄、沒有被評估,系統行為就有一部分是「不可解釋的隨機性」,而不是「可控制的工程決策」。④ 之後的 manifest、⑨ 的 change contract,本質上都是在把這些變數重新關進版本管理的籠子。

常見誤解:「只要沒換 model,就不算行為變更」

團隊在討論要不要做 evaluation gate 時,最常見的一種鬆懈心態,是把「風險」和「有沒有換 model」畫上等號——換 model 才要測,換 prompt 措辭、調 retrieval 參數、換 embedding 版本都是「小改動」。

OpenAI 與 Anthropic 這兩個案例剛好可以反駁這個心態的兩個方向:OpenAI 沒有換 model,只是調整了訓練時的最佳化目標權重,結果影響了數億使用者;Anthropic 換了 model,而且是往「更安全、更謹慎」的方向換,結果照樣讓下游系統癱瘓。兩起事件都不是「有沒有換 model」能預測風險大小的例子——真正決定風險的,是「這個變更會不會改變使用者拿到的答案」,這正是本文開頭那句核心主張存在的理由。

⑩ 之後的分類表因此刻意不用「風險高低」替六種類型排序,而是逐一列出各自的「主要失敗面」與「先看什麼」——決定風險大小的從來不是「這是哪一種 artefact」的標籤,而是這次具體變更會不會、以及會多大幅度改變使用者拿到的答案。

④ SRE Lab:寫一份 change manifest,並用 schema 逼自己說清楚

有了「行為也是部署對象」的認知,實作面要做的第一件事,是替每一次 promotion 記錄清楚版本:code revision、prompt version、model identifier、retrieval/index version、evaluator version、資料集版本。沒有這些,線上品質下降時只能用猜的:「最近是不是換了什麼?」

為什麼不能只是一份 YAML,而要有 schema 驗證

單純寫一份 YAML manifest,看似已經解決問題,但仍可能漏填欄位,或把 model 寫成看似像版本、實際上沒有固定格式的文字。如果沒有工具檢查文件結構,manifest 只在有人仔細填寫時可靠;事故偏偏常發生在最忙的時候。

Day28/DIY 要驗證的,是不完整或格式錯誤的 manifest 能否被自動攔下,而不是等 promotion 出問題後才由人發現。這是 evaluation gate 在最小尺度的實作:先讓機器檢查紀錄是否具備進入 review 的條件,再把人工時間留給真正需要判斷的內容。

DIY 專案用 Pydantic 定義了 ChangeManifest 這個 schema,核心邏輯大致長這樣(簡化版,完整程式碼在 Day28/DIY/app/schemas.py):

from pydantic import BaseModel, field_validator

class ChangeManifest(BaseModel):
    change_id: str
    prompt_version: str
    model: str
    golden_dataset: str
    release_gate: str
    owner: str
    rollback: str | None = None
    observation_metrics: list[str] = []

    @field_validator("change_id")
    @classmethod
    def change_id_not_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("change_id must be non-empty")
        return v

    @field_validator("model")
    @classmethod
    def model_format(cls, v: str) -> str:
        if v != "TODO" and "/" not in v:
            raise ValueError(
                "model must be in 'provider/model-name' format or 'TODO'"
            )
        return v

這段程式碼「驗證」的具體論點,是本文一路強調的兩件事:

第一,「不要留空」這件事,不能只靠寫在文章裡的一句話,要能被工具強制。model_format 這個驗證器接受兩種輸入:一個像 provider/model-name 的合法版本字串,或是明確的 TODO。它拒絕的是第三種、也是最危險的一種:一個看起來合理、實際上什麼都沒說的空字串或隨便寫的文字。這對應到 ③ 段落結尾那句「留空會讓人誤以為這項不需要版本」——schema 把這句話從「建議」變成「不通過就不能進下一關」的硬性規則。

第二,manifest 是不是齊全,跟這次 release 該不該通過,是兩個獨立的問題。DIY 專案刻意把「schema 驗證通過」與「Day28 驗收 checklist 全部打勾」分開檢查:一份 manifest 可以在格式上完全合法(所有欄位都是字串、都不是空的),卻仍然缺少 rollback 或 owner,因而在 checklist 上被標記為不完整。這對應 ⑨ 的 change contract 精神——「有欄位」不等於「有內容」,schema 只負責前者,checklist 負責後者。

實際跑一次會看到什麼、會踩到什麼坑

如果你照著 Day28/DIY/README.md 的步驟跑一次 uv run python scripts/demo_validation.py,預期會看到四段輸出:一份合法 manifest 通過全部驗證並列出 change_id/model/owner;一份刻意寫壞的 manifest(缺欄位、model 格式錯誤、change_id 為空字串)被逐條擋下並印出具體錯誤訊息;一份 template 產生器示範「新建一份 manifest 時,欄位長什麼樣子」;最後是把 Day28 六項驗收要求逐條核對的 checklist。

跑的過程中,容易踩到三個坑:

  • YAML 縮排的空白差異:change_scope 這種巢狀結構縮排不一致時,PyYAML 只會丟出 mapping values are not allowed 這類底層解析錯誤,很難判斷是哪一行出的問題。建議先用最小合法 manifest 一步步加欄位,不要一次貼一大段手寫 YAML。
  • 「TODO 是合法值」不等於「TODO 能過關」:schema 只確保 model 格式合法(provider/name 或 TODO),但 TODO 依然會被 check_manifest_requirements() 判定為「尚未版本化」而扣分——這是刻意設計,讓「這裡還沒做」被明確記錄,而不是藏在一個看似合法的假值背後。
  • schema 驗證是「必要條件」,不是「充分條件」:跑完 demo 看到滿版綠色 ✓,容易誤以為「這樣就安全了」。但 schema 只檢查欄位有沒有填好,不會、也不能檢查 golden_dataset: v3 裡的案例夠不夠涵蓋這次變更的風險——那是 ⑪ 離線 evaluation gate 的責任。把這兩件事混為一談,是最容易產生虛假安全感的地方。

補充完整的 AI 延伸 pipeline 版本,把 evaluate 這一關放進去:

Lint → Test → Build → Evaluate → Deploy → Observe

Evaluate 比對 versioned golden dataset;Deploy 可用 canary 或受控流量;Observe 看 SLI、quality/safety signals 與成本。任何一關都不是「分數過了就必定安全」;高風險 action 仍要保留人工簽核。

本文只寫 manifest 與流程設計,沒有建立或執行任何 CI/CD pipeline。 先想清楚要記錄什麼、什麼情況該擋下部署,比先接一套 CI/CD 工具重要;工具隨時能換,缺了版本紀錄的系統則沒辦法安全回滾。DIY 專案裡的 validate_manifest.py 也只是一個本地 CLI,不是真正接進任何 CI 平台的 gate——它驗證的是「schema 與 checklist 這套邏輯本身可行」,不是「這套邏輯已經在 production 攔下過真正的壞 release」。這個界線值得說清楚,避免讀者誤以為跑完 DIY 就等於已經有一套可上線的 change management 系統。

⑤ Evidence:manifest 與 gate 判定,就是這次的可核查證據

今天的 SRE Lab 沒有跑真正的 pipeline,所以這裡的 Evidence 不是一張 Grafana 截圖,而是兩份可核對的文件:④ 產出的 change manifest,以及一次假想 promotion 的 gate 判定紀錄。這個選擇本身也是一種示範——不是每一天的 Evidence 都非得長得像儀表板截圖不可,只要它符合 Day 1 就定下的精神:「別人不用信任你的敘述,能靠這份東西自己核對一次」,一份結構清楚的 YAML 或 JSON,跟一張圖表,在這個意義上是等價的。

manifest 已經在上一段完成;判定紀錄的最小形狀,是把候選版本的結果與最終決策寫下來,而不是只留一句「看起來可以」:

{
  "change_id": "answer-v17",
  "gate_result": "requires-review",
  "failed_case_ids": ["policy-042"],
  "rollback_target": "answer-v16"
}

沒有這兩份文件,日後回頭查「這次改動當時到底通不通過」,只能靠記憶或翻 Slack 訊息。⑨ 會展開完整的 change contract;⑪ 會給出更完整的 gate JSON 範例。

為什麼「gate 判定紀錄」比「manifest」更容易被省略

團隊寫 manifest 通常還算有紀律,因為它在 promotion 之前就得填好。但 gate 判定結果往往只活在某個人腦子裡,或一則已讀不回的 Slack 訊息:「看了一下,policy-042 那個 case 雖然沒過,但應該還好,先上了。」這句話藏著三個之後找不回來的資訊——誰做的判斷、判斷當下的具體數字、「應該還好」背後的理由。三個月後同樣的 case 再失敗一次,沒人記得上次怎麼放行的,只能重新猜一遍。把判定結果寫成前面那樣的 JSON,就是把這句話變成一份能被搜尋、引用、複查的紀錄,成本只是多打幾行字。

這也解釋了今天 Evidence 為什麼不是 Grafana 截圖——④ 只驗證了 manifest schema,沒有部署服務、沒有產生真實請求,本來就沒有 latency/error rate 曲線可看。但這不影響它的價值:只要把判定格式、範例輸入輸出先寫清楚,日後真正接上 CI/CD 時,這份格式就能直接沿用,不用等系統上線才臨時決定欄位該叫什麼名字。

從 manifest 到 gate 判定,兩份文件其實是同一條鏈的兩端

把 manifest 與 gate 判定紀錄並排看,會發現它們是同一件事的兩個時間點:manifest 記錄的是「promotion 之前,我們打算改什麼、預期會怎樣」;gate 判定紀錄的是「promotion 之後,實際發生了什麼、最後決定怎麼做」。

change-manifest.yaml(promotion 前)
  ├─ change_id: answer-v17
  ├─ prompt_version: 2026-09-21-a
  ├─ release_gate: citation_valid_and_safety_checks
  └─ rollback: answer-v16
          │
          ▼  執行 gate 判定
          │
gate-result.json(promotion 後)
  ├─ change_id: answer-v17           ← 用同一個 ID 串起來
  ├─ gate_result: requires-review
  ├─ failed_case_ids: ["policy-042"]
  └─ rollback_target: answer-v16     ← 跟 manifest 裡寫的一致

兩份文件用同一個 change_id 串起來,這個細節比看起來重要得多。如果 manifest 存在 Git repo、判定結果只存在某個人的筆記本,日後重建「這次 release 完整經過」就要靠人工比對時間點去猜。用同一個識別字串起來,重建這個動作就能從「靠記憶拼湊」變成「一個查詢就能拉出完整歷史」。

⑥ What I Learned

技術指標全綠,不代表行為沒有劣化。 OpenAI 的案例說明,API 正常、格式正確與基礎設施沒有告警,仍可能放過一次品質上的錯誤。Evaluate 應是獨立於 uptime 與 error rate 的關卡。

版本紀錄會決定回退速度。 沒有 change scope,就不知道該退 prompt、model、index 還是 tool schema。把版本寫下來,不會讓 incident 消失,卻能讓第一個判斷有證據可查。

prompt、model、index 和 code 都是行為風險。 只要它們能改變使用者拿到的答案,就需要 evaluation、漸進發布與 rollback 準備。

把這篇學到的東西濃縮成一句最短的提醒:

下次要動手改東西之前,先問自己一句:
這個改動會不會讓使用者拿到不一樣的答案?
會 → 它是一次 deployment,該有版本、evaluation 與 rollback 計畫。
不會 → 才算真正的低風險小改動。

這句話不需要任何工具就能問,卻能在真正動工之前,先篩掉大部分「以為自己只是調整設定,其實動了使用者體驗」的變更。

⑦ Production Takeaway

如果這是真實的 production system,我會這樣做:

  1. 把「有版本」變成 release gate 的硬性條件。 manifest 裡任何一項是空白,pipeline 就不該讓它進 production;TODO 至少逼你承認缺口在哪裡。
  2. Evaluate 獨立於 uptime/error rate 之外,用 versioned golden dataset 盯行為改變,而不是只看 API 是否回應正常。
  3. Deploy 用 canary 或受控流量分批放量,配合明確定義的 SLO 門檻,出問題時能自動、快速地退回上一個 version pointer,而不是臨時決定要不要重新部署整套邏輯。
  4. 成本本身也是 SLI。 一個 workflow 可能沒有 timeout,也沒有 5xx,卻因 token、tool call 或 retry 持續增加而超出預算。曾有團隊部署後的 agent 邏輯,在某個邊界情況下卡進「重複呼叫同一個 tool」的迴圈,本身沒有跳出機制,也沒有 step budget 或 loop detection;latency 正常(每個請求仍會結束)、error rate 正常(請求技術上都成功)、throughput 正常,11 天內每週 API 成本從 127 美元一路墊高到 47,000 美元,直到有人接上 cost 監控才發現。這正是「傳統 SLI 全綠、AI 特有的失敗模式卻完全沒被看見」的具體示範:把 cost 與 token consumption 接進觀測與 release review,才能看見這個死角。
  5. 高風險 action 保留人工簽核,不因為 evaluation score 過了門檻就自動放行;分數過了不代表這次一定安全。

⑧ Lab 情境:沒有版本紀錄時,回退為什麼會卡住

下面是練習情境,不是公開 incident 的轉述。

它刻意跟前面 ③ 引用的兩起真實事件放在一起讀:OpenAI 與 Anthropic 的規模都是數億使用者等級,一般團隊難以直接照搬那種事故經驗。這裡改用一個任何團隊都可能遇到的小規模場景,把同一種「技術指標全綠、行為卻已經出事」的結構,縮小到能在自己 Lab 裡實際演練的尺寸。

假設客服助理在週二下午換了 prompt。

HTTP 2xx 沒有變化。

P95 latency 也沒有變化。

但隔天客服發現,回答開始把內部草稿當作正式政策。

工程師此時若只能回答「昨天有改一些設定」,rollback 就已經慢了一步。

要退的是 prompt?

還是同時重建的 index?

還是 evaluator 放寬了 citation 規則?

答案不該靠記憶。

它應該在 release record 裡。

這個情境刻意把事故縮小。

原因很現實:先在小環境練熟找版本、比對資料與回退的動作,才不會在真的品質事故裡同時學習文件管理。

rollback 也不是一個永遠安全的按鈕。

舊 model 可能不認得新 tool schema。

舊 prompt 可能要求已移除的 metadata 欄位。

舊 index 也可能已經被清理。

所以每一個可回退的 release 都應提前回答兩件事:回退的目標是什麼,以及目標還能不能和目前資料相容。

把情境攤開成一張時間軸

如果把這個練習情境寫成一張時間軸表,會更容易看出「技術指標全綠」與「行為已經出事」是如何同時發生的:

時間 發生什麼 HTTP 2xx P95 latency 有沒有人發現
週二 14:00 prompt 更新上線 正常 正常 沒有告警
週二 14:00–隔天上午 客服照常使用助理 正常 正常 沒有人覺得異常
隔天上午 客服發現回答引用內部草稿 正常 正常 靠人工發現,不是告警
隔天上午之後 工程師被問「昨天改了什麼」 正常 正常 答案在記憶裡,不在系統裡

這張表刻意讓「HTTP 2xx」與「P95 latency」兩欄從頭到尾都寫著「正常」,用意是把讀者的眼睛按在這兩欄上,逼自己承認:如果 release record 不存在,這兩個最常被拿來當「系統健康」代表的指標,完全無法告訴你昨天下午發生了什麼。第四列尤其值得多看一眼——「答案在記憶裡,不在系統裡」意味著回退的速度,這時候完全取決於「昨天負責上線的那個人今天有沒有上班」這種運氣因素,而不是任何工程設計。

為什麼要刻意選一個「小」情境

有經驗的讀者可能會問:這個情境聽起來有點溫和,客服助理引用內部草稿,頂多是尷尬,遠不到 OpenAI 或 Anthropic 那種數億使用者等級的事故,值得寫這麼多字嗎?

答案正是因為它「小」。真正的品質事故往往同時伴隨壓力、時間緊迫、還有一堆不熟悉的工具要現學,在那種狀態下練習「怎麼查版本、怎麼比對資料、怎麼決定回退」,效果通常很差。先在一個影響有限、後果可控的情境裡走一遍,養成「先查 release record,不要先猜」的反射動作,之後遇到真正嚴重的事故時,才不必在最緊繃的時刻同時發明流程與執行流程。

⑨ 先定義 Change Contract,而不是先寫 pipeline

一個 change manifest 能記錄版本。

但它還沒有說明變更的風險、判斷門檻與授權範圍。

我會再放一份較完整的 change-contract.yaml。

它不是某個 CI 平台的專屬格式。

它是團隊對「這次要改什麼、怎麼知道變壞、誰能按下回退」的共同契約。

manifest 與 contract 這兩份文件常被混為一談,值得先用一組對照把界線畫清楚:

Change Manifest
=
「有哪些版本」的清單

Change Contract
=
manifest
+
風險說明(known_risks/non_goals)
+
判斷門檻(gates)
+
回退授權(rollback owner/trigger)

manifest 回答的是「是什麼」,一份清單就能講完;contract 回答的是「為什麼、怎麼判斷、誰能決定」,需要完整的敘述與門檻定義才夠。這也是為什麼 ④ 的 DIY 只做到 manifest 的 schema 驗證就停了——manifest 的欄位是否齊全,可以用程式判斷;但 contract 裡「known_risks 寫得夠不夠具體」「safe_fallback 是不是真的可評估」,這類問題目前還是要靠人類 reviewer 的判斷,這正是 ⑨ 開頭強調 contract 要讓人看得懂、而不是優先為機器設計格式的原因。

change_id: answer-v17
title: "調整政策問答的引用格式"
service: ask-api
owner: ai-platform
reviewers:
  - application-owner
  - safety-owner

change_scope:
  code_revision: "git:abc1234"
  prompt_version: "policy-answer-2026-09-21-a"
  model_route: "provider/model-name"
  embedding_model: "embedding-model-v3"
  retrieval_index: "policy-index-2026-09-20"
  tool_schema: "policy-tools-v4"
  evaluator_version: "citation-evaluator-v2"
  golden_dataset: "policy-golden-v3"

expected_behavior:
  user_visible_change: "回答會以條列方式附上可驗證的政策來源"
  non_goals:
    - "不改變可回答的政策範圍"
    - "不增加可執行 tool 的權限"
  known_risks:
    - "格式要求可能讓短回答變長"
    - "citation parser 可能拒絕舊格式輸出"

release_plan:
  strategy: canary
  initial_traffic_percent: 5
  observation_window_minutes: 30
  promotion_steps: [5, 25, 50, 100]
  freeze_windows:
    - "重大政策更新期間不發布"

gates:
  offline:
    citation_valid_rate:
      comparator: ">="
      value: 0.98
    task_success_rate:
      comparator: ">="
      value: 0.95
    safety_refusal_regression:
      comparator: "=="
      value: 0
  online:
    error_rate_delta:
      comparator: "<="
      value: 0.005
    p95_latency_delta_ms:
      comparator: "<="
      value: 250
    negative_feedback_rate_delta:
      comparator: "<="
      value: 0.01

rollback:
  target_change_id: answer-v16
  owner: ai-platform-oncall
  safe_fallback: "回覆暫時無法確認,要求使用者改查正式政策頁"
  compatibility_check:
    - "answer-v16 能讀取目前的 retrieval metadata"
    - "answer-v16 不會呼叫已移除的 tool"
  rollback_trigger:
    - "citation_valid_rate 低於門檻"
    - "安全 evaluator 出現新類型失敗"
    - "on-call 無法確認受影響資料範圍"

先看幾個容易被忽略的欄位。

non_goals 是避免需求在上線途中膨脹。

如果 PR 寫的是「修引用格式」,結果 reviewer 發現它順便換了 model route,就知道必須拆開。

known_risks 也不是認錯清單。

它讓觀測的人知道該看哪裡。

例如短回答變長,token 與 P95 latency 就應該列入 release 後的比較。

safe_fallback 要寫給使用者看得到的結果。

「catch exception」不是 fallback。

「暫時無法確認,請至正式政策頁核對」才是可評估的產品行為。

為什麼是 YAML 這種「人看得懂」的格式,而不是內部 JSON schema

寫這份 contract 時,很容易忍不住想把它設計成一個嚴謹的 JSON Schema,方便機器驗證。但這裡刻意選擇 YAML、而且欄位命名盡量貼近自然語言(known_risks、safe_fallback 而不是縮寫或代碼),原因跟④段落 DIY 專案裡的 schema 驗證邏輯,剛好是同一個道理的兩面:

Schema 驗證負責回答:這份文件「格式」合不合法?
人類 reviewer 負責回答:這份文件「內容」講不講得通?

change-contract.yaml 的第一個讀者,通常不是 CI pipeline,而是坐在 review 會議上的另一位工程師、或是安全團隊的 reviewer。如果格式太貼近機器語言,人類 reviewer 光是解讀欄位名稱就要花掉一半心力,真正該花時間判斷的「known_risks 寫得夠不夠具體」反而被忽略。contract 先為人服務,再為機器服務——machine-readable 的驗證邏輯永遠可以在後面補上(就像④做的那樣),但如果一開始就把格式寫死成只有機器看得懂的樣子,人類 review 這一關反而會先被犧牲掉。

reviewers 與 freeze_windows 這兩個容易被忽略的欄位

除了前面提過的 non_goals 與 known_risks,contract 裡還有兩個欄位值得多花一段解釋。

reviewers 列的是「這次變更需要誰簽核」,而不是「誰寫了這次變更」。這個區分很重要:如果寫 prompt 的人同時是唯一的 reviewer,這份 contract 的審查機制形同虛設——本質上跟沒有 review 沒有兩樣。範例裡刻意放了兩個角色,application-owner 負責判斷「這次變更符不符合產品需求」,safety-owner 負責判斷「這次變更有沒有引入新的安全風險」。兩個角色關心的問題不一樣,讓同一個人身兼兩者,容易在忙碌時只顧到其中一邊。

freeze_windows 記錄的是「這段時間不該發布,即使 gate 全部通過」。這個欄位存在的理由,跟版本號、golden dataset 這些「這次變更是什麼」的欄位完全不同——它記錄的是「即使這次變更本身沒有問題,時機也可能是問題」。例如範例裡寫的「重大政策更新期間不發布」:假設公司正在同步更新一批政策文件,這時候即使 prompt 和 model 都沒問題,貿然發布仍可能讓使用者在文件半新半舊的過渡期拿到自相矛盾的答案。這是一種只有 AI 系統才特別需要考慮的風險——因為系統的「知識」跟外部資料的同步狀態,本身就是行為的一部分。

Change Contract 的最小規則

先不用追求欄位齊全。

至少守住以下規則。

每個會改變行為的 artefact 都要有識別值。
每個 release 都要說明預期改變與不預期改變。
每個 gate 都要有指標、比較方式與門檻。
每個 rollback 都要有目標、owner 與相容性檢查。
每個高風險變更都要列出人工決策點。

若模型供應商只給「latest」,不要把 latest 當版本。

它只代表你失去可重現性。

至少記錄完整 model identifier、provider 回傳的 release 標示與切換時間。

若供應商沒有提供 immutable identifier,change contract 應明說這是限制。

不要假裝可精準重播。

rollback 欄位裡最容易被忽略的三個子問題

⑨ 範例 contract 裡的 rollback 區塊,表面上只有四個子欄位,但每一個都在回答一個容易被跳過的問題。

target_change_id 回答「退到哪裡」,這個欄位最直觀,卻不代表「隨便填上一個舊版本」就夠了——它應該永遠指向「上一個已知穩定、而且已經完整跑過 canary 全部階段」的版本,而不是「上上次改動」這種模糊描述。如果團隊習慣把 target_change_id 寫成「上一版」而不是明確的識別字,遇到需要連續回退兩次的情況(這次的候選失敗,上一版其實也有殘留問題),就會發現「上一版」這個相對指稱根本查不出具體是哪一個版本。

compatibility_check 回答「退了之後,舊版本還能不能正常運作」,這是最常被漏掉的一步,因為它牽涉到「時間」這個變數——寫 contract 的當下,answer-v16 當然能讀取目前的 metadata,因為兩個版本本來就是連續發布的。但如果 answer-v17 上線後三個月,answer-v16 才需要被拉出來當 rollback target,這三個月間資料 schema、tool 權限可能都已經默默演進,compatibility_check 寫在 contract 裡的內容也可能早已過期而沒人發現。這也是為什麼 ⑭ 的 rollback card 建議在正式上線前就把相容性檢查重新跑一遍,而不是完全信任 contract 裡寫過的舊結論。

rollback_trigger 回答「什麼情況該按下這個按鈕」,它跟 gate 的門檻應該是同一組數字,不能各寫各的——如果 gate 定的門檻是 citation_valid_rate >= 0.98,rollback_trigger 卻寫著一段模糊的「品質明顯下降」,值班的人在深夜看到 0.975 這個數字時,完全沒辦法判斷這算不算觸發條件,只能憑感覺猜。

下一篇會接著談:怎麼把六種 AI 變更類型分開評估、如何把 evaluation gate 與 release flow 寫成可審查的流程,以及 canary 與分層 rollback 具體怎麼做。


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


上一篇
Day 27(下)|Incident、Runbook、Postmortem:修好服務後,別把證據丟掉
下一篇
Day 28(下)|CI/CD 與 AI Change Management:model 換版也是 deployment
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 58 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言