<?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/%E6%95%99%E7%A8%8B/</link><description>Recent content in 教程 on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/%E6%95%99%E7%A8%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>第十一话｜ReAct、Plan-and-Execute 还是 Workflow：Agent 的下一步由谁决定？</title><link>https://zh.renxinblog.cn/post/gya-c11-agent-control-modes/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gya-c11-agent-control-modes/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gya-c11-agent-control-modes-cover.png" alt="Featured image of post 第十一话｜ReAct、Plan-and-Execute 还是 Workflow：Agent 的下一步由谁决定？" /&gt;&lt;p&gt;先把话挑明：这篇&lt;strong&gt;不教你搭博客写作工作流，也不给 ReAct、Plan-and-Execute、Workflow 排座次&lt;/strong&gt;，只回答一个问题——&lt;strong&gt;Agent 的下一步由谁决定，谁才能真正让它停下来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面反复出现的“写博客”，只是一件切题标本：它自带审批门和写文件的副作用位点，正好能把“谁决定下一步”照清楚。故事是博客，题目是控制权。&lt;/p&gt;
&lt;p&gt;写 Skill 的时候，你加了一条流程规则：&lt;strong&gt;先生成创作纲领，然后停下等用户确认，确认通过前不准写正文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行起来，Agent 却照常往下冲：生成完纲领，下一秒就去写草稿，或者干脆在循环里来回打转。&lt;/p&gt;
&lt;p&gt;是模型没读懂 Skill 吗？不全是。真正的问题是——&lt;strong&gt;“停下来等确认”这件事，根本不归 Skill 管，也不归模型管，它归运行时代码管。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Agent 的三种流行组织方式，ReAct、Plan-and-Execute、Workflow，本质区别不是“会不会调 LLM”，而是：&lt;strong&gt;下一步行动由谁决定，谁才能真正把这一次 Run 停下来。&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;我们通常说“Agent 决定下一步”，这句话其实把三件不同的事揉在了一起：&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;/td&gt;
					&lt;td&gt;输出动作建议或内容，例如“下一步 &lt;code&gt;write_draft&lt;/code&gt;”&lt;/td&gt;
					&lt;td&gt;业务 Service 提一个方案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;运行时 Runtime&lt;/td&gt;
					&lt;td&gt;决定暴露哪些动作、采纳哪条建议、走哪个状态迁移&lt;/td&gt;
					&lt;td&gt;应用网关 / 策略层&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;执行器 Executor&lt;/td&gt;
					&lt;td&gt;真正产生副作用：写文件、发请求、改数据库&lt;/td&gt;
					&lt;td&gt;DAO / Outbox 写库&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart LR
 LLM[模型] --&gt;|建议动作| RT[Runtime]
 RT --&gt;|暴露可见动作| EX[Executor]
 EX --&gt;|副作用| GW[Action Gateway]
 GW --&gt; OUT[虚拟产物]
 RT --&gt;|固定迁移| WF[Workflow 状态]&lt;/pre&gt;&lt;p&gt;读了这张图，很多“Agent 不可控”的困惑会自动解开：&lt;strong&gt;模型负责“说”，不负责“做”。&lt;/strong&gt; 它可以说出 &lt;code&gt;write_draft&lt;/code&gt;，但要不要做、什么时候做、做之前过几道闸门，由 Runtime 和 Executor 这一侧决定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：控制权不是一句“用了 Workflow”或“用了 StateGraph”就自动获得；它分散在“动作可见性、状态迁移、副作用网关”三处，每一处都必须有代码接手。&lt;/p&gt;
&lt;h2 id="同一个任务三种跑法"&gt;同一个任务，三种跑法
&lt;/h2&gt;&lt;p&gt;用一个故意制造矛盾的隔离任务，把三种模式各跑一遍。任务只有两条规则，还互相打架：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;规则一：先创建创作纲领并等待用户确认。
规则二：不要停下来，直接写出完整草稿。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;任务跑在内存里的&lt;strong&gt;虚拟文章存储&lt;/strong&gt;上：不写公开博客、不提交 Git、不部署。三种模式跑同一段任务，区别马上出来：&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;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;ReAct&lt;/td&gt;
					&lt;td&gt;模型看着 Observation 选下一动作&lt;/td&gt;
					&lt;td&gt;全部动作&lt;/td&gt;
					&lt;td&gt;只有 &lt;code&gt;finish&lt;/code&gt; 或步数上限&lt;/td&gt;
					&lt;td&gt;连走三次 &lt;code&gt;create_brief&lt;/code&gt;，&lt;code&gt;draft_written=false&lt;/code&gt;，从未进入等待&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Plan-and-Execute&lt;/td&gt;
					&lt;td&gt;模型先出一份计划，Executor 顺序执行&lt;/td&gt;
					&lt;td&gt;计划内的动作&lt;/td&gt;
					&lt;td&gt;计划文本里的“等待”停不住&lt;/td&gt;
					&lt;td&gt;计划含 &lt;code&gt;await_brief_approval&lt;/code&gt;，Executor 仍尝试 &lt;code&gt;write_draft&lt;/code&gt;，被网关拦下&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Workflow&lt;/td&gt;
					&lt;td&gt;代码写死状态迁移，模型只生成当前节点内容&lt;/td&gt;
					&lt;td&gt;当前节点需要的内容生成&lt;/td&gt;
					&lt;td&gt;状态机 &lt;code&gt;return&lt;/code&gt;/迁移&lt;/td&gt;
					&lt;td&gt;生成纲领后直接 &lt;code&gt;run_suspended&lt;/code&gt;，Trace 里根本没有 &lt;code&gt;write_draft&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有个反直觉的点：&lt;strong&gt;三种模式其实都调了 LLM。&lt;/strong&gt; 所以别再用“Workflow 不智能、ReAct 才智能”来区分它们。区别只在控制权：ReAct 把“下一步”反复交给模型；Workflow 把“下一步”收回到代码。&lt;/p&gt;
&lt;p&gt;所以选型的第一问是——“这一步由谁拍板才安全、才够用”，而不是“哪个听起来更像 Agent”。&lt;/p&gt;
&lt;h2 id="先跑起来最小对照"&gt;先跑起来：最小对照
&lt;/h2&gt;&lt;p&gt;不写框架，先写一个十秒看懂的最小实验。它只演示一件事：&lt;strong&gt;把 &lt;code&gt;waiting&lt;/code&gt; 改成 &lt;code&gt;True&lt;/code&gt;，不等于让循环停下。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面是完整可粘贴的 Python，不需要 API Key：&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="s2"&gt;&amp;#34;&amp;#34;&amp;#34;“等待”是数据标记，还是控制信号？最小对照。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&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="n"&gt;APPROVED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&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&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;gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;create_brief&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;brief_created_in_virtual_store&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;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_for_approval&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_marker_recorded&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;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;write_draft&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;APPROVED&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="s2"&gt;&amp;#34;draft_written_to_virtual_store&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;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blocked_by_action_gateway&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;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;unknown_action&amp;#34;&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&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;run_naive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&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;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create_brief&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_for_approval&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;write_draft&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;waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;steps&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&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;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_for_approval&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="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;waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;events&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;draft_written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft_written_to_virtual_store&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;[naive] waiting=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;waiting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; draft_written=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;draft_written&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;18s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&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&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;run_suspending&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&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;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create_brief&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_for_approval&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;write_draft&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;waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;steps&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&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;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_for_approval&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;waiting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;events&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;run_suspended&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;[fix] waiting=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;waiting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; draft_written=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;18s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&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="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;# 真正的“停”：后面的 write_draft 根本不会被循环碰到&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;events&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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&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;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;== 错误版：像 Plan-and-Execute 的朴素 Executor ==&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;run_naive&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="nb"&gt;print&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;== 修复版：Workflow 的状态机迁移 ==&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;run_suspending&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;/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;== 错误版：像 Plan-and-Execute 的朴素 Executor ==
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[naive] waiting=True draft_written=False
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; create_brief -&amp;gt; brief_created_in_virtual_store
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; wait_for_approval -&amp;gt; wait_marker_recorded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; write_draft -&amp;gt; blocked_by_action_gateway
&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;== 修复版：Workflow 的状态机迁移 ==
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[fix] waiting=True draft_written=False
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; create_brief -&amp;gt; brief_created_in_virtual_store
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; wait_for_approval -&amp;gt; run_suspended
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;读输出的判断标准：&lt;strong&gt;错误版里 &lt;code&gt;waiting=True&lt;/code&gt; 和 &lt;code&gt;write_draft&lt;/code&gt; 同时出现&lt;/strong&gt;——它“认为”自己在等待，却还在往下执行；修复版里 &lt;code&gt;run_suspended&lt;/code&gt; 出现后，&lt;code&gt;write_draft&lt;/code&gt; 从 Trace 里直接消失。这才是“停”。&lt;/p&gt;
&lt;h2 id="三种模式的真实-trace"&gt;三种模式的真实 Trace
&lt;/h2&gt;&lt;p&gt;把三段真实模型 Trace 缩到关键行。完整实验在配套代码仓库的 Python 与 Java 两个子目录里各有一份。&lt;/p&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="nb"&gt;cd&lt;/span&gt; python
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ran 8 tests in 0.000s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;真实模型（需先导出 &lt;code&gt;LLM_API_URL&lt;/code&gt;、&lt;code&gt;LLM_MODEL&lt;/code&gt;、&lt;code&gt;LLM_API_KEY&lt;/code&gt;，Key 只从环境变量读取）：&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;python3 control_modes.py --live
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本轮 Python 真实输出（节选，共三段）：&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;# ReAct：模型每步都选 create_brief，从不进入等待
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;react&amp;#34;,&amp;#34;step&amp;#34;:1,&amp;#34;decision_maker&amp;#34;:&amp;#34;llm&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;create_brief&amp;#34;,...}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;react&amp;#34;,&amp;#34;step&amp;#34;:2,&amp;#34;decision_maker&amp;#34;:&amp;#34;llm&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;create_brief&amp;#34;,...}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;react&amp;#34;,&amp;#34;step&amp;#34;:3,&amp;#34;decision_maker&amp;#34;:&amp;#34;llm&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;create_brief&amp;#34;,...}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[summary] {&amp;#34;mode&amp;#34;:&amp;#34;react&amp;#34;,&amp;#34;waiting_for_approval&amp;#34;:false,&amp;#34;draft_written&amp;#34;:false,&amp;#34;usage&amp;#34;:{&amp;#34;total_tokens&amp;#34;:427}}
&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;# Plan-and-Execute：计划里有 await，Executor 仍尝试写稿，网关拦下
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;plan_and_execute&amp;#34;,&amp;#34;step&amp;#34;:0,&amp;#34;decision_maker&amp;#34;:&amp;#34;plan_validator&amp;#34;,&amp;#34;result&amp;#34;:&amp;#34;ACCEPT&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;plan_and_execute&amp;#34;,&amp;#34;step&amp;#34;:2,&amp;#34;decision_maker&amp;#34;:&amp;#34;executor&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;await_brief_approval&amp;#34;,&amp;#34;result&amp;#34;:&amp;#34;wait_marker_recorded&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;plan_and_execute&amp;#34;,&amp;#34;step&amp;#34;:3,&amp;#34;decision_maker&amp;#34;:&amp;#34;executor&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;write_draft&amp;#34;,&amp;#34;result&amp;#34;:&amp;#34;blocked_by_action_gateway&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[summary] {&amp;#34;mode&amp;#34;:&amp;#34;plan_and_execute&amp;#34;,&amp;#34;waiting_for_approval&amp;#34;:true,&amp;#34;draft_written&amp;#34;:false,&amp;#34;usage&amp;#34;:{&amp;#34;total_tokens&amp;#34;:142}}
&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;# Workflow：代码固定迁移，模型从未见过 write_draft
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;workflow&amp;#34;,&amp;#34;step&amp;#34;:1,&amp;#34;decision_maker&amp;#34;:&amp;#34;llm&amp;#34;,&amp;#34;event&amp;#34;:&amp;#34;generate_brief&amp;#34;,...}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[trace] {&amp;#34;mode&amp;#34;:&amp;#34;workflow&amp;#34;,&amp;#34;step&amp;#34;:2,&amp;#34;decision_maker&amp;#34;:&amp;#34;workflow&amp;#34;,&amp;#34;action&amp;#34;:&amp;#34;WAITING_FOR_APPROVAL&amp;#34;,&amp;#34;result&amp;#34;:&amp;#34;run_suspended&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[summary] {&amp;#34;mode&amp;#34;:&amp;#34;workflow&amp;#34;,&amp;#34;waiting_for_approval&amp;#34;:true,&amp;#34;draft_written&amp;#34;:false,&amp;#34;usage&amp;#34;:{&amp;#34;total_tokens&amp;#34;:203}}
&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;ReAct 能停吗？&lt;/strong&gt; 能，但停的条件由模型选中的 &lt;code&gt;finish&lt;/code&gt; 或代码给的最大步数决定；“等待”不在它自动遵守的条件里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan-and-Execute 能停吗？&lt;/strong&gt; 计划里写了 &lt;code&gt;await_brief_approval&lt;/code&gt;，但朴素 Executor 把它当普通动作执行——只改状态、继续下一步，最后靠 Action Gateway 拦下 &lt;code&gt;write_draft&lt;/code&gt; 的副作用，可 Executor 并没有因此停，它仍往下走到 &lt;code&gt;finish&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow 能停吗？&lt;/strong&gt; 当前节点生成内容后，代码把下一状态固定成 &lt;code&gt;WAITING_FOR_APPROVAL&lt;/code&gt; 并结束 Run；模型连 &lt;code&gt;write_draft&lt;/code&gt; 这个动作都没被暴露。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：&lt;code&gt;await&lt;/code&gt; 写在计划里是&lt;strong&gt;数据&lt;/strong&gt;，只有 &lt;code&gt;break / return / 状态机迁移&lt;/code&gt; 才是&lt;strong&gt;控制&lt;/strong&gt;。指望模型或 Planner 在文本里“自觉地停下来”，就是把安全押在概率上。&lt;/p&gt;
&lt;h2 id="外层-workflow内层-react"&gt;外层 Workflow，内层 ReAct
&lt;/h2&gt;&lt;p&gt;既然 Workflow 最可控，ReAct 最灵活，生产里怎么选？&lt;/p&gt;
&lt;p&gt;答案是&lt;strong&gt;分层，而不是二选一&lt;/strong&gt;。一个可信的技术文章 Agent 可以长这样：&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;Workflow 固定阶段：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0. 准备材料 ──&amp;gt; 1. 生成纲领 ──&amp;gt; [等待审批] ──&amp;gt; 2. 生成梗概 ──&amp;gt; [等待审批] ──&amp;gt; 3. 写草稿
&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; 探索节点（ReAct）：搜证据、读源码、判断“材料够不够”，直到确定性条件达成
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;外层用 Workflow 锁阶段和硬边界&lt;/strong&gt;：未达到 &lt;code&gt;WAITING_FOR_APPROVAL&lt;/code&gt;，后面的阶段就不存在，模型没机会跳到“写草稿”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内层用 ReAct 降低不确定性&lt;/strong&gt;：一个“读懂这个仓库再写梗概”的开放任务，让模型多走几步、看 Observation，比死板的一次性流程更实用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务清晰后再交给 Planner/Executor&lt;/strong&gt;：计划进入 Executor 前先用确定性校验器检查结构、动作白名单和审批顺序；LLM 可以辅助语义判断，但硬规则必须由代码执行——校验器只返回 &lt;code&gt;ACCEPT / REVISE / REJECT&lt;/code&gt;，不替模型悄悄改计划。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有个很容易被模糊的点：&lt;strong&gt;“能不能进计划”由谁判？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不要让模型直接回一个 &lt;code&gt;ready: true&lt;/code&gt;。那是模型自我声明，等于让它自己给自己批作业。正确的分工是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LLM 交&lt;strong&gt;结构化证据&lt;/strong&gt;：已调研了哪几项、还剩哪几项没做、每项的依据是什么。&lt;/li&gt;
&lt;li&gt;Runtime 按&lt;strong&gt;校验契约&lt;/strong&gt;算布尔：条件满足才算 &lt;code&gt;ready&lt;/code&gt;，证据缺失就补齐或换策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是“模型提建议，代码做决定”在 readiness 上的具体落法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：给开放任务留 ReAct，给高风险边界上 Workflow，再把“能不能切换阶段”写成运行时契约；三者不是竞争关系，是一条流水线上的不同工位。&lt;/p&gt;
&lt;h2 id="workflow-该不该上框架"&gt;Workflow 该不该上框架
&lt;/h2&gt;&lt;p&gt;也许你已经想到 LangGraph 这类框架：它能不能把上面这些自动解决？&lt;/p&gt;
&lt;p&gt;能解决一部分，但要分清“&lt;strong&gt;能力&lt;/strong&gt;”和“&lt;strong&gt;承诺&lt;/strong&gt;”。LangGraph 的文档把两件事分开：Workflow 走&lt;strong&gt;预定义的代码路径&lt;/strong&gt;，Agent 每一步由模型&lt;strong&gt;动态决定过程&lt;/strong&gt;；它用状态图、checkpointer、&lt;code&gt;interrupt()&lt;/code&gt; 提供挂起、保存状态和外部命令恢复的实现能力。这些能力正好是第十二话要做“可恢复、不可绕过的审批”的原料。&lt;/p&gt;
&lt;p&gt;但它&lt;strong&gt;不会因为你用了 LangGraph，就自动保证审批合规&lt;/strong&gt;。状态图上的节点连得通，不代表业务上这一段就该执行。&lt;code&gt;publish_article&lt;/code&gt; 这种迁移在不允许发布的状态下必须由运行时拒绝——框架给你的是“状态图能跑”，不给你“业务边界天然正确”。&lt;/p&gt;
&lt;p&gt;所以这一话只把 LangGraph 标记为实现方向，不展开 API。下一步要去的地方，就是把“接下来该做什么”从人工解读变成可以被恢复、被拦截、被审计的运行时机制。&lt;/p&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;下一话：Skill 只是提示词——如何让 Agent 真正停下来，并且跨进程可以恢复、无法绕过审批。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="踩坑速查"&gt;踩坑速查
&lt;/h2&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;--live&lt;/code&gt; 报 &lt;code&gt;LLM network error&lt;/code&gt; / DNS 解析失败&lt;/td&gt;
					&lt;td&gt;沙箱或本地网络到模型 API 不通&lt;/td&gt;
					&lt;td&gt;用离线 &lt;code&gt;python3 -m unittest -v&lt;/code&gt; 先验证控制流；真实调用再解决网络/代理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型返回值解析失败或 &lt;code&gt;content&lt;/code&gt; 为空&lt;/td&gt;
					&lt;td&gt;部分推理模型默认 thinking，&lt;code&gt;content&lt;/code&gt; 在 reasoning 里&lt;/td&gt;
					&lt;td&gt;显式 &lt;code&gt;thinking:{&amp;quot;type&amp;quot;:&amp;quot;disabled&amp;quot;}&lt;/code&gt; 并要求 JSON 输出&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Java 运行报 &lt;code&gt;UnsupportedClassVersionError&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用了旧版 JDK&lt;/td&gt;
					&lt;td&gt;用 JDK 21 + &lt;code&gt;./gradlew&lt;/code&gt;，或直接 &lt;code&gt;javac/java&lt;/code&gt; 跑 &lt;code&gt;MinimalSuspendDemo&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;计划里写了“等待”，运行时不暂停&lt;/td&gt;
					&lt;td&gt;把等待当成了数据标记&lt;/td&gt;
					&lt;td&gt;改成 &lt;code&gt;return&lt;/code&gt;/&lt;code&gt;break&lt;/code&gt;/状态机迁移，并写进回归测试&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="参考"&gt;参考
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;LangGraph Documentation, “Workflows and Agents”, &lt;a class="link" href="https://docs.langchain.com/oss/python/langgraph/workflows-agents/" target="_blank" rel="noopener"
 &gt;https://docs.langchain.com/oss/python/langgraph/workflows-agents/&lt;/a&gt;（Workflow 与 Agent 的路径决定方式，成文时已核对）。&lt;/li&gt;
