iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 14

Day 14:監控與漂移 — 模型無聲壞掉的那 14 天

  • 分享至 

  • xImage
  •  

Day 04 曾說過「不能省的三件事」:可重現、監控、評測。Day 08 提到「只能投資一項就選監控」。今天就講為什麼。

如果這 30 天你只讀一篇,建議這篇要讀。

今天要解決的問題

軟體會壞掉,而且會大聲地壞掉:您會知道收到了 500 錯誤、服務逾時、告警大響。

模型不會。模型壞掉的時候,它依然回傳 200 OK、依然在 45 毫秒內回應、依然給出一個看起來很合理的數字。

它只是答錯了,只是開始答得不那麼準了。而且會持續答錯,直到某個業務單位的人覺得「最近怪怪的」。

這就是模型系統最難處理的問題之一:無聲失效(silent failure)
MLOps 這個職務存在的最核心理由。

📊 職缺訊號

在這次職缺資料中,「模型監控與可靠性」出現在 14.9% 的 MLOps 職缺裡,比例看起來不算高,但不要因此解讀成監控不重要。實際的 JD 很可能把這類責任寫在平台維運、模型生命週期、Observability、Reliability 或 Production ML 之下,不一定直接出現「模型監控」四個字。面試時如果能主動談到「模型上線後怎麼知道它正在失效」,通常也比只談部署工具更能顯示你理解 Production ML 真正困難的地方。


一、現象:那 14 天發生了什麼

效能衰退與應變的時間軸

圖 14-1:從上游變更、漂移告警到重訓練上線的完整時間軸(模擬資料,用於呈現事件結構)。注意第 30–44 天那段:效能已經在掉,但告警還沒響。

把這條時間軸拆開:

時間 發生什麼 誰知道
第 0–29 天 模型正常,AUC 約 0.90
第 30 天 上游新增付款管道,某特徵的分布開始改變 沒有人
第 30–43 天 AUC 從 0.90 一路掉到 0.79 沒有人
第 44 天 PSI 超過 0.2,告警觸發 MLOps 工程師
第 44–61 天 調查、準備資料、重訓、驗證 團隊
第 62 天 新模型金絲雀上線,AUC 回到 0.90 團隊

這 14 天裡,模型可能已經持續做出比較差的決策,但傳統系統監控看起來完全正常:

服務可用性:99.99%   ✅
P95 延遲:45 ms      ✅
HTTP 錯誤率:0.01%   ✅
模型輸出:開始漂移    ❌

SRE 的服務儀表板可能一片綠,模型卻已經開始偏離原本的行為。

監控做得好不好,就看這段「沒有人知道模型正在變差」的時間,能縮短多少。

二、原理:監控要分四層看

image

圖 14-2:監控的四層架構(示意架構)。多數團隊只做到第二層,而模型的失效發生在第三層。

關鍵洞察
可以把監控粗略拆成四層:

第 4 層  業務結果     →  詐欺損失、轉換率、核貸違約率
第 3 層  模型         →  輸入漂移、預測漂移、模型品質
第 2 層  服務         →  Latency、Error Rate、Throughput
第 1 層  基礎設施     →  CPU、Memory、GPU、Disk、Network

第 1、2 層已經是成熟的 SRE/Observability 領域。
第 3 層是 MLOps 的專屬責任,而它需要的技術與前兩層完全不同——因為你要監控的不是「有沒有回應」,而是

「模型現在看到的世界,還跟訓練時一樣嗎?」
「模型現在產生的答案,還跟原本一樣嗎?」

第 3 層又分三種訊號,偵測難度遞增:

訊號 偵測方式 需要標籤嗎 延遲
輸入漂移 比對特徵分布(PSI、KS 檢定) 即時
預測漂移 比對預測值分布 即時
效能衰退 比對預測與真實結果 依標籤取得時間

