<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>幂等 on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/%E5%B9%82%E7%AD%89/</link><description>Recent content in 幂等 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/%E5%B9%82%E7%AD%89/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;/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;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;业务已经成功了，但 Agent 还没来得及把它记下来，怎么办？&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;「查库存」成功了、进程没崩——这种时候落盘不落盘无所谓，大不了重查。「扣库存」成功了、但就在落盘前那几毫秒进程崩了——这才是有意思的窗口。它逼我们想清楚两件事：&lt;strong&gt;进度该怎么记&lt;/strong&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;p&gt;具体来说，有两条实打实的毛病：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;断不起，会重做&lt;/strong&gt;。所有数据都在内存里，进程一崩、一断电、一 OOM，全部归零。即便硬着头皮重跑，它也不知道自己&amp;quot;已经做过哪些事&amp;quot;了。有副作用的步骤（扣库存、发通知、退款）会被重复执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;看不出&amp;quot;做没做&amp;quot;的依据&lt;/strong&gt;。进度藏在对话里，你想知道&amp;quot;扣库存这一步做过了吗&amp;quot;，得把整段对话从头翻一遍，靠猜。没有一处能直接告诉你答案，也没有一处能在进程重启后还告诉你答案。&lt;/p&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;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;strong&gt;这一篇延续第五话的 ReAct 路线，&lt;code&gt;next_step&lt;/code&gt; 记录的是&amp;quot;运行阶段&amp;quot;，不是&amp;quot;该执行哪个业务步骤&amp;quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，ReAct 循环里每走一步，循环只会处于两种状态之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;waiting_model&lt;/code&gt;&lt;/strong&gt;：循环刚把上一轮工具结果回填给模型，正等着模型给出下一轮决策（要么再调一个工具，要么给出最终答复）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;waiting_tool&lt;/code&gt;&lt;/strong&gt;：模型已经发起了一次工具调用，循环正等着工具返回结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 &lt;code&gt;next_step&lt;/code&gt; 回答的问题是&amp;quot;&lt;strong&gt;循环现在卡在哪一步了&lt;/strong&gt;&amp;quot;——是卡在&amp;quot;等模型想&amp;quot;还是&amp;quot;等工具做&amp;quot;。而&amp;quot;下一步具体调哪个工具、发什么参数&amp;quot;，这个决定&lt;strong&gt;仍然由模型做&lt;/strong&gt;，跟第五话一样。&lt;/p&gt;
&lt;p&gt;为什么要把它显式成一个字段，而不是继续靠隐式状态？因为「等模型」和「等工具」之间的那条边界，恰恰是崩溃最容易发生的地方——模型刚发起&amp;quot;扣库存&amp;quot;、工具还没返回的那一瞬间进程死了，恢复时你就能从 &lt;code&gt;next_step&lt;/code&gt; 一眼看出&amp;quot;上一轮是模型在等工具结果&amp;quot;，从而决定是把工具结果补回去、还是让模型重新决策。&lt;strong&gt;把运行阶段显式化，是&amp;quot;断了能接回来&amp;quot;的第一步。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过这里要提前把一句&amp;quot;难听的话&amp;quot;说在前面，免得你读到后面觉得别扭：&lt;strong&gt;在 ReAct 这种模型自主决策的循环里，状态管理能给你的&amp;quot;进度&amp;quot;天然是粗粒度的。&lt;/strong&gt; 它只能告诉你&amp;quot;循环停在哪一次交互&amp;quot;——是停在第几次&amp;quot;等模型想&amp;quot;、第几次&amp;quot;等工具做&amp;quot;——却给不了你&amp;quot;业务已经做到第几步、还差几步&amp;quot;这种精细答案。因为下一步调哪个工具、做哪个业务动作，是模型临时决定的，不是一张能预先勾选的清单。&lt;/p&gt;
&lt;p&gt;这意味着，别指望靠状态管理本身去精确回答&amp;quot;扣库存到底做没做&amp;quot;。那件事，交给后面第四、五节的&lt;strong&gt;幂等&lt;/strong&gt;去兜底——状态管理管&amp;quot;能不能恢复&amp;quot;，业务正确性管&amp;quot;重做会不会错&amp;quot;，各司其职。先把这句话记住，后面会回到它。&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="c1"&gt;# 写临时文件 -&amp;gt; fsync -&amp;gt; 原子 rename，避免写到一半被中断留下半截文件&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;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;step&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;step&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;strong&gt;「每个有副作用步骤完成后立即落盘」是一个保存时机，不是恢复的全部秘密。&lt;/strong&gt; 落盘本身解决不了两个更棘手的问题——&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;落盘和业务执行之间有时间差。「扣库存成功」到「checkpoint 写成功」之间，始终有一个窗口；在这个窗口里崩溃，checkpoint 里没有记录，但业务已经发生了。&lt;/li&gt;
&lt;li&gt;就算 checkpoint 记了&amp;quot;扣库存做过了&amp;quot;，恢复时也未必能信它——因为「记了」这件事本身，和「业务真的成功了」之间，仍然隔着那几毫秒。&lt;/li&gt;
&lt;/ol&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;/p&gt;
&lt;p&gt;为什么会这样？因为**「从 checkpoint 恢复」这件事，本质是「至少一次」（at-least-once）语义，不是「恰好一次」（exactly-once）。** 它保证的是「断了能接回来、不会漏做」，但保证不了「已经做过的绝不重做」。扣库存那个动作，可能在「已经扣了」和「落盘记录了」之间那几毫秒里崩掉，恢复时谁都不知道它到底做没做。&lt;/p&gt;
&lt;p&gt;这里要把三个不同层次分清楚，它们各管各的，别混为一谈：&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;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Checkpoint&lt;/td&gt;
					&lt;td&gt;保存可恢复的执行状态（我记到哪了）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重试与恢复调度&lt;/td&gt;
					&lt;td&gt;决定未完成的操作是否继续尝试&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&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;有 checkpoint 不等于「保证不会漏做」；幂等也不代表工具实际只被调用一次。&lt;strong&gt;准确的说法是：允许恢复过程中重复尝试，但在明确的业务边界内避免重复生效。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么「在业务边界内避免重复生效」靠什么？靠&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="c1"&gt;# 查幂等 -&amp;gt; 扣库存 -&amp;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;已经扣过&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;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="n"&gt;扣库存&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;记录&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;（状态里的记录）：把&amp;quot;哪些副作用步骤已经做过&amp;quot;显式记下来，恢复时 runtime 和模型都能看到这份清单，作为&amp;quot;哪些不该重做&amp;quot;的参考依据；&lt;/li&gt;
