iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

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

Day 30(下)|Chaos Engineering for AI Reliability:實驗不是亂拔插頭

  • 分享至 

  • xImage
  •  

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

結論先說:光有實驗設計還不夠,得真的跑過一次、把 metrics、logs、traces 兜成同一條因果鏈,並讓每個 scenario 的預期結果寫清楚,才算完成一次可被檢視的 chaos experiment。

承接上文:上篇定義了合格實驗的骨架(hypothesis → steady state → injection → observe → rollback → learn),列出 AI workflow 專屬的注入點,寫了三份 DIY experiment card(provider timeout、DB unavailable、tool provider failure),並用 Cloudflare、Knight Capital、Meta 三個真實事故,說明安全欄杆與 kill switch 為什麼要設計成不依賴被測系統本身。下篇接著把這些設計真的跑一次,並把發現收斂成長期可靠性工作。

⑩ 觀測設計:同一個 request 要能在四種訊號裡找到

Day 17、Day 18 和 Day 20 分別把 metrics、traces、logs 和 alert 拉進 SRE Lab。

今天的最低要求是:每一個注入 request 都能由 request_id 找到四種證據。

request_id
  ├─ HTTP access log
  ├─ root trace and child spans
  ├─ outcome counter increment
  └─ experiment evidence record

不要把 request_id 放進 Prometheus label。

它的 cardinality 會失控。

metrics 只保留低 cardinality 的分類,例如 scenario、outcome、dependency 和 environment。

ASK_OUTCOMES.labels(
    workflow="ask",
    outcome=result.workflow_status,
    degraded=str(result.degraded).lower(),
).inc()

DEPENDENCY_FAILURES.labels(
    dependency="model_provider",
    failure_class="timeout",
).inc()

scenario 是否放進 metric label,要看你是否會無限制新增 scenario。

如果實驗 ID 每次都不同,就不要當 label。

把 experiment ID 放在 structured log、trace attribute 或 evidence file。

以下是建議的結構化 log。

logger.info(
    "ask workflow finished",
    extra={
        "request_id": request_id,
        "experiment_id": "ce-ask-provider-timeout-v1",
        "chaos_scenario": chaos.scenario,
        "technical_status": result.technical_status,
        "workflow_status": result.workflow_status,
        "task_status": result.task_status,
        "degraded": result.degraded,
        "prompt_version": prompt_version,
        "deployment_version": deployment_version,
    },
)

不要把完整 prompt、retrieved document 或 API key 一起記進去。

需要重現時,優先記錄版本、文件識別字、長度、hash 或已遮罩的分類欄位。

trace 的 attribute 也遵守同樣原則。

with tracer.start_as_current_span("model.generate") as span:
    span.set_attribute("ai.workflow.name", "ask")
    span.set_attribute("ai.chaos.scenario", chaos.scenario)
    span.set_attribute("ai.provider.name", "fake-provider")
    span.set_attribute("ai.prompt.version", prompt_version)
    span.set_attribute("ai.retrieval.index_version", index_version)

觀測不是「把所有東西都記下來」。

它是讓你以最少的敏感資料,還原足以做決策的因果鏈。

⑪ Step-by-step:在隔離 SRE Lab 跑一次 provider-timeout 實驗

前面十段講了骨架、注入點、experiment card、guardrail 與 observability,接下來把它們接成一條完整的迴路。這個迴路和上篇 ① 段那行 hypothesis → steady state → controlled failure injection → observe → rollback → learn 是同一件事,只是攤開成八個可以逐步執行的步驟。

