你的 AI demo 很神。
上線後它會這樣壞掉。
以下八種失效,全部是我們在實際專案裡處理過的失效型態,而且它們的共同點是——發生的時候系統沒有報錯。
這一頁沒有客戶見證、沒有成長曲線、沒有準確率。
八段症狀裡的金額、頁碼、百分比與分鐘數是說明失效形狀的示意情境,不是任何一次事故的紀錄,也不是量測結果。
八種上線後才會出現的失效
這八條的共同點不是「AI 會錯」——那是常識。共同點是:錯的時候你的系統回 200,你的 log 是乾淨的,你的儀表板是綠的。
這才是問題。
以下八段的金額、頁碼、百分比、分鐘數為說明失效形狀的示意情境,非量測結果。
F1抽出來的數字彼此對不起來,系統照樣入庫
症狀
明細加起來是 184,500,小計欄寫 184,050。系統兩個數字都收下了,一起寫進資料庫。
為什麼會這樣
抽取模型逐欄回答問題,它沒有義務、也沒有機制去檢查自己回答的這些數字之間是否一致。每一欄單獨看都很合理。
沒護欄的後果
錯誤會一路流到下游。它不會在辨識當下被發現,會在月底對帳、或客戶收到報價單之後被發現。屆時已經無法回推是哪一份文件、哪一列造成的。
F2模型講到一半被截斷,而半份結果看起來很完整
症狀
一份 200 頁的文件,回傳的 JSON 結構完整、格式正確、可以正常解析——但它只涵蓋到第 87 頁。
為什麼會這樣
輸出達到 token 上限被截斷。API 回應裡有一個 finish reason 欄位說明了這件事,但如果程式只檢查 HTTP 狀態碼跟 JSON 能不能 parse,這個訊號會被整個略過。
沒護欄的後果
資料庫裡有一批「完整」的紀錄,其實少了六成。而且因為每一筆看起來都正常,抽樣檢查也查不出來——你抽到的那幾筆都在前 87 頁。
F3模型拒答,整批任務直接失敗
症狀
批次跑到第 12 份文件時整條管線停住。錯誤訊息是一個空的回應。
為什麼會這樣
內容安全過濾器攔下了這次請求。在 API 層,這個失敗跟「輸出被截斷」長得非常像——都是拿不到完整結果。
沒護欄的後果
如果兩者被當成同一件事處理,你會用「重試」去解決一個永遠不會靠重試解決的問題。重試三次、退避、再重試,最後放棄,然後把整批標成失敗。而其實只要換一個處置方式,這一份是可以過的。
F4服務逾時被誤判成憑證失效
症狀
第三方辨識服務回應變慢。系統自動切換到備用金鑰,結果還是慢。監控上出現一連串「憑證切換」事件。
為什麼會這樣
failover 邏輯寫成「只要失敗就換 key」。但逾時是服務端負載問題,跟你手上這把金鑰有沒有效完全無關。
沒護欄的後果
兩個代價。第一,換 key 沒有解決問題,只是拖長了失敗時間。第二——而且更貴——你的監控訊號被汙染了。下次真的發生憑證失效時,你在一堆假的「憑證切換」事件裡分不出來。
F5長任務跑在 HTTP request 裡,一次部署全部歸零
症狀
一個要跑 10 到 30 分鐘的解析任務,在跑到第 18 分鐘時消失了。沒有錯誤、沒有 log、沒有部分結果。
為什麼會這樣
任務跑在 request handler 裡,或是狀態存在記憶體。部署、重啟、擴縮容、健康檢查失敗——任何一個都會讓它蒸發。
沒護欄的後果
在單機、低流量的 demo 環境完全看不出來,因為你不會在 demo 中途重新部署。上線後第一次 deploy 就會出現,而且會被誤診為「AI 服務不穩」。
F6跑到 46% 斷線,只能整份重跑——而且重跑要再付一次錢
症狀
一個跑了 40 分鐘、已經完成 46% 的批次任務中斷。重新啟動,從 0% 開始。
為什麼會這樣
沒有 checkpoint。任務的進度只存在於這一次執行的記憶體裡。
沒護欄的後果
時間成本之外,還有一個直接的財務後果:已經呼叫過模型、已經付過費的那 46%,會被重新呼叫、重新付費一次。批次規模愈大,這個代價愈非線性。
F7外部服務沒設定,系統靜默改走 mock,假資料進了正式庫
症狀
新環境部署完成,功能看起來全部正常。兩週後有人發現通知從來沒有真的寄出去過,而資料庫裡那些「已寄送」的紀錄都是假的。
為什麼會這樣
開發期為了方便,外部服務未設定時自動退回 mock 實作。這個行為在開發環境是體貼,在正式環境是災難——而且它們用的是同一段程式碼。
沒護欄的後果
這是這八條裡最晚被發現的一條。系統從頭到尾沒有報過任何錯,因為 mock 永遠成功。發現的時候,錯誤資料已經累積了幾週。
F8樣本不足時系統照樣給出結論,而介面上看不出來
症狀
一個帳號只有兩篇歷史資料,系統對它輸出一個三位數倍率的「異常」判定。畫面上那個數字跟樣本充足的帳號長得一模一樣。
為什麼會這樣
統計量在樣本數極小時本來就不穩定。但如果程式只是照公式算,它不會知道要在什麼時候閉嘴。
沒護欄的後果
使用者會依據一個沒有支撐的數字做決策。而且因為介面沒有任何區別,他無從得知這一筆跟旁邊那一筆的可信度差了一個數量級。
AMAX AI 上線護欄
八道護欄,編號對齊上面八條
每一道護欄對應上方同編號的失效。這一層全部是決定性程式碼——沒有模型呼叫、沒有評分器、沒有另一個 AI 在判斷。
G1用算術抓幻覺
↑ F1抽取結果先過兩條純算術的恆等式(小計 + 稅 = 總計;明細加總 = 小計),對不上就換路徑重跑,仍對不上就標記交人。
這道護欄的完整說明——包含兩條恆等式怎麼推導、處置的三個階段、以及它的盲區——寫在單據辨識那一頁:單據辨識引擎 → 14:00 發現數字對不起來。這裡不重述。
在這一頁要記住的只有一句:這是八道護欄裡成本最低的一道,因為它的判準完全不需要模型。一個純算術的檢查,可以被第三方拿計算機複算。八道護欄裡只有這一道具備這個性質。
G2把「講不完」跟「不講」分開處理
↑ F2模型回應裡的 finish reason 被明確分類,而不是只看 HTTP 狀態碼。
判定為輸出截斷時:自動把這一批切成更小的 chunk 遞迴重跑,而不是重試同一個過大的請求。
G3拒答走另一條處置線
↑ F3判定為內容過濾攔截時:先升溫重試,仍失敗才降級——不走 chunk 切分(切小了還是會被同一個過濾器攔)。
F2 與 F3 在 API 層長得幾乎一樣,但它們的正確處置完全相反。分類這件事本身就是護欄。
G4憑證問題用憑證的辦法,逾時用逾時的辦法
↑ F4雙金鑰 failover 只在 401 / 403 觸發;逾時不換 key。
實作參數(輪詢間隔、逾時上限、回退路徑怎麼接)寫在 單據辨識引擎 → 中場:技術怎麼做到的,這裡不重述。
這一頁只在乎它是不是一道護欄——而它是,理由在監控端:一個訊號如果同時代表兩件事,它就不再是訊號。憑證切換事件混進逾時之後,下一次真的憑證過期時,你的告警歷史裡分不出哪一次是哪一種。這道護欄保護的不是這一次請求,是下一次你查 log 的能力。
G5長任務落 DB,不落記憶體
↑ F5佇列在資料庫裡,不在行程記憶體裡。它被拆成幾個各自獨立的模組:
機制 解決什麼 CAS 樂觀鎖 多個 worker 不會重複領取同一個 job reaper 回收 卡死的 job 會被回收重排,不會永遠佔著 running 回收政策 什麼算卡死、回收幾次後放棄,是明確的策略而不是散落的 if timing 記錄 每一段耗時落地,效能退化查得出是哪一段 完成通知 任務結束主動通知,呼叫端不用一直輪詢 佇列的每一個模組都各自附單元測試。佇列是最容易寫出「看起來對但競態下會壞」的東西,不測等於沒寫。
G6跑到 46% 可以從 46% 接續
↑ F6checkpoint 以「一個工作單位」為粒度落地——不是整個任務一個進度數字,是逐單位記錄。
job 狀態機帶三個欄位:可否重試、已重試次數、從哪裡續跑。
前置條件不符時直接回 409,而不是假裝重跑。任務還在跑、或根本沒有 checkpoint,就明確拒絕——沉默地重跑一次是更糟的選擇。
成本面配套:批次評分以「尚未評分過」為條件過濾,重跑管線不會對已付費過的項目重複計費;並設每輪處理筆數硬上限。
G7fail-closed,不是 fail-silent
↑ F7外部服務未設定時直接關閉該功能並報錯,不靜默改走 mock。
供應商抽象層用環境變數切換 mock / 正式,業務程式碼零改動;遇到未知的供應商名稱時退回 mock 並發出警示——是警示,不是靜默。
這不是一個聰明的設計,是一個紀律。它在三個彼此獨立、不同技術棧、不同客戶的專案裡各自長出來,這件事本身就是它有效的證據。
G8「不知道」要說不知道
↑ F8歷史樣本少於門檻時,強制把統計量設回中性值、把信心度壓到極低,並在介面上明確標示「歷史樣本不足」。不輸出一個看起來跟其他筆一樣的數字。
同一個原則在評測工具上的版本:找不到對應標註時印出警告,而不是靜默回傳 precision = recall = 0。
靜默回 0 會讓一個根本沒被評測到的項目,在報表上看起來像「評測過但表現極差」。這兩件事的處置完全不同。
護欄本身也會壞
上面八道護欄有一個共同的失效模式:它們可能永遠回報綠燈。
一個從來不叫的警報器,跟一個沒安裝的警報器,在事故當下是同一個東西。差別是前者會讓你以為自己有保護。
新的檢查器交付前,必須先故意弄壞一次,證明它會叫。
這叫負面控制。沒有做過負面控制的檢查器,你只知道它在正常情況下不叫,不知道它在異常情況下會不會叫。這兩件事完全無關。
同時要在健康的輸入上連續跑,證明它不會誤叫。
這條比第一條更常被忽略。一個在健康系統上會誤報的檢查,等於沒有檢查——它一定會被關掉,而且通常是被一個不知道它為什麼存在的人關掉。我們有一個檢查器在誤報之後被主動移除,並在原處留下註解說明為什麼移除、當時量到什麼——這比留一個註定會被靜音的檢查好。
「還沒量到」不可以混進「壞掉」。
至少要有三種判定狀態:通過、失敗、尚未量到。第三種要有逾時升級機制,否則「尚未量到」會變成缺陷的藏身處——所有查不出來的東西都會沉澱在那一格。
靜默失敗的機制,必須有專門為它設計的斷言。
執行期的量測失敗不會讓編譯通過與否有任何改變。編譯期完全看不見它。所以斷言的結果要同時暴露在正式環境查得到的地方與開發環境的主控台——只放一邊,就等於只有一半的時候看得到。
這四條沒有一條是關於 AI 的。它們是關於「怎麼確認一個檢查真的在檢查」。這也是為什麼這一層不能用模型來做——你會需要另一個檢查器來檢查那個模型。
我們踩過的坑
以下每一條都對應到可以被指認的實作。這一區不放形容詞。
逾時不換 key。
這條規則存在,是因為曾經換過。換完還是逾時,而且監控上多了一串假的憑證事件。
信心度可以是常數。
我們有一條退讓式管線,最後一層的信心度不是算出來的,是一個寫死的低值。當初有人提議「至少讓它跟前幾層一樣有個計算式,不然看起來很隨便」——沒有採納。一個算出來的分數會讓下游以為那裡有資訊;一個明顯的常數不會。當一層真的沒有可用的信心度時,寫一個看起來像分數的東西,比不寫更糟。(那條管線的四層退讓長什麼樣,見 型錄辨識引擎。)
丟棄門檻校準自真實輸出,不是拍腦袋定的。
用來擋掉明顯無效結果的幾何門檻,數值全部回推自一批實際跑出來的壞資料。順序是「先蒐集錯誤形狀,再定門檻」,不是「先定一個看起來合理的門檻,再看它擋掉多少」。這兩種順序在程式碼上長得一模一樣,但只有前一種能在被追問時說出這個數字為什麼是這個數字。
前置條件不符回 409,不假裝重跑。
續跑 API 在任務仍在執行、或找不到 checkpoint 時明確拒絕。沉默地重跑一次會產生第二份部分結果,那比拒絕糟得多。
fail-closed 是三個獨立專案各自長出來的。
不同技術棧、不同客戶、不同時間,最後都收斂到同一個慣例。它不是某個人的偏好。
佇列的每一個模組各自附測試。
競態條件不會在手動測試中出現。它會在上線第三週的某個尖峰時段出現一次,然後你花兩天也重現不了。
上述實作的檔案路徑可在技術對談中逐條攤開,包含測試檔。
這不是什麼
不是模型微調服務。
我們不訓練模型、不做 fine-tune、不做 RLHF。這一層在模型外面。
不是「AI 保險」。
不承諾賠償、不承諾錯誤率上限、不承諾任何 SLA 數字。它讓錯誤被偵測到,不讓錯誤消失。
不是 AI 平台,也不是模型能力。
它是工程層。如果你要找的是一個模型或一個平台,這一頁不是。
不宣稱幻覺率下降多少。
我們沒有量過這件事,所以不寫。可以量的是:有多少筆被自洽檢查攔下、有多少筆退到了最後一層、有多少 job 被 reaper 回收——這些是計數,不是宣傳。
評測工具不是通用產品。
評測骨架(跑 → 比對 → 出報告 → 敏感度掃描)是通用的,但判準與語料是產業資產。換一個產業,語料要重建,這是一段真實的工作量。
不宣稱自訓模型。
相關專案裡的模型權重是開源官方預訓練權重,不是我們訓練的成果。
如果你手上那套已經在線上了
帶著它來。我們會問四個問題:模型失敗的時候,你的系統怎麼知道?長任務中斷的時候,進度在哪裡?外部服務沒設定的時候,會發生什麼事?以及——你上一次確認這些檢查真的還在運作,是什麼時候?
這四個問題你自己也能問。如果答案都在,那你不需要我們。
對談不需要你交出程式碼。我們先看架構圖跟 log 就夠了。