有一個「當某項量測值出現異常放大時通知使用者」的警報功能,上線好幾週,一次都沒觸發過。沒有人回報 bug——因為「沒有通知」看起來就像「最近沒有異常」,完全合理。直到某天一個明顯該觸發的情境它依然沉默,我才起疑。
查下去發現:警報邏輯拿 A 表的數值跟 B 表的門檻比較,而這兩張表記錄同一個概念用的單位差了一千倍。閾值永遠不可能被跨越,警報永遠不會響。
兩張表的身世不同:A 表是日彙總資料,當初依照上游資料源的慣例用細粒度單位存;B 表是即時資料,依照另一個資料源的慣例用粗粒度單位(1 粗粒度 = 1000 細粒度)。兩張表各自運作時都是對的,欄位名稱還長得一模一樣。
災難發生在第一次有人(就是我)寫了跨表比較的邏輯。程式碼看起來無懈可擊:
if today_value > threshold_from_other_table * MULTIPLIER:
notify(...) # 看起來合理,但兩邊單位差 1000 倍,永遠不成立
型別檢查抓不到(都是數字)、測試抓不到(測試資料是自己造的,兩邊自然用了同樣單位)、code review 抓不到(欄位名一樣,誰想得到單位不一樣)。
它屬於我私下分類為「語意型 bug」的家族:程式碼在語法、型別、邏輯結構上全部正確,錯的是資料的語意——單位、時區(Day 6)、幣別、基準日。這類 bug 的共同點:
第一道:把單位寫進看得到的地方。 欄位註解、CLAUDE.md 地雷清單裡明確記錄「A 表是細粒度單位、B 表是粗粒度單位,混用差 1000 倍且不報錯」。從此 agent 寫任何跨這兩張表的邏輯,都會主動處理換算。
第二道:換算收斂到一個函式。 跟 Day 6 的時區處理同一個思路——散落各處的 * 1000 遲早有人乘錯方向,統一入口的 to_fine_unit() / to_coarse_unit() 至少讓錯誤只可能發生在一個地方。
第三道:警報類功能要驗證「會響」。 這條事故給我的最深教訓其實是測試方法論的:驗證「不該響的時候不響」很容易,驗證「該響的時候真的會響」才是警報功能的核心測試。上線前造一筆必定觸發的資料跑一次全流程,這一步當初省掉了,代價是好幾週的靜默失效。
命名相同不代表語意相同。任何跨表、跨模組、跨 API 的數值搬運,先問一句:兩邊的單位、時區、基準是同一套嗎? 這一句話的成本是五秒鐘,欠下的債是三個數量級。