OOOMETA/OS
← 返回洞察

2026 年 11 月 · 5 分钟阅读

编排层能恢复状态,恢复不了已经发出去的那一笔

编排层能恢复状态,恢复不了已经发出去的那一笔

关键定义

幂等键 随一次外部动作一起发送的唯一标识。接收方按这个键去重,重放时返回第一次的结果,而不是再执行一次。它必须由写入双方共同承认,编排框架单方面无法提供。

检查点 编排运行时记录的执行位置与共享状态。它证明流程走到哪里,不证明外部系统执行过什么,两者之间有一段无法原子提交的窗口。

Outbox 先把打算做的外部动作写进本地表,再异步推送并回标记完成的写法。价值在于每次外部动作都先留下一行可核对的本地记录。

夜里两点四十,一个负责对账的智能体把一笔 4,800 元的退款提交给支付网关,拿到成功回执。它接下来要做的是把这次结果写进自己的数据库。进程在这两步之间被回收了。四分钟后调度器按重试策略把它唤醒,它从检查点里读到的最后一步是「提交退款」,于是它又提交了一次。

模型每一步都做对了,出错的是它脚下那套编排。检查点忠实地记录了它记得的东西,却记不了外面已经发生过的事。这个差别听上去像术语之争,落到账上就是同一笔退款付了两次,或者同一张订单下了两遍。

崩溃窗口只有两个位置,两个都漏

把外部调用和状态记录放在一起看,只有两种排法。先写检查点,再调用:崩溃发生在这两步之间,重放时系统认为没做,于是再做一次,外部写入重复。先调用,再写检查点:崩溃发生在调用返回之后、写入之前,重放时系统同样认为没做,结果一样。

中间没有第三种排法。本地的账和对方的账不能进同一个事务,因为事务的边界止于你自己的磁盘。两阶段提交要在对方也配合的时候才成立,而支付网关、邮件服务、ERP 不会因为你的编排框架支持 checkpoint 就额外开一个准备阶段。

所以 durable execution 能恢复的只有我们记得的那部分。外面发生过什么,只有对方系统知道。这句话接近常识,但 2026 年企业买编排层时问的问题几乎全在另一半:崩溃之后能不能续上、状态会不会丢、重试会不会无限跑。这三个问题都成立,也都能被框架解决,只是它们合起来并不等于「不会重复执行」。

还有一个细节容易被忽略:崩溃窗口里真正的风险不是崩溃本身,而是重试的节奏。为了尽快恢复,多数框架把第一次重试安排在几秒到几十秒之内,这正是对方系统还没来得及让你看到结果的时候。窗口越短,人越来不及发现,自动重放越容易补上一刀。

编排厂商把窗口写进了文档

Temporal 在十一月十日的文章里自己写出了这个窗口:如果一个进程在记账之后、发起付款之前崩溃,简单地重试就会变成重复扣款。他们的答案是工作流有可重复执行的历史,执行层负责重试和超时,状态存放在模型之外。这个设计确实解决了进程重启后的连续性问题,它没有解决的是那一笔已经发出去的调用。

LangGraph 这一类图运行时记录的是另一层东西:节点走到哪里、共享状态是什么、边上的条件有没有满足。checkpoint 保留执行状态,人工审批的挂起也依赖它。这些能力在调试长流程时非常有用,但它们记的都是图自己的位置,不是外部系统的账。Arize 的框架对比把这一层讲得很清楚:编排运行时提供的是图、事件、重试和检查点,而这恰好是状态层,不是副作用层。

两家产品的边界划在同一处,不是巧合。编排层能自负其责的只有它自己拥有的状态;外部写入的幂等性必须由写入的双方一起定义,一个框架没有办法单方面提供这个定义。

结构性的变化就在这里。过去两年,企业买编排买的是「复杂流程能不能跑通」;今年开始,买的是「出事之后能不能续上」。续上这个词被普遍理解成恢复状态,而买家的真实恐惧是重复副作用。两者被同一个词盖住了,验收清单跟着错位:大家开始问支不支持 checkpoint,而不是问每一个外部写入怎么幂等。

同一件事的三个变体

这个窗口不只出现在退款。发一封带附件的通知邮件,重放会发出两封,客户看到的是系统乱了。在工单系统里建一条记录,重放会建出两条,两条各自被人接手,最后合并的时候没人说得清哪条是真的。扣一次库存,重放会把仓位扣成负数,而负库存在报表上看起来像一次作弊。

