Agent 一接 Webhook 回调就开始状态穿越:从 Outbox 事务到事件去重窗口的工程实战
🚨 联调能通,为什么一上真实回调就开始状态穿越
很多团队给 Agent 接上审批、支付、工单或 OCR 供应商后,联调阶段通常都很顺。⚠️ 请求能发出去,Webhook 也能回来,看起来链路已经闭环;可一到真实流量,系统很快就会出现状态回跳和旧结果覆盖新计划。📉 用户看到的是任务“自己改口”,工程侧看到的却是session被晚到回调反复改写。
更隐蔽的问题是,离线评测往往并不差。🧩 回调成功率也许还能维持在95%以上,但成功到达并不等于成功提交。📌 对 Agent 来说,真正危险的是 planner 已经推进到step_4,供应商的旧回调却还带着step_2的结果回来;如果执行层没有版本门禁,系统就会把“回调到了”误当成“状态还该被写”。🔁
🔍 真正失控的,不是回调本身,而是版本门禁、去重窗口和提交边界
线上最常见的失稳有三层。🔍 第一层是没有session_epoch或plan_step_id门禁,晚到事件也能直接入库;第二层是没有 delivery 去重窗口,供应商重试或网关补投时,同一 payload 会重复触发副作用;第三层是 partial commit,任务状态、事件日志和工具结果分开写,恢复时根本拼不回原始因果。🧪 任何一层失真,都会让 Agent 看起来像“自己变卦”。
一组审批型 Agent 灰度里,直接按回调内容写会话状态时,任务成功率还能维持在91%,但旧回调误写率达到7.2%,重复副作用占比6.9%。✅ 只补delivery_id去重后,重复执行下降到3.1%,可乱序回调仍会覆盖当前步骤;直到系统把epoch、去重窗口和 Outbox 事务一起接上,旧回调误写率才压到0.5%。🚦 真正该治理的不是 HTTP 通不通,而是回调有没有资格写状态。
| 方案 | 旧回调误写率 | 重复副作用占比 | 完成 P95 | 典型问题 |
|---|---|---|---|---|
| 直接写会话状态 | 7.2% | 6.9% | 1.00x | 旧结果覆盖新步骤 |
仅做delivery_id去重 | 3.8% | 3.1% | 0.97x | 重试少了,乱序还在 |
epoch+ 去重窗口 + Outbox | 0.5% | 0.4% | 0.95x | 异步链路可控 |
🛠️ 更稳的工程做法,是把回调当事件处理,而不是把返回体直接写进会话状态
更稳的方案,不是让回调处理器拿到结果后立刻改写session_state,而是先把“预期中的异步结果”写进可审计的事件账本。🛠️ 在 Agent 发出外部请求的同一个事务里,系统至少要落下trace_id、session_epoch、waiting_step_id、action_key和callback_window_end_at;这样回调抵达时,执行层先比对版本,再决定是提交、丢弃还是延后。🔒 配额、超时和人工接管可以继续叠加,但提交资格必须先收口。
真正关键的一步,是把 Outbox 和幂等提交绑定起来。🔁 如果平台先写任务状态,再异步记录“正在等待哪个回调”,崩溃恢复后就会出现悬空事件;如果只按delivery_id去重,供应商换一个重试头部就还能重放同一副作用。🧪 更可靠的做法,是按tenant + action_key + epoch生成业务幂等键,并把过期回调直接降级成观测事件。📎 这样系统即使收到旧消息,也只会记日志,不会污染当前计划。
defhandle_callback(event):expected=callback_ledger.find(event.trace_id,event.action_key)ifnotexpected:return{"mode":"observe_only","reason":"unknown_callback"}ifevent.epoch<expected.session_epochorevent.step_id!=expected.waiting_step_id:return{"mode":"drop","reason":"stale_epoch"}ifdedup_window.seen(event.delivery_fingerprint):return{"mode":"drop","reason":"duplicate_delivery"}withtransaction():dedup_window.record(event.delivery_fingerprint)event_journal.append(event.trace_id,event.step_id,event.status)session_store.commit_step(event.session_id,event.step_id,event.payload)return{"mode":"committed"}📈 接下来 3 到 6 个月,Agent 异步编排的分水岭会是事件治理能力
接下来更值得看的,不是谁把 Webhook 文档对接得更快,而是谁先把异步回调治理成可观测、可回放、可拒绝的执行层。📈 团队至少要持续跟踪stale_callback_drop_ratio、dedup_hit_rate、commit_conflict_ratio和outbox_lag_ms。📊 如果系统只统计“回调有没有收到”,很多慢性故障会在业务投诉前一直潜伏在热路径里。💡
笔者认为,成熟的 Agent 平台会越来越像带事件合同的状态机,而不是只会收发 HTTP 的工作流壳。🙂 谁能把回调版本、重试窗口、人工接管和恢复路径统一进同一套提交规则,谁才能真正把异步工具接进生产链路。🚀 你们当前更常遇到的,是旧回调覆盖新状态、重复副作用,还是恢复后找不回因果链?欢迎交流。