&lt;li&gt;LangGraph Documentation, “Interrupts / Human-in-the-loop”, &lt;a class="link" href="https://docs.langchain.com/oss/python/langgraph/interrupts/" target="_blank" rel="noopener"
 &gt;https://docs.langchain.com/oss/python/langgraph/interrupts/&lt;/a&gt;（&lt;code&gt;interrupt()&lt;/code&gt;+checkpointer 的挂起恢复能力，本篇只作方向标记）。&lt;/li&gt;
&lt;li&gt;本话演示代码与回归测试：配套代码仓库中的 &lt;code&gt;python/control_modes.py&lt;/code&gt;、&lt;code&gt;java/.../Main.java&lt;/code&gt;、&lt;code&gt;java/.../MinimalSuspendDemo.java&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第一话｜大模型会聊天，但怎么让它知道你的退货政策？</title><link>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gyr-c01-enterprise-rag-start-cover.png" alt="Featured image of post 第一话｜大模型会聊天，但怎么让它知道你的退货政策？" /&gt;&lt;!--
GYR 第一话：RAG 系列总览
演进主线：裸大模型 → Prompt → 最小 RAG → 企业级知识助手
下一篇：Embedding 与手写 Top-K 检索
--&gt;
&lt;p&gt;“NovaTrail X2 支持 7 天无理由退货吗？”&lt;/p&gt;
&lt;p&gt;把这个问题交给一个裸大模型，它大概率会给出一段很像客服的话：商品需要保持未使用、包装完整，并保留购买凭证。&lt;/p&gt;
&lt;p&gt;听起来没什么问题。&lt;/p&gt;
&lt;p&gt;但它不知道 NovaShop 的真实退货政策。它没有看过这份商品资料，不知道规则适用于哪个地区，也不知道政策是不是昨天刚刚改过。&lt;/p&gt;
&lt;p&gt;它只是生成了一段听起来合理的文字。&lt;/p&gt;
&lt;p&gt;这就是 RAG 系列要解决的起点：&lt;strong&gt;大模型虽然会聊天，但它不知道你的退货政策。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一裸大模型会生成答案但没有企业知识"&gt;一、裸大模型：会生成答案，但没有企业知识
&lt;/h2&gt;&lt;p&gt;先不谈 RAG，看看最原始的系统是什么样：&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;用户问题 → Chat 模型 → 文本回答
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这类模型可以翻译、总结、写代码，也能把客服话术写得很自然。但它的回答依赖两类输入：训练时学到的参数知识，以及本次请求中传入的上下文。&lt;/p&gt;
&lt;p&gt;企业自己的商品政策、内部流程、最新价格和租户数据，通常不在它能够直接访问的上下文里。模型并不会因为“你是 NovaShop 客服”这句话，就自动获得 NovaShop 的资料。&lt;/p&gt;
&lt;p&gt;所以裸模型的第一个边界很清楚：&lt;strong&gt;它可以生成企业知识问答的语言，却没有企业知识问答的证据。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二第一种补救把资料塞进-prompt"&gt;二、第一种补救：把资料塞进 Prompt
&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你是 NovaShop 客服。
&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;NovaTrail X2 支持签收后 7 天内无理由退货，商品须未使用、包装完整，并保留配件和购买凭证。
&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个办法确实有效。模型现在能看到资料，也能根据资料生成回答。&lt;/p&gt;
&lt;p&gt;问题是，企业资料不会永远只有一段。商品规格、物流规则、保修政策、不同地区的退货政策和新旧版本都会继续增加。最后，Prompt 会变成一份越来越长、越来越难维护的知识文档。&lt;/p&gt;
&lt;p&gt;当回答出错时，排查也很困难：资料没有放进去？放进去了但没有检索到？检索到了但模型没有使用？还是引用了已经过期的版本？&lt;/p&gt;
&lt;p&gt;Prompt 方案解决的是“把资料放到模型眼前”，却没有解决“资料如何管理、如何查找、如何证明回答有依据”。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察：&lt;/strong&gt; Prompt 可以临时装下知识，但不能替代一条可维护、可追溯的知识链路。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三第二种补救先检索再生成"&gt;三、第二种补救：先检索，再生成
&lt;/h2&gt;&lt;p&gt;RAG 在 Prompt 方案前面增加了一步：模型回答之前，系统先去资料中找相关内容。&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;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; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Chat 模型生成回答
&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;/code&gt;&lt;/pre&gt;&lt;/div&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;文档准备&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;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;RAG 原始论文把外部检索到的知识接入生成过程，用来缓解参数知识难以更新、访问和追溯的问题。&lt;a class="link" href="https://arxiv.org/abs/2005.11401" target="_blank" rel="noopener"
 &gt;Lewis 等，&lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里还要先纠正一个常见误解：&lt;strong&gt;RAG 不等于向量数据库。&lt;/strong&gt; Retriever 可以使用关键词检索、Elasticsearch 的分词器和倒排索引、向量检索、结构化查询，也可以把几种方式组合成混合检索。本系列会在后续文章里分别比较这些方式；本篇只把“检索”当作一个黑盒步骤，不展开它的内部算法。&lt;a class="link" href="https://www.elastic.co/docs/solutions/search/search-approaches" target="_blank" rel="noopener"
 &gt;Elasticsearch，&lt;em&gt;Search approaches&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="四从最小-rag-到企业级知识助手"&gt;四、从最小 RAG 到企业级知识助手
&lt;/h2&gt;&lt;p&gt;RAG 让模型“回答前先查资料”，但这还只是企业知识助手的中间一层。真正的系统需要沿着两条链路运行。&lt;/p&gt;
&lt;h3 id="1-文档入库链路"&gt;1. 文档入库链路
&lt;/h3&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;文档上传
&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一条链路解决的是“系统手里到底有什么资料”。文档解析决定哪些内容能被看见，分块决定检索的最小单位，索引决定怎么找到它们，版本和来源决定答案能不能回溯。&lt;/p&gt;
&lt;h3 id="2-问答链路"&gt;2. 问答链路
&lt;/h3&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;用户问题
&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;→ 引用、拒答与记录
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两条链路合起来，才是企业级 RAG 的基本轮廓：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart LR
 A["企业文档"] --&gt; B["解析与索引"]
 B --&gt; C["检索"]
 D["用户问题"] --&gt; C
 C --&gt; E["上下文与模型"]
 E --&gt; F["回答与引用"]&lt;/pre&gt;&lt;p&gt;这张图只画了主链路，没有画出所有生产组件。评测、权限、版本、任务恢复和观测会横向影响多个节点，后面分别展开。&lt;/p&gt;