Step 1  填 experiment card ──────────┐
Step 2  取得 baseline               │ 這四步都還沒碰 chaos 開關
Step 3  確認 abort 開關真的能關      │ (對照上篇 ⑦ 的 Meta 案例:
Step 4  只對一筆 selected request 注入 timeout ── 注入開始
Step 5  查 metrics                  │ 這三步是 observe
Step 6  查 log 和 trace             │
Step 7  停止 injection 後再跑 baseline ── rollback
Step 8  填寫 actual result          ── learn,回到上篇 ⑥ card

前四步的順序刻意設計成「先確認能不能安全地停,再開始弄壞東西」——這正是上篇 ⑦ 段 Meta Oct 2021 案例要提醒的:如果 Step 3 沒有先驗證過 abort 開關不依賴被測系統本身,Step 4 一旦出狀況,你可能會發現自己正在複製一次小規模的「復原路徑跟著故障系統一起斷線」。

以下步驟由讀者在自己的隔離環境執行。

本文不會建立 DIY/、安裝 dependency、啟動 container、呼叫 provider 或宣稱驗證結果。

開始前,確認你具備以下條件。

  • 一個不連接真實 production 的 FastAPI 或等效 service。
  • 一個可替換的 model provider adapter。
  • Day 14 定義過的 SLI 或至少一個明確 outcome counter。
  • Day 17 的 metrics endpoint。
  • Day 18 或 Day 20 任一種能查到 request 的 trace 或 structured log。
  • 合成問題與合成 tenant,不能使用真實客戶 prompt。
  • 可以立即停掉 injection 的方式。

Step 1:先填完 card,不要先跑 command

把上一節的 experiment-card.yaml 複製到自己的工作目錄。

換掉 owner、service name、預期門檻與 rollback action。

確認 environment 不是 production。

確認 synthetic tenant 由程式拒絕其他 tenant 的機制保護,不能只靠人記得。

請另一位 reviewer 看過 abort condition。

預期觀測:此時不應有任何 request、metric 或 trace 因為寫文件而改變。

驗收:card 能在不口頭補充的情況下,讓 reviewer 說出何時開始、何時停止、什麼結果算失敗。

Step 2:取得 baseline

先以沒有 X-Chaos-Scenario 的 request 跑一筆合成問題。

範例 command 依你的 service address 調整。

curl --fail-with-body \
  -H 'X-Tenant: synthetic-lab' \
  -H 'Content-Type: application/json' \
  --data '{"question":"What is the synthetic remote-work policy?"}' \
  http://127.0.0.1:8000/ask

預期觀測:request 完成,workflow status 與 task status 符合你的 baseline contract。

預期觀測:log 有 request_id,trace 有 root span,metric 只有正常 outcome 增加。

驗收:你能用同一個 request_id 找到 log 與 trace;若找不到,先修 observability,不要繼續注入。

Step 3:確認 abort 開關真的能關

在注入前先確認你知道如何移除 header、停用 fake adapter 或關閉測試環境。

把實際操作寫進 card 的 rollback.action。

不要只寫「必要時 rollback」。

「必要時」不是 command。

預期觀測:此步不應改變 workflow outcome。

驗收:另一位 reviewer 只看文件就能描述恢復 baseline 的動作。

Step 4:只對一筆 selected request 注入 timeout

curl --fail-with-body \
  -H 'X-Tenant: synthetic-lab' \
  -H 'X-Chaos-Scenario: provider-timeout' \
  -H 'Content-Type: application/json' \
  --data '{"question":"What is the synthetic remote-work policy?"}' \
  http://127.0.0.1:8000/ask

預期觀測:fake provider 在 selected request raise ProviderTimeout。

預期觀測:workflow 回 provider_unavailable 或你已宣告的安全降級狀態。

預期觀測:沒有 tool action。

預期觀測:dependency failure metric 增加一次。

預期觀測:trace 顯示 provider child span failed,root span 保留 request_id 與 outcome。

驗收:如果 API 回 200,但 task_status=completed,實驗應判定失敗;這代表結果分類在降級路徑被寫壞。

Step 5:查 metrics,但不要只看一個圖

最少對照下列訊號。

ask_requests_total
ask_outcomes_total{workflow_status="provider_unavailable"}
dependency_failures_total{dependency="model_provider",failure_class="timeout"}
tool_action_attempted_total
circuit_breaker_state

預期觀測:timeout 類 failure 增加。

預期觀測:工具 action counter 沒有增加。

預期觀測:若已超過 breaker 門檻,breaker state 轉為預期狀態;若沒有超過,不能硬說「breaker 沒作用」。

驗收:metrics 數字與 request 數量、retry policy 和實驗 card 相符。

Step 6:查 log 和 trace,確認因果順序

從 selected request 的 request_id 開始。

確認你能依序看到:request accepted、retrieval、provider timeout、fallback decision、response emitted。

如果你的 workflow 沒有 retrieval 或 fallback,順序可不同。

重點是因果關係要可見,不是 span 名稱一定要長得一樣。

預期觀測:failure class 與 card 的 scenario 一致。

預期觀測:trace attribute 有版本資訊,但沒有未遮罩的 prompt 或 credential。

驗收:沒有靠猜測就能說明為何 task 沒完成、是否執行 tool、由哪個版本處理。

Step 7:停止 injection 後再跑 baseline

移除 X-Chaos-Scenario。

重新送一筆合成 baseline request。

預期觀測:service 回到原本 behavior。

預期觀測:不會因為測試留下全域 timeout、open breaker 或被污染的 fake state。

驗收:恢復後 baseline 和 Step 2 的 outcome 相容;如果不相容,先記錄為實驗副作用,暫停下一個 scenario。

Step 8:填寫 actual result,不要把預期複製回去

card 的結果區可長這樣。

result:
  started_at: "2026-09-22T10:00:00+08:00"
  stopped_at: "2026-09-22T10:04:00+08:00"
  selected_requests: 1
  expected:
    task_status: not_completed
    tool_action_attempted_delta: 0
  actual:
    task_status: not_completed
    tool_action_attempted_delta: 0
    dependency_timeout_delta: 1
  decision: passed
  follow_up: "add regression test for provider timeout classification"

實際結果若不同,就照實寫不同。

例如 timeout 被 retry 三次才回應、breaker 沒有打開、log 少了 deployment version,都比「整體成功」更有價值。

⑫ 第二個實驗:empty retrieval 不等於可以自由回答

RAG workflow 的一個常見錯覺是:retrieval service 正常回 200,就代表 answer 有足夠根據。

空結果、過期 index、權限過濾後空結果,以及結果雖多但都不相關,都是不同 failure class。

今天先從最容易重現的 empty result 開始。

在 retrieval adapter 內加入同樣的 selected-request guard。

def retrieve_documents(question: str, chaos: ChaosContext) -> list[dict]:
    if chaos.scenario is ChaosScenario.EMPTY_RETRIEVAL:
        return []

    return [
        {
            "document_id": "synthetic-policy-v1",
            "title": "Synthetic Remote Work Policy",
            "content": "Synthetic content for an isolated lab.",
        }
    ]

workflow 不要把空 list 直接交給 model,期待它「知道自己不知道」。

先寫明確的 policy decision。

def answer_workflow(question: str, chaos: ChaosContext) -> AskResult:
    documents = retrieve_documents(question, chaos)

    if not documents:
        return AskResult(
            technical_status="completed",
            workflow_status="insufficient_context",
            task_status="not_completed",
            quality_status="not_grounded",
            safety_status="no_tool_action",
            degraded=False,
            answer=None,
            reason="retrieval_empty",
        )

    # Provider path follows here.
    ...

這裡的 technical_status="completed" 是一個重要細節。

retriever 可以正常完成查詢,workflow 也可以正常做出「不能回答」的決定。

但使用者的原任務沒有完成。

把這三層狀態混成單一 boolean,dashboard 再漂亮也救不了調查。

Empty retrieval 的實驗 card 問題

  • 問題是否保證只使用合成資料?
  • index version 是否被記錄?
  • result_count=0 是否可在 trace 找到?
  • 系統會不會偷偷跳過 validator?
  • 會不會在沒有 evidence 的情況下產生看似確定的答案?
  • tool policy 是否仍是 deny?
  • task-success SLI 是否排除這一筆?
  • 使用者是否看到能理解的說明,而不是空字串?

預期觀測:model span 不應存在,或應清楚標註為 skipped。

預期觀測:output 含 workflow_status=insufficient_context。

預期觀測:citation counter 不會誤把空結果當成有效 citation。

驗收:任何 downstream retry 都不能改變「沒有足夠 context」這個事實。

反例:把 empty retrieval 當成 provider outage

若你把所有 failure 都重試 provider,empty retrieval 會白白多花 token。

更糟的是,retry 後可能剛好出現一個不相關文件,讓 bug 偶發地被掩蓋。

正確的修復方向可能是 index freshness、query rewrite、document permission 或資料治理。

它不一定是更大的 model。

⑬ 第三個實驗:tool schema mismatch 要停在 policy boundary

Agent 或 tool-enabled workflow 的風險不只「回答錯」。

它也可能組出錯誤參數、呼叫過期 schema,或把模型輸出的字串當成已授權 action。

因此,schema mismatch 的實驗不該追求「最後還是呼叫成功」。

它應驗證系統會拒絕未驗證的 action。

先為 tool call 定義一個極小 contract。

from dataclasses import dataclass


@dataclass(frozen=True)
class ToolCall:
    name: str
    schema_version: str
    arguments: dict[str, object]


class ToolSchemaMismatch(Exception):
    pass


def validate_tool_call(call: ToolCall, expected_schema: str) -> None:
    if call.schema_version != expected_schema:
        raise ToolSchemaMismatch(
            f"expected={expected_schema}, got={call.schema_version}"
        )

再將 chaos scenario 放在 tool planner 或 fake provider,讓它產生舊版 schema。

def plan_tool_call(chaos: ChaosContext) -> ToolCall:
    schema_version = "tool-v1"
    if chaos.scenario is ChaosScenario.TOOL_SCHEMA_MISMATCH:
        schema_version = "tool-v0"

    return ToolCall(
        name="lookup_policy",
        schema_version=schema_version,
        arguments={"policy_id": "synthetic-remote-work"},
    )

workflow 必須先驗證,才有 dispatch 的資格。

def execute_tool_if_allowed(call: ToolCall) -> dict:
    validate_tool_call(call, expected_schema="tool-v1")
    return tool_client.execute(call)

這個順序看似無聊。

事故發生時,它會決定你是在處理一筆被拒絕的 request,還是在處理一個真的發出去的錯誤操作。

這個實驗要看的訊號

檢查點 預期 不可接受結果
schema validator 明確拒絕 tool-v0 靜默轉換或忽略版本
dispatch counter 不增加 實際呼叫 tool provider
trace 有 policy decision 與 schema version 只有「tool failed」
response 要求修正、人工處理或安全拒絕 假裝 action 已完成
audit log 有 request_id、decision、actor scope 記錄完整敏感 argument

驗收:將 tool-v0 注入一次後,沒有任何非冪等 action 被重送或猜測成功。

如果 tool 是讀取型,產品可選擇較溫和的 fallback。

如果 tool 是寫入型、付費型或權限型,預設應更保守。

「先試看看」不是 authorization model。

⑭ Circuit breaker 實驗:測狀態轉換,不是只看 exception

Day 29 已經介紹 timeout、retry、backoff 和 circuit breaker。

今天驗證的是 breaker 會不會在真實 workflow 中做出正確狀態轉換。

先把狀態與門檻明確化。

closed
  └─ 連續或比例失敗超過門檻 → open

open
  └─ cooldown 到期 → half_open

half_open
  ├─ 探測成功 → closed
  └─ 探測失敗 → open

為什麼中間要多一個 half_open,不能 open 一段時間後就直接跳回 closed?

因為直接跳回 closed 等於用「全部流量」去賭下游是不是已經恢復。如果下游其實還沒好,你等於把剛剛才被 breaker 擋下來的整批流量,一次全部重新打過去。這和完全沒有 breaker 沒有兩樣,只是延後發生。half_open 的作用是先放行少量、可控數量的探測 request,用小規模的代價換取一個訊號:下游到底好了沒。探測失敗就退回 open,不會把整個系統重新暴露在還沒恢復的依賴前面。

不要把「五次 timeout 後 open」當成普世答案。

門檻需要根據流量、錯誤型態、下游恢復時間與產品降級能力決定。

例如低流量服務的五次 failure 可能代表半小時,等太久。

高流量服務的五次 failure 可能只是幾十毫秒的雜訊。

對隔離 Lab,你可以用很小門檻讓狀態容易觀察,但要在 card 註明它不是 production threshold。

from dataclasses import dataclass


@dataclass
class SimpleBreaker:
    failure_threshold: int = 2
    consecutive_failures: int = 0
    state: str = "closed"

    def before_call(self) -> None:
        if self.state == "open":
            raise RuntimeError("circuit_open")

    def record_success(self) -> None:
        self.consecutive_failures = 0
        self.state = "closed"

    def record_failure(self) -> None:
        self.consecutive_failures += 1
        if self.consecutive_failures >= self.failure_threshold:
            self.state = "open"

這是教學模型,不處理 clock、half-open、concurrency 或分散式狀態同步。

真正 service 不應直接複製它上 production。

但它足以讓你練習觀察兩次 selected provider timeout 後,第三次 request 是否不再打到 provider。

Breaker 實驗步驟

  1. 以 baseline request 確認 breaker 為 closed。
  2. 對兩筆 synthetic request 注入 provider timeout。
  3. 確認第二筆後 breaker metric 或 log 顯示 open。
  4. 送第三筆 selected request,但不要再注入 timeout。
  5. 確認第三筆被 breaker 快速拒絕或走既定 fallback,而非真的呼叫 provider。
  6. 依你的 cooldown 實作等候或手動推進測試 clock。
  7. 在 half-open 只允許已定義數量的探測 request。
  8. 確認成功後才回 closed。

預期觀測:open breaker 降低 downstream call 數與 request latency。

預期觀測:open breaker 不會把 task 失敗偽裝成 completed。

驗收:你能從 evidence 說明每一次狀態轉換的觸發 request,以及第三筆為何沒有呼叫 provider。

這整套狀態機看起來像是在防微杜漸的小題大作,直到你想起上篇 ⑦ 段那個沒有 kill switch 的 Knight Capital。Knight Capital 的系統裡完全沒有這種「異常累積到門檻就自動 open、擋下後續流量」的機制——沒有 breaker,只有一群工程師在異常已經持續好幾十分鐘後,才靠人工介入。SimpleBreaker 這種教學等級的實作,門檻設 2 看起來很隨便,但它體現的原則跟 Knight Capital 缺少的東西完全一樣:系統要有能力在異常持續發生時,自己先擋下後續流量,不必等到人類注意到、看懂、決定該怎麼辦。breaker 慢一秒鐘 open,都比完全沒有 breaker 要好。

常見錯誤:breaker 在 memory 裡,每個 worker 各自一份

如果 service 有多個 process 或 instance,in-memory breaker 可能只在其中一個 worker open。

這不一定錯。

它可能是有意識的 per-instance isolation。

但若你以為它是全域保護,實驗就會揭露不同結果:一半流量繼續打爆下游,另一半快速失敗。

把 scope 寫進 card:per-request、per-process、per-instance、per-region 或 global。

沒有 scope 的 breaker 名稱,和沒有 unit 的 latency 一樣危險。

Cloudflare 在上篇第 ⑦ 段那次中斷的事後報告裡,把「增設更多全域 kill switch」列為修復項目之一。換句話說,他們手上並不缺開關,缺的是一個範圍夠精準、能單獨關掉出問題 feature 的開關,而不必讓整條 proxy 陪著重啟。這正是這裡要驗證的東西:breaker 或 kill switch 的 scope,決定了你事故當下能不能只切掉壞的那一小塊,還是只能選擇「整個服務一起停」或「什麼都不做」這兩個極端。實驗時先問自己一句:如果這個 breaker 真的 open 了,它擋住的是一個 request、一個 provider,還是整個服務?答案往往和你原本以為的不一樣。

⑮ Queue delay 實驗:deadline 要跟著 request 走

AI workflow 常因為 token generation、tool execution 或 GPU queue 產生長尾。

此時只在 HTTP handler 設 timeout 不夠。

request 進 queue 後,worker 仍可能開始處理一件使用者早就離開的工作。

先把 deadline 放進 request context。

from dataclasses import dataclass
from time import monotonic


@dataclass(frozen=True)
class RequestDeadline:
    expires_at: float

    def expired(self) -> bool:
        return monotonic() >= self.expires_at

enqueue 時將 deadline 與 request_id 一起保存。

worker dequeue 時先檢查 deadline。

def process_job(job: dict) -> AskResult:
    deadline = RequestDeadline(expires_at=job["expires_at"])
    if deadline.expired():
        return AskResult(
            technical_status="completed",
            workflow_status="deadline_exceeded_before_execution",
            task_status="not_completed",
            quality_status="not_evaluated",
            safety_status="no_tool_action",
            degraded=False,
            answer=None,
            reason="queue_delay",
        )

    return answer_workflow(job["question"], job["chaos"])

這個例子不處理 job acknowledgement、lease 或 exactly-once delivery。

它要傳達的只有一件事:deadline 不能在跨 queue 時蒸發。

Queue delay 注入方式

在隔離 Lab,可以用 fake worker 在處理前等待固定時間。

from time import sleep


def fake_queue_delay(seconds: float, chaos: ChaosContext) -> None:
    if chaos.scenario.value == "queue-delay":
        sleep(seconds)

若你採用 async runtime,請用對應的非阻塞 sleep。

本文故意不指定框架,因為核心是 deadline contract,不是 sleep API。

預期觀測:queue age metric 增加。

預期觀測:過期 job 不會啟動 model call 或 write tool。

預期觀測:使用者端看到可理解的逾時結果,或可依產品設計查詢非同步 job 狀態。

驗收:沒有任何 request 在 deadline 過期後仍執行高成本或有副作用的工作。

反例:只提高 timeout

當 queue 延遲升高,把 timeout 從 30 秒改成 120 秒通常只是延後排隊爆炸。

更多 request 留在記憶體、更多 worker 被卡住、更多使用者同時重試。

有時候可接受的行為就是取消。

這表示服務誠實地遵守 deadline。

⑯ 「成功」也要驗證:fallback 資料的 freshness 與授權範圍

Chaos experiment 最容易只盯著 exception。

但安全的 fallback 也可能過期、越權或不適用。

假設你的 provider outage fallback 使用快取的政策摘要。

至少要有下列 metadata。

{
  "fallback_source": "policy-summary-cache",
  "source_version": "policy-2026-09-01",
  "generated_at": "2026-09-01T00:00:00+08:00",
  "expires_at": "2026-09-08T00:00:00+08:00",
  "audience": "synthetic-lab",
  "allowed_intents": ["read_only_policy_question"]
}

實驗要故意放一筆過期 fallback。

預期結果不該是「至少有答案」。

它應該被拒絕,或標示為無法提供最新資訊。

再放一筆 audience 不匹配的 fallback。

它也不應因為 provider 掛了就被放寬存取控制。

這類實驗把 reliability 與 governance 放在同一條路徑。

使用者通常不在乎你是因為 outage 還是 cache 拿到錯資料。

他只會記得答案害他做了錯決定。

⑰ Alert:實驗期間要分得出「預期注入」與「真的事故」

若你每次跑 chaos experiment 都 page on-call,團隊很快會討厭這個練習。

若你把實驗期間的所有 alert 靜音,真的事故也可能被一起蓋掉。

兩者都不好。

比較好的作法是讓 alert policy 能識別受控實驗。

例如在隔離環境使用獨立 routing key。

或是將 experiment window 和實驗 ID 寫進 alert annotation,而不是直接 suppress 每一個 dependency failure。

alert: AskProviderTimeoutRateHigh
expr: |
  sum(rate(dependency_failures_total{
    dependency="model_provider",
    failure_class="timeout"
  }[5m])) > 0.2
for: 10m
labels:
  severity: page
annotations:
  runbook: "docs/runbooks/provider-timeout.md"
  summary: "Model-provider timeouts exceed the declared threshold"

這只是 PromQL 概念範例。

門檻與 metric 名稱必須符合你的量測定義。

實驗 card 可要求 observer 在開始前確認 alert routing。

預期觀測可能是:Lab alert 會觸發 ticket,但不會呼叫 production on-call。

如果實驗預期不會跨過 alert threshold,也要寫清楚。

「沒有 alert」可能是符合預期,也可能是 alert 壞了。

判斷要靠你預先寫下的門檻,不靠結束後的解釋。

Google 的 emergency response 章節提到過受控測試意外失控時立即 abort 的案例;這正是 abort 條件應在開始前明確化的理由。Emergency Response

⑱ Evidence packet:讓結果能被重看,而不是留下截圖孤兒

一次實驗結束後,整理 evidence packet。

它可以是 Markdown、issue template 或內部系統的 record。

關鍵是資料之間能互相指向。

# ce-ask-provider-timeout-v1 result

## Scope
- Environment: isolated-lab
- Service version: ask-api@abc1234
- Prompt version: policy-answer-v3
- Retrieval index: synthetic-index-v1

## Hypothesis
- Selected provider timeout does not dispatch a tool action.

## Actual
- request_id: req-...
- workflow_status: provider_unavailable
- task_status: not_completed
- tool_action_attempted_delta: 0

## Evidence
- metrics query: ...
- trace_id: ...
- log query: request_id=req-...
- alert status: not expected to fire in this run

## Decision
- Passed with one gap: breaker state lacks a metric.

## Follow-up
- Owner: ...
- Due condition: before enabling provider fallback in staging

不要把 dashboard screenshot 當成唯一 evidence。

截圖沒有 query、時間窗、filter 和版本時,很難重新判讀。

至少保留 query、trace ID、log filter、request list 與資料版本。

也不要把「passed」當成實驗輸出本身。

它是根據 evidence 作出的 decision。

如果 card 的 hypothesis 只驗證了 fake provider,evidence 不能外推成「真實供應商 outage 已被證明安全」。

這是驗證邊界,不是謙虛用語。

⑲ 實驗結果的四種正確結論

結果 可以說什麼 不可以說什麼 下一步
通過 在這個環境、版本、範圍內,觀察結果符合假設 production 永遠安全 將關鍵路徑轉為 regression test 或擴大一個小範圍
假設被推翻 某個 contract 沒被滿足 chaos engineering 沒用 修正 failure handling,保留案例
結果不明 evidence 不足以判定 應該算通過 補 telemetry、縮小變因後重跑
中止 abort condition 被觸發,實驗停止 已驗證或失敗 先處理 blast radius/guardrail 缺口

「中止」不一定是負面結果。

若 abort 機制如預期阻止擴散,它本身是有用 evidence。

但被中止的主假設仍然沒有完成驗證。

不要在 report 裡把兩件事混在一起。

⑳ 把發現轉成長期可靠性工作,而不是一次性的 demo

每一個 experiment 至少選一個後續動作。

發現 合適的後續動作
provider timeout 被誤算 task success 加入 outcome classification regression test,更新 SLI query
empty retrieval 仍會呼叫 model 在 workflow 加 early return,建立 dataset case
schema mismatch 仍 dispatch tool 增加 server-side validator,收緊 permission boundary
request_id 無法串起 log 和 trace 修正 context propagation,補 runbook
breaker open 不可觀察 新增狀態 metric 和 dashboard panel
fallback 使用過期快取 版本化 cache metadata,加入 freshness gate
實驗造成真實 alert routing 分隔 Lab 與 production route,更新 abort check

Day 27 的 postmortem 強調 action item 要有 owner 與可驗證的完成條件。

Chaos experiment 的 follow-up 也一樣。

「改善韌性」不是 action item。

「在 answer_workflow 對 empty retrieval 早退,並以 synthetic case 驗證 model span 沒產生」才是。

Day 28 的 change manifest 也應列出這些行為變更。

修 provider fallback 可能改變使用者可見的結果,不能當作純 internal refactor。

㉑ 一個可重跑的 90 分鐘 Lab 節奏

以下是讀者可以自行調整的練習節奏。

它不是 production runbook。

它的好處是避免「準備 50 分鐘、注入 5 秒鐘、結論卻花一週猜」的常見狀況。

時間 動作 產出
0–10 分 選單一假設,填 card hypothesis、scope、abort
10–25 分 檢查 baseline 與 observability baseline request_id、dashboard query
25–35 分 演練 rollback 可重述的 stop procedure
35–50 分 注入一個 scenario selected request evidence
50–65 分 查 metrics、logs、traces actual outcome
65–75 分 停用 injection,重跑 baseline recovery evidence
75–90 分 寫 decision 與 follow-up evidence packet、owner

一開始不要在 90 分鐘內同時測 provider、DB、queue、tool、GPU 及 evaluator。

那是一場故障拼盤。

最好的第一個實驗通常很小:一筆合成 request、一個 fake timeout、一條清楚的 task-status contract。

它若失敗,修起來快。

它若通過,也能成為下一個實驗的 baseline。

㉒ 實驗前檢查表

[ ] 已指定隔離環境與合成資料範圍
[ ] 已定義一個可否證 hypothesis
[ ] 已確認 steady state,且沒有既有 incident
[ ] injection 只能由明確 selector 啟用
[ ] 有 max duration 與 max request 限制
[ ] 已寫 abort condition 與實際 rollback 動作
[ ] observer 知道要查哪些 metrics、logs、traces
[ ] request_id 不會進入 Prometheus 高 cardinality label
[ ] 已保護 prompt、文件、credential 與個資
[ ] 已指定是否允許 tool,以及預設拒絕行為
[ ] 已確認 alert routing 不會誤 page 不相關 on-call
[ ] 已準備 experiment card 與 evidence packet 位置

㉓ 實驗後檢查表

[ ] 已記錄實際開始與結束時間
[ ] 已保存 selected request_id 與版本資訊
[ ] 已比對 expected 和 actual outcome
[ ] 已確認 tool action delta
[ ] 已確認 metrics、logs、traces 的因果關係
[ ] 已執行 rollback,並取得 baseline recovery evidence
[ ] 已標示結果為 passed、failed、inconclusive 或 aborted
[ ] 已寫明驗證邊界,沒有外推到未測環境
[ ] 已建立具 owner 與完成條件的 follow-up
[ ] 已將可重現 failure 放回 regression suite 或 dataset

㉔ 常見失敗模式,以及為什麼它們會讓實驗失去意義

失敗模式一:先注入,之後才想要量什麼

結果通常是一堆 log。

但你不知道哪一筆是實驗造成,哪一筆是原本就存在的雜訊。

修法:先寫 hypothesis 和 observe 欄位,再開始任何 injection。

失敗模式二:把所有 layer 一次打壞

provider timeout 加 DB disconnect 加 queue delay,確實很像大型事故。

但第一次做時,你沒有辦法知道是哪個 guardrail 失效。

修法:一次一個 primary fault;後續再設計組合 scenario。

失敗模式三:只看 infrastructure green

Pod 活著、CPU 不高、HTTP 200,都可能和使用者任務失敗並存。

修法:同時寫 technical、workflow、quality、safety、task outcome。

失敗模式四:retry 把實驗吞掉

如果 retry 後 request 成功,可能代表系統韌性良好。

也可能代表你根本沒有驗證 retry 上限、成本與 deadline。

修法:記錄 attempt count、backoff、總 latency、token 或外部 call 數。

失敗模式五:實驗成功,卻沒有回復 baseline

這通常表示 fake state、cache 或 breaker 還留著。

修法:recovery baseline 是每張 card 的必填 acceptance criterion。

失敗模式六:只記錄「通過」

過兩週換了 prompt、模型或 retriever,這個結論已經很難解釋。

修法:記錄 code、prompt、model、index、evaluator 與 experiment revision。

失敗模式七:把 production 壓力當成學習捷徑

production chaos 需要成熟的 SLO、隔離、observability、on-call 協調與回退能力。

新系統先在 Lab 做得小且可重跑,比直接放大 blast radius 更有工程價值。

Netflix 對 chaos engineering 的原始論文強調它是建立系統承受真實擾動信心的實驗方法,不是追求破壞性的表演。Chaos Engineering

㉕ 把本系列七層可靠性框架帶回 Day 30

可靠性層 可注入的假設 可觀察的證據 安全限制
Infrastructure GPU worker 變慢 utilization、queue age、deadline 不對共用 production node 注入
Service /ask timeout request outcome、latency、error synthetic request selector
Dependency provider 或 vector DB timeout dependency span、retry、breaker fake adapter 或隔離 endpoint
Workflow parser 或 router 失敗 step outcome、fallback decision 不繞過 validator
Semantic context 不足 groundedness、citation、task status 不生成無根據答案
Economic retry amplification attempts、token、queue cost 設 request 上限
Governance schema/permission mismatch policy decision、audit event 預設 deny、無寫入 action

這張表不是要你在一天內把七層都測完。

它用來避免一個盲點:AI reliability 不是只多了一個 model provider。

你在 Day 30 驗證的,是跨層承諾是否能在 failure 中保持一致。

㉖ 給不同成熟度團隊的起點

剛開始的個人 Lab

先做 provider timeout。

使用 fake adapter、synthetic request 和一個 outcome counter。

目標是練習「失敗不等於 task completed」與 request correlation。

已有 RAG service 的小團隊

再做 empty retrieval 和 index version。

目標是讓 insufficient_context 成為可量測的正常結果,而不是 error bucket 的垃圾桶。

有 tool 或 agent 的團隊

優先做 schema mismatch、permission denied 和 deadline exceeded。

目標不是維持 agent 看起來很會做事。

目標是確保它失敗時不會執行未授權 action。

已有 SLO、on-call 與 release gate 的團隊

把實驗結果連到 Day 27 postmortem、Day 28 manifest、Day 29 degradation policy 和 Day 31 的 regression dataset。

目標是讓每次失敗都留下可被下一次 release 重跑的 evidence。

㉗ 今日驗收

以下清單是本文的讀者驗收條件。

[ ] 能清楚區分 unit test、integration test、load test 與 chaos experiment
[ ] 已寫出一個可否證 hypothesis,而不是「系統很有韌性」
[ ] 已定義 steady state、blast radius、abort 和 rollback
[ ] 已將注入限制在 selected synthetic request
[ ] 已設計 provider timeout、empty retrieval 或 tool schema mismatch 其中一個 scenario
[ ] 已為 scenario 定義 technical、workflow、quality、safety、task outcome
[ ] 已確認 request_id 可串起 log、trace、metrics 關聯與 evidence record
[ ] 沒有把 request_id 放入高 cardinality metric label
[ ] 已確認 fallback 不會回答沒有根據的內容,也不會繞過 tool policy
[ ] 已驗證或設計 breaker、deadline、retry 的可觀察行為
[ ] 已執行或規劃 recovery baseline,不把實驗殘留當成正常狀態
[ ] 已留下 experiment card、actual result 與具 owner 的 follow-up
[ ] 已寫明隔離 Lab 的結果不能外推成 production 已驗證

㉘ 這 30 天最後留下的是什麼?

這個系列從 Day 1 的 trace 開始。

當時我們只是在看一個 request 穿過 prompt、retrieval 和 LLM 後發生了什麼。

Day 30 的問題更嚴格。

當其中一段壞掉時,系統還會不會誠實地描述狀態、保護使用者、留下證據並回到可預期的 baseline?

答案不會是一個 dashboard、一個 framework 或某次 demo。

它是一連串明確的工程選擇:定義 SLI、保留 error budget、分開 task success 與 HTTP success、限制 retry、拒絕越權 tool、記錄版本、把失敗轉成 regression case。

Chaos experiment 的角色,是把「我們相信」改成「我們在這個範圍內測到」。

這句話沒有那麼熱血。

但它能讓下一次 incident 少一點猜測。

Day 31 特別篇會把這些 trace、failure、dataset、evaluation 與 release decision 接成一個 AI reliability learning loop。

工具可以換。

可靠性證據鏈不要斷。

延伸閱讀


這篇是 Learning SRE for the AI Era 系列的一部分:從 SRE Lab 到 Production AI Reliability。

Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 30(上)|Chaos Engineering for AI Reliability:實驗不是亂拔插頭
下一篇
Day 31 特別篇(上)|LangSmith 全服務解析:從 LLM Trace 到 Production AI Reliability
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 58 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言