&lt;li&gt;&lt;strong&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;p&gt;这也正是第二节埋下的那个伏笔的答案：&lt;strong&gt;ReAct 里状态管理能给的&amp;quot;进度&amp;quot;是粗粒度的，精确的&amp;quot;扣库存做没做&amp;quot;它给不了；而幂等这一层，恰好补上了这个缺口。&lt;/strong&gt; 状态管恢复，幂等管正确，二者合起来，才撑得起&amp;quot;断了能接回来、做过不重做&amp;quot;这句承诺。&lt;/p&gt;
&lt;h2 id="五幂等的关键原子性"&gt;五、幂等的关键：原子性
&lt;/h2&gt;&lt;p&gt;上一节那段 &lt;code&gt;deduct&lt;/code&gt;，看着已经&amp;quot;幂等&amp;quot;了，但其实藏着一个致命的漏洞。我把三步拆开看：&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;检查执行记录 → 扣库存 → 写执行记录
&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;p&gt;这就是这一篇真正要回答的那个问题——「业务已经成功了，但 Agent 还没来得及记下来」——在幂等这层上的具体化。答案不在&amp;quot;记得更快&amp;quot;，而在&lt;strong&gt;原子性&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本地数据库业务&lt;/strong&gt;：把「幂等记录」和「业务变更」放进&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-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;side_effect_log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&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="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;try&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;with&lt;/span&gt; &lt;span class="n"&gt;conn&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;if&lt;/span&gt; &lt;span class="n"&gt;已经扣过&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 class="n"&gt;sku&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="n"&gt;ALREADY_DONE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="n"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;INSERT&lt;/span&gt; &lt;span class="n"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;side_effect_log&lt;/span&gt; &lt;span class="n"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;deduct&amp;#39;&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 class="n"&gt;sku&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;except&lt;/span&gt; &lt;span class="n"&gt;IntegrityError&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="n"&gt;ALREADY_DONE&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;code&gt;ALREADY_DONE&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;远程工具调用&lt;/strong&gt;：扣库存的库如果是别人的服务，你没法用本地事务。这时只能依赖对方支持的&lt;strong&gt;幂等请求标识&lt;/strong&gt;（同一个幂等键，重复请求返回相同结果）或&lt;strong&gt;结果查询&lt;/strong&gt;（超时后去查一下到底成没成）。而且要注意：&lt;strong&gt;超时之后，操作可能处于&amp;quot;结果未知&amp;quot;——不能直接当作&amp;quot;没执行&amp;quot;，也不能当作&amp;quot;执行了&amp;quot;，得去核实。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一节是本篇分量最重的地方，也是你熟悉的 Java 事务经验最能派上用场的所在。原子性不是&amp;quot;写得快&amp;quot;，是&amp;quot;不存在中间态&amp;quot;。&lt;/p&gt;
&lt;h2 id="六动手实验一次真实的崩溃"&gt;六、动手实验一次真实的崩溃
&lt;/h2&gt;&lt;p&gt;光说不练假把式。这一节，我们把上面讲的每一件事，都用真实的两进程跑一遍——不是&amp;quot;模拟崩溃&amp;quot;，是真的让一个进程退出、另一个进程从落盘的数据里恢复。&lt;/p&gt;
&lt;p&gt;任务是「处理订单：查库存 → 扣库存 → 发通知」。配套代码在 &lt;code&gt;code/agent-tutorial/c06-state-management/&lt;/code&gt;（Python，零第三方依赖，业务数据落在 SQLite 里）：&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;# 离线回归（7 项测试，覆盖 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;p&gt;&lt;strong&gt;实验一：扣库存成功、checkpoint 尚未写时，进程被杀。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是最狠的窗口。子进程里直接调 &lt;code&gt;deduct&lt;/code&gt; 把库存扣了（业务成功），然后&lt;strong&gt;刻意不写 checkpoint&lt;/strong&gt;，进程退出。此时内存状态全丢，只有 SQLite 里的业务数据还在。父进程（新进程）重新执行同一笔扣库存——结果返回 &lt;code&gt;ALREADY_DONE&lt;/code&gt;，库存没有再扣。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;checkpoint 不存在（没来得及写）
重新 deduct -&amp;gt; ALREADY_DONE
库存仍 = 9（只扣了一次）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这验证的是第四节第二层防线、第五节原子性的结论：&lt;strong&gt;业务事实（SQLite 里的幂等记录 + 唯一约束）才是&amp;quot;不重复&amp;quot;的最终兜底，跟 checkpoint 有没有写无关。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验二：checkpoint 已保存后恢复，完成剩余操作。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;子进程里，模型决策到「扣库存」这一步、扣库存执行完并落盘 checkpoint 后，脚本耗尽、进程退出（模拟崩溃）。新进程恢复，模型接着做决策——它读到&amp;quot;扣库存已经做过了&amp;quot;的上下文，于是决定发通知，最终 &lt;code&gt;status=COMPLETED&lt;/code&gt;。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;--- (进程崩溃，全新进程恢复) ---
recovered_from_checkpoint
step tool=notify_shipped observation=NOTIFIED
status=COMPLETED
deduct_count=1 notify_count=1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这验证的是第二节的结论：&lt;strong&gt;恢复恢复的是决策上下文，而不是靠一个硬编码的位置指针&lt;/strong&gt;——模型接着 &lt;code&gt;messages&lt;/code&gt; 里的对话继续想、继续做。扣库存没被重放（&lt;code&gt;deduct_count=1&lt;/code&gt;），漏掉的发通知被补上了（&lt;code&gt;notify_count=1&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验三：对已完成的任务再次恢复，不会重新启动业务动作。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;任务已经跑完（checkpoint 里 &lt;code&gt;status=COMPLETED&lt;/code&gt;），再调一次 &lt;code&gt;recover()&lt;/code&gt;。结果没有任何业务动作被触发，&lt;code&gt;deduct_count&lt;/code&gt; 和 &lt;code&gt;notify_count&lt;/code&gt; 都保持 1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验四：验收看的是持久化数据，不是日志。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;四个实验的断言，全部落在 SQLite 里的库存数、幂等记录、以及 checkpoint 里的最终状态上——而不是&amp;quot;日志里打印了几次&amp;quot;。因为日志可以被重放误导，只有持久化数据不会说谎。&lt;/p&gt;
&lt;p&gt;需要诚实说明的是：这一篇的实验是&lt;strong&gt;确定性夹具&lt;/strong&gt;，模型决策被一个顺序脚本模型替代了，没有引入真实 LLM。这样做的理由，恰恰是这一篇的核心不是&amp;quot;模型聪明不聪明&amp;quot;，而是&amp;quot;状态怎么记、业务怎么不重复&amp;quot;——用真实 LLM 反而会引入不确定性和 API 依赖，干扰对恢复机制的验证。恢复机制本身（落盘、运行阶段、幂等、原子性）在这里是被真实验证了的。&lt;/p&gt;
&lt;h2 id="七总结和下一篇"&gt;七、总结和下一篇
&lt;/h2&gt;&lt;p&gt;到这里，我们把这半天的功夫盘一盘：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进度摆到明面&lt;/strong&gt;（&lt;code&gt;next_step&lt;/code&gt; 记录运行阶段，一眼看清循环卡在哪）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键步骤落盘&lt;/strong&gt;（有副作用的步骤执行完立即 checkpoint，断得起）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副作用幂等 + 原子性&lt;/strong&gt;（事务 + 唯一约束，重做不重复生效）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;循环从「只能一口气跑完」，变成了「断了能接着跑，做过的不重做」。&lt;/p&gt;
&lt;p&gt;但我们真正回答的那个问题，值得再复述一遍：&lt;strong&gt;「业务已经成功了，但 Agent 还没来得及记下来，怎么办？」&lt;/strong&gt; 答案是两层的——checkpoint 负责&amp;quot;记得快一点、记的是进度&amp;quot;，而&lt;strong&gt;业务数据自己的幂等记录 + 原子性，负责&amp;quot;就算没记下来，重复执行也不会改变业务结果&amp;quot;。&lt;/strong&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;，零第三方依赖，7 项离线测试）。崩溃恢复用真实两进程模拟，业务数据（库存、幂等记录）落在 SQLite，幂等靠事务 + 唯一约束落地，不依赖外部 API。&lt;/p&gt;

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