我的推論服務有一個 /drift 端點。今天早上它回這個:

{"requests": 65, "chars_seen": 162, "oov_rate": 0.0,
 "psi": 3.2015, "level": "顯著漂移", "retrain_suggested": true}

「顯著漂移,建議重訓」。

如果我照做,接下來是幾小時的 GPU、一輪評估、一次 promotion gate、一次上線。 在銀行還要加上一張變更單。

我沒照做。我先問了一個問題:這個數字在「什麼都沒發生」的時候會是多少?


先講結論

這個監控在說謊,而且它不可能不說謊。

原因不是公式錯,是樣本數不夠。162 個字元要去估一個 51 桶的分布, 平均每桶 3 個字,大多數桶是空的——而 PSI 對空桶的懲罰極重。

我用訓練資料自己去抽樣驗證了這件事。抽樣來源就是計算 baseline 的那份語料, 所以真實漂移嚴格等於零

樣本字元數 PSI 中位數 15 次的範圍 判定(門檻 0.25)
162 0.8954 0.673 – 1.181 🔴 顯著漂移
500 0.2527 0.116 – 0.444 🔴 顯著漂移
2,000 0.0264 0.017 – 0.030 ✅ 穩定
10,000 0.0053 0.003 – 0.007 ✅ 穩定
200,000 0.0002 0.0002 – 0.0003 ✅ 穩定

零漂移,但樣本低於 500 字元時它一定判成「顯著漂移」。

我線上量到的 162 字元對應 PSI 0.90(模擬)到 3.20(實際更高,因為中文 prompt 的字元比隨機抽樣更集中)。服務剛上線的那幾天,這個燈不可能不是紅的。

而它旁邊那個 oov_rate: 0.0 才是誠實的:所有字元都在訓練 vocab 裡, 沒有任何一個生字。真的漂移了的話,這個數字會先動。


三個具體的 bug

1. 沒有最小樣本數檢查

PSI 是統計量,統計量需要樣本。這個實作在 chars_seen = 162 的時候 照樣輸出一個數字,還附上 retrain_suggested: true

修法:低於門檻(依上表,這個設定是 2,000 字元)就回 insufficient_data, 不要回一個數字——回「我不知道」比回一個錯的數字有價值得多。

2. 門檻是抄來的

0.25 這個值,我程式碼的註解自己寫了它的出處:

PSI < 0.1 穩定、0.1–0.25 輕微漂移、> 0.25 顯著漂移

那是信用風險模型的業界慣例——用在評分卡的族群穩定度上, 分箱通常 10–20 桶、樣本上萬筆。我把它原封不動搬到一個 51 桶、 樣本 162 的字元分布上。

修法跟前一篇的邏輯一樣:先量你自己系統的自然波動。 拿同一份流量重放幾次,看 PSI 在「什麼都沒變」時抖多少,門檻取 2σ 以上。 沒量過的門檻就是一個你答不出理由的數字。

3. 它是累計的,不是滑動窗口

看實作:self.obs 是一個只加不減的 Counter,從服務啟動累計到現在。 這有兩個後果:

  • 重啟就歸零——所以「重啟一下看看」在這個監控上真的會讓紅燈消失, 但那不代表問題解決了,只代表你把證據刪了
  • 跑越久越遲鈍——三個月的累計分布裡,這週的新流量會被稀釋掉

修法:換成滑動窗口(近 N 筆或近 N 小時),並且把 baseline 也版本化—— 你要能回答「這是跟哪一版訓練資料比」。


那什麼時候才真的該重訓

先看階梯:重訓是最後一個選項

1. 重啟                  ← 治「狀態壞了」:記憶體洩漏、stale cache、KV-cache 污染
2. 改 prompt / 系統提示    ← 最便宜
3. 更新 RAG 檢索內容       ← 大多數「知識過期」在這裡就解決,不用碰模型
4. 路由 / 降級 / fallback
5. 後訓練 SFT / DPO       ← 治「不聽話、格式不對」
6. 繼續預訓練             ← 治「領域詞彙根本不認識」
7. 從頭重訓               ← 幾乎不該發生

