<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Checkpoint on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/checkpoint/</link><description>Recent content in Checkpoint on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/checkpoint/index.xml" rel="self" type="application/rss+xml"/><item><title>第六话｜状态管理：循环怎么记得住、断得了、恢复得回来</title><link>https://zh.renxinblog.cn/post/c06-state-management/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c06-state-management/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c06-state-management-cover.png" alt="Featured image of post 第六话｜状态管理：循环怎么记得住、断得了、恢复得回来" /&gt;&lt;p&gt;第五话我们让模型的「想」和「做」交替起来了：模型想一步、做一步、看一眼真实结果、再想一步。循环跑通了。&lt;/p&gt;
&lt;p&gt;但那个循环藏着一个没解决的口子：&lt;strong&gt;它跑的是「一口气」——从头到尾，中间不能断。&lt;/strong&gt; 一旦进程崩了、断电了、被 OOM 杀了，走到哪一步、做过什么事，全没了；硬要重跑，它又从第一步开始，把已经做过的事再从头做一遍。&lt;/p&gt;
&lt;p&gt;如果一个任务只是「查一次天气」，断了大不了重查，无所谓。但如果任务是「处理订单」——查库存 → 扣库存 → 发通知，三步里有两步是&lt;strong&gt;不可撤销的&lt;/strong&gt;（扣了钱、发了通知），那「断了重来」就会出大事：扣库存会被重复执行，用户被扣两次钱。&lt;/p&gt;
&lt;p&gt;所以这一篇要讲的，就是怎么让循环不再是「只能一口气跑完」：&lt;strong&gt;断了能接回来，重跑时做过的事不会重做。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而这件事的病根，其实只有一个——&lt;strong&gt;进度只活在内存里，没有任何可靠的、能查的痕迹。&lt;/strong&gt; 抓住这个病根往下看，问题会一个个现形，解法也会一个个浮出来。这一篇就从「先看清这些问题」开始。&lt;/p&gt;
&lt;h2 id="一没有状态管理的循环问题在哪"&gt;一、没有状态管理的循环，问题在哪
&lt;/h2&gt;&lt;p&gt;第五话的循环，是靠 &lt;code&gt;messages&lt;/code&gt; 这个列表撑起来的：模型说过什么、工具返回过什么，全都往里塞，靠它一路&amp;quot;喂&amp;quot;下去。这套东西跑一次性的任务没问题，但它没有「状态管理」——进度、做过的事，全都隐式地混在对话里、活在内存里。这带来几个实打实的毛病：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;看不清走到哪了&lt;/strong&gt;。进度藏在对话里，你想知道&amp;quot;现在到第几步了、还差几步&amp;quot;，得把整段对话从头翻一遍，靠猜。没有一处能直接告诉你答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;断不起&lt;/strong&gt;。所有数据都在内存里，进程一崩、一断电、一 OOM，全部归零。走到哪了、做过了什么，连个影子都不剩——只能从头再来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;会重做&lt;/strong&gt;。即便你硬着头皮重跑，它也不知道自己&amp;quot;已经做过哪些事了&amp;quot;。不可撤销的步骤（扣库存、发通知、退款）会被重复执行，用户被扣两次钱。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三条，是&amp;quot;没有状态管理&amp;quot;的循环共同的毛病。它们背后是同一个病根：&lt;strong&gt;进度没有被当成一等公民对待——它没被单独存下来、单独落盘、单独记录。&lt;/strong&gt; 后面几节，就是顺着这三条，一条条把它们解决掉。&lt;/p&gt;
&lt;h2 id="二提取状态不和消息耦合"&gt;二、提取状态，不和消息耦合
&lt;/h2&gt;&lt;p&gt;第一节说的第 1 条毛病（看不清走到哪），根子是&lt;strong&gt;进度和消息搅在一起&lt;/strong&gt;。所以先从这儿下手——把「进度」从「对话」里拆出来。&lt;/p&gt;
&lt;p&gt;第五话把什么都塞进 &lt;code&gt;messages&lt;/code&gt;，这一篇给它松绑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="c1"&gt;# 任务输入（order_id / sku）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="c1"&gt;# 只存「模型需要看到的对话」&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="c1"&gt;# 执行轨迹（只读审计）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt; &lt;span class="c1"&gt;# 运行状态&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="c1"&gt;# 走到哪了（进度）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;done_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="c1"&gt;# 已经做完了哪些步骤（防重复的依据）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;多出来的两个字段，正好对应那几条毛病：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;next_step&lt;/code&gt;&lt;/strong&gt;：走到哪了。之前是「模型隐式决定下一步」，现在是「一个明确的字段指出来」。进度不再藏在对话里，一眼能看清——第 1 条毛病（看不清走到哪）就此解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;done_steps&lt;/code&gt;&lt;/strong&gt;：做过什么。它是后面「断了之后，哪些不用重做」的依据——为第 3 条毛病（会重做）铺路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么这俩字段这么关键？因为**「看得清进度」和「不重复做」，都要求进度能被单独拎出来、一眼看清、能被存下来。** &lt;code&gt;next_step&lt;/code&gt; 告诉你走到哪，&lt;code&gt;done_steps&lt;/code&gt; 告诉你做过什么——有了这两样，后面落盘、恢复才有了抓手。&lt;/p&gt;
&lt;h2 id="三保存磁盘的时机选择看关键步骤"&gt;三、保存磁盘的时机选择：看关键步骤
&lt;/h2&gt;&lt;p&gt;进度摆到明面了，但它还只在内存里，进程一崩照样归零。要让它「断了还在」，就得把它存到磁盘上：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckpointStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 序列化落盘&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# 读回一个完整的进度&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;「存」这个动作谁都会写（&lt;code&gt;json.dumps&lt;/code&gt; 而已），真正关键的是&lt;strong&gt;什么时候存&lt;/strong&gt;。这里有一个工程上反复验证过的判断：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;每个「有副作用」的步骤执行完，立即落盘。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;为什么是「有副作用」的步骤？因为无副作用的步骤（比如查一次库存），重做一百次结果都一样，断了大不了重跑。但&lt;strong&gt;有副作用的步骤（扣库存、发通知、退款），一旦执行了就不可撤销&lt;/strong&gt;——必须在它执行完的那一刻，立刻把「我已经做过了」这个事实写到磁盘上。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deduct_inventory&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;notify_shipped&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;成功&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done_steps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;这一步&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 立即落盘，绝不拖延&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一步，是「断了还能接着跑」的全部秘密。&lt;/p&gt;
&lt;h2 id="四中断恢复时防重复执行"&gt;四、中断恢复时，防重复执行
&lt;/h2&gt;&lt;p&gt;进度落盘了，断点就能恢复。但恢复时最怕一件事：&lt;strong&gt;重放&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;还是那个订单：查库存 → 扣库存 → 发通知。进程在「扣库存」之后、「发通知」之前崩了。恢复时，模型从落盘的进度接着走，但它可能不知道自己「扣库存」已经做过了，于是&lt;strong&gt;又扣了一次&lt;/strong&gt;——用户被扣了两次钱。&lt;/p&gt;
&lt;p&gt;为什么会这样？因为**「从 checkpoint 恢复」这件事，本质是「至少一次」（at-least-once）语义，不是「恰好一次」（exactly-once）。** 它保证的是「断了能接回来、不会漏做」，但保证不了「已经做过的绝不重做」——扣库存那个动作，可能在「已经扣了」和「落盘记录了」之间那几毫秒里崩掉，恢复时谁都不知道它到底做没做。&lt;/p&gt;
&lt;p&gt;所以「防重复」不能只靠 checkpoint，还得靠&lt;strong&gt;幂等&lt;/strong&gt;：&lt;strong&gt;同一个有副作用的操作，执行多次和执行一次，效果得一样。&lt;/strong&gt; 幂等，才是把「至少一次」补成「恰好一次」的那一环。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deduct:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect_log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 这笔订单已经扣过&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ALREADY_DONE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 记下「做过了」&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;DEDUCTED&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意这里有&lt;strong&gt;两层防线&lt;/strong&gt;，缺一不可：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;done_steps&lt;/code&gt;&lt;/strong&gt;（进度里的记录）：恢复时告诉 runtime「这一步做过，跳过」；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;side_effect_log&lt;/code&gt;&lt;/strong&gt;（副作用自己的记录）：即使 runtime 漏判了、模型重放了，副作用本身也能认出「这笔订单扣过了」，返回 &lt;code&gt;ALREADY_DONE&lt;/code&gt; 而不是真扣。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第二层尤其重要——因为它对应的是&lt;strong&gt;真实世界&lt;/strong&gt;：数据库里的扣款记录、消息队列里的已发送标记，这些「外部世界的痕迹」才是「不重复」的最终兜底。光靠进程内的进度记录，护不住「进程都重启了」的场景——所以必须有一层落在外面的世界。&lt;/p&gt;
&lt;h2 id="五动手实验一次真实的崩溃"&gt;五、动手实验一次真实的崩溃
&lt;/h2&gt;&lt;p&gt;光说不练假把式，跑一个真的。任务是「处理订单：查库存 → 扣库存 → 发通知」，我们故意在「扣库存」之后、「发通知」之前把进程杀掉，然后恢复，看它到底会不会重复扣。&lt;/p&gt;
&lt;p&gt;配套代码在 &lt;code&gt;code/agent-tutorial/c06-state-management/&lt;/code&gt;（Python，零第三方依赖），三条工具都是带副作用记录的本地夹具：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;工具&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
					&lt;th&gt;副作用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;check_inventory&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;查库存&lt;/td&gt;
					&lt;td&gt;无（可安全重跑）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;deduct_inventory&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;扣库存&lt;/td&gt;
					&lt;td&gt;有（扣了不可撤销）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;notify_shipped&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;发通知&lt;/td&gt;
					&lt;td&gt;有（发了不可撤销）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;跑起来看两条路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 离线回归（4 项测试，覆盖 checkpoint 往返、正常跑、崩溃恢复、幂等重放）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; code/agent-tutorial/c06-state-management/python
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest test_state_management.py -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 演示脚本：正常跑完 + 崩溃恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 state_management.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是崩溃恢复那段最想让你看到的输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;===== 路径 2：崩溃恢复（发通知前崩溃）=====
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;--- 崩溃前状态 ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;step tool=check_inventory observation=SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;step tool=deduct_inventory observation=DEDUCTED
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checkpoint_saved
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;crashed_here
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status=RUNNING done_steps=[&amp;#39;deduct_inventory:...&amp;#39;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;--- (进程崩溃) ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;--- 恢复后状态 ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recovered_from_checkpoint
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;step tool=notify_shipped observation=NOTIFIED
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checkpoint_saved
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;model_final
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status=COMPLETED done_steps=[...扣库存..., ...发通知...]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;deduct_count=1 notify_count=1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;看最后一行：&lt;code&gt;deduct_count=1&lt;/code&gt;——扣库存&lt;strong&gt;只发生了一次&lt;/strong&gt;。尽管进程崩了、恢复了、模型可能想重放，但扣库存没有被重复执行。&lt;code&gt;notify_count=1&lt;/code&gt;——恢复后，漏掉的发通知被补上了。&lt;/p&gt;
&lt;p&gt;这证明的不是「进度能存」，而是**「断了能接回来，而且接回来的时候不会把已经做过的事重做一遍」**——这正是生产级 Agent 和 demo 的根本区别。&lt;/p&gt;
&lt;h2 id="六总结和下一篇"&gt;六、总结和下一篇
&lt;/h2&gt;&lt;p&gt;到这里，第一节列的那几条毛病，解决得差不多了：进度摆到明面上（看得清）、关键步骤落盘（断得起）、副作用幂等（不重做）。循环从「只能一口气跑完」，变成了「断了能接着跑」。&lt;/p&gt;
&lt;p&gt;但回头再看一眼我们这半天的功夫——它解决的，始终是「&lt;strong&gt;这一笔订单 O-1001&lt;/strong&gt;」从开始到结束的进度。而真实世界里，Agent 不会只处理一笔订单：它今天处理一百笔、明天再处理一百笔。于是有个更实际的问题冒出来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理第 1 笔订单时，Agent 学到「这个 SKU 经常缺货，得先查库存」；到第 100 笔，它&lt;strong&gt;还记得&lt;/strong&gt;吗？还是每一笔都像第一次那样，从头摸索一遍？&lt;/li&gt;
&lt;li&gt;上一笔订单处理到一半崩了，靠落盘接回来了——这是「这一笔」的进度。但「过去一百笔攒下的经验」，存在哪？进程一重启，是不是也一起没了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;「记住这一次任务走到哪了」，和「记住过去做过的那些事、攒下的那些经验」，是两码事。前者这一篇解决了；后者——&lt;strong&gt;让 Agent 跨任务、跨会话，还记得以前的事&lt;/strong&gt;——正是第七话要讲的：&lt;strong&gt;记忆&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;演示代码&lt;/strong&gt;：&lt;code&gt;code/agent-tutorial/c06-state-management/&lt;/code&gt;（Python：&lt;code&gt;state_management.py&lt;/code&gt; + &lt;code&gt;test_state_management.py&lt;/code&gt;，零第三方依赖，4 项离线测试）。崩溃恢复与幂等为离线确定性夹具验证，不依赖外部 API。&lt;/p&gt;

 &lt;/blockquote&gt;</description></item></channel></rss>