LLM API 會掛。逾時、限流、5xx、回傳格式跑掉——全部都會發生,而且往往挑在你睡覺的時候。如果 Day 20 那個每日摘要功能直接依賴 AI 呼叫成功,那麼 AI 供應商打個噴嚏,你的訂戶那天就收不到內容。
對免費功能,這也許能忍。對付費訂閱的核心功能,「今天沒有內容」等於直接違約——訂戶付錢買的就是每天那份摘要。
解法的核心觀念:把功能拆成「基本版」和「AI 增強版」兩層,AI 掛掉時退回基本版,而不是整個開天窗。
以每日摘要為例:AI 版是把資料寫成流暢的敘述;基本版則是純程式產生的結構化數據呈現——表格、數字、固定格式的條列。沒有 AI 的文采,但該有的資訊一項不少。訂戶收到基本版的感受是「今天的摘要比較乾」,收到空白的感受是「這服務壞了」——這兩者對信任的傷害完全不是一個量級。
容錯的實作有一個講究的細節。AI 呼叫的包裝函式,失敗時回傳的是可用的字串,而不是拋出例外:
def _call_llm_once(prompt: str, fallback: str) -> str:
"""呼叫 LLM 生成內容。任何失敗(逾時/限流/格式錯誤)回傳 fallback,
絕不拋例外——呼叫端永遠拿到可直接使用的字串。"""
try:
resp = llm_client.generate(prompt, timeout=TIMEOUT)
if not _looks_valid(resp): # 格式檢查:AI 回了但回得不對
return fallback
return resp.text
except Exception:
log.exception("llm call failed, using fallback")
return fallback
為什麼堅持「回字串不拋例外」?因為這個函式的呼叫端是排程流程——一條要服務全體訂戶的生產線。例外會讓整條線停下來,而「拿到一個保底字串」讓生產線無感地繼續走。把失敗處理封裝在最靠近失敗源的地方,讓上游永遠面對一個「不會失敗」的介面,整條流程的複雜度會斷崖式下降:呼叫端不需要 try/except、不需要判斷降級、不需要知道 AI 今天心情如何。
注意 _looks_valid 那一行:「AI 回了,但回的東西不對」(格式跑掉、內容截斷、答非所問)跟「AI 沒回」同樣是失敗。這是實戰裡學到的——LLM 的失敗模式裡,「回傳垃圾」比「回傳錯誤」更常見也更危險,因為後者你的 except 接得住,前者會直接送到使用者眼前。
容錯路徑的測試原則:fallback 路徑要當成正式功能來測,不是當成邊緣案例。
最後一點常被忽略:降級版本也是版本,它的品質決定了事故當天使用者對你的評價。
依賴任何外部智慧服務的功能,設計時先回答三題:它掛了,使用者看到什麼?它回了垃圾,攔得住嗎?降級版本自己有沒有被測試? 三題都有答案,才算把 AI 當成「增強」;答不出來,你做的是「依賴」——而依賴的東西,終將在最壞的時刻辜負你。