「需要標籤嗎」這一欄是重點。 效能衰退是最直接的訊號,但你往往要等很久才拿得到真實標籤(詐欺可能三個月後才確認、貸款違約要一年)。這就是Delayed Label(延遲標籤)問題,也是為什麼我們需要不需要標籤的漂移偵測作為早期預警

漂移不代表模型一定壞掉。

資料分布改變,但模型效能可能完全沒受影響;反過來,模型效能也可能下降,但你挑來監控的特徵分布看起來沒什麼變化。
所以正確的關係應該是:

發現漂移
   ↓
值得調查
   ↓
確認影響
   ↓
才決定是否重訓

而不是:

發現漂移
   ↓
立刻重訓 ❌

三、動手:漂移偵測與服務指標

3.1 PSI:最常用的漂移指標

PSI(Population Stability Index)是一個常見、容易解釋的分布差異指標。

它比較「參考期」與「目前資料」落在各區間的比例差異:

PSI = Σ (Actualᵢ − Expectedᵢ) × ln(Actualᵢ / Expectedᵢ)

實務上常見的經驗判讀方式是:

PSI < 0.1        → 分布大致穩定
0.1 ≤ PSI < 0.2 → 有變化,持續觀察
PSI ≥ 0.2        → 漂移較明顯,值得調查

但要特別注意:0.1 與 0.2 不是放諸四海皆準的統計顯著性標準。

不同資料量、特徵型態、產業風險與工具,適合的門檻都可能不同。實務上應該用歷史資料回測後,再決定自己的告警門檻。
以下是一個簡化版實作:

"""drift.py —— Population Stability Index(PSI)"""
import numpy as np


def psi(reference, current, bins=10):
    """計算 PSI。

    常見經驗判讀:
    < 0.1        分布大致穩定
    0.1–0.2      有變化,持續觀察
    >= 0.2       漂移較明顯,值得調查

    實際門檻應依資料與業務情境校正。
    """

    # 使用參考期分位數切桶。
    # 對偏態資料通常比單純等寬切分更容易保持每桶有足夠樣本。
    quantiles = np.linspace(0, 100, bins + 1)[1:-1]
    inner_edges = np.unique(np.percentile(reference, quantiles))
    edges = np.concatenate(([-np.inf], inner_edges, [np.inf]))

    ref_pct = np.histogram(reference, edges)[0] / len(reference)
    cur_pct = np.histogram(current, edges)[0] / len(current)

    # 避免 log(0)
    eps = 1e-4
    ref_pct = np.clip(ref_pct, eps, None)
    cur_pct = np.clip(cur_pct, eps, None)

    return float(
        np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
    )

資料漂移偵測:分佈比對與 PSI

圖 14-3:資料漂移偵測(模擬資料,PSI 為實算值)。左圖比較單一特徵在參考期與目前資料的分布;右圖逐特徵計算 PSI,協助快速定位漂移來源。

右圖比「整體漂移分數」更重要,因為真正開始調查時,工程師第一個問題通常不是:
「資料到底有沒有漂?」而是:「哪一個欄位先出問題?」

例如:

age                PSI = 0.03   ✅
income             PSI = 0.06   ✅
transaction_count  PSI = 0.08   ✅
payment_channel    PSI = 0.31   🚨

一眼就知道應該先查 payment_channel,而不是把整條資料管線全部翻一遍。

圖 14-3:資料漂移的偵測(模擬資料,PSI 為實算值)。左圖是單一特徵的分布比對,右圖是逐特徵 PSI——先找出「哪一個特徵在漂」,才知道該查哪裡。因為調查時你需要知道從哪個特徵查起,而通常只有一兩個特徵是元凶。

3.2 為模型服務加上 Prometheus 指標

接著回到 Day 13 的 FastAPI 推論服務。

傳統服務監控通常已經會記錄請求數與延遲:

"""在 Day 13 的 FastAPI 服務加上可觀測性。"""
from prometheus_client import Counter, Histogram, make_asgi_app

