Appearance
第16章 当路线不止一条——Workflow & Graph
小a最近同时接触了三套东西:自己在写的 Agent 循环,同事负责的“审批流程”,还有一篇讲“图编排”的文章。他有点慌,端着咖啡去找老z。
“我是不是得在这三种里选一个,选错了就完了?”他问。
“先别急。检索解决‘带什么资料’,不回答‘由谁决定下一步’。”老z说,“你看到的三样东西——循环、工作流和图——不是竞争的三选一,而是控制权与状态的不同安排。搞清楚它们各管什么,你自然知道怎么用。”
本章比较 Loop、Workflow 与 Graph;不绑定某个框架,也不把图形表示误当成可靠性保证。
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| Agent 循环 | Agent Loop | 模型输出与工具结果驱动的受限迭代 | 无边界的自主执行 |
| 工作流/图 | Workflow/Graph | 固定步骤/节点边和路由表达 | 彼此严格互斥 |
| 状态机 | state machine | 状态、事件与转移条件的明确模型 | 只画流程图 |
| 检查点/重放/幂等 | checkpoint/replay/idempotency | 保存恢复点、再次执行、避免重复副作用的契约 | 自动事务保证 |
控制权的三种安排
“那它们到底谁决定下一步?”小a问。
“看这张表,问题就清楚了——关键是‘下一步由谁决定’:”
| 形式 | 下一步主要由谁决定 | 适合的边界 |
|---|---|---|
| Loop | 运行时根据模型输出与停止条件迭代 | 探索性任务,必须有预算和中止 |
| Workflow | 开发者预先规定步骤 | 审批、ETL、可预测的业务流程 |
| Graph | 节点与边规定可达路线,条件决定分支 | 多路径、可恢复的状态流程 |
“那三种形式的停止条件,分别是谁负责?”小a追问。
“这一点最容易被忽略。”老z说,“Loop 的停止条件必须由宿主显式给出——轮数、token、费用或超时,模型不负责刹停;Workflow 的结束是定义好的末节点,走到就停;Graph 的停止由无出边或满足终止条件决定。任何形式都必须能回答‘什么时候停、停下来时留下什么状态’,否则就谈不上恢复和审计。”
“那 Graph 是不是一定比 Workflow 高级?”小a问。
“不是。Graph 不必包含模型;Workflow 也可以包含分支。”老z说,“这里的区别是表达方式与状态管理,不是严格互斥的分类。Workflow 是顺序展开的步骤,Graph 把分支显式画成节点和边;同一个业务逻辑,两者都能表达,只是改一个分支时,Workflow 改步骤定义、Graph 改路由条件。”
“那我一开始选错形式,后面能换吗?”小a问。
“能,但代价不同。”老z说,“从 Workflow 改成 Graph,通常只要把固定步骤重写成节点和转移;从 Graph 改成 Loop,等于把路由权从代码交给模型——这意味着要重新设计停止条件和护栏。**换形式的成本,本质是换‘路由权在谁手里’的成本。**所以开工前先问:这个任务的路径到底有多固定?”
“那怎么判断一个任务的路径有多固定?”小a追问。
“看能不能预先穷举分支。”老z说,“如果任务的每种可能走向都能写清楚——‘失败就重试,缺资料就请求补充,完成就结束’——路径就是固定的,用 Workflow 表达;如果分支数量大、且条件本身要运行时才知道,路径就不固定,才轮到 Graph 或 Loop。判断标准不是任务复不复杂,而是分支可不可预知。”
“能不能给个对照?”
“可以,用几个常见任务对照:”
| 任务 | 分支可预知? | 合适的形式 |
|---|---|---|
| 表单审批(通过/退回/拒绝) | 是 | Workflow |
| 发布流程(检查→审批→部署→验证) | 基本是 | Workflow / Graph |
| 客服工单(按类型分流,再按回复效果跟进) | 部分 | Graph + 受限 Loop |
| 新功能探索(方案生成→验证→迭代) | 否 | Loop + 人工确认 |
“这个对照说明,同一个人可以同时用三种形式?”小a问。
“对。同一个团队的不同任务,通常就用着三种形式。”老z说,“关键不是给每个任务贴一个‘形式标签’,而是清楚每个任务的路由权在谁手里——这是比形式名字更本质的判断。”
比喻:路线图
Workflow 是固定班次的地铁,Graph 是能换乘的线路图,Loop 是没固定路线的出租车——没有谁更好,只看你要去哪。
三种形式的工程特征
“除了‘谁决定下一步’,三者在工程上还有什么实质差别?”小a问。
“有,而且这些差别常常被‘形式选择’掩盖。”老z说,“从可观测性、成本、失败恢复三个角度看,三者差别很大:”
| 维度 | Loop | Workflow | Graph |
|---|---|---|---|
| 可观测性 | 每轮事件可记录,但下一步不确定 | 步骤预先可知,进度一目了然 | 节点与边可见,当前在哪可查 |
| 成本 | 每次迭代都产生模型调用成本 | 固定步骤,成本可预估 | 分支越多,测试组合越多 |
| 失败恢复 | 从停止原因恢复,可能丢中间状态 | 从失败步骤重放,需幂等 | 从 checkpoint 恢复,路径可重建 |
| 测试 | 需要大量边界输入样本 | 每条路径单独测试 | 每对节点转移单独测试 |
| 调试 | 问题可能藏在模型决策里 | 问题定位到具体步骤 | 问题定位到具体节点 |
“那这三者的测试重点也不一样?”小a追问。
“完全不一样。”老z说,“Loop 的测试重点是停止条件和边界输入——它可能在模型决策的任何一步出错,你要用样本覆盖‘该停的时候停没停’;Workflow 的测试重点是每条固定路径——输入 A 走 1→2→3,输入 B 走 1→2→4,路径错了就是定义错了;Graph 的测试重点是转移条件——同一个节点,条件满足该走哪条边,条件不满足又该走哪条。测试组合的数量,也是选择形式的一个隐性成本。”
“那有没有‘先按一种形式做,后期再切换’的常见路径?”
“有,而且很常见。”老z说,“大多数系统从 Workflow 起步,因为路径固定、可测试、成本可控;当任务复杂度上升、分支难以穷举时,才把局部改成 Graph 或 Loop。切换的方向通常是‘确定性 → 灵活性’,而不是反过来——因为确定性部分省下的成本和可审计性是宝贵资产,轻易不该丢掉。”
状态、路由与恢复
“那不管画成什么,工程上要关心什么?”小a问。
“无论画成什么形状,工程上需要回答同样的问题:当前状态是什么、谁能改它、分支条件如何得到、失败后从哪里恢复、动作是否幂等。”老z画了个简化图式:
text
接收请求 → 校验 → [资料充分?] ─否→ 请求补充 → 校验
│是
↓
执行任务 → 记录结果“这五个问题,为什么是‘状态’的问题而不是‘图形’的问题?”小a问。
“因为图是给人看的,状态是给程序用的。”老z说,“同一个流程,画成什么样都可以,但状态必须落在某个可持久化的地方——数据库、文件或内存对象——否则进程一重启,流程就不知道自己走到哪了。判断一个编排方案是否可靠,不是看它的图画得对不对,而是看五个问题的答案是否都有落点:状态存哪、谁有权改、条件由谁计算、从哪个点恢复、重跑会不会重复生效。”
“那这五个问题,分别对应什么工程部件?”小a追问。
“一一对应。”老z说,
| 状态问题 | 对应的工程部件 | 没实现时的表现 |
|---|---|---|
| 状态在哪 | 持久化存储(库/文件/内存) | 进程重启后流程丢失 |
| 谁能改 | 写入权限控制 | 多个进程并发改同一状态 |
| 条件怎么来 | 分支判断逻辑(规则/模型/外部输入) | 分支走错且无法追溯 |
| 从哪恢复 | checkpoint + 恢复入口 | 失败后只能重头再来 |
| 重跑是否幂等 | 幂等键/去重逻辑 | 重放后副作用重复 |
“这套对应关系,是检查一个编排实现是否完整的清单。”老z说,“五个问题各有一个答案,实现才算闭环。任何一个问题答不上来,那个位置就是故障点——不是‘可能出错’,而是‘出错时你没有处理它的手段’。”
“那‘执行任务’挂了怎么办?”小a问。
“若‘执行任务’会写入外部系统,恢复时不能简单重跑;应记录幂等键或采用补偿动作。”老z说,“图把路线画出来不会自动解决重复写入、竞争更新或模型分类错误。示意: 一个节点做了‘创建订单’并写入外部系统,但进程在写回本地的 checkpoint 之前崩溃。重放时若没有幂等键,就会创建第二张订单;有幂等键,外部系统能认出这是同一次调用。”
“那补偿动作是什么?”小a问。
“补偿是把已发生的副作用‘还回去’的动作——创建了订单就发取消,发了邮件就撤回通知。补偿并不等于回滚:外部系统可能已经执行、可能执行了一半、也可能根本没收到,补偿只能尽量收敛,不能保证回到执行前的样子。所以设计顺序永远是:先定义幂等,再谈补偿。”
“是不是不确定性越高,就一定要用 Loop?”小a问。
“也不一定。可把不确定部分限制在一个节点中,外层仍用确定的工作流保护审批、权限和交付。”老z说,“选择依据是可预测性、失败代价与可观测性,而不是‘越智能越好’。一个失败代价极高的固定流程,即使里面有模型参与,也宁可把模型决策关在一个有预算的节点里,而不是让整条流程都变成自由发挥。”
“那一个已经跑起来的系统,想往更灵活的方向演化,怎么动最安全?”小a追问。
“从‘局部替换’开始。”老z说,“不推翻整个 Workflow,而是挑一个分支点,把它从固定判断换成受预算限制的模型判断——比如原来‘异常类型固定分三类’,改成‘先用模型判断异常类型,再走原分支’。一次只换一个点,其余结构保持不动,替换点要有独立的预算、停止条件和回退路径。”
| 演化动作 | 风险 | 降险手段 |
|---|---|---|
| Workflow 的某步换成模型判断 | 模型判断不稳定 | 限定输入输出、给预算、设回退 |
| 固定分支改成 Graph 路由 | 路由条件测不全 | 先保留原分支作为 fallback |
| 某节点内部允许自由发挥 | 节点失控 | 限制轮数、强停止、人工确认兜底 |
| 整条流程换成 Loop | 失去可审计性 | 极少采用,一般只局部换 |
“那反过来,模型判断太多想收回来呢?”
“反向演化更简单。”老z说,“把模型判断的结果先经过一道规则校验,不合格就退回固定分支;等规则足够覆盖时,再彻底移除模型判断。从灵活到确定,是靠‘规则逐步取代模型’完成的——每步都要有数据证明规则覆盖了原模型的判断质量,而不是凭感觉砍掉模型。”
要点
编排的可靠性来自五个状态问题的答案,不来自流程图本身:状态在哪、谁能改、条件怎么来、从哪恢复、重跑是否幂等。
混合编排
“那我可以混着用吗?”小a问。
“可以,而且常见。常见的组合是确定的路由先选择任务类型,再把开放式分析交给有限轮次的 Loop;或在图节点中调用检索、工具和模型。”老z说,“每层都应有自己的输入输出 schema、超时、预算和审计事件。把一个大型 Loop 伪装成‘节点’而不暴露这些边界,只会让恢复与测试更困难。”
“那混合编排和‘没有编排’怎么区分?”小a追问。
“看有没有边界。”老z说,“混合编排的每一段都有明确的输入输出、预算和停止条件——它混的是形式,不混的是边界;‘没有编排’是整条流程糊在一起,既没有段落也没有边界。**判断标准很简单:能不能指着一段说‘这段最多跑 5 轮、这段有人工确认、这段走固定分支’。**能,是混合编排;不能,是混乱。”
| 特征 | 混合编排 | 混乱编排 |
|---|---|---|
| 段落边界 | 每段有输入输出契约 | 各段职责模糊 |
| 预算 | 每段有独立轮数/token 限制 | 全程无预算 |
| 停止条件 | 每段有明确停止原因 | 依赖模型自觉 |
| 审计 | 每段有事件日志 | 只有最终结果 |
| 恢复 | 每段可从 checkpoint 恢复 | 断了就重头再来 |
“那混合编排的维护成本,是不是比单一形式高?”
“高,但高在有回报的地方。”老z说,“你要维护的不再是一种形式的逻辑,而是多段的契约、切换点和各自的恢复策略。代价是真实存在的;回报是每段都用最合适的形式表达,且每段都能独立测试。如果一段的复杂度不值得单独维护,就不要为它单开一种形式——多数系统只要一两种形式就够了,第三种形式是用复杂度和成本换来的。”
“能给我一个看得见的混合结构吗?”小a问。
“可以,以下是一个简化示意:”
text
入口路由(确定)─── 任务类型 A → 固定审批流程(Workflow)
└── 任务类型 B → 受限调查节点(Loop,最多 5 轮)
↓
图编排:按调查结果选择分支
↓
人工确认(必须)“这个结构里,每种形式各管一段。”老z说,“入口路由和审批是确定的部分,负责授权与交付;调查节点是受限的 Loop,负责开放性分析;图负责在调查结果出来后选择下一步。每一段的边界都明确:Loop 有轮数上限,Workflow 有人工关卡,图有确定的分支条件。”
“那为什么不能把整个任务都交给 Loop?”
“因为代价不同。”老z说,“Loop 每多一轮,就多一次模型调用、多一点上下文成本、多一点行为不可预测性。把确定的部分用确定的形式表达,成本低、可审计;只把真正需要开放判断的部分交给模型,才是把预算花在刀刃上。混合编排的本质,是给‘不确定性’画一个有限的盒子。”
“那混合编排的边界,应该画在哪?”小a追问。
“画在‘失败代价会不会被放大’的地方。”老z说,“审批、付款、权限变更这类动作,失败代价高且不可逆,倾向放进确定的工作流,用人工关卡和审计兜底;开放式分析、信息收集、方案生成这类动作,失败代价低且可重试,才适合放进受限的 Loop。判断的准绳不是‘这里要不要用模型’,而是‘这里的失败会造成多大损失、能不能重来’。”
“那同一个任务里,确定部分和开放部分的先后顺序有讲究吗?”
“有,而且通常有固定的形状。”老z说,“常见的形状是外层确定、内层开放:入口用确定路由分流,中间用受限 Loop 分析,出口再回到确定流程收尾(审批、落库)。倒过来——外层开放、内层确定——很少见,因为那等于让模型决定整个流程的骨架,确定性部分反而成了模型的执行细节。”
恢复点必须属于状态,而非图形
“为什么说恢复点属于状态,不属于图形?”小a问。
“每个节点应定义输入、输出、提交点和幂等策略。”老z说,“示意: ‘生成草稿’可安全重跑,‘提交报销’须先查询幂等键或转入人工处理。Loop、Workflow 与 Graph 可以混合:确定工作流负责授权和交付,图负责分支,受轮次限制的 Loop 只处理开放式分析。这样牺牲了少量灵活性,换来可恢复和可测试的控制面。”
“那一个节点的定义,到底该写全哪些东西?”
“可以按四件事检查。”老z说,
节点的四份定义
- 输入:进入节点需要哪些数据,缺了会怎样
- 输出:节点成功时产出的结果,供下游消费
- 提交点:副作用落地的位置,提交前可重跑,提交后要幂等
- 幂等策略:重跑一次与跑一次的差别,靠幂等键、查询外部状态还是转人工
“最常见的错误,是把恢复点画在图里、却只落在脑子里。”老z说,“图上有‘已提交’,程序里却没有对应的字段,进程一重启就等于没提交。恢复点必须能从一个可持久化的状态里读出来——它属于状态,不属于那张图。”
把恢复设计在副作用之前
“那三种形式有没有一个统一的看法?”小a问。
“三种形式都可落到状态机:状态、事件、转移条件和可持久化 checkpoint。”老z说,“Workflow 通常把转移写成顺序步骤;Graph 显式记录节点和边;Loop 则把‘下一步’部分委托给模型,但仍需要轮次预算和停止状态。差异在谁拥有路由权,而不是是否能画成图。”
“那状态机的状态和转移,具体怎么定义?”小a问。
“状态是‘流程现在在哪’,转移是‘发生什么事件、满足什么条件,才从这里走到那里’。”老z说,“拿报销流程举例,一个最小状态机长这样:”
text
draft ──(提交)→ submitted ──(审批通过)→ approved ──(打款)→ paid
│ │
└──(退回)──→ draft └──(拒绝)→ rejected| 状态 | 含义 | 允许的转移 | 转移触发的事件 |
|---|---|---|---|
| draft | 草稿,可编辑 | → submitted | 用户提交 |
| submitted | 已提交,等待审批 | → draft / approved | 退回 / 审批通过 |
| approved | 已批准 | → paid / rejected | 打款 / 审批拒绝 |
| paid | 已打款,终态 | 无 | — |
| rejected | 已拒绝,终态 | 无 | — |
“定义状态机的价值是非法转移变得可见。”老z说,“比如‘已打款’的工单不该能回到‘草稿’——状态机里没有这条转移,代码想让它回去都回不去。没有状态机时,这类非法状态靠开发者自觉避免,一旦漏掉,就会在数据里留下‘已打款却还在审批’的矛盾记录。”
“那状态机和图的差别是什么?”小a问。
“状态机强调‘状态与转移的合法性’,图强调‘节点与路径的表达力’。”老z说,“状态机关心‘哪些转移是允许的’,图关心‘哪些路径是可达的’。同一个业务,两种视角可以同时存在——用图表达路由结构,用状态机约束转移合法性。它们不是替代关系,是互补的两个检查面。”
“能串个例子吗?”小a问。
“贯穿场景是创建发布工单。可先 checkpoint ‘验证完成’,再执行一次带幂等键的外部创建;重放时读取 checkpoint 和键,不能盲目再次创建。”老z说,“失败路径一是写入后进程崩溃,恢复需要查询外部状态;二是模型路由错误,Graph 应送到人工或安全终点;三是 checkpoint 未写入,必须把该风险与副作用顺序一并测试。混合编排可把固定审批包在外层,把受限调查 Loop 放在节点内;不能因节点使用模型就放弃重放、审计和恢复契约。”
“那这几种失败,各自怎么恢复?”小a追问。
“按副作用是否已经落地来分。”老z说,
| 失败位置 | 副作用状态 | 恢复方式 | 反例 |
|---|---|---|---|
| checkpoint 写入前 | 无副作用 | 重跑当前节点 | 若节点本身会写外部,仍需幂等 |
| checkpoint 写入后 | 副作用已落地 | 从 checkpoint 重放,靠幂等键去重 | 无幂等键 → 重复写入 |
| 外部系统已执行 | 不可见 | 查询外部状态、用幂等键、或转人工 | 盲目重发 → 重复订单 |
| 模型路由错误 | 走错分支 | 送到人工或安全终点 | 让模型自己纠正 → 可能再次走错 |
“这张表的反例列很关键。”老z说,“每一种恢复方式都有一个‘想当然会出事’的版本——以为重放安全就重放、以为重发没事就重发。恢复设计的本质,是先把‘会怎么出事’列出来,再决定恢复方式,而不是先写恢复代码再祈祷不出事。”
图的绘制与实现的落差
“我注意到一个问题:很多流程图画得很好看,代码却跟图对不上。”小a说。
“这是 Workflow 和 Graph 最容易踩的坑——图是给人看的,实现是给机器跑的,两者之间没有自动的一致性保证。”老z说,“画图时,你会自然省略错误处理、超时、幂等这些细节——因为图表达的是‘主路径’。但实现不能省略:每个节点都要处理失败,每条边都要有失败时的去向。”
“那图的作用到底是什么?”
“作为沟通与设计的起点,不是作为验收的终点。”老z说,“图的价值在于:让参与讨论的人看到‘主路径长什么样、谁决定下一步、哪里有人工关卡’。但图的正确性不代表实现的正确性——实现是否与图一致,要靠测试来核对,不能靠‘图是这么画的’来证明。”
| 图画层 | 实现层 | 常见落差 |
|---|---|---|
| 节点 | 函数/服务/子流程 | 节点里实际做的事比图上的多(隐藏副作用) |
| 边 | 函数调用/消息/路由 | 图上一条边,代码里可能有重试、降级、超时 |
| 分支条件 | if/switch/路由规则 | 图上的菱形,实现里可能有多层嵌套判断 |
| 终止 | 返回/退出/异常 | 图上没画的异常路径,实现里必须画 |
“那怎么缩小这个落差?”
“两个手段。”老z说,“一是在图上标注非主路径——用虚线或注解标出超时、失败、人工介入的走向,让讨论时就能看到边界;二是用实现反向核对图——代码写完,把每个节点的实际行为回填到图上,看图上的承诺和代码的实现是否一致。落差不可消除,但可以显式化——显式化的落差是已知边界,藏起来的落差是定时炸弹。”
“那模型参与的节点,图能表达吗?”
“能表达‘这个节点会调用模型’,但表达不了‘模型会怎么决策’。”老z说,“图只能画‘模型节点的输入输出和停止条件’,画不出模型的决策内容。**所以模型节点的图标注要特别小心:它画的是边界,不是行为。**边界是预算、停止条件、输入输出契约;行为是模型在边界内自己决定的,画不出来也不该假装画得出来。”
常见编排反例
“那有没有几类‘看起来合理、实际会翻车’的编排设计?”小a问。
“有三类很典型。”老z说,
三个编排反例
- 把整个任务塞进一个大 Loop,假装它是节点——外部看不到它的内部状态,预算、轮次、停止原因全被藏起来,恢复和审计无从谈起
- 图节点里直接做模型自由发挥,没有任何预算——图只保证了路由确定性,没保证节点内部不失控
- 把所有模型调用都当成可重放——模型调用有概率性,重放会得到不同结果,不能和确定性计算一样安全重跑
“这三个反例的共同点是什么?”小a问。
“都是把某一层的约束,误当成了整个系统的保证。”老z说,“Loop 有停止条件,不等于整条流程安全;Graph 有路由,不等于节点内不失控;重放有幂等键,不等于所有节点都幂等。每一层只能保证自己那一层的边界,跨层的假设必须显式声明和验证。”
“那做编排设计的时候,该从哪里开始想?”小a问。
“从最坏情况开始想。”老z说,“先问‘这个流程最贵的一次失败是什么’——是重复扣款、是发错通知、还是生成错了文档——然后从最贵的失败反推:它在哪一层发生、那一层靠什么拦住、如果拦不住会怎样。**编排设计的起点不是选形式,是把最坏情况写下来。**形式选择只是这个思考的落点,不是起点。”
“最后总结一句,怎么才算把编排想清楚了?”小a问。
“能同时回答三个问题就算。”老z说,“第一,每一步的路由权在谁手里——代码还是模型;第二,每个节点的预算和停止条件是什么——不会无限转;第三,每次失败后从哪恢复、会不会重复副作用。三个问题都有答案,形式选哪种都能站稳;缺一个,形式再对也会在某个角落塌掉。”
小结
Loop、Workflow 和 Graph 是三种表达控制权和状态的工具,它们不是互斥的,而是按"路径有多固定"来选择。Agent Loop 把控制权交给模型,让它在每一步自主决定下一步,适合开放性的、路径无法预先确定的问题。Workflow 把路径固定下来,每一步做什么、按什么顺序、满足什么条件才往下走,都写在定义里,适合出错代价高、需要可审计可回放的流程。Graph 介于两者之间,用节点和边表达可能的分支,既允许条件路由,又保留整体结构的可观测性。三种形式在工程特征上差别明显:可观测性、成本、失败恢复、测试组合都不同,选择形式时这些隐性成本往往比"形式名字"更重要。
检查点和重放是 Workflow 与 Graph 的配套能力:保存恢复点让流程可以中断后续接,重放要求每一步都幂等,否则中断重跑会重复扣款。恢复方式按副作用是否已落地分四类:checkpoint 前重跑、checkpoint 后靠幂等键去重、外部已执行时查外部状态或转人工、模型走错路时送人工或安全终点。三种形式都可落到状态机——状态、事件、转移条件和可持久化 checkpoint——状态机的价值在于让非法转移变得可见,图和状态机是互补的两个检查面,不是替代关系。
混合编排是常态:入口用确定路由分流,中间用受限 Loop 分析,出口回到确定流程收尾,每个开放节点都有预算和停止条件。常见的三个反例是:把整个任务塞进大 Loop 假装它是节点、图节点里做无预算的模型自由发挥、把所有模型调用当成可安全重放。它们的共同点是把某一层的约束误当成整个系统的保证——每一层只能保证自己那一层的边界,跨层的假设要显式声明和验证。
动手核验
- 将一个报销流程画成节点、状态和条件,给每个外部写入标出幂等策略。
- 把其中一个开放式“解释异常”节点改为最多三轮的 Loop,并定义停止原因。
- 注入一次超时和一次重复投递,验证流程能否恢复且不会重复提交。