Agentic Transaction 最值得先學的,不是 ACID 四個英文字母,而是一個很現實的判斷:Agent 呼叫扣款 API 後逾時,代表的是「不知道有沒有扣」,不是「一定沒扣」。如果程式把 timeout 當成 failed,換一把新 key 重試,同一張訂單就可能扣兩次。這篇會帶你拆解一個已在本機實跑的假訂單 Agent,並看它如何驗證回應遺失、程式崩潰、重送與兩個 worker 競爭時,仍只產生一次副作用。
你不需要先會分散式系統;只要知道 Python、SQL 的基本概念即可。讀完後,你會得到一個比「幫工具加 retry」更可靠的心智模型,也能看懂它和 AI Agent Harness、工作流引擎及 Saga 的分工。
Agentic Transaction 是什麼?
一句話定位:Agentic Transaction 是把 Agent 為完成一個目標而執行的多個工具動作,包成可追蹤、可驗證、可恢復的一個工作單位。本文採用 2026 年 8 月 14 日提交的 Agentic Transactions arXiv v1 預印本所提出的語意 ACID 框架;它是很新的研究提案,不應被誤讀成所有 Agent 平台都已採用的通用協定。
傳統資料庫的 ACID 能在同一個資料庫裡保護一筆交易,但 Agent 會跨出資料庫,呼叫付款、庫存、Email 或帳號 API。你無法用一個 SQL ROLLBACK 叫銀行忘記已經扣過的款,也無法把寄出的信收回來。因此這裡的「ACID」是語意目標,不是宣稱能把整個網路鎖進同一筆資料庫交易。
- Atomicity(原子性):整個任務要嘛符合成功條件,要嘛把已完成且可補償的步驟收尾;不是假裝中途發生的事從未存在。
- Consistency(一致性):每個動作都必須通過前置條件、輸入指紋與結果驗證,最後狀態要符合業務規則。
- Isolation(隔離性):兩個 worker 或兩次 replay 不能互相踩狀態;這份 demo 用唯一約束、compare-and-swap 與 lease expiry 協調。
- Durability(持久性):plan、effect、證據與恢復資訊必須落盤;process 死掉後,新的 worker 能從紀錄繼續,而不是靠模型「記得」。
可以把它想成旅館櫃台的交班簿:早班刷卡後電話斷線,晚班不能憑感覺再刷一次;要先拿同一個訂單號查刷卡機紀錄,確認結果後才繼續發房卡。
研究說了什麼?10.6 不能外推成付款成功率
論文把 Agent 的 transaction 拆成可持久化、可驗證與可恢復的工作單位,並把 idempotency key、write-ahead log、checkpoint 與自動補償列為可能的工程元件。論文以包含 104 個自然語言任務的 KramaBench 評估資料 Agent;表 2 的 overall Score (%) 欄顯示,同用 Qwen3.5-397B-A17B 時,Claude Code 是 64.0、ACID-Agent 是 74.6。兩者是相差 10.6 個分數百分點;論文正文稱為 10.6% improvement,但這不是 10.6% 的相對增幅、不是所有 Agent 的成功率,更不是支付系統實測。
本文核對的是官方 repository 的 f0beb2f504f375ec2da1d3ed4b7b5fec66f2ab08 commit;該版本根目錄的 LICENSE 是 Apache License 2.0,內容示範論文實驗與資料 Agent 工作流。本文的訂單 demo 是為了把論文概念轉成初學者可觀察的故障實驗,並不是該 repository 內建的付款功能。這個界線很重要:研究提供框架,付款可靠性仍要由你的程式與 provider 契約證明。
真正的 bug:timeout 後把 unknown 當 failed

