這個系列從頭到尾在講同一件事:你以為的檢查,其實沒在檢查。

三十天下來,我在同一套平台上撞到五種不同形態。把它們排在一起之後, 我發現它們不是五個 bug——是五種檢查設計上的缺陷,而且可以歸納。


形態一:看 log,不看產物

症狀:後訓練鏈的 log 印著 ALL DONE,每一段都有耗時,評估數字漂亮。 實際:checkpoint 的 mtime 全是三個月前,一步都沒跑

為什麼騙得過人:log 是程式印的,程式印什麼是程式決定的。 而且那條鏈的腳本沒有 set -e,加上 make ... | tail -6exit 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=Trueopendatahub 的 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: 0model_digest: ""data_quality_gate: false—— 每一個治理數據都是零值

原因:API 吃 snake_case,我送 camelCase它不認識那個欄位就用零值填上,而且不報錯。

為什麼這個最陰:那筆紀錄看起來完全正常——模型名對、版本名對、 時間對、S3 路徑對。只有治理數據是空的。data_quality_gatetrue 變成 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 內容

每一種都是「檢查的位置」和「失敗的位置」之間有一段距離。

而那段距離不會自己被發現——因為檢查在它自己的位置上,是通過的。


所以驗收要問什麼

把五種各壓成一句,就是一份可以帶去會議的清單:

  1. 產物的時間戳是什麼時候?(不要給我 log)
  2. 這次用的門檻是多少、誰填的?(不要問「有沒有閘門」)
  3. 哪些依賴不在叢集內?(那些是盲區)
  4. 請當著我的面打開那個面板/建一個 workbench。(不接受截圖)
  5. 寫進去的東西,讀回來一模一樣嗎?(不要相信 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.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-* 那一套)。 指令的邏輯可以照用,字串要自己對一次。