ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

跑到第 7 步崩了:一次重跑,7 个动作做了两遍

跑到第 7 步崩了:一次重跑,7 个动作做了两遍

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

一、第 7 步进程退出,账本从 9 行涨到 16 行

一次进程退出加一次重跑,账本从应有的 9 行涨到 16 行;多出来的 7 行,条条都是已经产生过外部效果的动作。这不是模型答错,是恢复策略写错。

任务本身很朴素,9 步:校验参数、读订单、查库存、锁库存、生成运单、扣库存、通知外部仓、记账、回执。第 7 步「通知外部仓」请求刚发出去、还没等到应答,进程退出。

本地一次跑通,把「中间失败」这件事藏起来了

本地跑这条流程,第 1 步到第 9 步一口气走完,中间没有失败,所以「会崩在中间」压根没被测到。上生产之后,外部仓接口会超时、会被限流,第 7 步之后随时可能断。

真正的坑在恢复那几行:捕获异常之后跟了一句「从头再跑一遍」。于是锁库存、扣库存、通知外部仓,又各做了一次。恢复逻辑写了三行,代价是三个已经生效的外部动作被重复提交。

实测:同一段代码,两种恢复策略

下面这段是第 7 步注入一次崩溃、再恢复一次的最小复现(只崩一次)。每个业务动作都会往账本里写一行,重复副作用就是账本里出现两次的行。

🧪 实测环境:Python 3.13.12 / macOS / 仅标准库

CKPT,LEDGER="ckpt.json","ledger.txt"STEPS=["校验参数","读订单","查库存","锁库存","生成运单","扣库存","通知外部仓","记账","回执"]CRASH_AT=7defside_effect(step):# 每个业务动作都写一行账本withopen(LEDGER,"a")asf:f.write(f"{step}:{STEPS[step-1]}\n")defrun_once(start,save_ckpt,crash):state={"done":start-1}foridxinrange(start-1,len(STEPS)):step=idx+1side_effect(step)# 先产生副作用state["done"]=stepifsave_ckpt:# 有检查点:每一步之后落一次快照json.dump(state,open(CKPT,"w"))ifcrashandstep==CRASH_AT:raiseRuntimeError(f"第{step}步之后进程退出")returnstate

原样输出:

============================================================ 场景 1 · 没有检查点:崩溃后只能从第 1 步重跑 第一次运行: 第 7 步之后进程退出 账本行数=16(应 9 行),其中重复副作用 7 条 重复的都是已产生过外部效果的步骤: ['1:校验参数', '2:读订单', '3:查库存', '4:锁库存', '5:生成运单', '6:扣库存', '7:通知外部仓'] 场景 2 · 有检查点:崩溃后从断点续跑 崩溃时记录到第 7 步,进程重启后从第 8 步继续 账本行数=9(应 9 行),其中重复副作用 0 条 账本内容: ['1:校验参数', '2:读订单', '3:查库存', '4:锁库存', '5:生成运单', '6:扣库存', '7:通知外部仓', '8:记账', '9:回执'] 结论:同样一次崩溃 + 一次恢复,无检查点重复执行 7 条已生效动作;有检查点重复 0 条。

把两个场景拉平看,差别只在「恢复起点」这一个变量上:

🧪 实测环境:Python 3.13.12 / macOS / 仅标准库(内存账本模拟、取数日期 2026-10-13)

场景恢复策略恢复起点账本行数重复副作用条数
场景 1无检查点,整体重跑第 1 步167
场景 2有检查点,断点续跑第 8 步90

口径:账本「应为 9 行」指 9 个动作各落一条;重复副作用条数 = 账本总行数减去去重后的行数;崩溃在第 7 步之后注入且只发生一次;两个场景跑的是同一份步骤定义、同一段副作用函数。

重复不报错,所以要拖到对账才暴露

第 4、6、7 步的重复执行不会抛异常——它们是「成功地做了第二次」。锁库存锁两次、扣库存扣两次、通知外部仓推两次,日志里全是成功;只有等到日终对账,才会发现库存少了、仓库多了一张单。这类故障的排查成本远高于一次崩溃,因为线索不在报错里,在差额里。

