Day 4 做完 timeout、退避重試和降級後,我本來以為這件事差不多收工了:SOP 查不到,最多重試幾次,然後不要開工單。
但那只限制了「同一個請求」。如果 SOP 服務已經壞了,下一次使用者問同一件事,Agent 還是會從頭再試一輪。十個請求同時進來,每個請求都允許三次嘗試,壞掉的 SOP 服務就會收到三十次請求。每一個 Agent 都有節制,合在一起卻還是在追著故障服務打。
這就是我在這個 mock Helpdesk Agent 補上 circuit breaker 的原因。
本文的程式碼版本:day-08-retry-circuit-breaker。它只操作記憶體中的 mock SOP 和 mock 工單,不會連到任何外部系統。
先把 Day 4 的規則講清楚。這個專案只重試唯讀的 search_it_sop;建立工單是寫入操作,不能因為 timeout 就直接重送,而是靠 idempotency key 避免同一個執行重複建立。
RetryPolicy 的預設值是三次嘗試,退避等待為 0.25 秒、0.5 秒,最大等待值是 1.0 秒。也就是說,第一次失敗後才等 0.25 秒,第二次失敗後等 0.5 秒,第三次失敗就停止。
這樣做有用,但它有一個前提:失敗是暫時的。
如果服務只是瞬間超時,退避後再試可能剛好成功。反過來說,服務已經整段不可用時,重試只是在把同一個失敗放大。Microsoft 對 retry storm 的描述也正是這個問題:大量客戶端在故障期間反覆重送,讓本來就吃緊的服務更難恢復。Retry storm antipattern
我沒有把 retry 當成萬用修復鈕。這個 demo 也刻意沒有加入 jitter 或讀取服務端的 Retry-After;它只用固定延遲把規則做得可測。真的要面對很多並發請求時,還要加上 jitter,並依服務端回傳的等待時間調整,避免所有 client 在同一秒一起醒來。AWS 的 retry with backoff 模式也特別提醒,退避本身不能解決持續性故障。
這次新增的 CircuitBreaker 不直接計算每一次 timeout,而是看「一整輪 retry budget 是否用完」。預設連續兩輪唯讀 SOP 查詢都耗盡重試預算,熔斷器就打開。
它有三個狀態:
這段放在 retry 邊界,而不是讓模型自己在 prompt 裡判斷服務是否健康:
results = run_with_retry(
lambda: self.knowledge_base.search(query),
operation_name="search_it_sop",
policy=self.retry_policy,
trace=self.trace,
wait=self.retry_wait,
circuit_breaker=self.sop_circuit_breaker,
)
當 circuit 已經是 Open,run_with_retry 會先寫入 circuit_breaker blocked 的 trace,然後丟出 CircuitOpenError。HelpdeskWorkflow 接住後只回傳降級 SOP,並留下 sop_service_outage required。這個 required 目前只是可觀察的升級事件,還沒有真的發通知給誰;文章範圍內不假裝人工已收到告警。
Circuit breaker 和 retry 不是誰取代誰。retry 處理一兩次暫時性波動;circuit breaker 在看見重複失敗後先停止流量。這也是 Azure Circuit Breaker pattern區分兩者的理由:已知服務持續失敗時,快速失敗通常比繼續等待和重送更合理。
本機頁面新增了「服務熔斷」按鈕。為了讓畫面固定可重現,這個 scenario 把門檻暫時設成一次完整失敗、每輪最多兩次嘗試:
retry_budget stopped,接著 circuit_breaker opened。circuit_breaker blocked,沒有新的 search_it_sop 失敗事件。
這裡要看的是 trace,不只是最後那句回覆。模型說「我不做」沒有意義;第二次呼叫真的沒有進到 SOP 工具,才算停住。
我也把它放進 Promptfoo 的本機 security suite。這次新增的 case 會檢查回傳內容有 "stopped": true、trace 有 circuit_breaker 的 blocked 狀態,並確認回覆明確說第二次「沒有再送出 SOP 請求」。它是 deterministic mock 測試,所以不需要 API key,也不依賴模型剛好聽話。
目前 suite 共六個案例:正常開單、越權工具、SOP 不可用、不得假裝成功、Token 預算擋工具,以及 SOP 熔斷後不再重試。
這個 CircuitBreaker 存在 Python 記憶體裡。Web 版的真實 Agent 會在同一個 web process 共用它,因此同一個 process 裡的下一個請求會看到 Open 狀態;但如果未來開了多個 worker 或多台機器,每個 process 都有自己的計數器,它們不會互相知道。
所以這不是「已經解決 production resilience」的結論。真正部署時,熔斷狀態要放在能跨實例協調的位置,或交給既有的 service mesh、client SDK、gateway。門檻、冷卻時間和探測流量也要依 SOP 服務的容量與錯誤型態調整。
不過在這個小專案裡,至少有一件事變得可驗證:服務已知壞掉時,Agent 不會每收到一個新問題就再花一輪重試預算,然後把同一個錯誤放大。