三个变体的共同点是:它们都不报错。第一次成功,第二次也成功,日志两行都写着 done。系统内部完全自洽,只有外面多了一份东西。这也是为什么这类问题通常由客户投诉暴露,而不是由监控暴露,监控看的是调用是否成功,它看不见语义上的重复。

行业里对这件事有一个现成的名字,叫 outbox:先把打算做的动作写进本地一张表,再从这张表里异步地推给外部系统,推成功才标记完成。它的关键不在异步,在于每一次外部动作都先在本地的账上留下一条,崩溃时可以从这条记录出发去核对。这个模式在传统后端用了很多年,现在需要被重新讲一遍,因为编排层把大家的目光吸引到了图的那一侧。

把重复的量算成一个数

这件事值得算,因为它通常比想象的大,也比想象的好修。假设一个月有十万次带外部写入的步骤,退款、下单、开票、发信都算,落在「已发出未记录」这个窗口里的比例是千分之三。这个假设不是测量,读的人应该换成自己系统的数。

千分之三乘十万,是三百次重复。每次要人工对账、冲正、回访客户,二十分钟,按两百元一小时算,一个月两万元。这还没算客户看到两笔退款之后对这家公司的判断,那部分不进账,但会进下一年的续约率。

三百次这个量级有个讨厌的特性:足以让对账团队加班,小到不会被任何告警自动发现。每一次都没有报错,没有超时,没有 5xx,只有一笔多出来的记录躺在对方系统里,等着某个客户先看见。

修的成本在另一头。给一次外部写入加幂等键,通常是一次带唯一约束的数据库插入,几毫秒。判据可以写得很朴素:如果重复的年度成本超过幂等实现成本的十倍,就先做幂等,再谈换编排。反过来说,如果一个季度只有个位数的外部写入,或者每一次都有人工复核,那这套工程的收益不够,不做也合理。

我们自己的账本放在哪

我们内部的任务队列是一张状态机表:待办、处理中、已完成。领取是一次状态更新,完成是写结果再把状态改掉。这两步之间崩溃,会留下最难查的一种记录:活干完了,记录没有。第二天另一个会话看到它还是待办,会再干一遍。

我们真踩过并发这一类。两个会话同时读到同一批待办,各自开工,最后互相覆盖结果。当时的第一反应是加一句提示词,让智能体开工前先确认状态。那没有用,因为读和写之间的空隙不在模型里,在数据库里。真正的修法是让领取变成一次条件更新,只有把一条待办从「待办」改成「处理中」成功的那个人才有权开工,失败的人去取下一批。

外部动作的处理更靠后一层。现在的做法是先在本地记一条「打算做」,带上幂等键和目标系统;再执行调用;成功之后记「做完了」。中间崩溃时,留在「打算做」的记录会被一个扫描任务捞出来,去对方系统核对到底做没做。这不是聪明的设计,只是把一件事分成三次写入,让每一次崩溃都留下一段可以追的痕迹。

说清局限。这是一家公司内部的任务队列,不是银行核心账务,规模小,并发低,最长的一次重复事故只多写了三条记录。相同的是机制:外部写入不能和本地记录原子提交,所以必须有一个中间态是可查的。规模变大只是让窗口里的次数变多,不会改变窗口本身。

还有一层是我们没做好的。中间态目前只存在一张表里,没有按时间窗对外暴露的查询接口,核对要靠人写 SQL。这意味着它对写的人可用,对值班的人不可用。一个只有作者会用的对账通道,在夜里三点等于不存在。

采购时该问的三个问题

第一个问题:每个外部写入有没有稳定的幂等键,键按什么规则生成,存在哪里。如果答案是「我们的框架会自动重试」,这个答案不成立,因为自动重试正是重复的来源。要注意区分「重试策略可配置」和「重复执行可避免」,前者的参数是次数和间隔,后者需要的是接收方认得你。

第二个问题:崩溃之后重放时,谁去对方系统确认那笔操作到底执行了没有。有没有一条对账通道,能不能按时间窗拉出「打算做但结果不确定」的记录。这个问题最能区分两类供应商:一种把账本放进自己的运行时,一种把账本交给客户的数据库。

第三个问题:编排层会不会把中间态暴露给运维。不是暴露在框架自己的控制台里,而是落在客户能查询的表或者事件流里。做不到这一条的产品,出事之后你只能读日志,而日志在崩溃窗口里通常不完整,缺的恰好是那一步。

