版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 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 步 | 16 | 7 |
| 场景 2 | 有检查点,断点续跑 | 第 8 步 | 9 | 0 |
口径:账本「应为 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()取到的当前时间、查实时库存,只要直接写在恢复会经过的路径里,重放时就会给出和第一次不同的值,命令序列对不上,任务卡在断点,而不是继续往下跑。
哪些步骤能重放,哪些必须隔离
| 步骤 | 能不能重放 | 依据 | 处置 |
|---|---|---|---|
| 校验参数、读订单、查库存 | 能 | 纯读取,不改变外部状态 | 直接重放 |
| 生成运单号 | 不能 | 结果每次不同(随机或时间戳) | 生成后写进快照,重放时读快照 |
| 锁库存、扣库存、记账 | 不能 | 有外部副作用 | 幂等键 + 隔离到重放路径之外 |
| 通知外部仓 | 不能 | 有外部副作用,且无法撤回 | 幂等键 + 隔离,并单独设重试上限 |
判定写成函数,进恢复路径前先问三句
把这套判定固化成三个问题,任何步骤在进入自动恢复之前都要过一遍:
- 这个步骤重新执行一次,结果会不会变?会变就把值写进快照,或把它挪出重放路径。
- 这个步骤有没有对外部世界产生效果?有就必须带幂等键。
- 这个步骤能不能接受「被重复执行一次」?不能就隔离到重放路径之外,交给支持幂等提交的下游承接。
三句里任何一句答不上来,这一步就不该进自动恢复,应该停在断点等人来看。
这份资料里有一节专门讲 LangGraph 的状态与流程编排:本文第二章提到的 checkpointer、super-step 边界,在那套视频课的第 5 章有从安装到 API 调用的完整演示,配合本文的最小复现看会更快对上。放在资料包里,扫码即可获取:
五、三件套缺一件就停手:最小基建与责任划分
三件套缺任何一件,正确做法都不是「多加几次重试」,而是让任务停在断点、转人工。没有幂等键的重试不是提高成功率,是把同一个故障多执行几遍。
三道闸门一览,以及没有它会怎样
| 闸门 | 拦什么 | 落地动作 | 没有它会怎样 |
|---|---|---|---|
| 第一道:进度快照 | 拦「整段重跑」 | 每个推进边界落一份可继续执行的状态,落在进程之外 | 每崩一次,前面所有动作再做一遍,实测多出 7 条 |
| 第二道:副作用唯一化 | 拦「重复扣款、重复发货」 | 每个外部动作带幂等键,键的唯一性由存储层保证 | 重复从必然变成偶发,问题拖到对账才暴露 |
| 第三道:可重放判定 | 拦「恢复路径里的不确定代码」 | 随机数、当前时间、实时查询隔离到重放路径之外 | 重放对不上历史,任务卡死在断点,两条路都走不通 |
最小基建:一个存储、一张键表、一个判定函数
落地成本不高,但三样要成套:
- 一个持久化存储:快照必须落在进程之外(数据库或对象存储)。LangGraph 文档专门提醒过内存型 checkpointer 的问题——进程重启后检查点全部丢失,生产要换成持久化实现。写在内存里的快照,等于没有快照。
- 一张幂等键表:键的写入要走唯一约束,别用「先查有没有、再写」。用唯一索引加冲突即跳过,并发恢复时才不会双双通过。
- 一个判定函数:把上一章那三个问题固化成代码,任何新步骤上线前都过它一遍,答不上来就不允许进自动恢复。
谁来为「可续跑」这件事负责
这三件套不是一个人能写完的:写业务步骤的人负责回答那三个问题(这一步可不可重放、有没有副作用);写编排与调度的人负责快照落点和键的唯一性;验收的人负责在测试环境里真的终止一次进程,看账本有没有多出重复行。
验收口径就一句:注入一次崩溃、恢复一次,账本行数必须等于应当的行数,重复副作用必须是 0。达不到就别上线。这不是稳定性优化,是任务能不能安全重跑的前提——顺序反过来做(先上重试、之后再补幂等),补的过程中每一次崩溃都在往账本里多记账。
这份资料里有从零到落地的 Agent 开发路径:状态持久化、幂等提交、重放判定这三件事怎么落到工程里,课程把它们放在 LangGraph 与 MCP 两个模块里串起来讲。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
- 事实: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)。本文位置:第二章。
- 事实:同一 super-step 内某个节点失败后,可以从最后一个成功步骤重启;恢复时不会重跑已经成功的节点(pending writes)。出处:Checkpointers — Docs by LangChain。本文位置:第二章。
- 事实:只能从检查点、也就是 super-step 边界恢复执行。出处:Checkpointers — Docs by LangChain。本文位置:第二章。
- 事实:内存型 checkpointer 不跨进程重启保存检查点,生产环境需改用持久化实现。出处:Persistence 页「MemorySaver does not persist between restarts」小节 — Docs by LangChain。本文位置:第五章。
- 事实:Temporal 对 Replay 的定义——重放是工作流继续推进的方式,重放期间生成的命令会与已有事件历史逐条比对。出处:Workflow Execution overview — Temporal Documentation(https://docs.temporal.io/workflow-execution)。本文位置:第四章。
- 事实:Temporal 要求工作流代码具备确定性;API 调用、模型调用、数据库查询等不确定操作应放进 Activity,因为 Activity 在重放路径之外执行并会被自动重试。出处:Workflow Definition — Deterministic constraints(https://docs.temporal.io/workflow-definition)。本文位置:第四章。
- 事实:两组实测数据——无检查点重复 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」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。