多智能体编排的长事务翻车实录:重试风暴、重复扣款与补偿失效怎么治
Demo 阶段的多智能体系统都很美好:规划智能体拆任务、执行智能体调接口、审计智能体写日志,一条链跑通,截图发群里满堂喝彩。上了生产环境,第一个月我们被三类问题按在地上摩擦:重试风暴把下游 QPS 打满三倍、超时重试导致同一笔采购申请重复提交两次、补偿事务自己先挂了留下半截脏数据。
这篇文章复盘这些坑,以及在优尼沃OS 的智能体编排内核里最终定型的一套工程做法。不讲愿景,只讲幂等键、去重表和补偿日志这三件具体的东西。
一、先看一次真实的连锁故障
业务场景是企业内部的采购申请自动化:编排流程包含五个节点------申请受理智能体、预算核验智能体、供应商比价智能体、ERP 下单接口(非智能体,普通 HTTP 服务)、通知智能体。整条流程在编排引擎里定义成有向图,每个节点失败按指数退避重试,最多 3 次。
出事那天 ERP 下单接口响应从平时 200 毫秒劣化到 9 秒。编排引擎在第 5 秒触发客户端读超时,重试;第二次请求在第 8 秒成功创建了采购单并返回,但响应包在网关被丢弃,引擎仍判定失败,重试第三次。ERP 侧没有幂等保护,三张采购单全部落库。与此同时,上游 40 个并发流程都在做同样的事,重试请求把 ERP 连接池占满,正常业务也跟着 502。
事后复盘,根因不是某一个 bug,而是四个设计假设同时破产:假设超时等于失败、假设下游天然幂等、假设重试只发生在本节点、假设补偿一定能跑成功。
二、第一道防线:业务幂等键穿透整条流程
治重复执行,唯一可靠的办法是让"同一笔业务"在每一个节点都可识别。我们的做法是为每一条编排流程生成一个业务幂等键,结构如下:
text
idem_key = sha256(tenant_id + business_type + biz_no + node_version)
其中 biz_no 是业务天然唯一号(采购申请单号),node_version 在节点定义变更时递增,避免旧流程缓存误吞新逻辑的请求。
这个键随调用上下文一路透传:智能体之间通过消息体传递,调外部 HTTP 接口时放在约定请求头 X-Idem-Key 里。下游 ERP 这类老系统不支持幂等头怎么办?在适配层兜底:维护一张去重表。
sql
CREATE TABLE idem_record (
idem_key CHAR(64) PRIMARY KEY,
node_name VARCHAR(64) NOT NULL,
status TINYINT NOT NULL, -- 0 执行中 1 成功 2 失败
response_ref VARCHAR(128), -- 成功结果的回放引用
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_key_node (idem_key, node_name)
) ENGINE=InnoDB;
```
节点执行前先 INSERT 一条 status=0 的记录,唯一键冲突说明已有同名调用:若状态是执行中,直接返回"处理中"而不是再打一遍下游;若已成功,回放 response_ref 指向的原始响应。一个容易漏掉的细节:状态卡在"执行中"的记录必须有租约超时回收,进程被 kill -9 后靠后台扫描器按 updated_at 超过阈值重新置为可执行,否则一次宕机留下永久死信。
## 三、第二道防线:把超时和失败拆开
"读超时不等于下游没执行",这句话应该刻在每个编排引擎的代码里。我们把节点的执行结果改成三态:成功、确定失败、结果未知。只有确定失败(连接被拒、4xx 业务拒绝)才允许立即重试;结果未知(读超时、502、断连)进入核查流程------先用幂等键查去重表或反查下游的单据查询接口,确认上一次到底执行成功没有,再决定重试还是补偿。
重试还必须有全局预算,而不是每个节点各自数 3 次。流程级令牌桶限制整条业务在 30 分钟窗口内最多产生 N 次外部调用,预算耗尽直接熔断并转人工。这一条上线后,下游接口收到的极限流量从理论上的正常量 27 倍(9 个节点各 3 次叠加并发)被压回 1.5 倍以内。
## 四、第三道防线:补偿不是反向调用,是一条独立事务链
早期我们把补偿写成 catch 块里的几个 HTTP 调用,事实证明这是最脆弱的部分:进程重启、补偿接口也超时、补偿执行到一半断电,任何一种都会让数据停在中间态。
定型后的方案是 WAL 思路的补偿日志。每个节点执行前先写一条意图记录(要对哪个系统、做什么动作、逆动作是什么),动作成功后更新记录状态;编排失败时,补偿器只认日志,按完成顺序逆序执行逆动作。补偿器是独立部署的轮询进程,与主流程不共享生命周期,主引擎宕机它照样扫表。每条补偿同样带幂等键,天然支持安全重放。
```text
正向: 预算冻结(可逆:解冻) -> ERP下单(可逆:整单作废) -> 通知(可逆:无, 标记撤回)
补偿: 从最后一条 SUCCESS 记录逆序执行,UNKNOWN 先核查再决定方向
```
通知这类不可逆动作不做假补偿,只记录"需人工知悉"状态,由值班台每日清表。承认有些动作技术上无法回滚,比假装一切都能自动恢复要诚实,也更安全。
## 五、给同类团队的加固顺序
如果你们正在把多个智能体串成业务链,建议按这个顺序加固,别等故障驱动:第一步把幂等键在整条流程上打通透传;第二步给所有非幂等下游加适配层去重表;第三步把节点结果改成三态并加流程级重试预算;第四步补偿日志独立成表、补偿器独立进程。四步做完,智能体在编排流程里扮演的才是"会做决策的节点",而不是"会随机重复下单的调用方"。
优尼沃OS 在私有化交付时,这一套随编排内核一起下发,去重表、补偿日志与企业原有 MySQL 或国产数据库适配,全部数据留在企业内网。我们自己踩了半年的坑,欢迎在评论区聊聊你们在多智能体长事务里遇到的具体事故形态。