REQUESTS = Counter(
    "model_requests_total",
    "請求總數",
    ["status"]
)

LATENCY = Histogram(
    "model_latency_seconds",
    "推論延遲",
    buckets=[.01, .025, .05, .1, .25, .5, 1, 2.5]
)

# 第 3 層的關鍵:
# 不只監控服務,也把模型的「輸出」留下來。
PREDICTION = Histogram(
    "model_prediction_score",
    "預測分數分布",
    buckets=[0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1.0]
)

app.mount("/metrics", make_asgi_app())


@app.post("/predict")
def predict(req: Req):
    with LATENCY.time():
        score = float(
            MODEL["m"].predict(
                pd.DataFrame([req.model_dump()])
            )[0]
        )

    PREDICTION.observe(score)       # ← 第 3 層監控的起點
    REQUESTS.labels(status="ok").inc()

    return {"score": round(score, 4)}

這裡最重要的其實只有一行PREDICTION.observe(score)卻是發現模型無聲失效的第一道防線。

因為從這一刻開始,Prometheus 不只知道模型有沒有回應?模型回應多快,還開始知道:

模型最近到底都在回答什麼?

例如原本預測分數大多落在 0.1 左右,最近突然整體移到 0.5;即使還沒有真實標籤,這已經是一個值得調查的訊號。

最簡單的第一版告警,可以先看平均預測分數是否明顯偏離基準

# 最近 1 小時平均預測分數
# 與過去 7 天平均值偏離超過 0.15 時觸發預警
abs(
  (
    rate(model_prediction_score_sum[1h])
    /
    rate(model_prediction_score_count[1h])
  )
  -
  (
    rate(model_prediction_score_sum[7d])
    /
    rate(model_prediction_score_count[7d])
  )
) > 0.15

假設:

過去 7 天平均      0.12
最近 1 小時平均    0.34
                   ────
偏離量             0.22

0.22 > 0.15
→ 🚨 Prediction Drift Warning

這裡的 0.15示範門檻,實務上應該用正常期間的歷史資料回測,再決定真正適合的值。

還有一個非常重要的限制:
平均值沒變,不代表分布沒變。 例如下面兩組預測:

A:0.1、0.2、0.8、0.9
B:0.5、0.5、0.5、0.5

兩組平均值都是0.5,但模型的行為顯然完全不同。

所以這條 PromQL 只適合當作第一版、低成本的預警。更完整的做法還會觀察 Histogram bucket 的比例、分位數,甚至將一段時間的預測樣本送到專門的 drift job,計算 PSI、KS、Wasserstein 或其他分布差異指標。

這也是為什麼我們選擇 Histogram 記錄 prediction score,而不只是留下單一平均值。

四、取捨:告警設計與 SLO

告警的第一原則:每個告警都必須是「可行動的」。

不可行動的告警會造成告警疲勞——當工程師習慣性忽略告警時,監控系統的價值歸零,而且是負的(因為你以為有監控)。

告警設計
觸發條件 基於症狀(使用者受影響) 基於原因(CPU 高但沒人受影響)
頻率 一週最多幾次 每天十幾封
內容 附上「該做什麼」與相關儀表板連結 只有一串指標名稱
分級 分成「立刻處理」與「上班再看」 全部都是 P1

把 SRE 的 SLO 觀念移植到模型上:

SLI(指標):模型預測的線上準確率(以每日抽樣人工複核估算)
SLO(目標):30 天滾動窗口內 ≥ 0.85
Error Budget:允許 15% 的錯誤空間

用法:
- 錯誤預算還很充足 → 可以大膽發布新模型、做實驗
- 錯誤預算消耗 80% → 凍結非必要變更,優先修復
- 錯誤預算用完 → 停止所有新功能,全員投入可靠性

這套機制的價值在於把「要不要冒險」變成數據決策,而不是誰聲音大

