我的漂移監控說「該重訓了」。它是錯的。
我的推論服務有一個 /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 現行的一致率 |

⚠️ 誠實邊界:我的 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.2、openshift-gitops-operator.v1.21.3
DataScienceCluster 開啟的元件
kserve、aipipelines、dashboard、workbenches、modelregistry、kueue(Unmanaged)- 其餘(
ray/trustyai/feast/aigateway…)為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 檔案
- YAML/Containerfile/腳本:github.com/ryanGTR/openshift-ai-30days
(含 Day 對照表;主機名是佔位符,跑
set-lab-host.sh換成你自己的) - 服務的那個模型:llm-from-scratch(從零手刻的小 GPT)
⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同
(我這裡是 opendatahub,商用版是 redhat-ods-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你們的模型重訓是什麼觸發的?指標、行事曆、還是有人抱怨?如果是指標,那個門檻是誰定的?