二、第一道闸门:进度快照,拦的是「整段重跑」

检查点要存的不是「刚才发生了什么」的日志,而是一份「拿到就能接着往下跑」的状态。存错了内容、存错了时机,都拦不住整段重跑。

快照存什么:可继续执行的游标,加上不可再生的值

至少要两样东西。一是游标,例如「已完成到第 7 步」;二是那些生成一次就不能再变的值,例如运单号、库存锁定号、外部系统返回的回执号——这类值往往来自随机数或当前时间,重跑一次就是一个新值,后续步骤就再也对不上账。

LangGraph 官方文档把这件事写得很直白:checkpointer 会把 thread 的图状态存成检查点(原文:「Checkpointers persist a thread’s graph state as checkpoints.」),而且是「A checkpointer saves a snapshot of graph state at each super-step」。这里的 thread 可以理解成一条状态主线,一次任务或一段会话的历次运行都挂在同一个 thread 上,用 thread_id 取回。super-step 可以理解成图的一次推进,同一拍里被调度的节点一起执行。截至 2026-10-13,LangGraph 的持久化分成两套:checkpointer 管 thread 内的短期状态,store 管跨 thread 的长期数据,两者职责不重叠。

保存时机:只有推进边界才是能用的恢复点

LangGraph 文档里有一句常被跳过:「you can only resume execution from a checkpoint (i.e., a super-step boundary)」——只能从检查点、也就是一次推进的边界恢复。换句话说,在某个节点执行到一半保存的半截状态,恢复的时候用不上。

它给出的容错说明也在同一条线上:「if one or more nodes fail at a given superstep, you can restart your graph from the last successful step」。再细一层是同页的 pending writes:某个节点中途失败时,同一拍里已经成功的其他节点结果会被记下来,「When you resume graph execution from that super-step you don’t re-run the successful nodes」。这条对你的任务很关键——同一批次里已经跑完的步骤,恢复时不必再跑。

快照拦不住的那半件事,正好是下一道闸门

快照把「从第 1 步重跑」换成了「从第 8 步接着跑」,但它管不了第 7 步已经发出去的那次通知。快照能让副作用不再重来,前提是它知道哪些副作用已经生效;而这件事,快照本身答不出来——它只知道「进度走到哪」,不知道「外部世界被改成了什么样」。

三、第二道闸门:副作用唯一化,拦的是「重复扣款」

只加进度快照、不加幂等键,等于把重复执行从「每次都发生」变成「偶尔发生」。必然重复还能靠一次对账抓住;偶发重复会藏在两次崩溃之间的缝隙里,等发现时已经过去很久。

快照只管「从哪继续」,不管「已经做过什么」

幂等键(idempotency key,一个让「同一个动作执行多次」「效果等于执行一次」的标识)解决的是后者。换一种时序就很清楚:副作用已经发出去、快照还没落盘,进程被终止,恢复时快照指向第 6 步,第 7 步的通知就会再发一次。快照保证的是「不从头再来」,不是「不重来」。这两句话差一个字,落到账本上差很多行。

幂等键取什么值:业务动作加业务主键

粒度不是「步骤名」,而是「业务动作 + 业务主键」。粒度太粗(例如只用步骤名),两笔不同订单会被当成同一笔而漏做;粒度太细(例如把时间戳拼进去),同一个动作每次都算新键,等于没有幂等键。

外部动作幂等键取值撞键时的行为这个粒度的理由
扣库存扣减 + 订单号返回「已扣减」,不再扣同一订单只能扣一次
通知外部仓通知 + 运单号返回「已通知」,不再推送重复推送会让仓库重复建单
记账记账 + 订单号 + 金额返回「已记账」,不再入账同一笔金额只能进一次账
回执回执 + 任务号覆盖写,不新增行回执是可覆盖的状态,不需要唯一

