如果一個工具函式本身不是冪等的(例如呼叫外部 API 下單),Agent 該怎麼在不修改這個外部 API 的情況下,讓自己的重試邏輯依然安全?
如果外部 API 本身沒有提供冪等鍵支援(例如你呼叫的第三方服務,它的下單端點就是設計成「每次呼叫都會產生一筆新訂單」,沒有讓你傳入識別碼去避免重複),你沒辦法修改對方的 API 行為,但可以在 Agent 自己的架構層,加上一層額外的保護,常見做法有兩種。
方法一:在呼叫外部 API 之前,先在自己的系統裡做請求去重。具體做法是,Agent 每次準備發起一個下單操作時,先在自己的資料庫裡記錄「這次操作的唯一識別碼」和「操作內容的雜湊值」,在真正呼叫外部 API 之前,先檢查是否已經有一筆內容完全相同、且最近時間內已經處理過的記錄——如果有,代表這可能是一次重複觸發(例如前面重試策略文章提到的,某個環節誤判導致同一個任務被觸發了兩次),直接跳過這次呼叫,不重複下單;如果沒有,才真正呼叫外部 API,並把這次呼叫記錄下來。這個方法的本質,是在「不冪等的外部 API」前面,加上一層自己控制的冪等層,把冪等性的責任從外部 API 轉移到自己的系統。
方法二:限制重試的時機和範圍,只在能確定「上一次呼叫確定失敗」時才重試。多數導致 Agent 誤重試非冪等操作的情況,根源是「不確定上一次呼叫到底成功了沒有」(例如網路逾時,你不知道是請求沒送達,還是請求送達了但回應沒收到),如果只在能明確判斷「上一次呼叫確定失敗,對方系統完全沒有處理」的情況下才重試(例如收到明確的錯誤回應碼,而不是逾時這種模糊情境),可以大幅降低誤觸發重複下單的機率——但這個方法不是萬無一失,因為「網路逾時」這種模糊情境本來就無法百分之百排除,這也是為什麼方法一(自己控制的冪等層)通常更可靠、更該優先採用的原因。
實務建議:如果這個操作涉及實際金錢或不可逆後果,優先採用方法一(自建冪等層),即使需要額外的工程投入;方法二可以作為輔助手段疊加使用,但不該是唯一的防線,因為網路環境的不確定性本來就無法完全消除。
設計冪等鍵(idempotency key)時,這個識別碼該由 Agent 產生,還是該由發起請求的使用者產生?兩種做法有什麼差異?
這兩種做法各自適合不同的情境,核心差異在於「同一個邏輯上的操作,重複判斷的範圍該有多廣」。
由 Agent 自己產生冪等鍵:適合「Agent 自主決定要重試」的情境——例如 Agent 判斷上一次呼叫因為網路逾時而失敗,決定要重試,這時候 Agent 應該沿用「同一次操作」原本已經產生的識別碼,而不是每次重試都產生一個新的識別碼(如果每次重試都用新識別碼,冪等鍵機制就完全失去意義,因為後端會把每次重試都當成一個全新的、沒有處理過的操作)。這種情境下,識別碼的產生時機是「這個操作第一次被提出的時候」,之後不管重試幾次,都沿用同一個識別碼。
由使用者(或使用者端的應用程式)產生冪等鍵:適合「同一個使用者意圖,可能透過不同管道被觸發多次」的情境——例如使用者在應用程式介面點擊了「執行」按鈕,因為網路延遲使用者不確定有沒有成功送出,又點了一次,這種情況下,如果冪等鍵是在使用者端(應用程式介面)產生並附著在這次「使用者意圖」上,即使這兩次點擊背後觸發了兩次獨立的 Agent 呼叫,只要兩次呼叫帶的是同一個由使用者端產生的識別碼,後端依然能正確判斷這是同一個意圖,只執行一次。
兩者的本質差異:Agent 自己產生的冪等鍵,保護的是「Agent 內部重試邏輯」這一層的重複;使用者端產生的冪等鍵,保護的是「使用者意圖」這個更上層、涵蓋範圍更廣的重複,包括使用者自己不小心的重複操作,也包括 Agent 內部的重試(因為使用者端產生的識別碼,理論上會沿用貫穿整個處理鏈路,不管中間 Agent 內部重試了幾次)。
實務建議:如果系統架構裡,使用者的意圖是透過明確的介面操作觸發的(例如點擊按鈕),優先讓識別碼在使用者端產生,這樣可以同時涵蓋「使用者重複操作」和「Agent 內部重試」兩種重複來源,保護範圍更完整;如果整個流程完全是 Agent 自主觸發、沒有使用者即時操作的環節(例如前面提到的自動化收益優化 Agent,任務是 Agent 自己判斷觸發的),則由 Agent 在任務產生的當下自己生成識別碼即可,不需要額外設計使用者端的識別碼生成邏輯。
如果 Agent 系統重新啟動或發生崩潰,之前記錄的冪等鍵資訊如果沒有持久化保存,重啟後是不是就失去保護作用了?該怎麼設計才不會有這個漏洞?
這是一個真實存在的風險,如果冪等鍵的記錄只存在記憶體裡(例如只是程式執行時的一個變數,沒有寫進資料庫或其他持久化儲存),Agent 系統一旦重啟或崩潰,這些記錄會全部遺失,重啟後如果收到一個「其實在崩潰前已經處理過」的重複請求,系統會誤判成一個全新的請求,重複執行——這正好是冪等鍵機制原本要防範的情境,如果冪等鍵記錄本身不夠可靠,保護機制形同虛設。
正確的設計原則:冪等鍵的記錄本身,必須寫入持久化儲存,而不是只存在記憶體裡。具體做法是,每次操作準備執行之前,先把「這次操作的識別碼」和「即將要執行的操作內容」,同步寫入資料庫(而不是等操作執行完才記錄,這點很關鍵,稍後會說明原因),操作完成後,再把「執行結果」也更新進這筆記錄。這樣即使系統在操作執行過程中突然崩潰,重啟後至少能查詢到「這個識別碼的操作,之前已經開始執行、但不確定是否完成」這個狀態,而不是完全沒有任何記錄。
為什麼要在「執行前」就先寫入記錄,而不是「執行後」:如果選擇在操作執行完成後才寫入冪等鍵記錄,會有一個時間窗口的風險——如果 Agent 在「操作已經實際執行完成」和「把這次執行的記錄寫進資料庫」這兩個時間點之間崩潰,重啟後,資料庫裡完全沒有這次操作的記錄(因為記錄還沒來得及寫入),Agent 會誤判成「這個操作還沒執行過」,重複執行一次,造成前面提到的重複扣款這類問題。反過來,如果先寫入「準備要執行」的記錄,再實際去執行操作,即使中途崩潰,至少資料庫裡有一筆「這個操作曾經被觸發過」的記錄,重啟後可以先去查證「這個操作在外部系統(例如鏈上或第三方 API)是不是其實已經執行成功了」,再決定下一步,而不是完全沒有頭緒地重複執行。
額外的實務考量:冪等鍵的記錄不能無限期保留(否則儲存空間會持續增長),通常需要設計一個合理的保留期限(例如保留 24 小時或 7 天,取決於業務邏輯合理的重試視窗有多長),超過這個期限的舊記錄可以清理,因為超過這個時間,重複請求的機率極低,繼續保留的邊際價值很低。
在多 Agent 系統裡,如果 Sub-agent A 已經對某個操作使用了冪等鍵成功執行,但因為 Handoff 過程中的資訊遺失,Sub-agent B 又重新對同一個邏輯操作發起了一次請求,這種跨 Agent 的重複,冪等鍵機制還能保護嗎?
能保護,但前提是「冪等鍵本身要跟著任務一起被交接」,而不是只存在於 Sub-agent A 自己內部,這呼應了前面 Handoff 詞條反覆強調的「交接封包要包含足夠的中繼資訊」這個原則。
跨 Agent 冪等性保護的關鍵設計:冪等鍵不應該被當成 Sub-agent A 內部的實作細節,而應該被當成「這個邏輯操作」本身的一個屬性,從任務一開始被賦予,並隨著 Handoff 封包,完整地傳遞給 Sub-agent B。具體來說,如果任務本身在最開始(例如由 Orchestrator 分派時)就已經產生了一個唯一的任務識別碼,這個識別碼應該貫穿整個任務的生命週期,不管中途經過幾次 Handoff、被幾個不同的 Sub-agent 處理過,同一個邏輯操作,都應該沿用同一個識別碼。
如果 Handoff 過程真的遺失了這個識別碼,會發生什麼:如同你問題裡描述的情境,Sub-agent B 因為沒有拿到原本的識別碼,只能自己生成一個新的識別碼發起請求,對後端(例如負責實際執行轉帳的系統)而言,這是一個帶著全新識別碼的請求,冪等鍵機制會判斷這是一個「沒有處理過的新操作」,因此正常執行,造成重複執行的問題——這代表冪等鍵機制本身沒有失效,失效的是「識別碼在 Handoff 過程中的傳遞完整性」,問題出在上游(Handoff 資訊遺失),而不是下游(冪等鍵機制本身)。
這個情境該怎麼防範:除了前面 Handoff 詞條提到的「交接封包完整性測試」這類測試框架該涵蓋的內容,還可以加上一層獨立於 Handoff 機制之外的保護——在真正執行操作之前,除了檢查「這個識別碼是否處理過」,也額外檢查「這個操作的具體內容(例如轉帳的目標地址、金額),是否和最近一段時間內已經處理過的某筆操作高度相似」,即使識別碼不同,如果操作內容幾乎一致且時間相近,也應該觸發額外的確認流程(例如人工介入確認,或至少記錄告警),而不是照單全收直接執行。這個內容層級的相似度檢查,可以作為識別碼機制的補充防線,在識別碼本身因為某種原因(如 Handoff 資訊遺失)失效時,依然能提供一層保護。
核心原則:多 Agent 系統的冪等性保護,不能只在單一 Sub-agent 內部設計,必須被當成貫穿整個任務生命週期、跨越所有 Handoff 環節的一致性需求,這是延伸前面 Handoff 詞條「交接封包完整性」原則到冪等性這個具體場景的必然結果——任何在單一環節設計得再嚴謹的機制,如果沒有考慮到多 Agent 協作時的資訊傳遞完整性,都可能在協作的縫隙裡失效。
一個 Agent 錢包轉帳操作的冪等性實作案例
某個負責幫用戶自動執行鏈上轉帳的 Agent,冪等性設計如下:
識別碼產生時機:任務由 Orchestrator 分派時,立即產生一個唯一的 task_id(結合時間戳記和隨機字串),這個 task_id 作為冪等鍵,寫入資料庫,狀態標記為 pending,這個動作發生在真正呼叫鏈上轉帳操作之前。
執行前檢查:Agent 準備發起轉帳前,先查詢資料庫裡是否已經有相同 task_id 且狀態不是 pending 的記錄——如果狀態已經是 completed,代表這筆轉帳已經成功執行過,直接回傳原本的執行結果,不重複發送交易;如果狀態是 failed_confirmed(代表上一次已經確認失敗,鏈上完全沒有留下痕跡),才真正重新發起交易。
執行中的模糊狀態處理:如果查詢到狀態是 pending(代表上次執行到一半,不確定鏈上有沒有實際發生),Agent 不會直接假設「還沒執行」就重新送出交易,而是先去查詢鏈上,用之前記錄的交易雜湊值(如果有記錄下來的話)確認這筆交易是否已經存在鏈上,只有確認鏈上真的沒有這筆交易時,才會重新發起,避免了因為模糊狀態誤判導致的重複轉帳。
跨 Sub-agent 傳遞:如果這個轉帳任務涉及多個 Sub-agent 協作(例如一個 Sub-agent 負責判斷該不該轉帳,另一個負責實際執行),task_id 會作為 Handoff 封包裡的必要欄位,明確傳遞給執行 Sub-agent,執行 Sub-agent 收到任務時,第一步就是用這個 task_id 去檢查前面提到的執行前狀態,而不是自己生成一個新的識別碼。
實際運作中發現的問題:工程團隊在測試階段模擬了「Agent 在交易已經送出、但還沒收到鏈上確認回應時系統崩潰重啟」這個情境,發現如果只單純檢查「資料庫裡有沒有這筆記錄」,重啟後系統會因為記錄顯示 pending(不確定狀態)而卡住不知道該怎麼處理,加入「主動查詢鏈上狀態來釐清模糊情況」這一步後,系統才能在這種崩潰重啟的情境下,正確判斷該重新發起交易還是該直接回報之前已經成功,避免了人工介入才能排除的窘境。
Idempotency 設計的核心取捨是「保護的嚴謹度 vs 額外的工程與儲存成本」。冪等鍵記錄越詳細(記錄操作內容的完整快照、附加內容層級的相似度檢查),能防範的重複場景越廣,但需要的資料庫寫入、查詢、儲存空間也越多,對高頻操作的系統會增加不小的額外負擔。另一個取捨是「識別碼生成層級(Agent 內部 vs 使用者端)」:使用者端生成的識別碼保護範圍更廣(涵蓋使用者重複操作和 Agent 內部重試),但需要在使用者端額外設計識別碼生成邏輯,增加前後端協作的複雜度;純 Agent 內部生成的識別碼實作簡單,但保護範圍較窄,無法涵蓋使用者端的重複觸發。建議:涉及實際金錢或不可逆操作的核心流程,即使增加工程成本,也應該採用最嚴謹的持久化冪等鍵設計,並在可行的情況下讓識別碼從使用者端就開始生成,貫穿整個處理鏈路;非核心、低風險的操作(例如純粹的資訊查詢或分析),可以省略冪等性設計或採用較簡化的版本,把工程資源留給真正需要嚴謹保護的環節。