結論先說:分流政策要能落地,還得再過三關——資料契約撐不撐得住 pager 與 evaluation queue 各自需要的欄位、規則能不能寫成可測試的純函式、以及你用來校準門檻的評估分數,本身是不是可信的。
上篇把 AI alerting 的訊號拆成 pager、evaluation queue、security escalation 三條路,並定出一張可討論的分流 policy 草案:技術可用性訊號沿用既有 burn-rate pager,單一品質樣本先進評估流程,安全風險則不必等統計門檻。這篇接著把這張政策草案落地:怎麼寫成可測試的路由邏輯、怎麼接上 Prometheus、三個完整的判讀案例,以及一個容易被忽略的問題——評估器本身也可能出錯。
這一節的目標不是設定真實告警。
使用者會自行完成 DIY。
本文不會建立 queue、連接 PagerDuty、發送通知、呼叫 provider,或驗證任何 policy 已在 production 生效。
在動手寫程式碼之前,先說清楚這個 Lab 到底在驗證什麼。前面講了很多「應該」——單筆 hallucination 不該直接進 pager、被成功執行的未授權 tool call 必須立刻升級、品質趨勢要同時有樣本數與 baseline 才算數。這些「應該」如果只停留在文字敘述,遇到真正的事故時很容易在壓力下被打折扣:值班的人看到嚇人的訊號會忍不住手動升級,真正該升級的安全事件也可能因為訊號不起眼而被漏掉。把分流邏輯寫成可被單元測試驗證的純函式,等於把「應該」變成「能被自動化反覆檢查的事實」——一套 alerting policy 如果沒辦法寫成測試案例,它多半只是一份沒人會真的照做的文件。
這裡選擇用一個不依賴任何外部 SDK、不連接任何真實系統的純函式來示範:沒有 async、沒有網路呼叫、沒有資料庫,route() 只是把事件資料轉成一個列舉值的決策函式。alerting 的分流邏輯應該可以脫離基礎設施單獨驗證,而不是散落在 Prometheus rule、PagerDuty escalation policy、Slack webhook 設定裡、只能靠「等真的出事才知道對不對」的隱性知識。把「這個事件該去哪」抽成一個純函式,就能在真正接上 alert manager 之前,用測試攔下最容易犯的兩種錯誤:規則太敏感(單筆壞事件也 page)與規則太寬鬆(真正的安全事件被晾在旁邊)。
你可以在自己的隔離環境,用固定事件檔和一個純函式先練習路由決策。
先準備一個資料夾。
day26-alert-lab/
├── events/
│ ├── provider-timeout.json
│ ├── citation-drift.json
│ ├── unsafe-tool-attempt.json
│ └── single-bad-answer.json
├── alert_router.py
└── README.md
這個範例刻意不依賴外部 SDK。
它的價值是把 alert policy 變成可測試的條件,而不是把一段口號貼進文件。
alert_router.py:
from dataclasses import dataclass
from enum import StrEnum
class Destination(StrEnum):
PAGER = "pager"
EVALUATION_QUEUE = "evaluation_queue"
SECURITY_ESCALATION = "security_escalation"
REVIEW_SAMPLE = "review_sample"
DASHBOARD = "dashboard"
@dataclass(frozen=True)
class AIEvent:
event_type: str
error_budget_fast_burn: bool = False
task_failure_rate: float = 0.0
quality_ratio: float | None = None
baseline_quality_ratio: float | None = None
sample_size: int = 0
unsafe_tool_attempt: bool = False
tool_execution_blocked: bool = False
tool_execution_succeeded: bool = False
def route(event: AIEvent) -> Destination:
if event.unsafe_tool_attempt and event.tool_execution_succeeded:
return Destination.SECURITY_ESCALATION
if event.unsafe_tool_attempt and not event.tool_execution_blocked:
return Destination.SECURITY_ESCALATION
if event.error_budget_fast_burn:
return Destination.PAGER
if (
event.quality_ratio is not None
and event.baseline_quality_ratio is not None
and event.sample_size >= 100
and event.quality_ratio < event.baseline_quality_ratio - 0.03
):
return Destination.EVALUATION_QUEUE
if event.event_type == "single_bad_answer":
return Destination.REVIEW_SAMPLE
return Destination.DASHBOARD
這個範例有刻意保守的設計。
品質下滑必須同時滿足四件事。
它必須有目前比例。
它必須有可比較的 baseline。
它必須有至少 100 筆樣本。
它的差距必須大於這個示範設定的三個百分點。
100 和 0.03 不是通用門檻。
它們只是用來表達:沒有樣本數與比較基準的品質分數,不應自動升成 alert。
這裡刻意把安全判斷寫在函式最前面,而不是排在品質判斷或技術判斷後面,這個順序本身就是一個決策。想像一個事件同時符合 error_budget_fast_burn=true 的技術故障,又剛好是一次成功執行的未授權 tool call(例如 agent 在 provider timeout 引發的重試風暴中,意外觸發了一個原本該被擋下的高風險動作)。如果判斷順序反過來,先看 error_budget_fast_burn,這個事件會被路由到 Pager,安全層級的緊急性就被技術層級蓋過去了——on-call 忙著處理服務可用性,卻不知道背後藏著一個更需要立即隔離的安全事件。把安全判斷放在最前面,是用程式碼順序落實③段那句話:safety escalation 不必等待統計趨勢,因為有些行為一旦成功執行就不可逆,這個權重必須高於「使用者正在受影響」。
接著為規則寫測試案例。
from alert_router import AIEvent, Destination, route
def test_fast_burn_pages_on_call() -> None:
event = AIEvent(
event_type="provider_timeout",
error_budget_fast_burn=True,
task_failure_rate=0.28,
)
assert route(event) is Destination.PAGER
def test_sustained_quality_drift_enters_evaluation_queue() -> None:
event = AIEvent(
event_type="citation_drift",
quality_ratio=0.94,
baseline_quality_ratio=0.98,
sample_size=186,
)
assert route(event) is Destination.EVALUATION_QUEUE
def test_one_bad_answer_does_not_page() -> None:
event = AIEvent(event_type="single_bad_answer", sample_size=1)
assert route(event) is Destination.REVIEW_SAMPLE
def test_successful_unsafe_tool_execution_escalates() -> None:
event = AIEvent(
event_type="tool_execution",
unsafe_tool_attempt=True,
tool_execution_succeeded=True,
)
assert route(event) is Destination.SECURITY_ESCALATION
讀者可在自己的環境執行:
cd day26-alert-lab
python -m pytest
預期觀測不是「所有事件都升級」。
預期觀測是四個情境走到不同目的地。
provider timeout with fast burn -> pager
quality drift with enough evidence -> evaluation_queue
one bad answer -> review_sample
unsafe successful tool execution -> security_escalation
如果單筆壞回答也跑進 pager,規則太敏感。
如果成功的未授權 tool execution 只跑進 review_sample,規則太寬鬆。
兩種錯誤都應在真的接 alert manager 之前修掉。
python -m pytest 會在幾百毫秒內回報四個測試全部通過,這件事本身沒有太多懸念——它畢竟是照著程式碼邏輯寫的測試。真正值得花時間的,是刻意把測試改壞、觀察它怎麼失敗,因為那才是這個 Lab 真正在練習的技能:判斷「這個改動悄悄改壞了哪一種保護」。試著改動 route() 裡任何一個判斷順序或門檻:
event.sample_size >= 100 改成 >= 10,既有測試依然全過,但你已經在悄悄降低品質判斷需要的證據強度——這是「測試綠燈≠規則安全」的具體示範:光看測試覆蓋率是不夠的,還得問「這個測試案例覆蓋到的邊界,是不是我真正在意的邊界」。error_budget_fast_burn 判斷之後,既有測試依然全過(因為它沒有同時設定 error_budget_fast_burn=True),但你已經製造出前一段講的「技術故障蓋過安全事件」的陷阱——值得自己新增一個同時觸發兩種條件的事件,補上這個測試沒覆蓋到的組合。if event.event_type == "single_bad_answer" 這個分支,任何不滿足前面條件的事件都會落到 Destination.DASHBOARD 而非 REVIEW_SAMPLE,既有測試同樣不會變紅,但單筆壞回答已從「有人會去看一眼」悄悄降到「反正 dashboard 上有一筆記錄」,這個差異平常不會被注意到,直到某天回頭想找某類問題的樣本,才發現它們一開始就沒被分類進正確的桶。這三個練習的共通點是:都不會讓既有測試變紅,卻都真實地改變了系統的行為邊界——這正是 production alerting policy 上線之後最需要提防的事:規則的漂移往往不是一次性的重寫,而是一次次「看起來無害」的小調整,每一次都通過既有測試,十次調整之後,整套分流邏輯可能已經面目全非。
建立兩個事件。
第一個是 provider timeout。
第二個是 HTTP 200 但 citation 無法支持結論。
{
"event_type": "provider_timeout",
"error_budget_fast_burn": true,
"task_failure_rate": 0.28
}
{
"event_type": "citation_drift",
"quality_ratio": 0.94,
"baseline_quality_ratio": 0.98,
"sample_size": 186
}
兩者都不理想,但第一個事件有明確的可用性傷害,第二個事件則需要確認 evaluator、prompt、檢索資料與樣本組成。把兩者送同一種通知,會讓 owner 在一開始就做錯工作。
值得多想一步的是:這兩個事件的「使用者感受」可能完全一樣——provider timeout 意味著使用者根本沒收到回應,citation drift 則是使用者收到了一個看起來完全正常的回應,只是裡面某個論點其實站不住腳。前者的傷害立即且明顯,後者延遲且隱形,使用者可能當下毫無察覺,直到根據這個答案做了錯誤決定。這也是為什麼③段反覆強調 hallucination 不適合直接接 pager,卻也不代表它不重要——它的緊急程度和明顯程度是兩件不同的事,處理方式要對應「明顯程度低、傷害可能延遲發作」這個特性,而不是套用「使用者有沒有立刻抱怨」這個粗糙的判準。
許多人看到 tool_execution_blocked=true 就認為事件可以直接忽略。
這要看 policy。
如果來源是已知的 red-team 測試帳號,記錄到 dashboard 可能足夠。
如果來源是 production 使用者,而且 tool policy 差一點被繞過,至少要建立 security review。
以下是一個可檢視的事件範例:
{
"event_type": "tool_execution",
"unsafe_tool_attempt": true,
"tool_execution_blocked": true,
"tool_execution_succeeded": false,
"tool_name": "export_customer_records",
"source": "production"
}
你要在 policy 裡明確寫出:
不要把這些判斷留給第一位看到通知的人臨時猜。
這條練習容易被輕忽的原因,是「被攔下來」聽起來像是一個好結局:policy engine 做了它該做的事,動作沒有執行,表面上沒有人受傷。但從資訊安全的角度看,一次被攔下的攻擊嘗試和一次從未發生的攻擊價值完全不同——前者告訴你系統正在被測試邊界,後者你什麼都不知道。如果 production 來源持續出現被攔下的未授權嘗試,而只在 dashboard 上默默記錄,可能會錯過一個正在逐步摸索防線的行為模式——單看任何一次事件都不嚴重,連續起來看卻是一種偵察行為。這也是為什麼「被攔下的攻擊」不該只被當成技術上的正確結果,而要被當成一份情報,交給有能力判斷模式的人。
品質閾值不是永遠正確的常數。
請為每個閾值寫一句理由。
quality drop threshold: 3 percentage points
reason: below this change, historical evaluator disagreement makes the signal unstable
minimum sample size: 100
reason: fewer samples are treated as review samples, not a population signal
window: 60 minutes
reason: balances enough requests for comparison against slow detection
如果你寫不出理由,先把它放在 dashboard——一個無法解釋的 0.95,很容易變成「上週有人填過,所以不要碰」的魔法數字。寫理由這件事看似多餘的文書工作,實際上是在強迫團隊誠實面對一個問題:這個門檻是從資料算出來的,還是從感覺猜出來的?如果理由寫的是「歷史上這個範圍內的下降大多是雜訊,不是真實退化」,代表數字背後有分析支撐,之後想調整可以拿新資料出來討論;如果理由寫不出來,或寫出來的是「上次事故後大家覺得應該更嚴一點」,這個數字其實只是一次性的情緒反應被固化成了常數——情緒反應不是不能參考,但需要被轉譯成可檢驗的規則,而不是原封不動地變成程式碼裡的 magic number。半年後接手這段程式碼的人,如果看不到這行理由,唯一能做的只有繼續沿用,或者賭一把改掉它,這兩個選項都不理想。
Prometheus 很適合處理技術可用性與已收斂的 outcome。
它不適合直接承載整段 prompt、回答文字或高 cardinality 的 trace id。
以下是假設已有 ai_task_total 的示範 recording rule。
groups:
- name: ai-workflow-recording
rules:
- record: job:ai_task_failure_ratio:5m
expr: |
sum(rate(ai_task_total{task_status="failed"}[5m]))
/
clamp_min(sum(rate(ai_task_total[5m])), 1)
- record: job:ai_quality_review_ratio:1h
expr: |
sum(rate(ai_task_total{quality_status="needs_review"}[1h]))
/
clamp_min(sum(rate(ai_task_total[1h])), 1)
第一條可作為 SLO 計算的輸入之一。
第二條只是一條趨勢指標。
它不自動代表 hallucination,也不直接表示每個 needs_review 都是模型錯誤。
品質的分類器、人工 review 與資料涵蓋範圍都可能改變這個比例。
因此 alert rule 的寫法要區分兩種風險。
groups:
- name: ai-workflow-alerts
rules:
- alert: AskWorkflowFastBurn
expr: job:ai_task_failure_ratio:5m > 0.10
for: 10m
labels:
severity: page
owner: api-on-call
annotations:
summary: "/ask task failure ratio is above 10% for 10 minutes"
runbook: "https://example.invalid/runbooks/ask-fast-burn"
- alert: AIQualityReviewTrend
expr: job:ai_quality_review_ratio:1h > 0.08
for: 2h
labels:
severity: ticket
owner: model-quality
annotations:
summary: "AI quality review ratio has remained above its review threshold"
action: "Create an evaluation queue item; do not page without user-impact evidence."
這些數字同樣只是範例。
它們不能被複製後宣稱符合任何人的 SLO。
第一條的意義是 page。
第二條的意義是 ticket。
若 alert manager 沒有支援不同 receiver,至少要用 label 讓下游自動化可以分流。
你還要把 deployment context 接進 annotation 或 dashboard link。
否則每次品質趨勢發生,都得重新問「剛剛有沒有換 prompt?」
annotations:
dashboard: "https://grafana.example.invalid/d/ai-workflow?var-workflow=policy_rag"
trace_query: "workflow_name=policy_rag&quality_status=needs_review"
change_context: "Inspect prompt, index, model route, and evaluator versions."
這裡刻意不把 deployment id 寫成 label。
部署版本通常也是有限但可能快速輪替的維度。
若你的 retention 和流量允許,可以在 metrics 上保留有限版本值。
更保守的做法是讓 dashboard 在告警時間範圍內連到 deployment event 與 trace metadata。
Error budget 的前提是你定義了可觀測、可計算的服務目標。
/ask 的 technical success 可以有 availability SLI。
task_status=completed 可以有任務完成 SLI。
但「所有回答都正確」通常沒有可即時、無歧義的 measurement。
這是 AI alerting 最危險的混淆。
不要因為 dashboard 上有一條 score,就把它包裝成和 HTTP 5xx 一樣確定的 SLO。
可以先分成三條不同的敘述:
technical availability SLI:
request 是否在可接受時間內得到可解析的服務回應?
task completion SLI:
workflow 是否完成既定任務,包含必要的 validation?
quality evidence:
在有覆蓋的樣本上,evaluator 或人工審查如何判定回答?
三條數字都可以被量測。
它們不能被混成一個「AI health」百分比。
如果 technical availability 正常而 task completion 下滑,先看 parser、retrieval、tool policy、fallback 是否變動。
如果 task completion 正常而 quality evidence 下滑,先檢查資料集、prompt、模型 route、evaluator 版本。
如果 quality evidence 上升但 user feedback 變差,也不要立即宣告模型變好。
可能是 evaluator 被 prompt 格式討好,也可能是採樣偏了。
這些相互矛盾的訊號正是 evaluation queue 存在的理由。
Day 20 的 burn rate 公式建立在一個前提上:分子與分母都是可以即時、無歧義計算的事件計數。
5xx / total requests 這種比例,每一筆請求發生的當下就能決定它算不算失敗。
quality evidence 做不到這件事。
一筆回答有沒有 hallucination,往往需要事後才能判定——可能是 evaluator 跑完才知道,也可能是使用者回報才確認,時間點可能落後於請求發生好幾分鐘到好幾小時。
如果硬把這種延遲判定的資料塞進 burn rate 公式,你會得到一個看起來很精確、實際上一直在被回溯修正的數字:現在算出來的「過去一小時品質分數」,可能在兩小時後因為更多評估結果進來而整個改變。
burn rate 的隱含假設:
event 發生的當下就能判定 good / bad
分子分母都是即時、穩定、不會被回溯修改的計數
quality evidence 的實際狀況:
event 發生的當下只有「completed / failed」這種技術狀態
good / bad 的判定要等 evaluator 或人工介入才會出現
這個判定可能在數小時後才補齊,而且可能被覆寫
這不代表 quality evidence 不能量化,只是它需要一套不同的統計方式:固定窗口、固定樣本數、事後回填,而不是即時滾動的 burn rate。這正是為什麼 alert_router 裡的品質判斷用的是「窗口內樣本數 + 比例差距」,而不是「這一分鐘燒了多少預算」——後者的公式對延遲判定的資料類型並不成立。
假設 policy_rag 在週二早上完成了一次檢索索引更新。
API dashboard 很平。
P95 latency 沒變。
5xx rate 沒變。
task_status=completed 也沒有下降。
然而抽樣 evaluation 在接下來兩個一小時窗口出現同一件事。
08:00-09:00
sampled completed tasks: 186
citation-valid ratio: 94.1%
baseline: 98.2%
top reason: unsupported_claim
09:00-10:00
sampled completed tasks: 201
citation-valid ratio: 93.8%
baseline: 98.0%
top reason: unsupported_claim
這時候不該先問「誰要被叫醒」。
應該先問 evidence 是否足夠:
合理路徑是建立 quality queue item。
quality owner 先抽樣 trace,檢查回答引用的 document chunk 是否過時、排名是否改變、citation parser 是否漏抓。
同時記錄 change context。
candidate changes:
- retrieval_index_version policy-2026-09-22
- no prompt version change
- no model route change
- evaluator version citation-check-v2 unchanged
若人工 review 發現引用真的錯了,而且這個 workflow 處理的是法律、醫療或財務等高風險決策,升級門檻可以比一般 FAQ 低。
但理由必須寫在 policy 裡。
不是因為「AI 很可怕」,而是錯誤的傷害與可逆性不同。
這句話不是修辭。OpenAI 的 Whisper 語音轉錄工具曾被超過三萬名醫療專業人員用於病歷聽打,後續抽樣研究發現,約 1% 的轉錄段落包含音檔裡完全不存在的整句內容,其中將近四成屬於有害捏造,包括不存在的用藥交互作用、虛構的治療建議,甚至病人從未說過的自傷描述(LLM Hallucinations: 5 Real Examples with Impact)。這正是 quality_status=needs_review 卻毫無系統錯誤訊號的教科書案例:轉錄格式完整,沒有 5xx,沒有 timeout,錯誤只存在語意層,傳統監控從頭到尾不會亮燈。如果 policy_rag 的下游是理賠核定或用藥建議,同樣是 94% 的 citation-valid ratio,該不該立即升級就不能套用 FAQ 場景的門檻,差別不在分數本身,而在這則錯誤答案一旦被使用者採信,能不能被輕易撤回。
若再次確認索引更新是根因,安全動作可能是回滾索引或暫停該資料來源。
這時才可能變成 incident。
incident 的理由不是分數掉了四個百分點。
理由是已有證據顯示某個變更持續讓重要使用者任務得到錯誤資訊,而安全的 rollback 已經可執行。
再看一個技術面案例。
09:15 開始,primary model provider 的 timeout 變多。
fallback_triggered=true 的比例從 2% 升到 37%。
由於 fallback 模型仍可完成任務,task failure ratio 暫時維持在 1%。
這時 alert 怎麼走?
答案不一定是 Pager。
先看服務承諾。
如果 fallback 延遲仍在 latency SLO 內,且它的品質已事前被接受,可以先建立 high-priority ticket 或 dashboard annotation。
如果 fallback 成本極高,但尚未傷害使用者,應走 capacity 或 FinOps 路徑。
如果 fallback 的 task completion 很低,或 queue backlog 正在把 P95 推過 SLO,才進 Pager。
可以為此定義一張小表:
| Primary timeout | Fallback completion | SLO 狀態 | 建議去向 |
|---|---|---|---|
| 上升 | 正常 | 正常 | Dashboard 或 ticket |
| 上升 | 正常 | latency 快接近門檻 | On-call review |
| 上升 | 下降 | task SLO burn | Pager |
| 上升 | 不可用 | availability burn | Pager |
這個案例反駁一個常見直覺:
「fallback 被觸發」不是事故定義。
它通常是防線正在工作的證據。
Day 27 的 postmortem 應該記下這件好事,同時追問為什麼 primary 失敗、fallback 是否有容量餘裕。
這個案例也是 Day 25「actionable alert」原則的延伸應用。如果 fallback 正常吸收了故障,此刻發出去的任何通知,都不該要求任何人立刻採取行動——它的作用只是留下記錄,讓白天上班的人知道發生過什麼、消耗了多少 fallback 容量。硬要把它包裝成 Pager,等於在製造一個沒有 safe first action 的通知:值班的人半夜醒來,發現系統其實運作正常,只是走了另一條路,除了確認「喔,原來是這樣」之外什麼都不用做——這正是 alert fatigue 最常見的來源之一,一次無害卻打斷睡眠的通知。
反過來,如果你的系統完全沒有 fallback,primary timeout 上升就直接等於使用者受影響,這時候即使 task completion 暫時還沒掉到門檻以下,也該提早進入 On-call review,而不是等 SLO 燒穿才動作——因為沒有 fallback 這個安全網,故障的惡化速度會比有 fallback 的系統快得多。這也是為什麼上面那張小表把「fallback completion」單獨列成一欄:有沒有 fallback、fallback 好不好用,直接決定同一個症狀該多快被人看到。
一個 agent 收到使用者訊息後,嘗試呼叫 export_customer_records。
policy engine 擋下來了。
HTTP response 是 200,因為系統安全地拒絕完成動作。
這不能只被歸類成成功或失敗。
至少要保留這幾個判斷:
who requested it?
which workflow and prompt version were active?
was the tool blocked before execution?
did any partial side effect occur?
is this a known test or a production input?
does the event match a previously seen attack pattern?
若確認是 internal red-team 的預期測試,儀表板紀錄可能足夠。
若是一般 production 輸入,工具確實被擋住且沒有副作用,通常該進 security review,而不是 pager。
若工具成功執行,或 policy decision 與 execution result 不一致,必須立刻走安全 incident 路徑。
這個差異不是 technical severity 的文字遊戲。
它決定誰有權隔離 tool、旋轉 credential、通知資料 owner,以及保留哪些證據。
假設 policy_rag 的一次 prompt 改版,在上線前跑過完整的 golden dataset。
100 個標準案例。
citation-valid ratio 99%。
安全性檢查全數通過。
release gate 亮綠燈,版本上線。
三週後,quality owner 在例行抽查裡發現一件怪事。
golden dataset(上線前):
citation-valid ratio: 99%
sample: 100 fixed cases
result: PASS
production(上線三週後):
citation-valid ratio: 91%
sample: 1,200 sampled completed tasks
result: sustained drift, below baseline
同一個模型,同一個 prompt 版本,兩個數字差了 8 個百分點。
這不是評估器故障,也不是模型忽然變笨。
差距來自一件很容易被忽略的事:golden dataset 是固定的,生產流量不是。
上線前的 100 個案例,覆蓋的是產品團隊「想得到」的使用情境。
生產環境三週內累積的流量,覆蓋的是使用者「真正會問」的情境——包含大量產品團隊沒預料到的邊界問題、跨語言問題、上下文很長的多輪對話。
這正是本篇③段引用的生產追蹤研究想指出的落差:offline benchmark 用的是靜態、固定的測試集,無法捕捉真實流量下的行為漂移;而系統本身也可能在無形中學會優化評估指標,而不是真正解決底層問題。golden dataset 測的是「這個系統能不能通過這 100 題」,不是「這個系統在三週後面對十萬種提問時還撐不撐得住」。
這個落差,加上前面提到的另一個發現——評估環境本身可能改變模型行為——會疊加出一個更難處理的問題。
測試環境:模型可能「感覺到」自己在被評估
生產環境:同樣的訊號幾乎消失(不到 1% 的對話出現)
如果這個訊號真的會影響行為(哪怕影響幅度很小),代表你在 release gate 量到的分數,天生就帶著一點「模型知道這是考試」的偏差。
這不代表 release gate 沒有用。
它仍然能攔下最明顯的迴歸——例如一次改版讓拒答率暴增、或安全性檢查直接失敗。
但它不能被當成「生產環境會一直維持這個水準」的保證書。
這正是為什麼本篇從①段開始就主張品質訊號要走滾動窗口監控,而不是「上線前測過就結案」。
對照回這個案例,合理的處理方式不是回頭怪 release gate 「沒把關好」,而是承認兩種評估各自能回答的問題不一樣。
| 評估方式 | 能回答的問題 | 不能回答的問題 |
|---|---|---|
| Golden dataset release gate | 這次改版有沒有明顯迴歸? | 三週後的真實流量會不會出現新的失敗模式? |
| Production sampling | 真實流量下的品質趨勢是否穩定? | 這個下滑是這次改版造成,還是別的變數? |
兩者缺一不可,而且要對得上同一套 workflow_version、prompt_version 欄位,事後才回答得出「三週前的哪個決定,對應到三週後的哪個症狀」。
如果你的團隊只做其中一種評估,這個案例值得帶回去問一句:我們現在只回答得出上面表格裡的哪一格?
AI evaluation 常把一個 evaluator 的結果轉成品質分數。
這很方便。
它不是客觀真相。
evaluator 可能因 prompt 改版而變嚴格。
它可能把格式完整的 citation 當成有根據的 citation。
它也可能無法處理某些語言、文件格式或拒答情境。
這種不穩定的根源不是 evaluator 沒調好,而是它本質上也是一個生成式模型在做判斷。就算把 temperature 鎖到 0,evaluator 仍然可能對「格式工整、篇幅較長」的回答存在系統性偏好,也就是常說的 verbosity bias 與 position bias,把「寫得漂亮」誤判成「寫得正確」;換一批 prompt 範例,它對同一種錯誤的容忍度也可能跟著漂移。③ 提過的解法是同一案例評分三次取中位數,做法其實很單純:
import statistics
def score_with_median(evaluator, trace, attempts: int = 3) -> float:
scores = [evaluator.score(trace) for _ in range(attempts)]
return statistics.median(scores)
把這段函式套進 quality_event 的產生流程,score 欄位記的就不再是單次抽樣的結果,而是三次評分的中位數;evaluator_version 則負責標出這次判斷用的是哪一批偏好與哪一版 prompt。少了這一步,quality_status 看起來很客觀,其實只是把雜訊包裝成一個數字,跟著它去升級,只會讓 evaluation queue 塞滿不需要判讀的假警報。
所以一條品質趨勢至少要紀錄 evaluator version。
quality signal without evaluator version: hard to compare
quality signal without sample links: hard to audit
quality signal without human spot check: easy to overtrust
建立一個小型 disagreement 樣本池。
當 evaluator 判為失敗、人工判為成功,或相反時,把案例分到 evaluator_disagreement。
這些樣本不應隨便混進模型 regression dataset。
先確認是模型、資料、還是評估規則的問題。
讀者可以在 review 記錄中保留這種最小欄位:
{
"trace_id": "trace_01",
"evaluator_version": "citation-check-v2",
"automated_label": "unsupported_claim",
"human_label": "supported",
"disagreement_reason": "citation parser omitted appendix reference",
"resolution": "fix parser before changing model"
}
這個例子顯示一個重要順序。
先修 measurement,再判定模型退化。
不然 team 可能回滾 prompt,卻把真正壞掉的 parser 留在原地。
Day 25 用 Etsy 的 Opsweekly 談過 actionable alert 的重要性。
Day 26 可以把它具體化成回顧表。
每週對每一種通知記錄以下欄位:
| 欄位 | 問題 |
|---|---|
| notification count | 這一週送了多少次? |
| acknowledged time | 有沒有在期待時間內被接手? |
| actionable | 收到的人能否照 runbook 做事? |
| user impact confirmed | 有沒有證實使用者影響? |
| route correct | Pager、queue、ticket 是否走對? |
| follow-up completed | 後續改善是否真的完成? |
不要只算通知總數。
通知少可能是規則太安靜,也可能是真的很健康。
通知多也不必然代表差,若系統在一週內經歷多個真實事故。
route correct 比純數量更能找出設計問題。
例如:
week: 2026-W39
quality-drift notifications: 8
sent to pager: 3
should have been evaluation queue: 3
actionable pager ratio: 0/3
policy action: change quality-drift receiver from pager to queue
這不是 production 實績範例。
它是你應該保留的報表形狀。
沒有這種回顧,團隊只能靠「最近通知好多」的感覺調整規則。
感覺通常在半夜特別不可靠。
這張回顧表也回答了本篇①段提出的那個組織問題:誰有權叫醒誰。
如果每週回顧持續顯示某類通知的 actionable pager ratio 是 0/3、0/5,這不只是一個統計數字,它是在告訴你,目前這條規則的設計者對「什麼值得叫醒 on-call」判斷錯了,而且錯誤沒有因為時間過去自動修正。
多數團隊處理這種情況的直覺是「先忍一忍,之後再優化」,但 Etsy 的資料已經說明這種忍耐的代價:一旦值班者學會把某類通知當雜訊,連帶會影響他們對其他通知的反應速度,包括真正重要的那些。
把 route correct 這一欄的檢討常態化,等於是把「這條規則還配不配得上打斷別人的睡眠」變成一個每週都要重新回答的問題,而不是上線時決定一次就不再更動的既定事實。
這也呼應了⑫段最後那個練習帶出的提醒:規則的漂移往往不是一次性的重寫,而是一連串通過測試、卻沒人回頭檢查整體效果的小調整。每週回顧,就是攔住這種漂移的機制。
AI incident 的風險常在於過度修復。
品質分數掉下來時,on-call 不應直接改 prompt。
provider timeout 時,也不應無限制 retry 到把 queue 塞滿。
未授權 tool attempt 發生時,不應把完整敏感 prompt 貼到公共聊天室。
因此每個 runbook 都需要禁止動作。
## AI quality drift runbook
Do:
- 固定告警窗口與版本資料。
- 抽樣連回 trace 與來源文件。
- 比較最近的 prompt、index、model route、evaluator change。
- 若 policy 已授權且因果有證據,採取已演練的 rollback。
Do not:
- 不要用單筆樣本宣告大規模幻覺。
- 不要直接在 production 編輯 prompt 來「試看看」。
- 不要把 request_id 當 metric label。
- 不要在未脫敏的 ticket 複製使用者內容。
Do not 的價值不在管束工程師。
它把高壓下容易發生、且可能放大傷害的動作寫成明確邊界。
Day 27 的 incident commander 就能根據這些邊界分配工作,而不是每次臨場討論什麼行為安全。
把時間軸拉回最真實的情境:凌晨三點,手機震動。
通知寫著 AIQualityReviewTrend 觸發。
值班者剛被吵醒,腦袋還沒完全清醒。
這時候,runbook 的品質決定了接下來五分鐘會發生什麼事。
如果 runbook 只寫了「品質下降,請檢查」,值班者面對的是一片空白。
他不知道能不能動 prompt。
不知道能不能 retry。
不知道這件事十分鐘內解決得完,還是本來就該等白天。
於是他大概率會做兩件事的其中一件:隨便點一下 acknowledge 然後回去睡覺,或者,因為心裡不安,開始亂試——改個 prompt 看看、切個模型版本看看。
前者製造了未來的技術債(沒人真的處理這個訊號)。
後者製造了立即的風險(在沒有 rollback 計畫的情況下,對 production 做未經測試的變更)。
如果 runbook 寫的是本篇 AI quality drift runbook 那樣的格式——先固定窗口與版本、再抽樣連回 trace、比較最近的變更、只有在因果有證據時才動用已演練的 rollback——值班者的五分鐘會完全不同。
他不需要判斷「這個決定安不安全」,因為安全邊界已經在他清醒之前就被劃好了。
他只需要照著清單走:截圖、抽樣、比對版本、寫進 queue,然後回去睡覺,把真正的判讀留給白天的 quality owner。
這才是 Do not 清單真正的價值——它不是限制值班者的能力,而是在他認知資源最少的時刻,替他先做完最容易出錯的那個判斷。
完成上面的隔離 Lab 後,請逐項檢查。
[ ] 事件模型有 technical、task、quality、safety 的獨立狀態。
[ ] request_id、trace_id、task_id 沒有進 Prometheus label。
[ ] 每一條 Pager 規則都連到可觀測的使用者影響或 error-budget burn。
[ ] 每一條 Pager 規則都有明確 owner 與安全的第一個動作。
[ ] 品質趨勢包含窗口、baseline、樣本數、evaluator 版本與 trace evidence。
[ ] 單筆壞回答會進 review sample,不會直接進 Pager。
[ ] 被成功執行的未授權 tool action 會走 security escalation。
[ ] 被攔截的 tool attempt 有去向,不會悄悄消失。
[ ] 測試或 red-team 流量有可辨識的抑制規則。
[ ] 每週回顧會記錄 notification 是否走到正確 route。
[ ] runbook 同時寫了安全動作與禁止動作。
這份清單驗證的是設計是否能說清楚。
它不證明你的告警門檻已經適合 production。
真正的門檻需要在已授權環境中,對照流量、SLO、回顧與 incident 演練持續調整。
品質分數可用來找問題。
它不會自動得到 availability SLO 的證據強度。
修正方式是保留 sample size、evaluator version 與人工抽查,讓它先走 evaluation queue。
如果 dashboard 顯示 citation-valid ratio 下滑,卻沒有 prompt、index、model route 的 change context,排查會退化成猜測。
修正方式是讓 trace 與 deployment event 用穩定關聯欄位連起來。
這會讓 red-team 與被防線成功攔住的事件淹沒真正的 breach。
修正方式是依 executed、來源、資產敏感度與副作用分級,並指定 security owner。
Fallback 存在的目的就是吸收依賴故障。
修正方式是看 fallback 後的 task completion、latency、成本與容量,而不是只看 route 被切換。
這會讓可觀測性系統承受不必要的 series 壓力。
修正方式是 metrics 做聚合,trace 與 log 保存 request 級 evidence。
沒有 owner 的 alert 最後會變成大家以為別人會處理的通知。
修正方式是把 receiver、負責角色、交接條件與 escalation policy 寫進 rule 與 runbook。
這六種失敗模式各自獨立,卻經常成對出現。
一個沒有 owner 的通知,往往也是一個把品質分數當 SLO 的通知——因為沒有人真的負責,也就沒有人去質疑它的判準對不對。
檢查自己的告警清單時,值得把這六條當成一份交叉清單,而不是各自獨立的健檢項目。
golden dataset 測試全過,不代表這個版本在生產流量下會維持同樣的品質。
⑱段的案例示範了這個落差可以在三週後才現形,而且完全不會觸發任何一條上線前的檢查。
修正方式是讓 release gate 與生產環境的滾動監控各自負責它們能回答的問題,兩者的資料要能用 workflow_version、prompt_version 對得起來,而不是上線後就假設「反正測試過了」。
本文示範的是 policy 與隔離環境中可測試的 routing logic。
本文沒有替你設定 Prometheus、Alertmanager、PagerDuty、LangSmith、Langfuse 或任何雲端帳號。
本文沒有發送 alert。
本文沒有用真實使用者資料跑 evaluator。
本文也沒有驗證範例門檻能符合特定產品的 SLO。
特別是 0.03、100、10m、2h 都是教學用的起點,不是可直接複製的 production 建議。
每一個數值都必須對照你的 request volume、品質風險、資料延遲、owner 可用時間與既有 error budget 再決定。
本文引用的三則外部研究——evaluator 評估意識、生產環境評估落差、幻覺偵測的取樣實踐——同樣只是起點,不是可以直接套用的規則。
它們告訴你這些現象「存在」,且已經被具名的研究團隊觀察並量化過。
它們沒有告訴你,你自己的 policy_rag 或任何一個 workflow,落差會是多少、抽樣比例該設多少。
那些數字只有在你自己的生產環境裡,跑過幾個月的 evaluation queue,累積出足夠的樣本後,才回答得出來。
Day 26 的產出不是一堆新 alert。
它是一張可回頭質疑的分流政策。
當真的事件發生時,Pager 應告訴你哪個使用者承諾正在壞掉。
Evaluation queue 應保留足夠 evidence,讓品質 owner 能確認是模型、資料、版本還是測量壞了。
Security escalation 應把不可逆風險交給有權處理的人。
Day 27 會接著把這些資料放進 incident、runbook 與 postmortem。
那時候,timeline 不會只剩「模型怪怪的」。
它會有版本、樣本、決策、owner 與被採取的安全動作。
它也會有一件本篇特別想留下的提醒:任何一筆被拿來佐證「系統正常」或「系統異常」的品質分數,都該附上它是從哪個環境、哪個評估版本、抽樣多少比例算出來的。
少了這行 metadata,postmortem 讀起來會很像一份自信的報告。
多了這行 metadata,它才是一份誠實的報告——誠實到願意承認,連「有沒有出事」這件事,有時候都需要留一點空間讓答案被之後的證據修正。
下一篇會接著處理「Incident、Runbook、Postmortem」。
這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.