同一个键第二次进来,直接跳过

🧪 实测环境:Python 3.13.12 / macOS / 仅标准库

defapply(action,key):done=json.load(open(KEYS))ifos.path.exists(KEYS)else[]ifkeyindone:# 键已存在 -> 直接返回,不再产生副作用return"skip"withopen(LEDGER,"a")asf:# 先落副作用f.write(action+"\n")done.append(key)# 再落键json.dump(done,open(KEYS,"w"))return"done"print(apply("扣库存:O-1001","deduct:O-1001"))# 第一次print(apply("扣库存:O-1001","deduct:O-1001"))# 恢复重放,同一个键print(apply("扣库存:O-1002","deduct:O-1002"))# 另一笔订单print("账本行数 =",len(open(LEDGER).read().strip().split("\n")))
done skip done 账本行数 = 2

这段代码留了一个真实的缺口:副作用写在落键之前,两者之间如果进程退出,键没记上,重放仍会再扣一次。生产里有两种补法——要么先「抢占键」(把键当锁,抢到才允许执行),要么把「副作用 + 落键」交给一个支持幂等提交的下游,让它们同生共死。只靠「先查一下有没有、没有就写」,在并发恢复下会双双查到「没有」。

四、第三道闸门:可重放判定,拦的是「恢复路径里的不确定代码」

恢复在很多编排引擎里不是「跳过已完成的步骤」,而是「把代码从头再跑一遍,再拿历史记录把结果对上」。Temporal 把这个机制叫 replay:官方对它的定义是,replay 是工作流继续推进的方式,重放期间生成的命令会和已有的事件历史逐条比对,对不上就报非确定性错误。

重放会真的再执行一遍代码

Temporal 的确定性约束页写得更直接:「Workflow code must be deterministic to support replay. To handle non-deterministic operations like API calls, LLM/AI invocations, database queries, and other external interactions, put them in Activities.」——工作流代码必须确定,才能支撑重放;API 调用、模型调用、数据库查询这类不确定操作要放进 Activity,因为 Activity 在重放路径之外执行,并且会被自动重试。同一页还有一句收口的表述:「all operations that do not purely mutate the Workflow Execution’s state should occur through a Temporal SDK API.」

落到本文这条任务上:随机生成的运单号、now()取到的当前时间、查实时库存,只要直接写在恢复会经过的路径里,重放时就会给出和第一次不同的值,命令序列对不上,任务卡在断点,而不是继续往下跑。

哪些步骤能重放,哪些必须隔离

步骤能不能重放依据处置
校验参数、读订单、查库存能纯读取,不改变外部状态直接重放
生成运单号不能结果每次不同(随机或时间戳)生成后写进快照,重放时读快照
锁库存、扣库存、记账不能有外部副作用幂等键 + 隔离到重放路径之外
通知外部仓不能有外部副作用,且无法撤回幂等键 + 隔离,并单独设重试上限

判定写成函数,进恢复路径前先问三句

把这套判定固化成三个问题,任何步骤在进入自动恢复之前都要过一遍:

  1. 这个步骤重新执行一次,结果会不会变?会变就把值写进快照,或把它挪出重放路径。
  2. 这个步骤有没有对外部世界产生效果?有就必须带幂等键。
  3. 这个步骤能不能接受「被重复执行一次」?不能就隔离到重放路径之外,交给支持幂等提交的下游承接。

三句里任何一句答不上来,这一步就不该进自动恢复,应该停在断点等人来看。

这份资料里有一节专门讲 LangGraph 的状态与流程编排:本文第二章提到的 checkpointer、super-step 边界,在那套视频课的第 5 章有从安装到 API 调用的完整演示,配合本文的最小复现看会更快对上。放在资料包里,扫码即可获取:

五、三件套缺一件就停手:最小基建与责任划分

三件套缺任何一件,正确做法都不是「多加几次重试」,而是让任务停在断点、转人工。没有幂等键的重试不是提高成功率,是把同一个故障多执行几遍。