漂移偵測到之後呢? 這是最多人卡住的地方——很多團隊做了漂移偵測,告警響了卻不知道該做什麼。給一份決策順序:

  1. 先確認不是資料管線壞了。 特徵突然全變 0、某欄位變成 NULL——這不是漂移,是 bug,要修的是管線不是模型。這一步排除不掉,後面全都是白工。
  2. 確認是輸入漂移還是概念漂移。 輸入分布變了但關係沒變(例如新市場的客群結構不同),可能調整閾值就夠;如果是關係本身變了(例如通膨改變了價格與需求的關係),就必須重訓。
  3. 評估影響範圍。 漂移的是重要特徵還是次要特徵?影響全體客戶還是特定切片?這決定緊急程度。
  4. 決定行動:調整決策閾值(最快)→ 用新資料重訓(中期)→ 重新設計特徵(最慢,但有時是唯一解)。

第 1 步最常被跳過,也最常出錯。 如果團隊因為漂移告警而緊急重新訓練,結果是用「上游壞掉的資料」訓練出一個更糟的模型——這正是 Day 11 說「自動化會放大問題」的具體案例。

💡 給小團隊的最小可行監控

沒有 Prometheus、沒有 Grafana、沒有時間?做這三件事就好,成本不到一天:

  1. 記錄每次預測的輸入與輸出(寫進資料庫或日誌,含時間戳)。
  2. 每天跑一次腳本,算主要特徵的 PSI 與預測值的平均與標準差。
  3. 超過門檻就寄一封信給自己。

這個土法煉鋼的版本,就能把開頭那 14 天縮短到 1 天。有監控與沒監控的差距,遠大於監控做得精緻與粗糙的差距。

限制與提醒

  • 靜態門檻的偽陽性/偽陰性風險:本篇提供的經驗法則(如 $\text{PSI} \ge 0.2$ 或均值偏離 $>0.15$)在週期性、季節性波動(如黑五購物節)強烈的業務中容易頻繁誤報,若未結合動態基線或時間序列檢定,易引發告警疲勞。

  • 高維度特徵與多重共線性侷限:逐特徵計算 PSI 僅能捕捉「單一特徵邊際分佈」的變化,無法偵測特徵間「相互關聯性改變」(Covariate Shift / Joint Distribution Shift)帶來的隱性漂移。

  • 本篇未深入探討概念漂移(Concept Drift)的偵測困境:當特徵輸入分佈完全未變,但環境底層規則改變(如突發政策法令、市場競爭者改價),在缺乏真實標籤的情況下,單靠 PSI 與輸出分佈仍可能漏報,也請使用時留意。

今日小結

  • 軟體大聲地壞,模型無聲地壞——依然 200 OK、依然 45 毫秒,只是答錯了。這是 MLOps 存在的核心理由。
  • 監控分四層:基礎設施、服務、模型、業務。多數團隊只做前兩層,而模型失效發生在第三層。
  • 第三層有三種訊號:輸入漂移、預測漂移(皆不需標籤、可即時)、效能衰退(需標籤、有延遲)。用前兩者當早期預警
  • PSI 要逐特徵算(< 0.1 穩定、0.1–0.2 輕微、> 0.2 顯著),才知道該查哪裡。
  • PREDICTION.observe(score) 一行就是無聲壞掉的第一道防線。
  • 告警必須可行動,否則造成告警疲勞,監控價值歸零。
  • 小團隊的最小可行監控只要三步,就能把 14 天縮成 1 天。

明天預告

主軸一的最後一天,回到那個最高頻的任務訊號(58.5%):平臺與基礎設施。但我要從主管最在意的角度切入——。GPU 該租還是該買的損益平衡點怎麼算、為什麼你的 GPU 利用率可能只有 12%,以及「買新卡之前,先把利用率從 12% 救到 60%,等於免費多了四張卡」。

延伸閱讀


上一篇
Day 13:推論服務 — 延遲 vs 吞吐的取捨曲線
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言