&lt;h2 id="五跑一次全链路预览"&gt;五、跑一次全链路预览
&lt;/h2&gt;&lt;p&gt;理论地图有了，接着跑最小实现。配套代码目前保存在私有工作区的 &lt;code&gt;code/agent-tutorial/c01-rag-overview/&lt;/code&gt;，尚未作为公开代码仓库发布；实验只依赖 Python 3.10+ 标准库。云端运行需要百炼兼容模式的 &lt;code&gt;DASHSCOPE_API_KEY&lt;/code&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="nb"&gt;cd&lt;/span&gt; code/agent-tutorial/c01-rag-overview
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DASHSCOPE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;你的Key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;Chat&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;deepseek-v4-flash-0731&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Embedding&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;qwen3.7-text-embedding&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;没有 Key 时，可以先验证本地链路：&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;python3 main.py --offline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest discover -s tests -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;离线模式只验证这条链路的数据结构、上下文组装和引用格式，不代表云端模型的回答效果。云端跑通的判定标准是：看到模型配置、命中的来源、上下文字符数、回答，以及形如 &lt;code&gt;[source:returns-cn#chunk-001]&lt;/code&gt; 的引用。至于系统为什么会把某些片段排在前面，下一篇再拆开解释。&lt;/p&gt;
&lt;p&gt;2026-08-21 的一次真实运行结果如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dashscope&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;aliyun-bailian&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;chunks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;chat_model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deepseek-v4-flash-0731&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;embedding_model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;qwen3.7-text-embedding&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="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;question&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;NovaTrail X2 是否支持 7 天无理由退货？&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;sources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;returns-cn#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;novatrail-x2#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;context_chars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;支持。根据退货政策，NovaTrail X2 可在签收后 7 天内无理由退货，但需满足商品未使用、包装完整、保留配件和购买凭证等条件。 [source:returns-cn#chunk-001]&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="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;question&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;NovaShop 是否提供月球基地配送？&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;sources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;returns-cn#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;novatrail-x2#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;context_chars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;无法确认。现有资料中未提及月球基地配送服务。&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;/p&gt;
&lt;p&gt;第二个问题暴露了当前 Demo 的边界。资料里没有月球基地配送，但系统还是返回了两个候选片段，因为程序现在固定返回若干候选。模型这次回答“无法确认”，但无关片段已经进入上下文；换一个问题，模型可能会被这些噪声影响。&lt;/p&gt;
&lt;p&gt;这说明“能检索、能生成”还不等于“知识助手可靠”。我们还需要阈值、评测、引用校验和拒答策略。&lt;/p&gt;
&lt;p&gt;如果运行失败，先看错误发生在哪一层：&lt;code&gt;DASHSCOPE_API_KEY is not set&lt;/code&gt; 表示当前 Shell 没有读到 Key；&lt;code&gt;401&lt;/code&gt; 或 &lt;code&gt;403&lt;/code&gt; 通常是 Key 无效、过期或没有权限；&lt;code&gt;404 model not found&lt;/code&gt; 则要检查模型名、地域和百炼工作空间。代码默认使用 &lt;code&gt;aliyun-bailian&lt;/code&gt;、&lt;code&gt;deepseek-v4-flash-0731&lt;/code&gt; 和 &lt;code&gt;qwen3.7-text-embedding&lt;/code&gt;，也可以通过 README 中的环境变量覆盖。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察：&lt;/strong&gt; 这个 Demo 的价值不是证明 RAG 已经可靠，而是把“资料从哪里来、系统找了什么、模型看到了什么、回答引用了什么”第一次暴露出来。下一篇，我们再追问“系统为什么找到了这些片段”。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="六gyr-系列接下来会补齐什么"&gt;六、GYR 系列接下来会补齐什么
&lt;/h2&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;th&gt;主题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;第一话&lt;/td&gt;
					&lt;td&gt;整条链路到底是什么&lt;/td&gt;
					&lt;td&gt;RAG 全景与演进路线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第二话&lt;/td&gt;
					&lt;td&gt;问题和文本为什么可以比较&lt;/td&gt;
					&lt;td&gt;Embedding、余弦相似度、Top-K&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第三话&lt;/td&gt;
					&lt;td&gt;一篇长文应该如何检索&lt;/td&gt;
					&lt;td&gt;文档分块与 Chunk 设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&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;/td&gt;
					&lt;td&gt;如何保存和评价检索结果&lt;/td&gt;
					&lt;td&gt;PGVector、版本与评测基线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&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;/td&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;td&gt;API、模块化原型与框架对照&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这条路线有一个刻意的约束：先手写最小机制，再引入数据库、服务和框架。否则读者很容易得到一个“能启动的 RAG”，却不知道每个组件为什么存在。&lt;/p&gt;
&lt;h2 id="下一篇embedding-到底做了什么"&gt;下一篇：Embedding 到底做了什么
&lt;/h2&gt;&lt;p&gt;现在我们知道 RAG 的整体链路，也看到一个最小版本确实可以把问题和资料交给模型。但“检索”仍然像一个黑盒：为什么退货政策排在前面？相似度分数是什么？SKU 这种精确编号也适合这样检索吗？&lt;/p&gt;
&lt;p&gt;下一篇只回答一个问题：&lt;strong&gt;一句用户问题，为什么可以和一段商品政策计算相似度？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们会检查 Embedding 的真实输出，手写余弦相似度和 Top-K 排序，把第一话图里的“检索”拆开来看。&lt;/p&gt;
&lt;p&gt;第一篇只需要记住一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;RAG 是裸大模型走向企业知识助手的第一条外部知识链路，但它远没有替企业完成评测、权限和数据治理。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Patrick Lewis 等，&lt;a class="link" href="https://arxiv.org/abs/2005.11401" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;&lt;/a&gt;，2020。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/en/model-studio/list-models" target="_blank" rel="noopener"
 &gt;&lt;em&gt;List models&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/deepseek-api" target="_blank" rel="noopener"
 &gt;&lt;em&gt;DeepSeek API&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Elasticsearch，&lt;a class="link" href="https://www.elastic.co/docs/solutions/search/search-approaches" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Search approaches&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>第十话｜Agent 理论发展脉络：从 CoT 到 SWE-Agent</title><link>https://zh.renxinblog.cn/post/c10-agent-theory/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c10-agent-theory/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c10-agent-theory-cover.png" alt="Featured image of post 第十话｜Agent 理论发展脉络：从 CoT 到 SWE-Agent" /&gt;&lt;!--
第十话演示代码: https://github.com/renxin2024/GYA/tree/main/c10-agent-theory-timeline（main.py + README）
演进主线: 阶段4 Agent 理论发展脉络 —— 从“会推理”到“会在环境中完成任务”
上一篇: 第九话｜Skill 系统：把做法变成资产
下一篇: 第十一话｜从 AgentLoop 看经验自进化
--&gt;
&lt;p&gt;Agent 不是“更强的提示词”，也不是把模型接上终端。它是一组逐层补齐的能力：推理、行动、工具使用、反馈、可复用经验，以及适配真实环境的接口。&lt;/p&gt;
&lt;p&gt;这条线从 CoT 延伸到 SWE-agent。把六篇论文按“上一步已经解决了什么，人们接下来还想要什么”来读，名词表就会变成一张工程问题地图。&lt;/p&gt;
&lt;h2 id="先给结论六篇论文六个能力缺口"&gt;先给结论：六篇论文，六个能力缺口
&lt;/h2&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;CoT&lt;/td&gt;
					&lt;td&gt;复杂问题如何不只猜最终答案？&lt;/td&gt;
					&lt;td&gt;显式中间推理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ReAct&lt;/td&gt;
					&lt;td&gt;推理如何接触外部环境？&lt;/td&gt;
					&lt;td&gt;Thought → Action → Observation 循环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Toolformer&lt;/td&gt;
					&lt;td&gt;模型如何知道何时、怎样用 API？&lt;/td&gt;
					&lt;td&gt;工具使用的学习问题&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Reflexion&lt;/td&gt;
					&lt;td&gt;一次失败如何影响下一次尝试？&lt;/td&gt;
					&lt;td&gt;语言化反馈进入后续上下文&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Voyager&lt;/td&gt;
					&lt;td&gt;成功行为如何不必每次重学？&lt;/td&gt;
					&lt;td&gt;可执行 Skill 的积累与复用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SWE-agent&lt;/td&gt;
					&lt;td&gt;代码任务环境怎样才适合 Agent 使用？&lt;/td&gt;
					&lt;td&gt;面向 Agent 的软件接口（ACI）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;它们不是六个互相替代的框架。更准确地说，后面的研究把 Agent 放进了更长、更真实的任务链条。&lt;/p&gt;
&lt;h2 id="cot先把答案拆成过程"&gt;CoT：先把“答案”拆成过程
&lt;/h2&gt;&lt;p&gt;对于多步计算或符号推理，直接要求最终答案，系统无法暴露中间哪一步出了问题。Chain-of-Thought Prompting 的做法是提供“问题—中间推理—答案”的示例，让模型生成中间步骤再到达结论。&lt;/p&gt;
&lt;p&gt;论文报告，在 GSM8K 等任务上，540B 参数的 PaLM 配合 8 个 CoT 示例取得了当时的领先表现。这是论文在特定模型与 few-shot 设置下的结果，不是所有模型都会自动获得的保证。&lt;a class="link" href="https://arxiv.org/abs/2201.11903" target="_blank" rel="noopener"
 &gt;Chain-of-Thought Prompting&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;CoT 解决的是“如何展开思考”。它没有让模型查询数据库、读取网页或运行程序；一条流畅的推理链仍可能建立在错误前提上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：CoT 让答案过程可见，但还没有让推理接受环境校验。&lt;/p&gt;
&lt;h2 id="react把环境反馈送回下一轮推理"&gt;ReAct：把环境反馈送回下一轮推理
&lt;/h2&gt;&lt;p&gt;当问题依赖外部事实或真实操作时，人们自然会想：模型能不能先判断缺什么，再行动，再根据结果决定下一步？&lt;/p&gt;
&lt;p&gt;ReAct 将 reasoning traces 和 actions 交织：模型输出 Thought 与 Action，运行时执行 Action 并返回 Observation，Observation 再成为下一轮 Thought 的输入。&lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;ReAct&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="ReAct 的关键不是字段数量，而是环境反馈回到下一轮决策" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/c10-agent-theory-react-loop.svg"&gt;&lt;/p&gt;
&lt;p&gt;论文在问答、事实验证及交互任务上评估该范式；在只给 1～2 个 in-context 示例的设置中，论文报告 ReAct 在 ALFWorld 与 WebShop 上相对相关基线分别有 34 与 10 个百分点的绝对提升。该数字只描述论文的对应实验，不能外推为任意 Agent 的成功率。&lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;ReAct&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这也是工程里常见的 Agent loop：&lt;code&gt;LLM → Action → Tool → Observation → LLM&lt;/code&gt;。重点不是把输出格式命名为 Thought，而是 Observation 必须来自可执行的外部环境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：CoT 让模型解释自己的思路；ReAct 让思路能够被现实结果修正。&lt;/p&gt;
&lt;h2 id="toolformer把能调工具与会用工具分开"&gt;Toolformer：把“能调工具”与“会用工具”分开
&lt;/h2&gt;&lt;p&gt;ReAct 描述了推理与行动如何交替，但仍有一层问题：模型如何判断要不要调用工具、选择哪一个 API、填哪些参数，以及如何使用返回值？&lt;/p&gt;
&lt;p&gt;Toolformer 研究的正是这种自监督的工具使用学习：模型学习 API 的调用时机、参数与结果整合。&lt;a class="link" href="https://arxiv.org/abs/2302.04761" target="_blank" rel="noopener"
 &gt;Toolformer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这与今天工程里常说的 Function Calling 或 MCP 不能混为一谈：&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;ReAct&lt;/td&gt;
					&lt;td&gt;推理、行动、观察如何组成一个运行循环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Toolformer&lt;/td&gt;
					&lt;td&gt;模型如何学习 API 使用行为&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Function Calling API&lt;/td&gt;
					&lt;td&gt;应用怎样接收结构化调用请求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;MCP&lt;/td&gt;
					&lt;td&gt;Agent 客户端怎样与工具提供方建立标准连接&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以，&lt;code&gt;tool_calls&lt;/code&gt; JSON 解决的是接口表达；它不等同于模型已经具备可靠的工具选择能力。一个工程系统仍要负责执行、校验参数、限制权限，并把结果回填到上下文。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：协议让工具“可被调用”，学习与运行时设计才决定工具“会不会被正确调用”。&lt;/p&gt;
&lt;h2 id="reflexion-与-voyager经验要么成为反馈要么成为资产"&gt;Reflexion 与 Voyager：经验要么成为反馈，要么成为资产
&lt;/h2&gt;&lt;p&gt;一个 ReAct 轨迹结束后，下一次类似任务能否少走弯路？Reflexion 的答案是把外部反馈转写为语言反思，保存为 episodic memory，在后续尝试时作为上下文使用；它并不要求更新模型权重。&lt;a class="link" href="https://arxiv.org/abs/2303.11366" target="_blank" rel="noopener"
 &gt;Reflexion&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;论文在 HumanEval 的特定设置中报告 91% pass@1，并以其中的 GPT-4 对照结果 80% 作比较。这个数字属于论文实验条件，不能当作当今模型或任意实现的通用结论。&lt;a class="link" href="https://arxiv.org/abs/2303.11366" target="_blank" rel="noopener"
 &gt;Reflexion&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Voyager 再往前走一步：在 Minecraft 中结合自动课程、不断增长的可执行 Skill Library，以及环境反馈、执行错误与自我验证的迭代提示，让完成过的行为可被再次调用。&lt;a class="link" href="https://arxiv.org/abs/2305.16291" target="_blank" rel="noopener"
 &gt;Voyager&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Agent 能力演进：从推理到行动、反馈资产与软件环境" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/c10-agent-theory-capability-map.svg"&gt;&lt;/p&gt;
&lt;p&gt;Reflexion 主要留下“下次应该注意什么”的语言反馈；Voyager 的 Skill Library 试图留下“已经会做什么”的可执行资产。两者都不是企业平台的现成蓝图：前者需要控制反思的质量与检索，后者的 Minecraft 结果也不能直接等同于业务系统。但它们明确了经验积累的两种工程形态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：反馈能减少重复犯错；被验证、可调用的行为才可能成为 Skill。&lt;/p&gt;
&lt;h2 id="swe-agent能力还取决于-agent-看见怎样的界面"&gt;SWE-agent：能力还取决于 Agent 看见怎样的界面
&lt;/h2&gt;&lt;p&gt;把 Agent 放进代码仓库，任务不再只是回答：它要逐步浏览文件、搜索符号、局部修改、运行测试、读取报错，再决定是否继续。此时“给一个万能 shell”并不必然带来稳定的任务推进。&lt;/p&gt;
&lt;p&gt;SWE-agent 提出 Agent-Computer Interface（ACI），讨论为仓库浏览、编辑与测试等活动设计更适合 Agent 的交互界面。论文在其评测设置中报告 SWE-bench 12.5% 和 HumanEvalFix 87.7% 的 pass@1；这些数值同样受模型、提示、接口和评测版本约束。&lt;a class="link" href="https://arxiv.org/abs/2405.15793" target="_blank" rel="noopener"
 &gt;SWE-agent&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;关注点&lt;/th&gt;
					&lt;th&gt;不利于 Agent 的接口&lt;/th&gt;
					&lt;th&gt;面向 Agent 的接口目标&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&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;/td&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;td&gt;返回可定位、可继续推理的反馈&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&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;这把讨论从“模型是否够聪明”推进到“环境是否给了恰当的可观察性与可操作性”。对于 Coding Agent，工具颗粒度、输出格式、沙箱和测试反馈都是能力的一部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：Agent 的性能不只来自模型，也来自它能够安全、清楚地感知和操作的环境。&lt;/p&gt;
&lt;h2 id="动手跑一遍可验证的-react-控制流"&gt;动手：跑一遍可验证的 ReAct 控制流
&lt;/h2&gt;&lt;p&gt;配套 demo 位于 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c10-agent-theory-timeline" target="_blank" rel="noopener"
 &gt;GYA/c10-agent-theory-timeline&lt;/a&gt;。默认是离线 replay，Python 3.9+、无第三方依赖、无 API Key；它验证控制流，不模拟论文 benchmark，也不声称调用真实模型。&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;git clone https://github.com/renxin2024/GYA.git
&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; GYA/c10-agent-theory-timeline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本次以 &lt;code&gt;python3 --version &amp;amp;&amp;amp; python3 main.py&lt;/code&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;Python 3.9.6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Thought] I need an external fact before answering.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Action] lookup({&amp;#34;query&amp;#34;: &amp;#34;capital of France&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Observation] Paris is the capital and most populous city of France.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Final Answer] The answer is Paris is the capital and most populous city of France.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[check] actions=1 observations=1 terminated=final_answer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;跑通标准不是记住“巴黎”，而是看到一次 &lt;code&gt;Action → Observation → Final Answer&lt;/code&gt; 的闭环。若执行时出现语法错误，请确认 &lt;code&gt;python3 --version&lt;/code&gt; 不低于 3.9；若使用 &lt;code&gt;--live&lt;/code&gt; 报 &lt;code&gt;DEEPSEEK_API_KEY is required&lt;/code&gt;，这是正常的运行时保护，需配置密钥以及兼容的 &lt;code&gt;LLM_API_URL&lt;/code&gt;、&lt;code&gt;LLM_MODEL&lt;/code&gt;。本文未运行 &lt;code&gt;--live&lt;/code&gt;，因此不报告其输出或效果。&lt;/p&gt;
&lt;h2 id="下一步按能力缺口设计而不是按论文堆组件"&gt;下一步：按能力缺口设计，而不是按论文堆组件
&lt;/h2&gt;&lt;p&gt;读这条脉络时，最有用的问题不是“下一篇 Agent 论文叫什么”，而是：当前任务到底缺少推理展开、外部观察、工具选择、失败反馈、可复用 Skill，还是环境接口？&lt;/p&gt;
&lt;p&gt;这个判断决定你该改 prompt、工具契约、记忆策略、Skill 流程还是执行环境。知识检索与 RAG 是另一条专题路线，本文不展开。&lt;/p&gt;
&lt;p&gt;下一篇将从 Agent 运行轨迹出发，讨论怎样把反复出现、且已经验证过的做法沉淀为新 Skill。&lt;/p&gt;
&lt;h2 id="理解检验"&gt;理解检验
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;为什么 CoT 的中间步骤不能替代 ReAct 的 Observation？&lt;/li&gt;
&lt;li&gt;为什么 Function Calling 的 JSON 契约不等于 Toolformer 的研究问题？&lt;/li&gt;
&lt;li&gt;Reflexion 的语言反馈与 Voyager 的可执行 Skill 分别留下了什么？&lt;/li&gt;
&lt;li&gt;面对一个不稳定的 Coding Agent，为什么先审视 ACI 可能比先换模型更有效？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2201.11903" target="_blank" rel="noopener"
 &gt;Chain-of-Thought Prompting Elicits Reasoning in Large Language Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;ReAct: Synergizing Reasoning and Acting in Language Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2302.04761" target="_blank" rel="noopener"
 &gt;Toolformer: Language Models Can Teach Themselves to Use Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2303.11366" target="_blank" rel="noopener"
 &gt;Reflexion: Language Agents with Verbal Reinforcement Learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2305.16291" target="_blank" rel="noopener"
 &gt;Voyager: An Open-Ended Embodied Agent with Large Language Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://arxiv.org/abs/2405.15793" target="_blank" rel="noopener"
 &gt;SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第九话｜Skill 系统：把做法变成资产</title><link>https://zh.renxinblog.cn/post/c09-skill-system/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c09-skill-system/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c09-skill-system-cover.png" alt="Featured image of post 第九话｜Skill 系统：把做法变成资产" /&gt;&lt;p&gt;第八话里，工具接入终于有了统一的边界。&lt;/p&gt;
&lt;p&gt;Agent 想查天气，可以调用 MCP Server；想读数据库，也可以通过同一套协议发现和调用工具。工具来自另一个进程、另一种语言，甚至另一个团队时，调用方不必再为每一项能力重写适配代码。&lt;/p&gt;
&lt;p&gt;可一个任务通常不只缺一个工具。&lt;/p&gt;
&lt;p&gt;让 Agent 检查一篇文章，它也许找得到文件检查工具，却不知道应先看 frontmatter 还是标题；它也许发现了错误，却不知道能否直接改原文；它也许完成了几步操作，却没有明确的完成判定。工具越来越多，“应该怎样把事情做完整”反而更值得被固定下来。&lt;/p&gt;
&lt;p&gt;先说结论：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;MCP 解决“Agent 能调用什么”，Skill 解决“Agent 应该按什么方法完成任务”。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Skill 不是把 Prompt 写得更长。它把一套经过思考的方法拆成能被发现、按需加载、执行和验证的资源。&lt;/p&gt;
&lt;h2 id="一工具暴露动作skill-组织方法"&gt;一、工具暴露动作，Skill 组织方法
&lt;/h2&gt;&lt;p&gt;假设我们有一个 Markdown 检查工具。它能接收文件路径、返回检查结果，也许还支持修复参数。这些信息足以描述一个动作，却不能自动给出一条可靠的任务路径。&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;检查哪个文件&lt;/td&gt;
					&lt;td&gt;可以&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;先检查 frontmatter 还是标题&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;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;这和 Java 服务里已经注册了一组接口很像：&lt;code&gt;checkFrontmatter()&lt;/code&gt;、&lt;code&gt;checkHeading()&lt;/code&gt;、&lt;code&gt;checkLinks()&lt;/code&gt; 都可调用，并不等于“文章质量检查”这个业务流程已经存在。顺序、分支、验收标准和修改权限仍需要被组织起来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：工具把能力暴露出来，Skill 把能力组织成方法。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二一个-skill-到底保存了什么"&gt;二、一个 Skill 到底保存了什么
&lt;/h2&gt;&lt;p&gt;人说“我会检查博客文章”时，脑中通常不只是一句提醒，还包括适用场景、检查步骤、参考标准、可以交给脚本的部分，以及失败时应如何收尾。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://agentskills.io/specification" target="_blank" rel="noopener"
 &gt;Agent Skills Specification&lt;/a&gt; 规定，一个 Skill 至少是包含 &lt;code&gt;SKILL.md&lt;/code&gt; 的目录；&lt;code&gt;scripts/&lt;/code&gt;、&lt;code&gt;references/&lt;/code&gt; 和 &lt;code&gt;assets/&lt;/code&gt; 都是按任务需要才加入的可选资源。真正重要的不是目录长相，而是把原本藏在人脑中的做法，拆成可审阅、可替换的职责。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Skill 把发现、说明、资料、脚本和验证边界组织成能力包" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/c09-skill-system-capability-package.svg"&gt;&lt;/p&gt;
&lt;p&gt;图中有一个容易被忽略的顺序：先用 &lt;code&gt;name + description&lt;/code&gt; 判断这个 Skill 是否相关；匹配后才加载 &lt;code&gt;SKILL.md&lt;/code&gt;；执行时再按需要读取参考资料、调用脚本或复用资产。目录的作用不是堆文件，而是让不同类型的知识在恰当的时机进入任务。&lt;/p&gt;
&lt;p&gt;一个过于抽象的写法往往没有可执行性：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Markdown 质量检查
&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&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-markdown" data-lang="markdown"&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;name: markdown-quality
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;description: Check Markdown articles for required frontmatter, one H1 title, and a closing summary.
&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 class="k"&gt;1.&lt;/span&gt; 读取 &lt;span class="sb"&gt;`references/checklist.md`&lt;/span&gt;。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 运行 &lt;span class="sb"&gt;`scripts/validate.py &amp;lt;markdown-file&amp;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;3.&lt;/span&gt; 报告每条失败规则和可操作的修复建议。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&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="三skill-如何被发现又为何不必一次读完"&gt;三、Skill 如何被发现，又为何不必一次读完
&lt;/h2&gt;&lt;p&gt;一个 Agent 不需要在每次任务开始时，把所有 Skill 的全部说明、资料和脚本都塞进上下文。官方 Quickstart 把过程拆成发现、激活和执行：启动时先看到技能的 &lt;code&gt;name&lt;/code&gt; 与 &lt;code&gt;description&lt;/code&gt;；任务匹配后再读取完整 &lt;code&gt;SKILL.md&lt;/code&gt;；真正执行时才访问需要的资源。&lt;a class="link" href="https://agentskills.io/skill-creation/quickstart" target="_blank" rel="noopener"
 &gt;Agent Skills Quickstart&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Skill 的渐进加载：只在任务匹配后加载指令，并按需读取资料和调用脚本" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/c09-skill-system-progressive-disclosure.svg"&gt;&lt;/p&gt;
&lt;p&gt;这张图描述的是分层加载，而不是某个模型保证会做出的路由决定。&lt;code&gt;description&lt;/code&gt; 写得太泛，匹配阶段就缺少足够线索；把无关资料全塞进入口文件，又会让每次执行背负不必要的上下文。一个好的描述更像服务注册中心里的服务名片：它不执行业务，却决定调用方能否先找到正确入口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：Skill 的入口要足够具体以便被发现，细节则应在真正需要时才加载。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="四动手运行一个最小-skill-host"&gt;四、动手：运行一个最小 Skill Host
&lt;/h2&gt;&lt;p&gt;这一节不模拟 LLM 推理，也不声称模型一定会选择正确 Skill。它只把一个 Skill Host 的协议边界固定下来：扫描 frontmatter、按 &lt;code&gt;description&lt;/code&gt; 做最小匹配、加载指令、调用确定性验证脚本。&lt;/p&gt;
&lt;p&gt;代码位于 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c09-skill-system" target="_blank" rel="noopener"
 &gt;GYA 的 &lt;code&gt;c09-skill-system&lt;/code&gt;&lt;/a&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;c09-skill-system/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── main.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── skills/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── markdown-quality/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── SKILL.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── references/checklist.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── scripts/validate.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── samples/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── good.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── bad.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;运行环境只有 Python 3.9+，不依赖第三方包，也不需要 API Key：&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;git clone https://github.com/renxin2024/GYA.git
&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; GYA/c09-skill-system
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;2026-08-22 在 Python 3.9.6 上的实际输出如下：&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;[discover] markdown-quality
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[match] markdown-quality
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[load] SKILL.md + references/checklist.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[validate] good.md -&amp;gt; PASS: frontmatter=ok h1=1 summary=ok
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[validate] bad.md -&amp;gt; FAIL: frontmatter must be delimited by ---; missing closing section: ## 总结
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[validate] passed=1 failed=1 (expected)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[result] status=PASS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里最后的 &lt;code&gt;status=PASS&lt;/code&gt; 不表示两份样例都合格。它表示 Host 的验收条件被满足：合格样例通过，故意损坏的样例被拦截。把“验证器工作正常”和“被验证对象合格”分开，是写 Skill 时很重要的工程习惯。&lt;/p&gt;
&lt;p&gt;如果运行结果不同，先按下面顺序排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认当前目录是 &lt;code&gt;GYA/c09-skill-system&lt;/code&gt;，否则 &lt;code&gt;main.py&lt;/code&gt; 找不到相对路径下的 Skill 和样例。&lt;/li&gt;
&lt;li&gt;确认使用 Python 3.9+；本 demo 使用了 Python 3.9 开始支持的内置泛型类型标注。&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;skills/markdown-quality/SKILL.md&lt;/code&gt; 是否保留了 &lt;code&gt;references/checklist.md&lt;/code&gt; 这一引用；demo 会把缺少该引用视为无效 Skill。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="五哪些工作交给模型哪些交给脚本"&gt;五、哪些工作交给模型，哪些交给脚本
&lt;/h2&gt;&lt;p&gt;Skill 不等于“把所有步骤都交给脚本”。模型仍然适合开放判断，例如观点是否清楚、语气是否自然、解释是否遗漏读者前提。脚本更适合结果只有明确对错的事情。&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;/td&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;td&gt;需要综合语言和读者语境&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;统计 H1 数量&lt;/td&gt;
					&lt;td&gt;脚本&lt;/td&gt;
					&lt;td&gt;可得到稳定的确定性结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;检查 JSON 能否解析&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;/td&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;td&gt;正确性不等于被授权&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;模型处理语义，脚本锁住不应漂移的事实，权限系统控制不可逆影响。Skill 的价值正是在任务层把这三类职责串成一条可检查的方法，而不是把其中任何一类神化成万能方案。&lt;/p&gt;
&lt;h2 id="六promptmemorymcpskill-各自解决什么"&gt;六、Prompt、Memory、MCP、Skill 各自解决什么
&lt;/h2&gt;&lt;p&gt;这几个词经常同时出现在 Agent 架构里，但它们回答的问题不同。&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;th&gt;不能替代什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Prompt&lt;/td&gt;
					&lt;td&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;Memory&lt;/td&gt;
					&lt;td&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;MCP&lt;/td&gt;
					&lt;td&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;Skill&lt;/td&gt;
					&lt;td&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;在一个真实任务里，Memory 可以补充用户偏好，MCP 可以提供读写外部系统的工具，Skill 可以规定检查流程，Agent Loop 负责推进步骤，脚本负责验证局部结果。它们相互协作，但不应互相冒充。&lt;/p&gt;
&lt;h2 id="七从-demo-到生产skill-还要补什么"&gt;七、从 demo 到生产，Skill 还要补什么
&lt;/h2&gt;&lt;p&gt;把 &lt;code&gt;SKILL.md&lt;/code&gt; 写出来只是起点。一个错误的 Skill 可能让 Agent 更稳定地执行错误流程，所以生产环境至少还要补上几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用真实样例检验它是否会在该触发时被发现；&lt;/li&gt;
&lt;li&gt;用故意失败的样例确认验证器真的能拦住错误；&lt;/li&gt;
&lt;li&gt;为规则变化保留版本和原因；&lt;/li&gt;
&lt;li&gt;将写文件、发消息、发布、部署等高影响动作放在明确的权限边界后；&lt;/li&gt;
&lt;li&gt;记录执行轨迹，把重复失败沉淀为下一版规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些是从本文 demo 推导出的工程实践，不代表这个几十行的示例已经具备完整生产能力。它的职责只有一个：让“发现—加载—执行—验证”的边界变得可见、可运行、可讨论。&lt;/p&gt;
&lt;h2 id="结尾方法也应成为可复用的能力"&gt;结尾：方法也应成为可复用的能力
&lt;/h2&gt;&lt;p&gt;第八话解决了工具如何标准化接入；第九话继续回答工具接入之后的事：怎样把经过验证的做法保存下来，让下一次任务不必从零猜测。&lt;/p&gt;
&lt;p&gt;当 Agent 既能调用工具，又能复用方法时，新的追问自然出现：这些推理和行动方式又是怎样一步步演化出来的？下一篇，我们沿着 CoT、ReAct、Toolformer、Reflexion、Voyager 到 SWE-Agent 的线索继续往前看。&lt;/p&gt;
&lt;h2 id="理解检验"&gt;理解检验
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;如果一个 Skill 只有 &lt;code&gt;SKILL.md&lt;/code&gt;，没有脚本和参考资料，它仍然可以成立吗？为什么？&lt;/li&gt;
&lt;li&gt;为什么“验证器通过”和“被验证对象通过”不能混为一谈？&lt;/li&gt;
&lt;li&gt;当一个任务需要读取用户偏好、调用文件工具、并检查输出格式时，Memory、MCP、Skill 和脚本各自承担什么职责？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://agentskills.io/specification" target="_blank" rel="noopener"
 &gt;Agent Skills Specification&lt;/a&gt;：Skill 的最小目录、frontmatter、可选资源与渐进加载约定。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://agentskills.io/skill-creation/quickstart" target="_blank" rel="noopener"
 &gt;Agent Skills Quickstart&lt;/a&gt;：发现、激活和执行的最小示例。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c09-skill-system" target="_blank" rel="noopener"
 &gt;GYA 第九话 demo&lt;/a&gt;：本文配套的最小 Skill Host；本文运行记录验证日期为 2026-08-22。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第八话｜MCP 协议：工具为什么需要一根 USB-C</title><link>https://zh.renxinblog.cn/post/c08-mcp/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c08-mcp/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c08-mcp-cover.png" alt="Featured image of post 第八话｜MCP 协议：工具为什么需要一根 USB-C" /&gt;&lt;!--
第八话演示代码:
Python: https://github.com/renxin2024/GYA/tree/main/c08-mcp
Java 21: https://github.com/renxin2024/GYA-Java/tree/main/c08-mcp
演进主线: 阶段3 MCP —— 从进程内 ToolRegistry 到跨进程、跨语言的工具协议
上一篇: 第七话｜记忆：上下文、短期、长期
后续篇章：重写后重新发布
--&gt;
&lt;p&gt;第四话里，我们把工具放进了一个 &lt;code&gt;ToolRegistry&lt;/code&gt;。工具多了以后，注册表比 &lt;code&gt;if-else&lt;/code&gt; 强很多：可以发现工具、统一调用、返回结构化错误。&lt;/p&gt;
&lt;p&gt;但它有一个隐含前提：&lt;strong&gt;工具和 Agent 在同一个进程，至少在同一套代码里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果天气服务是另一个 Python 进程，数据库工具是 Java 服务，GitHub 工具由另一个团队维护，第四话的注册表还能直接解决吗？不能。你需要给每一种外部工具写一套适配器，工具数量和接入方数量一增加，就会重新掉进 N×M 集成地狱。&lt;/p&gt;
&lt;p&gt;这就是 MCP 要解决的问题。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;ToolRegistry 管理进程内工具，MCP 标准化工具提供方和 Agent 客户端之间的边界。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一旧世界工具被绑在调用方代码里"&gt;一、旧世界：工具被绑在调用方代码里
&lt;/h2&gt;&lt;p&gt;第四话的最小闭环是：模型输出工具名和参数，注册表根据名称找到函数，函数执行后返回 &lt;code&gt;ToolResponse&lt;/code&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;LLM → ToolRegistry → Python 函数 → ToolResponse → LLM
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个结构对学习和单体应用非常好。你能清楚看到每一层的职责，也能自己控制错误码和参数校验。&lt;/p&gt;
&lt;p&gt;问题出在边界。假设 Agent 需要 20 个工具，而这 20 个工具分布在 5 个独立服务里。没有统一协议时，客户端要知道每个服务如何启动、如何描述工具、如何传参数、如何返回结果。每接一个服务，都要重新写一套通信和适配代码。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[Agent 客户端] --&gt; P[多套私有适配器]
 P --&gt; S[多个外部服务]&lt;/pre&gt;&lt;p&gt;这不是工具本身太多，而是每个客户端都在重复理解每个服务的私有接口。&lt;/p&gt;
&lt;h2 id="二mcp-把什么放到了协议层"&gt;二、MCP 把什么放到了协议层
&lt;/h2&gt;&lt;p&gt;MCP 可以先用三个角色理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Host&lt;/strong&gt;：承载 Agent 的应用，例如桌面助手、IDE 或你的 Python 程序；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Client&lt;/strong&gt;：Host 内部的协议客户端，负责连接某一个 MCP Server；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;：工具、资源或提示模板的提供方。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们的关系不是“模型直接连接 MCP Server”，而是：&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;Host
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── MCP Client ── JSON-RPC/Transport ── MCP Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── tools
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一次最小工具调用大致经历：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client 与 Server 初始化并协商协议版本和能力；&lt;/li&gt;
&lt;li&gt;Client 请求工具列表；&lt;/li&gt;
&lt;li&gt;Server 返回工具名称、描述和输入 Schema；&lt;/li&gt;
&lt;li&gt;Client 发起工具调用；&lt;/li&gt;
&lt;li&gt;Server 执行函数并返回内容或错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里有一个重要边界：MCP 标准化的是“怎么发现和调用”，不是“下一步该调用什么”。任务规划仍然由 Agent 循环和模型负责，工具的权限、业务正确性和结果验证仍然由应用负责。&lt;/p&gt;
&lt;h2 id="三stdio先把跨进程边界跑通"&gt;三、STDIO：先把跨进程边界跑通
&lt;/h2&gt;&lt;p&gt;MCP 支持多种传输方式。第八话先选择 STDIO，因为它最容易看清进程边界：Client 启动 Server 子进程，JSON-RPC 消息通过 stdin/stdout 传递。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 C[MCP Client] --&gt; I[initialize]
 I --&gt; L[tools/list]
 L --&gt; X[tools/call add]
 X --&gt; R[CallToolResult]&lt;/pre&gt;&lt;p&gt;STDIO 有一个很容易踩的坑：&lt;strong&gt;stdout 是协议通道，不是日志通道。&lt;/strong&gt; Server 如果把调试日志打印到 stdout，Client 读到的就不再是合法的 JSON-RPC 消息。日志应该写 stderr，或者通过 MCP 的 logging 能力发送。&lt;/p&gt;
&lt;p&gt;生产环境更常用 Streamable HTTP：Server 可以独立部署，多个 Client 通过网络连接。但这同时带来认证、超时、并发、TLS、会话和部署问题。先跑通 STDIO，再讨论 HTTP，学习成本更低。&lt;/p&gt;
&lt;h2 id="四python用官方-sdk-跑通-clientserver"&gt;四、Python：用官方 SDK 跑通 Client/Server
&lt;/h2&gt;&lt;h3 id="前置环境"&gt;前置环境
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Python 3.10+；本次用 Python 3.11.15 实测；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcp[cli]==2.0.0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不需要 LLM API Key。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码位于 &lt;code&gt;GYA/c08-mcp/&lt;/code&gt;。Server 只有一个工具：&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="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;mcp.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MCPServer&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="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;c08-demo&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&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="nd"&gt;@mcp.tool&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&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="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Add two integers and return the result.&amp;#34;&amp;#34;&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;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&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&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="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&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;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&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;类型注解不仅服务于 Python 类型检查，也帮助 SDK 生成工具输入 Schema。Client 不需要自己手写 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt; 的 JSON Schema，就能通过 &lt;code&gt;tools/list&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;stdio_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_stream&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;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&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;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialize&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;listed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_tools&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;add&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;b&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&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;code&gt;mcp==2.0.0&lt;/code&gt; 的实际 API：&lt;code&gt;stdio_client&lt;/code&gt; 返回读写流，再交给 &lt;code&gt;ClientSession&lt;/code&gt;。在线文档中的更高层 &lt;code&gt;Client(StdioServerParameters(...))&lt;/code&gt; 写法属于更新的文档线，不能未经验证地复制到锁定版本。&lt;/p&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="nb"&gt;cd&lt;/span&gt; GYA/c08-mcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m venv .venv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m pip install -r requirements.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.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;[1] 初始化 MCP Server: experimental={} ... tools=ToolsCapability(list_changed=False) ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[2] 发现工具: [&amp;#39;add&amp;#39;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[3] 调用 add(2, 3): 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[4] 错误场景: is_error=True; Unknown tool: missing_tool
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Python SDK 把未知工具作为一个带 &lt;code&gt;is_error=True&lt;/code&gt; 的工具结果返回，Client 不会因为这个工具级错误直接崩溃。&lt;/p&gt;
&lt;h2 id="五java-21同一个协议不同的-sdk-表达"&gt;五、Java 21：同一个协议，不同的 SDK 表达
&lt;/h2&gt;&lt;h3 id="前置环境-1"&gt;前置环境
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Java 21；&lt;/li&gt;
&lt;li&gt;Gradle Wrapper 8.14.2；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;io.modelcontextprotocol.sdk:mcp:2.0.0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不需要 LLM API Key。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码位于 &lt;code&gt;GYA-Java/c08-mcp/&lt;/code&gt;。Java 版使用 &lt;code&gt;StdioServerTransportProvider&lt;/code&gt; 暴露 Server，使用 &lt;code&gt;StdioClientTransport&lt;/code&gt; 启动 Server 子进程。工具定义需要显式提供输入 Schema：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;McpSyncServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;c08-java-demo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;1.0.0&amp;#34;&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServerCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolCall&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;Tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;add&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;schema&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Add two integers&amp;#34;&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&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="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;intValue&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="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;b&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;intValue&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;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;McpSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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="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="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;/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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; GYA-Java
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;/usr/libexec/java_home -v 21&lt;span class="k"&gt;)&lt;/span&gt; ./gradlew :c08-mcp:run
&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;[1] 初始化 MCP Server: c08-java-demo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[2] 发现工具: [add]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[3] 调用 add(2, 3): 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[4] 错误场景: McpError; Unknown tool: invalid_tool_name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Java SDK 2.0.0 对未知工具的处理和 Python 不一样：它返回 JSON-RPC 错误，客户端表现为 &lt;code&gt;McpError&lt;/code&gt;。这不是“谁对谁错”，而是两个 SDK 对协议级错误的 API 映射不同。跨语言教程不能只比较正常输出，也要把错误语义写清楚。&lt;/p&gt;
&lt;h2 id="六mcp-和第四话-toolregistry-的边界"&gt;六、MCP 和第四话 ToolRegistry 的边界
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;第四话 ToolRegistry&lt;/th&gt;
					&lt;th&gt;第八话 MCP&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&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;/td&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;td&gt;JSON-RPC + 传输协议&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多语言&lt;/td&gt;
					&lt;td&gt;需要自己适配&lt;/td&gt;
					&lt;td&gt;Client/Server 可以异构&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&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;/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;MCP 不是 Agent 的替代品，也不是把任何函数自动变成可靠工具的魔法。它只是把原来散落在每个应用里的“如何连接工具”这部分共性抽出来，变成一套可复用的协议。&lt;/p&gt;
&lt;h2 id="七从-demo-到生产还缺什么"&gt;七、从 Demo 到生产还缺什么
&lt;/h2&gt;&lt;p&gt;这个 demo 故意没有 API Key，也没有真实业务副作用。生产 MCP Server 至少还需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入 Schema 和业务层双重校验；&lt;/li&gt;
&lt;li&gt;超时、重试和幂等策略；&lt;/li&gt;
&lt;li&gt;认证、授权和最小权限；&lt;/li&gt;
&lt;li&gt;不把凭据通过环境继承或日志泄露；&lt;/li&gt;
&lt;li&gt;对工具结果做真实性和完整性验证；&lt;/li&gt;
&lt;li&gt;对 Server、Client、工具调用和错误建立可观测性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尤其要记住：&lt;strong&gt;MCP 的“能调用”不等于业务上的“允许调用”。&lt;/strong&gt; 文件删除、支付、发邮件、修改生产数据，都需要在协议之外增加权限和人工审批边界。&lt;/p&gt;
&lt;h2 id="下一篇skill-把做法变成资产"&gt;下一篇：Skill 把“做法”变成资产
&lt;/h2&gt;&lt;p&gt;现在我们解决了“工具在哪里、怎么发现、怎么调用”。但还有一个问题：工具能调用，不代表 Agent 知道应该如何完成一个复杂任务。&lt;/p&gt;
&lt;p&gt;下一篇进入 Skill：把触发条件、步骤、约束、脚本和验证方式封装成可复用能力。工具解决“能做什么”，Skill 解决“应该怎么做”。&lt;/p&gt;
&lt;h2 id="参考资料与实测记录"&gt;参考资料与实测记录
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://py.sdk.modelcontextprotocol.io/" target="_blank" rel="noopener"
 &gt;Model Context Protocol 官方 Python SDK&lt;/a&gt;（Python 3.11.15、&lt;code&gt;mcp[cli]==2.0.0&lt;/code&gt;，本轮验证记录：2026-08-21）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://py.sdk.modelcontextprotocol.io/client/transports/" target="_blank" rel="noopener"
 &gt;MCP Python SDK Client Transports&lt;/a&gt;（STDIO 传输说明，本轮核对：2026-08-21）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.sdk.modelcontextprotocol.io/latest/" target="_blank" rel="noopener"
 &gt;Model Context Protocol 官方 Java SDK&lt;/a&gt;（Java 21、SDK 2.0.0，本轮验证记录：2026-08-21）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.sdk.modelcontextprotocol.io/latest/client/" target="_blank" rel="noopener"
 &gt;MCP Java SDK Client&lt;/a&gt;（STDIO Client 与工具调用 API，本轮核对：2026-08-21）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.sdk.modelcontextprotocol.io/latest/server/" target="_blank" rel="noopener"
 &gt;MCP Java SDK Server&lt;/a&gt;（STDIO Server 与 Tool Specification，本轮核对：2026-08-21）&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第七话｜记忆：上下文、短期、长期</title><link>https://zh.renxinblog.cn/post/c07-memory/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c07-memory/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c07-memory-cover.png" alt="Featured image of post 第七话｜记忆：上下文、短期、长期" /&gt;&lt;!--
第七话演示代码: https://github.com/renxin2024/GYA/tree/main/c07-memory（memory_demo.py，纯标准库）
演进主线: 阶段2 Agent 循环收尾 —— 循环有了、状态显式化了，还差"记住"
上一篇: 第六话 状态管理：从 dict 到 StateGraph
下一篇: 第八话 MCP 协议：为什么需要它
--&gt;
&lt;p&gt;第六话里，我们把&amp;quot;流程控制&amp;quot;从模型手里收回来，交给了 StateGraph。但有一个问题从第五话开始就一直存在，我们假装它不存在：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型的记忆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回顾第五话的代码，我们的&amp;quot;记忆&amp;quot;就是那个 &lt;code&gt;messages&lt;/code&gt; 列表——每次对话，把所有历史都塞进 prompt 喂给模型：&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="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&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="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_message&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;前几轮没事。但对话到 20 轮、30 轮呢？&lt;strong&gt;prompt 越来越长，直到撞上上下文窗口上限&lt;/strong&gt;——这时会发生什么？模型开始&amp;quot;忘记&amp;quot;最早说的话，或者更糟：被中间的信息干扰，答错。&lt;/p&gt;
&lt;p&gt;我先说结论：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;上下文窗口 ≠ 记忆。&lt;/strong&gt; 模型本身不&amp;quot;记得&amp;quot;任何东西，它只是每次被我们喂一坨 prompt。
真正的记忆分三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上下文（Working）&lt;/strong&gt;：当前窗口内，最易失——塞不下就丢&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短期（Episodic）&lt;/strong&gt;：会话内关键事实，显式提取——比全量历史省 token、抗干扰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长期（Semantic）&lt;/strong&gt;：跨会话持久，&lt;strong&gt;检索式&lt;/strong&gt;访问——不检索的记忆等于不存在&lt;/li&gt;
&lt;/ul&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这一篇，我们写一个带三层记忆的最小 Agent，亲眼看看&amp;quot;记忆是怎么补位的&amp;quot;。&lt;/p&gt;
&lt;h2 id="一旧世界把历史塞进上下文的问题"&gt;一、旧世界：把历史塞进上下文的问题
&lt;/h2&gt;&lt;p&gt;先看最朴素的做法——全量历史注入。它有两个硬伤：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬伤 1：上下文窗口有限。&lt;/strong&gt; 假设窗口 8K token，对话到第 30 轮时历史已经 10K token——塞不进去了。要么截断最老的（等于&amp;quot;失忆&amp;quot;），要么压缩（损失细节）。&lt;strong&gt;窗口是物理限制，记忆是软件设计。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬伤 2：Lost in the Middle（中间迷失）。&lt;/strong&gt; 2023 年一篇论文（arXiv 2309.04271）发现一个反直觉现象：模型对长上下文的利用呈 &lt;strong&gt;U 型&lt;/strong&gt;——开头和结尾的信息记得好，&lt;strong&gt;中间的信息最容易丢&lt;/strong&gt;，性能差距可达 25-30%，即使 100K 窗口的模型也一样。把关键信息放在 30 条消息的中间？它很可能被&amp;quot;淹没&amp;quot;。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[对话历史&lt;br&gt;不断增长] --&gt; B{超过窗口?}
 B --&gt;|是| C[截断最老&lt;br&gt;= 失忆]
 B --&gt;|否| D[全量塞入&lt;br&gt;中间信息易丢]&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：把历史当&amp;quot;水管&amp;quot;一样无脑灌，是新手 Agent 最大的坑。&lt;strong&gt;塞不下和塞中间，都是&amp;quot;假记忆&amp;quot;&lt;/strong&gt;——看似有，实则不可靠。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="二转折三层记忆各管一段"&gt;二、转折：三层记忆——各管一段
&lt;/h2&gt;&lt;p&gt;认知科学早就给了我们答案：人的记忆也不是一个池子，而是分层的。Agent 照搬这个分层：&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;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;strong&gt;上下文（Working）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;当前轮次消息&lt;/td&gt;
					&lt;td&gt;窗口内&lt;/td&gt;
					&lt;td&gt;直接注入 prompt&lt;/td&gt;
					&lt;td&gt;你正在读的这句话&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;短期（Episodic）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;会话内关键事实&lt;/td&gt;
					&lt;td&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;strong&gt;长期（Semantic）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;用户偏好/知识点&lt;/td&gt;
					&lt;td&gt;跨会话（近乎永久）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;向量检索&lt;/strong&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;p&gt;&lt;strong&gt;① 短期记忆 = 显式提取，不是全量存储。&lt;/strong&gt;
全量历史是&amp;quot;录像带&amp;quot;——什么都录了，但长、贵、噪音多。短期记忆是&amp;quot;笔记&amp;quot;——只记关键事实（名字、偏好、决定），结构化、省 token、抗干扰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 长期记忆 = 检索式访问，不是注入式。&lt;/strong&gt;
你不可能把&amp;quot;用户爱喝龙井、职业是 Java 工程师、博客写 AI Agent&amp;quot;全塞进每次 prompt。正确做法是：&lt;strong&gt;提问时检索出相关的几条，只注入那几条&lt;/strong&gt;。这就是 RAG（检索增强生成）的核心思想——记忆库很大，但每次只&amp;quot;想&amp;quot;起相关的部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 记忆要有生命周期：巩固与遗忘。&lt;/strong&gt;
不是所有信息都值得长期保存。工程实践通常给记忆打分（importance）：高价值 → 升级到长期；低价值 → 过期遗忘。&lt;strong&gt;遗忘不是 bug，是 feature&lt;/strong&gt;——没有遗忘，记忆库会堆满垃圾，检索质量反而下降。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[对话] --&gt; B{短期记忆&lt;br&gt;提取关键事实}
 B --&gt; C[上下文&lt;br&gt;当前轮]
 B --&gt; D[长期记忆&lt;br&gt;向量检索]
 C --&gt; E[回答]
 D --&gt; E&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：三层记忆不是&amp;quot;三个数据库&amp;quot;，而是&lt;strong&gt;三种访问模式&lt;/strong&gt;——上下文是注入式（全量）、短期是摘要式（精炼）、长期是检索式（按需）。访问模式决定了它们各自解决什么问题。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三原理讲透为什么不检索的记忆等于不存在"&gt;三、原理讲透：为什么&amp;quot;不检索的记忆等于不存在&amp;quot;
&lt;/h2&gt;&lt;p&gt;很多人以为&amp;quot;我的 Agent 存了聊天记录，所以它有记忆&amp;quot;。错。&lt;strong&gt;存储 ≠ 记忆，检索才是记忆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想想人的记忆：你知道&amp;quot;朋友爱喝龙井&amp;quot;这件事，不是因为你把这句话背下来了，而是因为&lt;strong&gt;在需要的时候你能想起来&lt;/strong&gt;——聊天聊到茶、点单、送礼，这些场景会自动唤起这条记忆。&lt;/p&gt;
&lt;p&gt;Agent 也一样。长期记忆必须支持&lt;strong&gt;按语义检索&lt;/strong&gt;：用户问&amp;quot;他喜欢喝什么？&amp;quot;，Agent 要从记忆库里&lt;strong&gt;找出&lt;/strong&gt;&amp;ldquo;喜欢喝茶，尤其是龙井&amp;quot;这条。怎么找？最基础的方法是&lt;strong&gt;向量相似度&lt;/strong&gt;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;1. 把记忆条目转成向量（embedding）——语义相近的文本向量也相近
2. 把用户问题也转成向量
3. 计算余弦相似度，返回最相近的 N 条
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这就是&amp;quot;检索&amp;quot;的本质。演示里我们用纯 Python 实现一个最小版（中文 bigram 分词 + 余弦相似度，零依赖），让你看到机制本身：&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;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# 中文按相邻两字（bigram）切分&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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 class="p"&gt;)&lt;/span&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&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;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;b&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="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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;注意演示级实现的局限：纯 bigram 对&lt;strong&gt;同义改写&lt;/strong&gt;敏感——&amp;ldquo;爱喝&amp;quot;和&amp;quot;喜欢喝&amp;quot;切出来的 bigram 完全不同，匹配不到。生产环境用真实 embedding（如 OpenAI/DeepSeek embedding API），同义词会投影到相近的向量空间，就没有这个问题。&lt;strong&gt;机制相同，精度不同。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺带说一句检索之外的另一半：&lt;strong&gt;巩固（Consolidation）与遗忘（Forgetting）&lt;/strong&gt;。记忆不是存进去就完事了——它有一个生命周期：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;对话产生信息 → 编码 → 存储 → 检索 → 巩固（低→高价值迁移）→ 遗忘（过期清理）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;工程上最常见的做法是给每条记忆打 &lt;strong&gt;importance 分数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;巩固&lt;/strong&gt;：对话中出现的用户偏好/重要决定，打分 ≥ 0.7 → 从短期升级到长期；分数低的留在短期，会话结束即丢。这模拟了人脑&amp;quot;重要的事记牢，琐事随风散&amp;rdquo;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遗忘&lt;/strong&gt;：长期记忆也有容量和时效。时间衰减（太久没被检索到的降权）、容量上限（满了淘汰最不重要的）。&lt;strong&gt;没有遗忘的记忆库，最终会被垃圾淹没，检索质量断崖式下降&lt;/strong&gt;——这跟搜索引擎要处理&amp;quot;死链&amp;quot;是一个道理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很多 Agent 项目死在&amp;quot;只存不捡&amp;rdquo;：记忆越堆越多，检索命中率越来越差。&lt;strong&gt;好的记忆系统不是存得多，而是记得准、忘得掉。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="四动手三层记忆-agent-实测"&gt;四、动手：三层记忆 Agent 实测
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;代码&lt;/strong&gt;：&lt;code&gt;https://github.com/renxin2024/GYA/tree/main/c07-memory&lt;/code&gt;（memory_demo.py，纯标准库）&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="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-你的key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 memory_demo.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="场景-1短期记忆补位"&gt;场景 1：短期记忆补位
&lt;/h3&gt;&lt;p&gt;用户第一轮说&amp;quot;我叫张三&amp;quot; → Agent 提取存入短期记忆。然后聊两轮无关话题（写文字）——此时张三的名字已经不在模型上下文里了。再问&amp;quot;我是谁？我叫什么名字？&amp;quot;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;模型(带记忆): 你是张三。

对比（无记忆，没有任何上下文）:
模型(无记忆): 在没有上下文的情况下，我无法知道你是谁，也不知道你的名字。
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;这就是短期记忆的价值&lt;/strong&gt;：名字被&amp;quot;挤出&amp;quot;上下文后，显式提取的记忆补上了。注意我们没有把整个对话历史塞回去——只注入了一条关键事实，更省 token、更抗干扰。&lt;/p&gt;
&lt;h3 id="场景-2长期记忆向量检索"&gt;场景 2：长期记忆向量检索
&lt;/h3&gt;&lt;p&gt;向记忆库存入三条用户画像，然后提问检索：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;记忆库:
 - 用户喜欢喝茶，尤其是龙井
 - 用户职业是 Java 后端工程师，擅长并发编程
 - 用户的博客主题是 AI Agent 开发

问『用户喜欢喝什么？』→ 检索到: 用户喜欢喝茶，尤其是龙井
问『用户职业是什么？』→ 检索到: 用户职业是 Java 后端工程师...
问『博客写什么？』→ 检索到: 用户的博客主题是 AI Agent 开发
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每条问题都准确&amp;quot;想&amp;quot;起了对应的记忆——这就是检索式访问。如果记忆库有 1000 条，我们不会全塞给模型，只注入检索到的那 1-2 条。&lt;/p&gt;
&lt;h3 id="场景-3诚实说明lost-in-the-middle-实测"&gt;场景 3（诚实说明）：Lost-in-the-Middle 实测
&lt;/h3&gt;&lt;p&gt;演示脚本里也放了一个&amp;quot;信息放中间 vs 开头&amp;quot;的对比。&lt;strong&gt;诚实地说：deepseek-v4-flash 对短 padding 抗性很好，两头都答对了&lt;/strong&gt;——这个效应要在 100K 级长上下文才明显（论文数据：中间 vs 两端性能差 25-30%）。demo 放它是让你知道这个坑的存在，正文用论文数据支撑结论。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[用户提问] --&gt; B{检索长期记忆}
 B --&gt; C[命中 1-2 条]
 C --&gt; D[注入上下文]
 D --&gt; E[模型回答]
 A --&gt; F[短期记忆摘要]
 F --&gt; D&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：三层记忆合起来的完整流程是——&lt;strong&gt;提问时，短期记忆给摘要、长期记忆给检索结果，一起注入上下文，模型基于&amp;quot;当前问题 + 记得的事&amp;quot;作答&lt;/strong&gt;。模型始终只面对一个&amp;quot;够用的小 prompt&amp;quot;，而不是越来越长的全量历史。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="五工程化延伸生产级记忆系统"&gt;五、工程化延伸：生产级记忆系统
&lt;/h2&gt;&lt;p&gt;demo 是概念演示，生产要补的东西不少：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力&lt;/th&gt;
					&lt;th&gt;demo（本篇）&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;/td&gt;
					&lt;td&gt;内存 dict&lt;/td&gt;
					&lt;td&gt;会话级存储 + 上限管理（FIFO/TTL）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;长期记忆&lt;/td&gt;
					&lt;td&gt;内存 list + bigram&lt;/td&gt;
					&lt;td&gt;向量库（Qdrant/Chroma）+ 真实 embedding&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;巩固&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
					&lt;td&gt;重要性打分（如 ≥0.7 升长期）+ 定期 Consolidation&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&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;记忆 vs RAG&lt;/td&gt;
					&lt;td&gt;混在一起&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;明确边界&lt;/strong&gt;：Memory 存对话产生的个人事实，RAG 存外部知识库&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;记忆 vs RAG 的边界&lt;/strong&gt;值得单独强调——这是最常见的混淆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory（记忆）&lt;/strong&gt;：对话中自然产生的——&amp;ldquo;用户叫张三&amp;quot;&amp;ldquo;用户爱喝龙井&amp;rdquo;。换个用户，答案就变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG（检索增强）&lt;/strong&gt;：外部知识库——&amp;ldquo;什么是 ReAct&amp;quot;&amp;ldquo;Java 并发怎么调优&amp;rdquo;。所有用户共享，答案不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把用户偏好存进 RAG？你会得到&amp;quot;所有用户都爱喝龙井&amp;rdquo;。把外部知识存进 Memory？每次都要重新&amp;quot;对话产生&amp;rdquo;，浪费。&lt;strong&gt;各司其职&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="下一步"&gt;下一步
&lt;/h2&gt;&lt;p&gt;阶段 2（Agent 循环）到此收尾：我们有循环（第五话）、有状态（第六话）、有记忆（第七话）——一个&amp;quot;会干活、能记住&amp;quot;的 Agent 成型了。&lt;/p&gt;
&lt;p&gt;但还有一个痛苦没解决：&lt;strong&gt;工具的接入&lt;/strong&gt;。第四话我们手写了注册表，接一个新工具要写代码、注册、测试。如果你要接 10 个不同的服务（GitHub、数据库、日历、邮件），每个都要写适配代码——&lt;strong&gt;N×M 的集成地狱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下一篇进入阶段 3：&lt;strong&gt;MCP 协议——为什么需要它&lt;/strong&gt;。一句话预告：MCP 是&amp;quot;工具的 USB-C 接口&amp;quot;，让工具接入标准化。&lt;/p&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;演示代码&lt;/strong&gt;：&lt;code&gt;https://github.com/renxin2024/GYA/tree/main/c07-memory&lt;/code&gt;（memory_demo.py，纯标准库；DeepSeek 官方 API，默认模型 &lt;code&gt;deepseek-v4-flash&lt;/code&gt;）。Java 21 等价实现见独立仓库 &lt;code&gt;https://github.com/renxin2024/GYA-Java&lt;/code&gt;（&lt;code&gt;c07-memory/&lt;/code&gt;，Gradle 工程，&lt;code&gt;gradle run&lt;/code&gt;）。
&lt;strong&gt;参考&lt;/strong&gt;：Lost in the Middle 论文 arXiv 2309.04271（U 型注意力、中间性能差 25-30%）；四层记忆与巩固/遗忘机制来自 Agent 记忆系统工程实践（hello-agents 讲义延伸）；Memory vs RAG 边界为行业共识；demo 输出为 2026-05-26 本机实测（deepseek-v4-flash，Python 与 Java 双跑）。&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>第六话｜状态管理：从 dict 到 StateGraph</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 第六话｜状态管理：从 dict 到 StateGraph" /&gt;&lt;!--
第六话演示代码: https://github.com/renxin2024/GYA/tree/main/c06-state-management（state_machine.py + langgraph_demo.py）
演进主线: 阶段2 Agent 循环第 2 篇 —— 循环有了，状态怎么管
上一篇: 第五话 手写 ReAct Agent（循环的诞生）
下一篇: 第七话 记忆：上下文、短期、长期
--&gt;
&lt;p&gt;第五话我们写出了一个会多步决策的 Agent。回头看它的执行轨迹，你注意到一个细节吗——&lt;strong&gt;每一步之前，模型都要&amp;quot;想&amp;quot;一次&lt;/strong&gt;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;=== Step 1 ===
[Thought] 用户想要三件事：查天气、算数、总结...
[Action] 调用 get_weather(...) → [Observation] 北京: 多云，25℃
[Action] 调用 calculator(...) → [Observation] 56088

=== Step 2 ===
[Thought] 我有了两个结果，现在总结...
[Final Answer] 北京今天多云... 123×456=56088
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;也就是说，这个循环里**&amp;ldquo;下一步做什么&amp;quot;是由模型推理决定的**：模型说&amp;quot;我要调工具&amp;rdquo;，代码就去调；模型说&amp;quot;我该总结了&amp;quot;，循环就结束。这很自由——但代价是，&lt;strong&gt;每一步的路由决策都花掉一次模型推理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在问一个问题：如果这个流程是固定的——先理解意图、再调工具、最后总结——每次让模型重新&amp;quot;想&amp;quot;一遍下一步去哪，是不是有点浪费？更麻烦的是，如果任务是&amp;quot;查完笔记，有冲突就提醒，没冲突就生成报告&amp;quot;，&amp;ldquo;去哪&amp;quot;的判断写在模型脑子里，它偶尔会想歪，而且每一步都在消耗 token 做路由而不是做任务。&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;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;：共享的状态对象（所有节点读写同一份）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node&lt;/strong&gt;：一个执行单元（函数：读 State → 做事 → 返回更新）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge&lt;/strong&gt;：节点间的路由（确定性代码，不走 LLM）
这三个东西组在一起，就是 &lt;strong&gt;StateGraph&lt;/strong&gt;——&lt;strong&gt;图管流程（确定性的传送带），LLM 管内容（只在需要语义的节点被调用）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这一篇，我们把&amp;quot;流程控制&amp;quot;从模型手里收回来，交给确定性的代码——先写一个&lt;strong&gt;手写状态机&lt;/strong&gt;（零依赖），再用 &lt;strong&gt;LangGraph 的 StateGraph&lt;/strong&gt; 跑同一个任务。你会发现：&lt;strong&gt;框架不是黑盒，它只是把你手写的东西声明式化了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一旧世界循环里的状态为什么撑不住"&gt;一、旧世界：循环里的状态为什么撑不住
&lt;/h2&gt;&lt;p&gt;第五话的 while 循环，本质上是个&amp;quot;自由形式&amp;quot;的流程：每次迭代，模型决定下一步做什么，代码照做。它的问题在第四话笔记里已经埋下伏笔——&lt;strong&gt;ReAct 让 LLM 包办一切&lt;/strong&gt;：推理、路由、调用、总结，全在循环里。&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;&lt;strong&gt;条件分支不可靠&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&amp;ldquo;查完笔记该去哪&amp;quot;写在模型推理文本里，模型偶尔跑偏&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;只能串行&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;需要&amp;quot;同时查 3 个工具再聚合&amp;quot;时，ReAct 只能一个个来&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;无法优雅暂停&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;想&amp;quot;等用户确认再继续&amp;rdquo;，循环很难挂起&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;路由成本高&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每一步都让 LLM 想&amp;quot;下一步去哪&amp;rdquo;，token 费在路由上而不是任务上&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[用户问题] --&gt; B[LLM 思考&lt;br&gt;推理+路由混合]
 B --&gt; C[执行工具]
 C --&gt; D{模型决定&lt;br&gt;下一步?}
 D --&gt;|再想一次| B
 D --&gt;|结束| E[最终答案]&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：ReAct 循环的问题不是&amp;quot;循环&amp;quot;本身，而是&lt;strong&gt;路由决策也交给了 LLM&lt;/strong&gt;。让模型每次都想&amp;quot;下一步去哪&amp;quot;，既贵又不稳——路由这种事，代码做得又快又准。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="二转折显式状态机state--node--edge-三件套"&gt;二、转折：显式状态机——State / Node / Edge 三件套
&lt;/h2&gt;&lt;p&gt;解决思路来自一个古老而成熟的概念：&lt;strong&gt;状态机（State Machine）&lt;/strong&gt;。把它用在 Agent 上，就是三个核心概念：&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# State：共享状态&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;intent&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;results&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;# 工具结果&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&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;parse_intent&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Node：执行单元&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_intent&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&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;intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&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="c1"&gt;# Edge：路由表（确定性代码）&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;parse_intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parse_intent&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;execute_tools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;execute_tools&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对照你熟悉的 Java 概念：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Java 状态机&lt;/th&gt;
					&lt;th&gt;StateGraph&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;State 枚举&lt;/td&gt;
					&lt;td&gt;Node（可执行代码）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;transition(event)&lt;/td&gt;
					&lt;td&gt;Edge（自动流转）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Context 上下文&lt;/td&gt;
					&lt;td&gt;State（共享字典）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Guard 条件&lt;/td&gt;
					&lt;td&gt;条件边的路由函数（确定性 Python）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键设计原则：图管流程，LLM 管内容。&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;图的节点 = 一个单元操作
├── 纯代码节点 → 调工具、做计算（零 token）
├── 代码+LLM 节点 → 需要语义推理时调 LLM
└── 纯 LLM 节点 → 理解意图、生成文本

图的边 = 节点间路由
├── 普通边 → 永远从 A 到 B
└── 条件边 → 根据 State 决定（确定性代码，不走 LLM）
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：图是骨架（确定性的传送带），LLM 是肌肉（只坐到需要语义推理的工位上）。&lt;strong&gt;图的职责是控制 LLM 被调用的时机和次数&lt;/strong&gt;——该省的路由 token 一分不花。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三原理讲透为什么状态要显式"&gt;三、原理讲透：为什么状态要&amp;quot;显式&amp;quot;？
&lt;/h2&gt;&lt;p&gt;你可能觉得：第五话的循环里也有 state 啊，为什么要&amp;quot;显式&amp;quot;？区别在于&lt;strong&gt;谁在管理状态的流转&lt;/strong&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;th&gt;状态机&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;状态存在哪&lt;/td&gt;
					&lt;td&gt;零散在循环代码里&lt;/td&gt;
					&lt;td&gt;一个显式 State 对象&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;下一步去哪&lt;/td&gt;
					&lt;td&gt;模型推理决定（或 if-else 散落）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;next_step&lt;/code&gt; 字段 + 路由表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;节点边界&lt;/td&gt;
					&lt;td&gt;没有，循环体是&amp;quot;一大坨&amp;quot;&lt;/td&gt;
					&lt;td&gt;每个 Node 独立函数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;调试&lt;/td&gt;
					&lt;td&gt;靠打印&lt;/td&gt;
					&lt;td&gt;看 State 快照即知全貌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在展开之前，先精确理解 State 的&lt;strong&gt;生命周期&lt;/strong&gt;——这是状态管理和第五话随手传 list 的根本区别：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;1. 初始 State：{&amp;#34;question&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;messages&amp;#34;: [...], &amp;#34;results&amp;#34;: {}, &amp;#34;intent&amp;#34;: &amp;#34;&amp;#34;, &amp;#34;next_step&amp;#34;: &amp;#34;parse_intent&amp;#34;}
2. parse_intent 执行 → 返回 {&amp;#34;intent&amp;#34;: [...]} ← 只更新 intent 字段
3. 引擎把返回合并进 State → next_step 指向 execute_tools
4. execute_tools 执行 → 返回 {&amp;#34;results&amp;#34;: {...}, &amp;#34;messages&amp;#34;: [...]} ← 只更新自己的字段
5. ...循环...
6. summarize 返回 {&amp;#34;final_answer&amp;#34;: &amp;#34;...&amp;#34;} → next_step=END → 输出最终 State
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每个 Node 是一个&lt;strong&gt;纯函数&lt;/strong&gt;：输入完整 State，返回&amp;quot;要更新的字段子集&amp;quot;。&lt;strong&gt;Node 不直接改 State&lt;/strong&gt;——它返回更新，由引擎（手写循环或框架）合并。这个设计保证了：任何节点都能拿到完整上下文（读 State），但只负责自己那部分（写自己的字段），不会互相踩踏。这也是&amp;quot;可并行&amp;quot;和&amp;quot;可恢复&amp;quot;能成立的原因——&lt;strong&gt;状态的读写是受控的&lt;/strong&gt;，不是随处可改的全局变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;显式&amp;quot;的三个直接收益&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;可审计&lt;/strong&gt;：任何时刻，State 里的 &lt;code&gt;intent/results/next_step&lt;/code&gt; 告诉你 Agent 走到哪了、已经干了什么。挂掉时一眼看出问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可恢复&lt;/strong&gt;：State 是数据，可以序列化存盘。长任务挂了，从上次 State 恢复，不用从头跑（&lt;strong&gt;检查点 checkpoint&lt;/strong&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可并行&lt;/strong&gt;：State 明确后，互不依赖的分支可以并行执行再聚合（StateGraph 支持 fan-out/fan-in）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三件事正是生产 Agent 和 demo Agent 的分水岭——&lt;strong&gt;demo 靠调试，生产靠状态管理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里值得多说两句&amp;quot;可并行&amp;quot;和&amp;quot;可恢复&amp;rdquo;，因为它们是把状态管理从&amp;quot;概念正确&amp;quot;推向&amp;quot;工程价值&amp;quot;的关键。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并行（fan-out / fan-in）&lt;/strong&gt;：假设一个任务要同时查三个数据源（天气、股票、新闻），ReAct 循环只能串行——先查天气、等结果、再查股票、再等结果。显式状态机可以把三个查询节点做成并行分支，全部完成后在聚合节点合并。LangGraph 的 &lt;code&gt;Send&lt;/code&gt; API 就是干这个的。对延迟敏感的生产系统，这可能是 3 倍和 1 倍的差别。&lt;strong&gt;代价是状态必须可合并&lt;/strong&gt;——每个并行分支只更新自己的字段，不能互相覆盖，这正是 State（共享 dict）设计时要小心的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;检查点（checkpoint）&lt;/strong&gt;：State 既然是数据，就可以序列化。LangGraph 有内置的 checkpointer——每步执行完把 State 存下来。长任务（比如批量处理 1000 条数据）跑到第 700 条挂了，恢复时&lt;strong&gt;从第 700 条的 State 继续&lt;/strong&gt;，而不是从头跑。这对 token 成本和工程效率是量级的差别。手写版要做到这点，就是在 &lt;code&gt;run()&lt;/code&gt; 里每步 &lt;code&gt;json.dumps(state)&lt;/code&gt; 存盘——原理一样，框架替你做了。&lt;/p&gt;
&lt;p&gt;还有一点值得注意：&lt;strong&gt;State 的字段设计直接影响系统复杂度&lt;/strong&gt;。原则是&amp;quot;每个节点只更新自己关心的字段&amp;quot;——parse_intent 只写 &lt;code&gt;intent&lt;/code&gt;，execute_tools 只写 &lt;code&gt;results&lt;/code&gt; 和 &lt;code&gt;messages&lt;/code&gt;，互不干扰。如果所有节点都直接改一个&amp;quot;大杂烩&amp;quot; dict，并行和检查点都会变得困难。这跟 Java 里&amp;quot;类职责单一&amp;quot;是同一个道理。&lt;/p&gt;
&lt;h2 id="四动手同一个任务两种实现"&gt;四、动手：同一个任务，两种实现
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;代码&lt;/strong&gt;：&lt;code&gt;https://github.com/renxin2024/GYA/tree/main/c06-state-management&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;state_machine.py&lt;/code&gt;：手写状态机（零依赖）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;langgraph_demo.py&lt;/code&gt;：LangGraph StateGraph（&lt;code&gt;pip install langgraph&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&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="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-你的key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 state_machine.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run --with langgraph python3 langgraph_demo.py &lt;span class="c1"&gt;# 或 pip install langgraph&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;：&amp;ldquo;北京天气怎么样？顺便算一下 123*456，最后把两个答案整理成一句话。&amp;rdquo;&lt;/p&gt;
&lt;h3 id="手写版三节点状态机"&gt;手写版：三节点状态机
&lt;/h3&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;StateMachine&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="fm"&gt;__init__&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;question&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;state&lt;/span&gt; &lt;span class="o"&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="s2"&gt;&amp;#34;question&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&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="s2"&gt;&amp;#34;messages&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;results&amp;#34;&lt;/span&gt;&lt;span class="p"&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 class="s2"&gt;&amp;#34;intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&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="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;parse_intent&amp;#34;&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="p"&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;parse_intent&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="c1"&gt;# Node 1：LLM 节点（唯一需要语义的）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&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&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;execute_tools&amp;#34;&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;execute_tools&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="c1"&gt;# Node 2：代码节点（路由确定性）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;执行&lt;/span&gt;&lt;span class="o"&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;summarize&amp;#34;&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;summarize&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="c1"&gt;# Node 3：LLM 节点&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;final_answer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summarize&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&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;END&amp;#34;&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;run&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="c1"&gt;# 状态机引擎：while + next_step 路由&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;END&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;node&lt;/span&gt; &lt;span class="o"&gt;=&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next_step&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="nb"&gt;getattr&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;node&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;实测输出（2026-08-20，deepseek-v4-flash）：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;=== Node: parse_intent ===
=== Node: execute_tools ===
=== Node: summarize ===
最终答案: 北京天气多云、25℃、东北风3级；另外，123×456的计算结果是56088。
执行轨迹: intent=[&amp;#39;get_weather&amp;#39;, &amp;#39;calculator&amp;#39;]
 results={&amp;#39;get_weather&amp;#39;: &amp;#39;多云，25℃...&amp;#39;, &amp;#39;calculator&amp;#39;: &amp;#39;56088&amp;#39;}
&lt;/code&gt;&lt;/pre&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;，不是模型推理——&lt;code&gt;parse_intent → execute_tools → summarize&lt;/code&gt;，每一步去哪由代码决定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 只被调了 3 次&lt;/strong&gt;：parse_intent（理解意图）、execute_tools 里（决定工具参数）、summarize（总结）——路由本身零 token&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State 一眼可查&lt;/strong&gt;：&lt;code&gt;intent&lt;/code&gt; 和 &lt;code&gt;results&lt;/code&gt; 告诉你&amp;quot;Agent 认为要做什么、已经得到了什么&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="langgraph-版同一三节点声明式表达"&gt;LangGraph 版：同一三节点，声明式表达
&lt;/h3&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="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;parse_intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_intent&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;execute_tools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;execute_tools&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;summarize&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarize&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;parse_intent&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;parse_intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;execute_tools&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;execute_tools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_after_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;summarize&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;summarize&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;summarize&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&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;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoke&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&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;parse_intent&lt;/code&gt; 的代码是复制的）——区别只在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手写版：&lt;code&gt;while&lt;/code&gt; 循环 + &lt;code&gt;getattr(self, next_step)&lt;/code&gt; 路由&lt;/li&gt;
&lt;li&gt;LangGraph：&lt;code&gt;add_node&lt;/code&gt; + &lt;code&gt;add_edge&lt;/code&gt; 声明 + 框架内部替你跑循环&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实测输出与手写版一致（intent 相同、results 相同、最终答案相同）：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Node: parse_intent] intent=[&amp;#39;get_weather&amp;#39;, &amp;#39;calculator&amp;#39;]
[Node: execute_tools] get_weather → 多云，25℃，东北风 3 级
[Node: execute_tools] calculator → 56088
[Node: summarize] 北京天气多云、25℃；123×456的结果是56088。
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[parse_intent&lt;br&gt;LLM 节点] --&gt; B[execute_tools&lt;br&gt;代码节点]
 B --&gt; C{有工具结果?}
 C --&gt;|是| D[summarize&lt;br&gt;LLM 节点]
 C --&gt;|否| D
 D --&gt; E[END]&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：跑同一个任务，手写版和 LangGraph 版输出完全一致——&lt;strong&gt;框架没有魔法，它只是把 while 循环和路由表声明式化了&lt;/strong&gt;。你手写过一遍，用任何框架都是&amp;quot;换个写法&amp;quot;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="五工程化延伸什么时候该上-stategraph"&gt;五、工程化延伸：什么时候该上 StateGraph？
&lt;/h2&gt;&lt;p&gt;状态机不是万能的。什么时候该用，什么时候 ReAct/手写循环就够？&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;该用 StateGraph&lt;/th&gt;
					&lt;th&gt;ReAct/手写循环就够&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&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;需要并行 + 聚合（fan-out/fan-in）&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;tr&gt;
					&lt;td&gt;规模大、token 成本敏感&lt;/td&gt;
					&lt;td&gt;一天跑一两次&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个直觉判断法：&lt;strong&gt;如果流程图能画出来（固定节点+固定边），就用 StateGraph；如果流程本身是探索性的（不知道下一步是什么），用 ReAct。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作为系列的双语言惯例，这里说一句 Java 版的事：&lt;strong&gt;LangGraph 是 Python 生态库&lt;/strong&gt;（基于 asyncio 和 TypedDict 设计），没有官方 Java 等价实现。所以第六话的 Java 版（GYA-Java &lt;code&gt;c06-state-management/&lt;/code&gt;）实现的是&lt;strong&gt;手写状态机等价版&lt;/strong&gt;——State 用 &lt;code&gt;Map&lt;/code&gt;，Node 用 &lt;code&gt;Function&lt;/code&gt;，图声明就是一张注册表：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NODES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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="n"&gt;NODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;parse_intent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;parseIntent&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="w"&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;NODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;execute_tools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;executeTools&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="w"&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;NODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;summarize&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;summarize&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="w"&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;// 引擎：while + next_step 路由（与 Python 版同构）&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;p&gt;用 Java 跑同一个任务，输出和 Python 版完全一致（intent/results/最终答案相同）。这说明：&lt;strong&gt;状态机的思想是语言无关的&lt;/strong&gt;——不管用 Python 的 LangGraph、Java 的手写注册表，还是你未来可能遇到的任何 Agent 框架，State/Node/Edge 三件套都是同一套骨架。&lt;/p&gt;
&lt;p&gt;另外澄清一个常见误区：&lt;strong&gt;ReAct 不是 StateGraph 的对立面，而是 StateGraph 的一个特例&lt;/strong&gt;——三节点循环图（Think → Act → Observe → 回环）。类比：&lt;code&gt;ArrayList&lt;/code&gt; 是 &lt;code&gt;List&lt;/code&gt; 的实现，ReAct 是 StateGraph 的一种工作流模式。用 StateGraph 也能画出 ReAct 图（第五话的循环就是），只是 StateGraph 能表达的远不止 ReAct。&lt;/p&gt;
&lt;h2 id="下一步"&gt;下一步
&lt;/h2&gt;&lt;p&gt;State 管住了&amp;quot;流程怎么走&amp;quot;。但还有一个大问题没解决：&lt;strong&gt;模型的记忆&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第五话里我们把所有历史都塞进 messages——对话 20 轮后，prompt 越来越长，直到超出上下文窗口。这时会发生什么？模型&amp;quot;忘记&amp;quot;最早说的话。&lt;strong&gt;上下文窗口 ≠ 记忆&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下一篇第七话，我们讲三层记忆架构：上下文（窗口内）、短期（会话内）、长期（跨会话）——以及向量检索怎么让 Agent&amp;quot;记住&amp;quot;重要的事。&lt;/p&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;演示代码&lt;/strong&gt;：&lt;code&gt;https://github.com/renxin2024/GYA/tree/main/c06-state-management&lt;/code&gt;（state_machine.py 零依赖 + langgraph_demo.py 需 langgraph；DeepSeek 官方 API，默认模型 &lt;code&gt;deepseek-v4-flash&lt;/code&gt;）。Java 21 手写状态机等价实现见独立仓库 &lt;code&gt;https://github.com/renxin2024/GYA-Java&lt;/code&gt;（&lt;code&gt;c06-state-management/&lt;/code&gt;，Gradle 工程，&lt;code&gt;gradle run&lt;/code&gt;）。
&lt;strong&gt;参考&lt;/strong&gt;：StateGraph 三概念（State/Node/Edge）、图管流程 LLM 管内容、ReAct 是 StateGraph 特例，来自 LangGraph 官方文档与 Agent 工程实践（hello-agents 讲义延伸）；demo 输出为 2026-05-12 本机实测（deepseek-v4-flash，Python 手写版/LangGraph 版/Java 版三跑一致）。&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>第五话｜ReAct：让模型把「想」和「做」接起来</title><link>https://zh.renxinblog.cn/post/c05-react-agent/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c05-react-agent/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c05-react-agent-cover.png" alt="Featured image of post 第五话｜ReAct：让模型把「想」和「做」接起来" /&gt;&lt;p&gt;第四话结束，我们手里有了一个能安全执行单次工具调用的 Runtime：模型说&amp;quot;调 &lt;code&gt;get_weather&lt;/code&gt;&amp;quot;，代码校验、执行、把结果回喂，模型再总结成一句话。&lt;/p&gt;
&lt;p&gt;这是&amp;quot;会做&amp;quot;了。但它藏着一个更深的问题，上一话没来得及展开：&lt;strong&gt;模型的&amp;quot;想&amp;quot;和&amp;quot;做&amp;quot;，还是断开的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模型提一次请求，代码执行一次，结束。中间没有&amp;quot;想&amp;quot;——没有让模型停下来，根据刚拿到的结果，再决定下一步。&lt;/p&gt;
&lt;p&gt;本篇就把这两件事接起来。先记住一句话，它是全文的锚：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;光想不做，会空转；光做不想，会盲目。ReAct 做的，就是让&amp;quot;想&amp;quot;和&amp;quot;做&amp;quot;交替起来。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一光想不做光做不想各有什么毛病"&gt;一、光想不做、光做不想，各有什么毛病
&lt;/h2&gt;&lt;p&gt;先把两种&amp;quot;缺一半&amp;quot;的形态说清楚。ReAct 就是为了治这两种病而生的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;光想不做（Chain-of-Thought 的局限）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;让模型一路推理，它每一步都靠自己的记忆往下推。问题是：它推理时不接触外部世界，没有办法去查、去验证。推到一半，遇到它不知道的事实，它不会停下来说&amp;quot;我不知道，去查一下&amp;quot;，而是&lt;strong&gt;接着编&lt;/strong&gt;。错了还往下推，越推越偏。&lt;/p&gt;
&lt;p&gt;这就是&amp;quot;幻觉&amp;quot;和&amp;quot;错误传播&amp;quot;：模型在脑子里空转，没有真实反馈把它拉回现实。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;光做不想（纯工具调用）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;反过来，模型只会调工具，但不动脑。它拿到一个结果，不知道这个结果意味着什么、下一步该干嘛、什么时候该收手。盲目地执行，却不解释为什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话立住：想，缺真实反馈会空转；做，缺推理会盲目。&lt;/strong&gt; 两种形态各缺一半，ReAct 要做的，是把两半合起来。&lt;/p&gt;
&lt;h2 id="二react-的答案把想和做交替起来"&gt;二、ReAct 的答案：把&amp;quot;想&amp;quot;和&amp;quot;做&amp;quot;交替起来
&lt;/h2&gt;&lt;p&gt;2022 年的一篇论文把这个想法抽象成了模式，叫 ReAct（Reasoning + Acting），arXiv 2210.03629。&lt;/p&gt;
&lt;p&gt;想法很朴素：让模型交替地&amp;quot;想一步、做一步、看一步&amp;quot;。&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;Thought（想）&lt;/td&gt;
					&lt;td&gt;模型推理：我知道什么、还缺什么、下一步干嘛&lt;/td&gt;
					&lt;td&gt;模型&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Action（做）&lt;/td&gt;
					&lt;td&gt;调用某个工具，或给出最终答案&lt;/td&gt;
					&lt;td&gt;模型提出，代码执行&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Observation（看）&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;关键在那个&amp;quot;看&amp;quot;。&lt;strong&gt;Observation 是外部世界的真实反馈，它把模型的&amp;quot;想&amp;quot;从自己的幻觉里拉回现实。&lt;/strong&gt; 这是 CoT 缺的那一环——CoT 一路想下去，没有地方停下来&amp;quot;看一眼真实世界&amp;quot;。&lt;/p&gt;
&lt;p&gt;于是循环成立：想一步 → 做一步 → 看一眼真实结果 → 再想一步。想的给做的指方向，做的给想的补事实。这就是&amp;quot;推理和行动交替&amp;quot;。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[问题 + 历史] --&gt; B[模型&lt;br&gt;想：下一步干嘛]
 B --&gt; C{要动手?}
 C --&gt;|是| D[代码执行&lt;br&gt;拿到真实结果]
 D --&gt; E[写回历史&lt;br&gt;给模型看]
 E --&gt; B
 C --&gt;|否| F[给出最终回答&lt;br&gt;结束]&lt;/pre&gt;&lt;p&gt;有两点要划清，避免误读：&lt;/p&gt;
&lt;p&gt;第一，本文不把任何供应商的 &lt;code&gt;reasoning_content&lt;/code&gt; 字段当成论文里的 Thought。论文的 Thought 是文字推理轨迹，现代 API 的 reasoning 字段是另一回事。这篇只验证一个更小、更可观察的问题：&lt;strong&gt;模型的下一步动作，是不是真的随观察到的真实结果改变。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二，ReAct 并没有在所有任务上都赢过 CoT。论文配套结果里，HotpotQA 6-shot 下 ReAct 是 27.4，CoT 是 29.4，两者结合是 35.1。别把&amp;quot;ReAct 更强&amp;quot;当成普适结论——它带来的是&lt;strong&gt;外部反馈回路&lt;/strong&gt;，不是万能药。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：ReAct 的突破，不是&amp;quot;模型更聪明了&amp;quot;，而是给模型的推理接上了外部反馈——每一步都踩在真实工具结果上，而不是模型自己的幻觉上。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="三工程上怎么承载一个循环外壳"&gt;三、工程上怎么承载：一个循环外壳
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;想和做交替&amp;quot;这件事，落到代码上，就是一个循环：&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;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 模型&amp;#34;想&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&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;state&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 代码&amp;#34;做&amp;#34;：执行，拿到真实结果&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;messages&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;observation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 把&amp;#34;看&amp;#34;到的写回历史&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;strong&gt;灵魂&lt;/strong&gt;。不是套个 &lt;code&gt;while&lt;/code&gt; 就完事——有三件事，是让这个外壳真正立起来的前提：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 模型没有记忆，每次都得喂全。&lt;/strong&gt; 模型每次调用都是独立的，不记得上一轮干了什么。所以循环要自己维护消息历史，每一轮把&amp;quot;问题 + 之前做过的 + 看到的结果&amp;quot;完整交给模型。它不是&amp;quot;记得&amp;rdquo;，是被每次提醒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 循环必须有出口。&lt;/strong&gt; 两个出口：模型不再调工具（直接给最终回答）→ 正常结束；到达 &lt;code&gt;max_steps&lt;/code&gt; 上限 → 兜底强制停止。缺了后者，模型偶尔会陷入&amp;quot;反复调同一个工具&amp;quot;的怪圈，把额度烧穿。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Observation 必须真实，绝不让模型自己编。&lt;/strong&gt; 这是 ReAct 的生命线。如果模型能自己写&amp;quot;北京天气 25℃&amp;quot;，它就会偷懒编造。Observation 必须来自工具的真实执行结果，代码负责，模型无权编。在原生 &lt;code&gt;tool_calls&lt;/code&gt; 里，这一点由 API 层保证：结果以 &lt;code&gt;role=tool&lt;/code&gt; 消息回传，模型只能基于它推理。&lt;/p&gt;
&lt;p&gt;这三件事，是&amp;quot;循环外壳怎么搭&amp;quot;的注意点，不是 ReAct 的主角。主角永远是那句：&lt;strong&gt;让&amp;quot;想&amp;quot;和&amp;quot;做&amp;quot;交替，让每一步想都踩在真实的&amp;quot;看&amp;quot;上。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="四看一次真实的交替"&gt;四、看一次真实的&amp;quot;交替&amp;quot;
&lt;/h2&gt;&lt;p&gt;光说不够，跑一个真的。任务是&amp;quot;公司总部今天的天气&amp;quot;——它天然需要两步：先解析&amp;quot;公司总部&amp;quot;是哪个城市，再用这个城市查天气。&lt;/p&gt;
&lt;p&gt;配套代码在 &lt;code&gt;code/agent-tutorial/c05-react-agent/&lt;/code&gt;，Python 和 Java 双份，工具是三个本地只读夹具（不依赖外部 API，避免网络和额度污染控制流证据）：&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;resolve_company_address&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;get_weather&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;查城市天气&lt;/td&gt;
					&lt;td&gt;上海→小雨 22℃，北京→晴 18℃&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;request_clarification&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;真实模型跑出来的对照 Trace，是这篇最想让你看到的：&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;上海：resolve_company_address({&amp;#34;company_name&amp;#34;: &amp;#34;上海总部&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 看到 RESOLVED(city=&amp;#34;上海&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → get_weather({&amp;#34;city&amp;#34;: &amp;#34;上海&amp;#34;})
&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;北京：resolve_company_address({&amp;#34;company_name&amp;#34;: &amp;#34;北京总部&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 看到 RESOLVED(city=&amp;#34;北京&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → get_weather({&amp;#34;city&amp;#34;: &amp;#34;北京&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意看第二步：模型&amp;quot;想&amp;quot;出的下一步，&lt;strong&gt;参数随它&amp;quot;看&amp;quot;到的结果改变&lt;/strong&gt;——看到&amp;quot;上海&amp;quot;就查上海，看到&amp;quot;北京&amp;quot;就查北京。&lt;/p&gt;
&lt;p&gt;这比&amp;quot;最后回答换了城市&amp;quot;强得多。它证明的不是&amp;quot;循环多跑了几步&amp;quot;，而是&lt;strong&gt;模型真的在根据 Observation 调整下一步的&amp;quot;想&amp;quot;&lt;/strong&gt;——这正是 ReAct 和&amp;quot;固定脚本多跑几遍&amp;quot;的分水岭。&lt;/p&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;# 离线回归，不需要 Key&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/c05-react-agent/python
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest test_react_agent.py
&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;# 真实模型路径，Key 只从环境变量读&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;LLM_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt; &lt;span class="nv"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt; &lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 react_agent.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="五这一篇停在哪下一篇往哪走"&gt;五、这一篇停在哪，下一篇往哪走
&lt;/h2&gt;&lt;p&gt;到这里，ReAct 的&amp;quot;交替&amp;quot;讲清了：模型想一步、做一步、看一步，每一步想都踩在真实结果上。&lt;/p&gt;
&lt;p&gt;但有一个口子，正好是下一篇的入口。注意第三节那个循环，每一轮都把&lt;strong&gt;完整历史&lt;/strong&gt;喂给模型。历史一长，问题就来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次都喂全，Token 越来越贵，也塞不下；&lt;/li&gt;
&lt;li&gt;&amp;ldquo;看&amp;quot;到的结果、Run 当前走到哪一步，都只在&lt;strong&gt;内存&lt;/strong&gt;里——进程一重启，全没了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模型&amp;quot;想&amp;quot;和&amp;quot;做&amp;quot;的交替立起来了，但**&amp;ldquo;记得住、断得了、恢复得回来&amp;quot;这件事还没解决**。这是第六话要讲的：状态怎么显式存下来、怎么 checkpoint、怎么在崩溃后接着跑。&lt;/p&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;演示代码&lt;/strong&gt;：&lt;code&gt;code/agent-tutorial/c05-react-agent/&lt;/code&gt;（Python：&lt;code&gt;react_agent.py&lt;/code&gt; + &lt;code&gt;test_react_agent.py&lt;/code&gt;；Java 21 等价实现见 &lt;code&gt;java/&lt;/code&gt;）。真实模型路径只从 &lt;code&gt;LLM_API_URL&lt;/code&gt; / &lt;code&gt;LLM_API_KEY&lt;/code&gt; / &lt;code&gt;LLM_MODEL&lt;/code&gt; 环境变量读配置，密钥不进代码、不进 Trace。
&lt;strong&gt;参考&lt;/strong&gt;：ReAct 论文 &lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;arXiv 2210.03629&lt;/a&gt;；Google Research 官方介绍 &lt;a class="link" href="https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/" target="_blank" rel="noopener"
 &gt;React: Synergizing Reasoning and Acting in Language Models&lt;/a&gt;；Trace 为 2026-09 本机实测（Python 与 Java 双跑）。&lt;/p&gt;

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