资讯动态

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

发布时间:2026/10/13 22:44:28 来源:尧图企业网站定制
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、第 7 步进程退出账本从 9 行涨到 16 行一次进程退出加一次重跑账本从应有的 9 行涨到 16 行多出来的 7 行条条都是已经产生过外部效果的动作。这不是模型答错是恢复策略写错。任务本身很朴素9 步校验参数、读订单、查库存、锁库存、生成运单、扣库存、通知外部仓、记账、回执。第 7 步「通知外部仓」请求刚发出去、还没等到应答进程退出。本地一次跑通把「中间失败」这件事藏起来了本地跑这条流程第 1 步到第 9 步一口气走完中间没有失败所以「会崩在中间」压根没被测到。上生产之后外部仓接口会超时、会被限流第 7 步之后随时可能断。真正的坑在恢复那几行捕获异常之后跟了一句「从头再跑一遍」。于是锁库存、扣库存、通知外部仓又各做了一次。恢复逻辑写了三行代价是三个已经生效的外部动作被重复提交。实测同一段代码两种恢复策略下面这段是第 7 步注入一次崩溃、再恢复一次的最小复现只崩一次。每个业务动作都会往账本里写一行重复副作用就是账本里出现两次的行。 实测环境Python 3.13.12 / macOS / 仅标准库CKPT,LEDGERckpt.json,ledger.txtSTEPS[校验参数,读订单,查库存,锁库存,生成运单,扣库存,通知外部仓,记账,回执]CRASH_AT7defside_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)):stepidx1side_effect(step)# 先产生副作用state[done]stepifsave_ckpt:# 有检查点每一步之后落一次快照json.dump(state,open(CKPT,w))ifcrashandstepCRASH_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-13LangGraph 的持久化分成两套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):donejson.load(open(KEYS))ifos.path.exists(KEYS)else[]ifkeyindone:# 键已存在 - 直接返回不再产生副作用returnskipwithopen(LEDGER,a)asf:# 先落副作用f.write(action\n)done.append(key)# 再落键json.dump(done,open(KEYS,w))returndoneprint(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 LangChainhttps://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 Documentationhttps://docs.temporal.io/workflow-execution。本文位置第四章。事实Temporal 要求工作流代码具备确定性API 调用、模型调用、数据库查询等不确定操作应放进 Activity因为 Activity 在重放路径之外执行并会被自动重试。出处Workflow Definition — Deterministic constraintshttps://docs.temporal.io/workflow-definition。本文位置第四章。事实两组实测数据——无检查点重复 7 条、有检查点重复 0 条幂等键第二次进入时返回跳过——均为本机实跑得到。出处本文实测内存账本模拟、取数日期 2026-10-13Python 3.13.12 / macOS / 仅标准库。本文位置第一、三章。写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