先看最小失敗時間線:Agent 送出 charge(order-42);付款端完成扣款;回應在網路上遺失;Agent 只看到 timeout。此時外部世界有兩種可能:已扣款或未扣款。只要兩種都合理,狀態就必須是 unknown。
截至 2026 年 8 月 18 日,Stripe API v1 的低階錯誤文件把連線錯誤描述為結果不確定,建議保留相同 idempotency key 重試,並用 webhook 或後續查詢對帳。換句話說,retry 不是「再做一次」,而是「再次詢問同一個意圖的結果」。這只有在下游 provider 明確實作同 key 去重時才成立。
可靠 Agentic Transaction 的 5 個零件
1. 穩定的 idempotency key:意圖不變,key 就不變
痛點:process 重啟後若重新產生 UUID,provider 會把 replay 當新請求。解法:從 tenant、environment、provider、業務 transaction ID、action 與版本導出或綁定穩定 key;本機 demo 簡化成 SHA256("order:42:charge:v1").hexdigest()[:24],並保存排序後的 canonical payload JSON。同一 key 若配到不同金額,立刻拒絕,而不是猜使用者要哪一筆;production 可另外保存密碼學 hash 方便比對,但不能只靠一個未綁定原始意圖的字串。
Stripe API v1 的 idempotent requests 文件同樣要求重用 key 時參數一致,且文件允許系統在 key 建立至少 24 小時後清除紀錄。因此本機 ledger 的保存期要依退款、爭議與稽核需求設計,不能把 provider 的短期 cache 當永久帳本。
2. Append-only ledger:目前狀態可更新,事件歷史另行保留
痛點:只存最後一個 status,事後看不出狀態如何演變。解法:operations 表保存目前狀態,ledger 表依程式規則只追加事件:pending → executing → unknown → executing(reconcile) → confirmed。這份 demo 的 ledger 每列固定保存 tx_id、action、state、時間與安全的 detail;op_key、lease owner 與期限則保存在 operation/transaction 表。它沒有用 SQLite trigger 或權限把 ledger 做成不可竄改儲存;production 若要求這項保證,還要加資料庫權限、trigger 或外部稽核存放層。
這和 DeepSeek Harness用 event log 保存 Agent 上下文的想法相近,但目的不同:這裡的 ledger 是外部副作用的可稽核證據,不能只放在 prompt 或聊天歷史裡。
3. confirmed/failed/unknown:三態不能硬壓成 boolean
痛點:success=false 同時混入「provider 明確拒絕」和「回應沒到」。解法:confirmed 表示有可驗證結果;failed 表示確定沒有形成該副作用或已收到明確拒絕;unknown 表示必須停下後續不可逆步驟,先 lookup、接 webhook 或進人工佇列。
注意:一次 HTTP 500 也不必然等於「沒做」。如果 provider 文件把它列為 indeterminate,就只能依該 provider 的查詢與事件機制收斂。LLM 可以解釋錯誤文字,但不該有權把 unknown 自行改成 failed。
4. Prepare → execute → validate → commit:先寫計畫,再跨邊界
痛點:工具一呼叫就產生副作用,尚未留下恢復線索。解法:prepare 先落盤 transaction、operation、canonical payload(production 可另存 hash)與 key;execute claim 該 operation 後呼叫 provider;validate 用回應、lookup 或 webhook 驗證後置條件;commit 才把本機交易設為完成,並在同一筆 DB commit 寫入 outbox。
這個順序把「模型想做什麼」和「系統允許做什麼」分開。前者可以由 Agent 規劃;後者應由型別、狀態機、唯一約束與權限規則決定。既有的 Code-Implemented Tool Calls 已明確談到 journal commit window、replay 不等於 exactly-once,以及 idempotency、transaction/outbox/compensation 與 CAS;本文新增的不是再講一次原則,而是用分離的 provider DB 實作並故障注入 unknown → lookup、並行 claim 與可持久化補償。
5. Compensation:不是刪紀錄,而是新增反向動作
痛點:庫存已保留、款已扣,但最後驗證失敗。解法:先持久化反向計畫,再依相反順序執行 refund 與 release;每個補償動作也要有自己的 stable key、unknown、lookup 與 replay。AWS 的 Saga pattern把這類跨服務恢復描述為一連串 compensating transactions。
「補償」不等於時光倒流。退款是新增一筆反向 effect,不會抹除原扣款;寄出的 Email 也不能假裝從未存在。每種 action 都要先確認 provider 定義的反向 API 與限制。所以不可逆通知要延後到本機 commit 後,由 outbox 發送;真的無法自動判定時,終點應是 manual_review,不是無限 retry。
動手做:SQLite 假訂單 ACID Agent
這次 demo 故意用兩個 SQLite 檔:agent.db 模擬自己的 durable state,provider.db 模擬遠端付款/庫存/Email 服務。分開的好處是不能作弊:自己的 DB rollback 不會消除 provider 已寫入的 effect。
transactions(tx_id PRIMARY KEY, status, lease_owner, lease_until, version)
operations(op_key PRIMARY KEY, tx_id, action, payload, state,
lease_owner, lease_until, remote_id,
UNIQUE(tx_id, action))
ledger(seq AUTOINCREMENT, tx_id, action, state, detail, created_at)
outbox(event_key PRIMARY KEY, tx_id, kind, payload, state,
lease_owner, lease_until, remote_id)
compensation_steps(op_key PRIMARY KEY, tx_id, action, payload, ordinal, state,
lease_owner, lease_until, remote_id)
最重要的不是資料表名稱,而是不可跨越的 invariant:同一個 (tx_id, action) 只有一個 operation;unknown 必須先 lookup;所有必要 operation 都 confirmed 才能 commit;Email outbox 與 transaction commit 在同一筆本機交易落盤。
def recover(step):
claim = compare_and_swap(step, lease=30)
if claim.previous in {"executing", "unknown"}:
result = provider.lookup(step.op_key, step.payload)
if result.found:
return confirm(step, result.remote_id)
if result.unavailable:
return keep_unknown_or_manual_review(step)
try:
result = provider.apply(step.action,
idempotency_key=step.op_key,
payload=step.payload)
except ResponseLost:
return mark_unknown(step)
return validate_then_confirm(step, result)
這段提供的是可移植的設計與驗收協定,不是下載式完整程式,也不是可直接接真實金流的 SDK。為了讓核心故障窗口看得清楚,它省略了 API 認證、webhook 簽章驗證、資料加密、provider 專屬錯誤分類、連線池、退避與 retry budget、跨區一致性、監控告警及人工審批 UI;上線前必須逐一補齊。
故障驗收:replay 之後仍只能扣一次

