這是什麼、解決什麼問題

假綠 = 檢查點顯示成功,但事情沒有發生。

它比失敗危險,因為失敗會叫你,假綠不會。 你會在幾個月後、通常是在別人問你的時候,才發現。

這篇把我在自己 lab 上撞到的五種整理出來—— 它們有共同的結構,認得那個結構,就能自己看出新的。


什麼時候你會用到

  • 設計驗收清單(Day 26)
  • 檢討「為什麼這個問題沒被抓到」
  • 每次你要寫一個檢查點的時候

形態一:看 log,不看產物

pipeline 印了 ALL DONE,退出碼 0,UI 上綠色。 但那個 step 的訓練根本沒跑。

因為 ALL DONE 是程式最後一行 print, 它證明程式跑到最後一行,不證明中間做了什麼。

檢查要改成:去看產物存不存在、大小合不合理、時間戳對不對。

# ❌ 看這個
oc logs <pod> | grep "DONE"
# ✅ 看這個
aws s3 ls s3://models/output/ --recursive

形態二:看結果,不看門檻是誰填的

品質閘門顯示「通過」。但門檻是這次自己填進去的。

threshold = current_score - 0.01    # ❌ 永遠會過

這種寫法看起來很合理(「不要比上次差太多」), 但它讓每一次都合格,只要退步得夠慢。

檢查要改成:門檻的來源是什麼?誰核定的?什麼時候改的? 能不能在這次執行中被改?


形態三:檢查的範圍小於系統的範圍

你檢查了 A、B、C 三個元件,全綠。 而系統其實有 A 到 F 六個。

這種最難發現,因為檢查本身是對的,只是不完整。

檢查要改成:從系統盤點出發,不從既有的檢查清單出發。

# 系統上實際有什麼
oc get pods -A --field-selector status.phase=Running -o name | wc -l
# 你的清單涵蓋幾個

形態四:檢查在資料層,壞在呈現層

資料是對的、API 查得到、資料庫裡也有。 但畫面上是空白的。

我撞到的版本是:Prometheus 有資料, Grafana 九個面板全空——因為 datasource 的 uid 沒指定, Grafana 自己生了一個,而 dashboard JSON 裡寫死的是 prometheus。

⚠️ 我原本寫「隨機的」,那是錯的(Day 17 對帳時查清楚了): 那個 uid 是從 datasource 的名字算出來的,同一個名字每次都一樣。

這個差別會改變你的處置方式:

如果是 那你要
隨機 每次部署後去查一次,寫進 dashboard
從名字算出來(實際) 可以事先算好寫死,或乾脆在 datasource 裡明確指定 uid

「查一次」和「可以固定」是兩種完全不同的維運成本。

檢查要改成:用眼睛看最終的呈現。 API 通不代表使用者看得到。


形態五:寫入成功、讀得回來、內容是空的

API 回 201 Created,GET 回來也有那個物件。 但你送的欄位不見了。

原因是 API 對不認識的欄位不報錯,直接忽略—— 我用 camelCase 送了幾個欄位,它期待的是別的形式, 於是我得到一個成功建立、但少了一半資料的紀錄。

檢查要改成:寫進去之後讀回來比對內容,不只比對存在。

resp = create(payload)
got = get(resp["id"])
assert got["author"] == payload["author"]    # ← 這一行

五種的共同結構

每一種都是同一件事:檢查的對象,比你以為的淺一層。

形態 你檢查了 應該檢查
一 程式跑完 產物存在
二 有沒有通過 門檻是誰定的
三 清單上的 系統上的
四 資料在 使用者看得到
五 物件在 內容對

左欄都是「有沒有發生」,右欄都是「發生的是不是我要的」。

認得這個結構之後,寫任何一個檢查點都可以自問: 我檢查的是「有沒有」,還是「對不對」?


怎麼把它變成可操作的規則

寫檢查點的時候,套這三個問題:

  1. 這個訊號,在事情沒發生的時候會不會也出現? 會 → 換一個訊號。
  2. 這個判定的標準,能不能被被檢查的東西自己改? 能 → 標準要外部化。
  3. 這個檢查,涵蓋了系統的全部還是一部分? 一部分 → 說清楚哪一部分。

⭐ 一個誠實的補充

我把 lab 的檢查寫成一支腳本(37 個檢查點)跑第一次, 顯示 5 個失敗。查下去,其中 4 個是腳本自己的錯—— jsonpath 在雙引號裡的巢狀引號沒跳脫,還有一個是 oc logs deploy/X 只讀了三個 pod 中的一個。

這也是一種假綠的反面:假紅。 而它們的成因一樣——你以為你在檢查 A,實際上檢查的是別的東西。

所以清單修好之後我加了一條:檢查工具本身也要被檢查。 最簡單的做法是故意弄壞一個東西,確認腳本會抓到。


怎麼確認你沒有假綠

  做法 說明
1 每個檢查點套上面三個問題  
2 故意弄壞,確認會被抓到 ⭐ 最有效的一項
3 檢查工具跑在乾淨環境一次 排除工具自身的 bug
4 找人重跑一次 發現隱含的前提

第 2 項是所有方法裡投報率最高的: 護欄要驗過它會擋,才算存在。


常見問題

Q:這樣檢查會不會太慢? A:會慢一點。但假綠的代價是「幾個月後在別人面前爆開」, 兩個成本不在同一個量級。

Q:能不能用測試框架處理? A:可以,而且該做。 但框架不會替你決定要檢查什麼—— 一個測試 assert response.status_code == 200 一樣是形態五。

Q:五種以外還有嗎? A:一定有。這五種是我自己撞到的,不是完整的分類。 但共同結構應該是穩定的,用那個結構去看新的情況比背這五種有用。

Q:其中幾種是你自己犯的? A:兩種。形態二那個「門檻減 0.01」是我寫的, 形態四那個 Grafana uid 也是我設的。 這不是別人的問題,是設計檢查的人容易犯的問題。


你們最近一次「以為做完了其實沒有」,是怎麼被發現的?

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

本篇在 ODH 3.6.0-ea.1 上對帳(共用區塊寫的 3.5.0 是 Day 1–9 的版本)。

叢集

  • 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 檔案

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