第 1–4 階能解決的問題佔絕大多數,成本比重訓低兩三個數量級。 所以第一個動作不是查指標,是先重啟一次——好了就不是模型的問題。

(注意這跟上面的 bug 3 有張力:重啟會清掉漂移證據。所以順序是 先記錄現在的指標,再重啟。)

線上真的量得到的訊號(都不需要 label)

這裡有個很多人踩的坑:線上不能用 loss 或 BPC 當觸發條件。

loss 的定義是 -log P(下一個真實 token)。線上你有使用者的輸入, 但沒有「正確的下一個 token」——沒有標準答案,這個數字算不出來。 你唯一算得出 loss 的地方是 held-out set,而那份資料定義上就不是線上分布

盯著它只會告訴你「模型在舊資料上還是一樣」——那恰好是你不需要知道的事。

能量的是這些:

指標 備註
輸入 PSI(要有最小樣本數) 分布漂移
  OOV 率 對固定 vocab 的模型特別致命,新詞直接變 UNK
  序列長度分布 使用者問法變了的早期訊號
輸出 拒答率/空回應率  
  重複率 退化的第一個症狀,而且好量
  格式違規率 該回 JSON 卻不是
行為 重問率、放棄率 最真實,但延遲
  人工接手率 有客服流程的話,這條最直接
對照 shadow agreement 候選 vs 現行的一致率

LLM 服務監控面板

⚠️ 誠實邊界:我的 lab 沒有真實使用者,所以「行為層」那三條我一條都沒有量過。 它們是我認為最有價值的,但我沒有證據。

金融業有一個特殊觸發:行事曆,不是指標

新法上路、新產品上架、費率調整、報表格式改版——這些是已知的知識截止點

用漂移指標去偵測一件行事曆上早就有的事,是把簡單問題複雜化。 這類重訓應該事先排程,不是等紅燈。


一句話

漂移監控的用途不是叫你重訓,是讓你有依據地決定「不重訓」。

而一個在零漂移時也會亮紅燈的監控,連這件事都做不到—— 它只會製造一種「我們有在監控」的感覺,然後在你真的需要它的時候, 你已經學會忽略它了。


我想知道:你們的模型重訓是什麼觸發的?指標、行事曆、還是有人抱怨? 如果是指標,那個門檻是誰定的?

🧪 這篇的實驗環境與 lab 檔案(最後更新 2026-08-30)

叢集

  • CRC 2.63.0 · OpenShift 4.22.7 · Kubernetes v1.35.6
  • 單節點:13 vCPU / 40 GiB RAM / 120 GB disk
  • 宿主:Framework Laptop 16(Ryzen AI 7 350 · 8C/16T · 64 GB RAM · 1 TB NVMe · RTX 5070 顯卡模組)
  • ⚠️ 叢集內看不到 GPU(CRC 是 VM,RTX 5070 未 passthrough)

Operator

  • opendatahub-operator.v3.5.0 ← 即 RHOAI 3.x 的上游開源版
  • cert-manager-operator.v1.20.0(3.x 的必要相依;2.x 不需要)
  • openshift-pipelines-operator-rh.v1.23.2openshift-gitops-operator.v1.21.3

DataScienceCluster 開啟的元件

  • kserveaipipelinesdashboardworkbenchesmodelregistrykueue(Unmanaged)
  • 其餘(raytrustyaifeastaigateway…)為 Removed

叢集外的依賴(跑在宿主的 podman 上,crc start 不會帶起來)

  • Harbor v2.15.2(私有 registry)· MinIO(S3)

模型端

  • Python 3.12.13 · PyTorch 2.11.0+cu128 · FastAPI + uvicorn · prometheus-client

lab 檔案

⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同 (我這裡是 opendatahub,商用版是 redhat-ods-* 那一套)。 指令的邏輯可以照用,字串要自己對一次。