假綠的五種形態
這個系列從頭到尾在講同一件事:你以為的檢查,其實沒在檢查。
三十天下來,我在同一套平台上撞到五種不同形態。把它們排在一起之後, 我發現它們不是五個 bug——是五種檢查設計上的缺陷,而且可以歸納。
形態一:看 log,不看產物
症狀:後訓練鏈的 log 印著 ALL DONE,每一段都有耗時,評估數字漂亮。
實際:checkpoint 的 mtime 全是三個月前,一步都沒跑。
為什麼騙得過人:log 是程式印的,程式印什麼是程式決定的。
而且那條鏈的腳本沒有 set -e,加上 make ... | tail -6 讓
exit code 被 pipe 尾端接管——每一步都失敗,整條鏈回 0。
戳破它的一句:看產物的時間戳,不看 log。
ls -la比一千行 log 誠實。
形態二:看結果,不看門檻是誰填的
症狀:promotion gate 顯示 SUCCEEDED,模型上線。
實際:模型沒變好,是門檻從 6.0 被改成 8.0。
三顆能力幾乎相同的模型(7.2343 / 7.2370 / 7.2428), 一顆上線兩顆被擋。決定的不是模型。
為什麼騙得過人:那是一條全綠的四棒交付鏈, 在任何看板、報表、稽核截圖上都是健康的。
戳破它的一句:去看 run 的 input parameters。 不要問「有沒有品質閘門」,要問「這次的門檻是多少、誰填的」。
形態三:檢查的範圍小於系統的範圍
症狀:DSC 說 KserveReady=True,opendatahub 的 pod 零異常。
實際:模型服務是死的,Init:Error。
根因在叢集外面——模型在 MinIO、image 在 Harbor,
兩個都是主機上的 podman 容器。crc start 不會把它們拉起來,
而叢集的健康檢查只看得到叢集裡的東西。
戳破它的一句:要求對方列出「不在叢集內的依賴」。 外部 S3、私有 registry、外掛 DB、授權伺服器——那些就是盲區。
形態四:檢查在資料層,壞在呈現層
症狀:Grafana pod Running、Prometheus target up、指標有值、dashboard 開得起來。
實際:九個面板全部查不到資料,因為 datasource 沒指定 uid,
Grafana 自動產了一個隨機值,而 dashboard JSON 裡 20 處都寫死 uid: "prometheus"。
為什麼騙得過人:每一個自動化檢查都在資料層,而壞的地方在呈現層。 「有沒有人真的打開過那個面板」——沒有任何檢查在問這件事。
戳破它的一句:打開它,看一眼。 一個沒有人打開過的 dashboard,跟沒有 dashboard 的差別, 只在於前者讓你以為自己有在監控。
形態五:寫入成功、讀得回來、內容是空的
症狀:把模型登記進 Model Registry,HTTP 200,條目建立,讀得回來。
實際:test_bpc: 0、model_digest: ""、data_quality_gate: false——
每一個治理數據都是零值。
原因:API 吃 snake_case,我送 camelCase,
它不認識那個欄位就用零值填上,而且不報錯。
為什麼這個最陰:那筆紀錄看起來完全正常——模型名對、版本名對、
時間對、S3 路徑對。只有治理數據是空的。
而 data_quality_gate 從 true 變成 false,
任何讀這個欄位做決定的流程,會拿一個假的值去判斷。
戳破它的一句:寫進去之後,讀回來逐欄比對。 不要相信 HTTP 200。
補記:後來又撞到兩個,而且都是形態五的變種
寫完這篇之後,我在同一套平台上又撞到兩個,形狀跟形態五一樣—— 格式正確、內容錯誤、沒有任何提示:
① Connection 的憑證是錯的,但 dashboard 顯示正常。
UI 檢查的是 label 和必填欄位有沒有填,不會拿那組憑證去連一次。
直到在 workbench 裡真的用它,才拿到 SignatureDoesNotMatch。
② 鏡像的 repo 對、IDMS 也生效,但 digest 不對。
Harbor 裡有那個 repo、名字完全正確、IDMS 設了而且改導向成功——
只是裡面那顆是另一個版本。oc 只會說 ImagePullBackOff。
所以形態五可以推廣成一句:
「東西在那裡」和「東西是對的」是兩件事, 而所有只檢查前者的機制,都會放行後者的錯誤。
而這類錯誤有一個共同的偵測法:不要驗證它存在,驗證它能用。 不要看 connection 在不在,去連一次;不要看 image 在不在,去拉一次; 不要看台帳有沒有那筆,把它讀回來比對。
五種的共同結構
排在一起就看得出來,它們是同一個東西的五個側面:
| # | 檢查看的 | 實際壞的 | 距離 |
|---|---|---|---|
| 1 | 程式說什麼 | 程式做了什麼 | 宣告 vs 行為 |
| 2 | 結果 | 判準 | 輸出 vs 標準 |
| 3 | 叢集內 | 叢集外 | 範圍 |
| 4 | 資料層 | 呈現層 | 層級 |
| 5 | 寫入動作 | 寫入內容 | 形式 vs 內容 |
每一種都是「檢查的位置」和「失敗的位置」之間有一段距離。
而那段距離不會自己被發現——因為檢查在它自己的位置上,是通過的。
所以驗收要問什麼
把五種各壓成一句,就是一份可以帶去會議的清單:
- 產物的時間戳是什麼時候?(不要給我 log)
- 這次用的門檻是多少、誰填的?(不要問「有沒有閘門」)
- 哪些依賴不在叢集內?(那些是盲區)
- 請當著我的面打開那個面板/建一個 workbench。(不接受截圖)
- 寫進去的東西,讀回來一模一樣嗎?(不要相信 200)
⭐ 而最通用的那一句,我在寫 gate 那篇時整理出來的:
「這條鏈上,哪一個點會因為檢查沒過而讓部署失敗?」
不要問「有沒有做 X」——那個答案永遠是「有」。 要問失敗會在哪裡發生。 如果對方講不出一個具體的點, 那條鏈上的檢查全部是產生報告,不是擋事。
而這兩者在稽核截圖上看起來一模一樣。
最後:其中兩種是我自己犯的
形態一是我踩到別人的,形態二、三、四是我在平台上撞到的。 但這三十天我自己也製造了兩個:
- 用錯的字數量法,差點把 12 篇稿子拆成 24 篇
- 消融實驗的評估沒套用消融,兩篇文章的數字全錯
兩個都不是我的檢查抓到的。 一個是有人問我「要不要拆」我才回頭重量, 另一個是另一個實驗跑出矛盾數字才追出來。
錯誤的數字不會自己招供。
這是為什麼我最後留下的不是一份檢查清單,是一個習慣: 當一個數字要求你做一個很貴的決定時,先回頭驗那個數字。
你遇過最久沒被發現的「假綠」是哪一個?後來是怎麼發現的?
🧪 這篇的實驗環境與 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-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你遇過最久沒被發現的「假綠」是哪一個?後來是怎麼發現的?