三道闸门一览,以及没有它会怎样

闸门拦什么落地动作没有它会怎样
第一道:进度快照拦「整段重跑」每个推进边界落一份可继续执行的状态,落在进程之外每崩一次,前面所有动作再做一遍,实测多出 7 条
第二道:副作用唯一化拦「重复扣款、重复发货」每个外部动作带幂等键,键的唯一性由存储层保证重复从必然变成偶发,问题拖到对账才暴露
第三道:可重放判定拦「恢复路径里的不确定代码」随机数、当前时间、实时查询隔离到重放路径之外重放对不上历史,任务卡死在断点,两条路都走不通

最小基建:一个存储、一张键表、一个判定函数

落地成本不高,但三样要成套:

  • 一个持久化存储:快照必须落在进程之外(数据库或对象存储)。LangGraph 文档专门提醒过内存型 checkpointer 的问题——进程重启后检查点全部丢失,生产要换成持久化实现。写在内存里的快照,等于没有快照。
  • 一张幂等键表:键的写入要走唯一约束,别用「先查有没有、再写」。用唯一索引加冲突即跳过,并发恢复时才不会双双通过。
  • 一个判定函数:把上一章那三个问题固化成代码,任何新步骤上线前都过它一遍,答不上来就不允许进自动恢复。

谁来为「可续跑」这件事负责

这三件套不是一个人能写完的:写业务步骤的人负责回答那三个问题(这一步可不可重放、有没有副作用);写编排与调度的人负责快照落点和键的唯一性;验收的人负责在测试环境里真的终止一次进程,看账本有没有多出重复行。

验收口径就一句:注入一次崩溃、恢复一次,账本行数必须等于应当的行数,重复副作用必须是 0。达不到就别上线。这不是稳定性优化,是任务能不能安全重跑的前提——顺序反过来做(先上重试、之后再补幂等),补的过程中每一次崩溃都在往账本里多记账。

这份资料里有从零到落地的 Agent 开发路径:状态持久化、幂等提交、重放判定这三件事怎么落到工程里,课程把它们放在 LangGraph 与 MCP 两个模块里串起来讲。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表

  1. 事实:LangGraph 的 checkpointer 会把 thread 的图状态存成检查点,并在每个 super-step 落一份快照。出处:Persistence、Checkpointers — Docs by LangChain(https://docs.langchain.com/oss/python/langgraph/persistence 、https://docs.langchain.com/oss/python/langgraph/checkpointers)。本文位置:第二章。
  2. 事实:同一 super-step 内某个节点失败后,可以从最后一个成功步骤重启;恢复时不会重跑已经成功的节点(pending writes)。出处:Checkpointers — Docs by LangChain。本文位置:第二章。
  3. 事实:只能从检查点、也就是 super-step 边界恢复执行。出处:Checkpointers — Docs by LangChain。本文位置:第二章。
  4. 事实:内存型 checkpointer 不跨进程重启保存检查点,生产环境需改用持久化实现。出处:Persistence 页「MemorySaver does not persist between restarts」小节 — Docs by LangChain。本文位置:第五章。
  5. 事实:Temporal 对 Replay 的定义——重放是工作流继续推进的方式,重放期间生成的命令会与已有事件历史逐条比对。出处:Workflow Execution overview — Temporal Documentation(https://docs.temporal.io/workflow-execution)。本文位置:第四章。
  6. 事实:Temporal 要求工作流代码具备确定性;API 调用、模型调用、数据库查询等不确定操作应放进 Activity,因为 Activity 在重放路径之外执行并会被自动重试。出处:Workflow Definition — Deterministic constraints(https://docs.temporal.io/workflow-definition)。本文位置:第四章。
  7. 事实:两组实测数据——无检查点重复 7 条、有检查点重复 0 条;幂等键第二次进入时返回跳过——均为本机实跑得到。出处:本文实测(内存账本模拟、取数日期 2026-10-13,Python 3.13.12 / macOS / 仅标准库)。本文位置:第一、三章。

写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进