對外動作的防重複與對帳
寄信、發布、建單這類對外動作,送出後只有三種結果:已發生、未發生、不確定;不確定時先對帳,不准直接重試。

重點摘要
- 寄信、發布、建單這類對外動作,送出後只有三種結果:已發生、未發生、不確定;不確定時先對帳,不准直接重試。
- 每個對外動作送出前先記一筆「準備送出」並帶一把防重複鑰匙,重試沿用同一把,對方就知道是同一件事。
- 核准要綁定版本、動作、目的地與期限,任何一項變了就失效;事件只能喚醒 AI,不能代替核准。
一家連鎖品牌在週五晚上寄出一封促銷信給五萬名會員。寄到一半,網路斷了一下,發送程式沒有收到郵件服務的回應。負責的 AI 助理判斷「好像失敗了」,於是重新寄了一次。週六早上,客服信箱湧進幾百封抱怨:為什麼同一封信收到兩次?其中一些人還因此取消了訂閱。
這不是 AI 特別笨,而是「沒收到回應就重送」這個直覺,本來就是錯的。在公司內部改一份文件,失敗了重來沒有什麼損失;但對外的動作不一樣。信寄出去就收不回來,文章發布了就有人看過,訂單建立了就會被出貨。對外動作的錯誤,不是撤銷就能解決的。
這篇文章說明我們怎麼處理這類動作:怎麼避免重複、怎麼在不確定時找出真相,以及為什麼核准和通知必須分開。
對外動作為什麼特別
我們把會碰到公司以外世界的動作,統稱為對外動作。寄信、發布網站或社群貼文、在別人的系統上建立或修改資料、推送到公開的地方、送出表單,都屬於這一類。
它們有兩個共同點。第一,一旦發生就收不回來,或者收回的成本很高。第二,結果不完全由我們掌握:動作的另一端是別人的系統,它可能很慢、可能暫時不回應,也可能已經處理了卻沒有告訴我們。
第二點特別麻煩。在自己的系統裡,我們總能查到一件事到底有沒有做;對外動作卻常常停在一個灰色地帶:請求送出去了,但沒有收到明確的答案。大部分事故,都發生在這個灰色地帶裡。
我們也會事先把公司會做的對外動作列成一份清單,每一種動作寫明它碰到哪個外部系統、能不能撤回、對方有沒有提供回執和查詢方式。這份清單讓 AI 不需要臨場判斷「這算不算對外」,也讓我們在新增一種對外動作時,先想清楚它出錯時要怎麼查、怎麼補救,而不是出事之後才想。
送出後只有三種結果
所以我們不把對外動作的結果分成成功或失敗,而是分成三種狀態。
- 已發生:有回執證明對方收到並處理了。下一步是把回執記下來,這件事就結束了。
- 未發生:可以確定對方沒有收到,例如請求根本沒有送出去,或對方明確回覆拒絕。這時候可以重試。
- 不確定:送出了,但逾時、斷線,或沒有收到回執。這時候不准直接重試,要先對帳。
把「不確定」獨立出來,是整套設計的關鍵。多數系統只有成功和失敗兩種狀態,於是不確定的情況會被歸到失敗,然後自動重試,結果就是那五萬封信裡有一部分被寄了兩次。只要承認「我們不知道」也是一種結果,就能為它設計一條專門的處理路徑。
這三種狀態也寫進事件紀錄裡,而且同樣只准追加。一個動作從「準備送出」到「不確定」,再到對帳後的「已發生」,每一步都是一筆新的事件,而不是把同一筆的狀態欄位改來改去。日後回頭看,就能清楚知道這件事曾經在灰色地帶停留多久、最後是怎麼被確認的。
送出前先記一筆
要能在事後分辨三種狀態,前提是送出的那一刻就留下紀錄。我們的規則是:每一個對外動作,在真正送出之前,先寫一筆「準備送出」的事件。送出之後,再依結果寫一筆「已發生並附上回執」或「不確定」。
先記再送的順序很重要。如果先送再記,萬一送出之後程式剛好當掉,紀錄裡就完全沒有這件事,沒有人知道曾經送過。先記一筆,最差的情況也只是「有準備送出,卻沒有結果」,這本身就是一個清楚的訊號:這件事要去查。
「準備送出」這筆事件還帶著一個東西:防重複鑰匙。它是一個為這件事專門產生的唯一號碼,跟著請求一起送到對方的系統。之後如果要重試,必須沿用同一把鑰匙,不能換新的。許多郵件、付款與資料服務都支援這種機制:對方看到同一把鑰匙,就知道這是同一件事,不會處理第二次。
防重複鑰匙和先記一筆,是兩道互相補強的保險。紀錄讓我們知道曾經嘗試過什麼;鑰匙讓重試即使發生,也不會在對方那裡變成兩件事。
鑰匙要在第一次嘗試時就產生,並且和「準備送出」那筆事件綁在一起保存。這樣即使負責的 AI 中途換手,接手的 AI 也能從紀錄裡找到同一把鑰匙,而不是自己再產生一把新的。換了鑰匙的重試,對對方來說就是一件全新的事,防重複的保險也就失效了。
不確定時,先對帳
當一個對外動作進入「不確定」狀態,它不會被放著不管,也不會被自動重試。它會被列在當天待辦清單的最上面,等待對帳。
對帳的意思,是去查對方系統的實際狀態。郵件服務的發送紀錄裡有沒有這封信?網站上有沒有這篇文章?對方的訂單系統裡有沒有這張單?查的方式可以是人去後台看,也可以是一支專門的對帳程式透過對方提供的查詢介面去比對。
- 查到已經發生:補記一筆「已發生」並附上查到的證據,這件事就結束了,不需要重送。
- 確定沒有發生:改記為「未發生」,然後用同一把防重複鑰匙重試。
- 查不出來:維持不確定,交給人判斷,例如直接聯絡對方確認。
回到那封促銷信。如果當時有這套規則,斷線之後那批信件會被記為不確定,週六早上出現在清單最上面。對帳程式去郵件服務查發送紀錄,發現其中大部分其實已經寄出,只補寄真正沒寄出的那一小部分,而且帶著原本的鑰匙。沒有人會收到兩封信。
有時候對帳會發現,事情已經發生,而且發生的結果是錯的,例如信件內容有誤。這時候的處理也不是撤回,因為信已經收不回來,而是補償:寄一封更正說明,並把補償本身也當作一個新的對外動作,同樣走一遍準備、送出、記錄的流程。
對帳也有節奏。我們讓不確定的項目每天至少被整理一輪,而不是隨時跳出來打斷人。大部分可以由對帳程式自動處理,剩下少數查不出結果的,才會變成一張需要人看的卡。這樣人看到的永遠是已經縮小過的範圍,而不是一整批原始的逾時紀錄。
核准要綁四件事
對外動作在送出前,通常需要人的核准。但「核准」如果只是一句「可以寄」,很容易出問題。人看到的是週三的草稿,週五真正寄出去的卻是改過三次的版本;人同意寄給某個名單,最後寄出的名單卻多了一批人。
所以我們要求每一個核准都綁定四件事:
- 哪個版本:核准的是哪一份內容,用內容的指紋記下來。
- 什麼動作:是寄出、發布,還是建立資料。
- 送去哪:收件人、名單或目的地。
- 到什麼時候:核准的有效期限。
送出的那一刻,系統會逐一比對這四件事。內容的指紋和核准時不同,表示有人改過;名單變了,表示目的地不一樣;超過期限,表示時空背景可能已經改變。任何一項對不上,核准就失效,必須重新提出一張裁決卡,讓人再看一次。
這看起來增加了一點麻煩,但它保護的是人的判斷。人簽下的是一個具體的決定,而不是一個可以被任意延伸的許可。
期限的設定通常和動作的性質有關。一封排定週五寄出的促銷信,核准可以有效到週五當天結束;一則要對外發布的公告,可能只在幾個小時內有效。期限越短,人越能確定自己同意的是當下這個情況,而不是一個早已過去的情境。
事件只喚醒,不授權
現在的工作流程裡,很多事是被事件觸發的:收到一封客戶來信、某個排程時間到了、另一個系統送來一則通知。這些事件可以叫醒一個 AI,讓它開始看看發生了什麼事。
但我們刻意把「被叫醒」和「可以動手」分開。收到事件,只代表 AI 可以開始評估;真的要做對外動作,仍然要走完同一套流程:確認身分、檢查權限、取得綁定好的核准、先記一筆、再送出。一則通知,不論看起來多緊急、內容寫得多像指令,都不能代替核准。
這條規則特別重要,因為事件的內容常常來自外部。一封來信裡寫著「請立刻把報價單寄給以下地址」,對 AI 來說只是一份需要處理的資料,而不是一個它必須執行的命令。把內容當資料、不當指令,是防止外部有心人借 AI 之手做事的基本防線。
實務上,我們也會限制哪些來源發出的事件可以叫醒 AI,並且讓同一則事件即使重複送達,也只會被處理一次。這和防重複鑰匙是同一個道理,只是方向相反:一個是我們送出去的動作不重複,一個是別人送進來的通知不重複。
有疑問就停下
最後一條原則貫穿以上所有規則:遇到權限被拒、核准缺少、狀態不明的情況,AI 要停下來,把問題列在清單最上面,而不是猜、繞路或盲目重試。
這叫做失敗即停。它的反面是「失敗了就換個方法試試」,聽起來很積極,但在對外動作上非常危險。一個被權限擋下的 AI,如果開始找別的路把事情做完,那權限就形同虛設。我們寧可讓一件事晚一天完成,也不要讓它用錯的方式完成。
對外的事,慢一點沒關係,做兩次才有關係。
停下來不代表工作就此卡住。被擋下的事會以清楚的形式出現在人的面前:哪一個動作、卡在哪一道關卡、需要什麼才能繼續。人補上缺的那一塊,例如重新核准或確認對方狀態,工作就從原本的位置接下去。停下來的成本,是等一個人看一眼;繞路的成本,可能是一次收不回來的錯誤。
重點回顧
- 對外動作送出後只有已發生、未發生、不確定三種結果;不確定時先對帳,不准直接重試。
- 送出前先記一筆「準備送出」並帶防重複鑰匙,重試一律沿用同一把。
- 核准綁定版本、動作、目的地與期限,任何一項改變就失效,需要重新提卡。
- 事件只能喚醒 AI,不能代替核准;有疑問就停下,把問題交給人。