我把 timeout 注入在 provider 寫入 effect 之後、回傳 response 之前,再重新執行同一張訂單。接著測 process 在 provider call 前崩潰、收到 response 後但本機 commit 前崩潰、兩個 worker 同時搶單,以及 refund/release 的回應也遺失。七項 assertions 的實際結果如下:
PASS crash before call: lookup-before-apply, charge=1 email=1
PASS effect committed/response lost: charge=1 email=1
PASS response before local commit: charge=1 email=1
PASS concurrent workers: reserve=1 charge=1 email=1
PASS compensation replay: refund=1 release=1 email=0
PASS manual review: compensation_unknown -> manual_review
PASS post-commit outbox: no email observed before local commit
這些 PASS 證明的是這個 dependency-free 假 provider 與 SQLite harness 的不變量,不是替任何真實金流背書;SQLite 會序列化 writer,這項兩 worker 測試也不代表已涵蓋分散式 contention。真正接 Stripe、Adyen 或內部帳務時,必須重做 contract test:同 key 是否真的去重、key 保存多久、能否按 key 查結果、webhook 是否可能重送,以及退款本身如何識別重複。
上線最常踩的 7 個坑
- 只在 Agent 端存 key:下游不承諾同 key 去重,仍可能執行兩次。先用 provider 的官方契約與整合測試證明。
- timeout 直接標 failed:先進
unknown,停止相依的不可逆動作,交給 reconciliation。 - key 有效期短於 ledger:provider cache 過期後重送,可能再次形成 effect;本機需保留 terminal result,並依 provider 規則封鎖過期 replay。
- 同 key 換 payload:金額從 100 改成 1,000 卻沿用 key,應 hard fail;key 必須綁定同一份 canonical payload/意圖指紋。
- DB commit 與通知雙寫:資料庫成功、queue 發送失敗會漏信,反過來會重信。用 transactional outbox把本機狀態與待發事件一起 commit,consumer 仍要可去重。
- 只有 mutex,沒有 durable lease:process 一死,記憶體鎖也消失。這份 demo 用唯一約束、compare-and-swap 與 lease expiry;production 若允許舊 worker 在 lease 過期後繼續寫,還要把真正的 fencing token 帶到每次寫入並做代數比較,本 demo 沒有實作這層。
- 補償失敗就一直自動重試:補償一樣可能 unknown。設 retry budget 與人工佇列,保留原 effect 和每次 lookup 的證據。
若你已經在做成本與 queue 控制,DeepSeek API 成本控制解釋了 atomic claim、outbox 與 crash window;本文則把焦點推進到「外部 effect 已經發生但結果未知」的恢復協定。
決策層:該用 DB transaction、Saga,還是工作流引擎?
- 只改同一個資料庫:優先使用真正的 DB transaction;不要為了「Agentic」硬加 Saga。
- 跨 2~3 個服務、流量不大:用 durable operations、outbox、idempotent provider 與簡單補償器,本文 demo 就是起點。
- 流程跨數分鐘到數天、分支多:用 Temporal、AWS Step Functions 或類似 durable workflow engine 承擔 timer、retry 與 resume,但每個 activity 的副作用仍需 idempotency 與 reconciliation。
- 補償涉及退款爭議、法律或高額權限:自動化只做到蒐集證據與停機,最後進人工審批。工作流能幫你記住進度,不能替你承擔業務判斷。
如果還在搭建 Agent 的基本 runtime,可先完成 AI Agent Harness 實作,再把本篇狀態機放在工具邊界。判斷順序永遠是:先縮小單一 DB 的原子範圍,再為不得不跨出去的 effect 設計恢復。
Agentic Transaction 常見問題 FAQ
1. Idempotency key 能保證 exactly-once 嗎?
不能單獨保證。它只在下游按相同 key、相同意圖回傳同一結果時,讓 retry 不重做。你還要處理 key 期限、payload 衝突、對帳、consumer 重送與本機 commit window。
2. timeout 後可以立刻用同一把 key 重試嗎?
不一定。若 provider 的官方契約允許同 key 安全重送,可以依規則做;若有 lookup 或 webhook,本文偏好先查結果。查詢也不可用時,維持 unknown 並退避或升級人工。
3. unknown 和 failed 差在哪?
證據不同。failed 是有證據知道動作未成立或遭明確拒絕;unknown 是兩種世界都可能,不能據此啟動另一筆不可逆請求。
4. Compensation 就等於 rollback 嗎?
不等於。rollback 取消尚未提交的本機變更;compensation 是已提交之後再新增退款、釋放庫存等反向 effect,原始歷史仍存在。
5. 為什麼 Email 要等 commit 後才寄?
因為寄出後很難補償。先在 commit 中寫 outbox,再由可去重 worker 發送,可避免訂單最後失敗卻先寄成功通知,也減少 commit 與發信之間的雙寫缺口。
6. 有了 Temporal 或 Step Functions 就不用 ledger 嗎?
還是需要副作用證據。引擎能保存 workflow 進度,但 provider 的 idempotency key、remote ID、payload 指紋、webhook 與補償結果仍要能被稽核。
7. 可以讓 LLM 決定要不要 retry 嗎?
不要讓它直接決定高風險副作用。LLM 可以分類情境或提出計畫;是否允許 transition,應由 deterministic policy、provider 契約、金額門檻與人工審批控制。
8. 這套方法只適用付款嗎?
不是。建立帳號、發優惠券、扣庫存、送通知、部署與刪除資源都有相同的「副作用已發生但回應遺失」窗口;每一種 action 都要另外定義查詢、成功證據與補償。
給新手的 5 個重點
- 看到 timeout,先寫
unknown,不要假設 failed。 - 同一 business intent 永遠導出同一把 key,並綁定 payload 指紋。
- 跨 provider 前先把 plan 與 operation 落盤;所有 state transition 追加到 ledger。
- 補償也是會失敗、會 timeout 的新 transaction,必須可 replay。
- 先跑 crash、重送、併發與 compensation fault injection,再談「production-ready」。
接著閱讀
左右滑動查看更多推薦
結語:先做一個「故意會壞」的 Agent
Agentic Transaction 的核心不是漂亮的縮寫,而是承認「我不知道」也是正式狀態。今天就拿一個會產生副作用的工具,加入 stable key、canonical payload 綁定、append-only event path 與 timeout-after-effect fault;只有在 replay 後仍能證明 charge = 1,才把下一個工具接上去。
想把這個練習延伸成完整 AI 專案,可從 AlphaLab 的 AI 課程與實作資源繼續;也可以回到 AI 專區挑選下一個 Agent 基礎題目。