三个问题都答不上来,不代表不能买,只代表这套系统在第一次重复事故之前是黑盒。可以先买,但要把重复事故的检测放在上线之前,而不是放在事故之后。

我们的判断

未来一年编排层的竞争重点,会从「支持多少种节点、多少种模型」移到「账本放在哪一侧」。账本放在框架里的产品,客户换框架时会把对账能力一起丢掉;账本放在客户数据库里的产品,切换成本低,但要求客户自己想清楚表和状态机怎么设计。这是一次明确的取舍,没有免费的中间路线。

这个判断有一个可以检验的推论。如果一家企业换了编排框架之后,重复副作用的事故率没有变化,说明它的幂等做在应用层,这是好消息;如果事故率跟着框架一起变,说明账本在框架里,它已经被锁定了,而且自己不知道。

再往前看一步。智能体的可靠性正在被拆成两类问题:判断对不对,属于模型;动作重复不重复,属于分布式系统。第一类靠评测和上下文改善,第二类靠幂等和对账。把两类混在一起谈,会得出一个很常见的错误结论,认为模型再强一点,重复的问题会自己消失。不会,因为重复不是推理错误,是提交时机的问题,跟模型能力无关。

这也是我们不太相信「编排框架选对了就安全了」这句话的原因。框架能帮你把状态存好,能帮你把重试跑完,能让一次长流程在三天后接着走。这些都真实有用。但它替你决定不了那笔钱该不该再发一次,因为那件事只有你和收钱的一方说得清。

三个可以今天就做的动作

第一,数出上个月带外部写入的步骤有多少次,其中多少次带幂等键。这个数不需要新平台,调用日志按工具名分组就有。比例低于五成的话,先不谈编排升级。

第二,找一次真实的重复事故,量出「从发出调用到写入记录」这段窗口有多长。它通常只有几百毫秒,但一个月被踩中的次数比谁估计的都多,因为踩中时不报错。

第三,把这个窗口写进一个每天跑的扫描任务,而不是写进操作规程。规程靠人遵守,扫描任务靠时间跑。扫描的判据很简单:有没有一条记录停在中间态超过十分钟,超过就有人去看。

留给做架构决定的人一个问题:你的智能体崩溃之后,最先恢复的是它的记忆,还是它的账。这两件事如果只有一件被设计过,另一件会在某个夜里自己找上门。

来源

外部资料来自下列公开页面;内部数字来自我们自己的任务队列与调用记录。

Temporal,Who needs workflows when you have agents(2026 年 11 月 10 日,Tao Guo;记账之后崩溃、重试变成重复扣款的窗口描述,以及「状态存放在模型之外」的设计主张)

Arize,AI agent frameworks compared(编排运行时与检查点所记录内容的层次说明:图、事件、分支、重试、检查点)

常见问题

为什么检查点不能防止重复付款+

检查点记录的是流程位置和本地状态,外部写入与本地记录无法进同一个事务。无论先写检查点再调用还是先调用再写检查点,都存在崩溃后重放导致的重复。

编排框架能解决哪些可靠性问题+

进程崩溃后的状态恢复、重试与超时、长流程挂起与续跑、执行历史可审计。它解决不了外部写入的幂等,那需要接收方认得同一笔请求。

重复的量级怎么估+

用带外部写入的步骤数乘落在已发出未记录窗口的比例。举例十万次乘千分之三等于三百次重复,每次二十分钟对账,一个月两万元。比例应替换为自己系统的实测值。

什么时候不值得做幂等+

外部写入极少、每季度个位数,或者每一次都有人工复核,这类场景做幂等的收益覆盖不了成本。判据是年度重复成本是否超过实现成本的十倍。

采购编排层该问什么+

问每个外部写入的幂等键规则与存放位置,问崩溃后由谁去对方系统核对,问中间态是否暴露在客户可查询的表或事件流里。

你们自己的队列怎么处理+

领取改成一次条件更新,只有把待办改成处理中成功的人才有权开工;外部动作分三次写入,先记打算做,再执行,再记做完了,中间态由扫描任务捞出来对账。

拿你自己的执行日志跑一遍

交一份脱敏的近 30 天 agent 执行日志,48 小时内拿到八行表:八项技术项每项一行,每格带分子、分母和判据,缺口按是否要改 schema 排序。前三单 ¥8,000。

看八项字段检查 →