<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GYA（Get Your Agent） on Renxin's Blog</title><link>https://zh.renxinblog.cn/series/gya/</link><description>Recent content in GYA（Get Your Agent） 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/series/gya/index.xml" rel="self" type="application/rss+xml"/><item><title>第一话｜大模型只会补全文字，Agent 的“手脚”是怎么来的？</title><link>https://zh.renxinblog.cn/post/c01-chat-only-model/</link><pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c01-chat-only-model/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c01-chat-only-model-cover.png" alt="Featured image of post 第一话｜大模型只会补全文字，Agent 的“手脚”是怎么来的？" /&gt;&lt;p&gt;几年前，我还在用 ChatGPT 查技术资料、翻译文档、写文案前先理个大纲。那时候它是“聊天窗口”：我打字，它回文字，聊完就关掉。&lt;/p&gt;
&lt;p&gt;后来出现的 Agent 工具，真的能帮我干活了——写代码、执行命令行、调用 API 查数据。&lt;/p&gt;
&lt;p&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;strong&gt;给你一段上文，它预测下一个最可能出现的词，然后把这个词接上去，再预测下一个，一直循环，直到吐出一段完整的文字。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个过程有个名字，叫“自回归生成”。它的输入是文字，输出还是文字。&lt;/p&gt;
&lt;p&gt;所以模型天生有几个边界，跟它聪不聪明没关系，是它的结构决定的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它&lt;strong&gt;没有手&lt;/strong&gt;，不会真的去点一个按钮、执行一行代码；&lt;/li&gt;
&lt;li&gt;它&lt;strong&gt;没有网络&lt;/strong&gt;，不会自己发一个 HTTP 请求去查天气；&lt;/li&gt;
&lt;li&gt;它&lt;strong&gt;没有文件系统&lt;/strong&gt;，不会自己创建一个文件、改一行代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它唯一能做的，是根据上文，猜下一段文字该怎么写。&lt;/p&gt;
&lt;p&gt;理解了这一点，再回头看那个问题就清楚了一半：一个只会补全文字的模型，是怎么“干起活”来的？&lt;/p&gt;
&lt;h2 id="让它说出要干什么"&gt;让它“说”出要干什么
&lt;/h2&gt;&lt;p&gt;答案藏在一个很朴素的技巧里。&lt;/p&gt;
&lt;p&gt;模型只会写字，那我们就&lt;strong&gt;让它把“想干什么”用固定格式写出来&lt;/strong&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-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;{&amp;#34;action&amp;#34;: &amp;#34;get_weather&amp;#34;, &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;然后用户问“北京今天天气怎么样”。模型还是只会补全文字，只不过这一次，它补全出来的不是一段聊天，而是一行长得很像代码的 JSON：&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;action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;city&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;code&gt;get_weather&lt;/code&gt;，参数是北京。&lt;/p&gt;
&lt;p&gt;真正动手的是模型外面的程序。它拿到这行 JSON，解析出 &lt;code&gt;action&lt;/code&gt; 是 &lt;code&gt;get_weather&lt;/code&gt;、&lt;code&gt;city&lt;/code&gt; 是北京，然后&lt;strong&gt;自己去调用真正的查天气函数&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;re&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;# 模型输出的那行文字&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;model_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{&amp;#34;action&amp;#34;: &amp;#34;get_weather&amp;#34;, &amp;#34;city&amp;#34;: &amp;#34;北京&amp;#34;}&amp;#39;&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;# 程序解析它&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_output&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;action&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;get_weather&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;拿到天气结果后，程序再把这个结果塞回给模型，模型补全成一句人话：“北京今天多云，25 度”。&lt;/p&gt;
&lt;p&gt;看明白了吗？整条链路里，&lt;strong&gt;模型从头到尾没执行过任何东西&lt;/strong&gt;。它只是把“想干什么”用固定格式说了出来，动手的是外面那段解析加执行的代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;看起来模型“长出了手脚”，其实是外面套了一双手。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="这套做法其实是-function-calling-的前身"&gt;这套做法，其实是 Function Calling 的前身
&lt;/h2&gt;&lt;p&gt;上面那套“提示词逼出格式、代码解析执行”的做法，不是谁凭空想出来的玩具。在原生 Function Calling 出现之前，它是开发者让模型调用工具的标准办法——先用提示词约定好输出格式，再用正则表达式或者 JSON 解析器去抠出工具名和参数。&lt;/p&gt;
&lt;p&gt;它好用，但也很脆。模型会输出残缺的 JSON、在 JSON 外面多写一句“好的，我帮你查”、或者干脆忘掉约定格式。这些问题，逼着人们后来把这件事从“提示词技巧”升级成了“协议”——也就是第二话要讲的 Function Calling。&lt;/p&gt;
&lt;p&gt;但不管怎么升级，&lt;strong&gt;底层逻辑从没变过&lt;/strong&gt;：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;模型只负责用固定的格式，把“要干什么”说清楚；真正的执行，永远发生在模型外面。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;想通这一点，整个 Agent 世界就有一把钥匙了。你之后会看到的所有花活——多步循环、记忆、MCP、Skill——都在做同一件事的不同部分：&lt;strong&gt;让模型把意图说清楚，让外面那套代码把事做成，再把结果喂回去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而外面这套代码，我们给它起了个名字，叫 Runtime。它才是 Agent 真正“长”出来的那只手。&lt;/p&gt;
&lt;p&gt;下一篇，我们就从这套做法的标准化形态开始：模型怎么用一套协议，规规矩矩地提出一次工具调用——Function Calling。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://developers.openai.com/api/docs/guides/function-calling" target="_blank" rel="noopener"
 &gt;Function Calling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yao et al., &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;/ul&gt;</description></item><item><title>第二话｜Function Calling：谁把“调用工具”做成了一套协议？</title><link>https://zh.renxinblog.cn/post/c02-function-calling/</link><pubDate>Tue, 17 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c02-function-calling/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c02-function-calling-cover.png" alt="Featured image of post 第二话｜Function Calling：谁把“调用工具”做成了一套协议？" /&gt;&lt;p&gt;上一话我们搞懂了两件事：模型只会补全文字；想让它干活，得让它把“想干什么”说成某种格式，再由外面的代码去执行。&lt;/p&gt;
&lt;p&gt;但上一话那个办法很脆——用提示词逼模型输出 JSON，模型经常给你残缺的 JSON、在前后多写几句废话、或者干脆忘了格式。于是就有了 Function Calling：它把“让模型提出一次工具调用”这件事，从碰运气的提示词技巧，做成了一套规规矩矩的协议。&lt;/p&gt;
&lt;p&gt;这一话就讲这套协议：它长什么样，谁先做出来的，以及为什么它能让模型“说清楚要干什么”。&lt;/p&gt;
&lt;h2 id="一协议要解决的就是上一话那个脆"&gt;一、协议要解决的，就是上一话那个“脆”
&lt;/h2&gt;&lt;p&gt;先看上一话的提示词 hack 是怎么翻车的。你在 system prompt 里写“想查天气就输出 &lt;code&gt;{&amp;quot;tool&amp;quot;:&amp;quot;get_weather&amp;quot;,&amp;quot;city&amp;quot;:&amp;quot;...&amp;quot;}&lt;/code&gt;”，然后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型输出的不是纯 JSON，前后还夹着“好的，我帮你查一下”这种话；&lt;/li&gt;
&lt;li&gt;JSON 本身残缺，少个引号、少个逗号，&lt;code&gt;json.loads&lt;/code&gt; 直接崩；&lt;/li&gt;
&lt;li&gt;换了个问法，模型就把格式忘得一干二净。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次翻车，你都得靠更长的提示词、更严的正则去兜，兜到最后还是不可靠。&lt;/p&gt;
&lt;p&gt;协议的意义就在这里：&lt;strong&gt;别再用文字去“暗示”模型输出格式，而是给一个结构化的位置，让模型把调用填进去。&lt;/strong&gt; 这个位置是 API 里明确规定好的，模型要么填对，要么 API 直接拒绝，没有“碰运气”的中间地带。&lt;/p&gt;
&lt;h2 id="二协议长什么样tools-进tool_calls-出"&gt;二、协议长什么样：&lt;code&gt;tools&lt;/code&gt; 进，&lt;code&gt;tool_calls&lt;/code&gt; 出
&lt;/h2&gt;&lt;p&gt;整个协议，一进一出两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求里传 &lt;code&gt;tools&lt;/code&gt;&lt;/strong&gt;：你告诉模型“现在有哪些工具可以用”。这是一份菜单，写清楚每个工具叫什么、干什么、参数长什么样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应里收 &lt;code&gt;tool_calls&lt;/code&gt;&lt;/strong&gt;：模型告诉你“我想调哪个工具、带什么参数”。这是一张申请单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一份声明了本地天气工具的菜单：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;TOOLS&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="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;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;function&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="s2"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&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;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&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="s2"&gt;&amp;#34;description&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;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;parameters&amp;#34;&lt;/span&gt;&lt;span class="p"&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;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;object&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="s2"&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;&lt;span class="p"&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;city&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;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;string&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;description&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;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 class="s2"&gt;&amp;#34;required&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;city&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="s2"&gt;&amp;#34;additionalProperties&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&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&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 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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意，&lt;code&gt;tools&lt;/code&gt; 不是把 Python 函数“上传”给模型。它只是一份描述：函数叫 &lt;code&gt;get_weather&lt;/code&gt;、查天气的、需要一个 &lt;code&gt;city&lt;/code&gt; 字符串参数。模型拿到的是这些文字，不是那个函数本身。&lt;a class="link" href="https://api-docs.deepseek.com/api/create-chat-completion" target="_blank" rel="noopener"
 &gt;Chat Completions API&lt;/a&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;assistant_message&lt;/span&gt; &lt;span class="o"&gt;=&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;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&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;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&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;messages&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;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&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;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;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;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;required&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&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-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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;call_00_...&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="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;function&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="nt"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&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="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&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="nt"&gt;&amp;#34;arguments&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{\&amp;#34;city\&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="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&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;code&gt;id&lt;/code&gt;：这条调用的唯一标识，回传结果时要靠它关联；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：固定是 &lt;code&gt;function&lt;/code&gt;，标记这是一次函数调用；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;function.name&lt;/code&gt;：想调哪个工具；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;function.arguments&lt;/code&gt;：一个 &lt;strong&gt;JSON 字符串&lt;/strong&gt;——注意它还是字符串，不是对象，代码里得再 &lt;code&gt;json.loads&lt;/code&gt; 一层才能用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它表达的就是一句话：“请调用 &lt;code&gt;get_weather&lt;/code&gt;，参数是北京。”到这里，模型既没有运行 Python，也没有拿到天气数据。它只是把“想干什么”用协议规定的格式写清楚了。&lt;/p&gt;
&lt;h2 id="三这套协议是谁先做出来的"&gt;三、这套协议，是谁先做出来的
&lt;/h2&gt;&lt;p&gt;它不是我编的，也不是模型厂商某天灵机一动。时间线很清晰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2022 年，&lt;strong&gt;ReAct&lt;/strong&gt;（Yao et al., arXiv 2210.03629）提出“推理 + 行动”交替，用提示词让模型按 &lt;code&gt;Thought / Action&lt;/code&gt; 的格式输出，再让代码去执行；&lt;/li&gt;
&lt;li&gt;2023 年 2 月，&lt;strong&gt;Toolformer&lt;/strong&gt;（Meta AI, arXiv 2302.04761）证明“模型自己决定何时调 API”这个能力是能被训练出来的；&lt;/li&gt;
&lt;li&gt;2023 年 5 月，&lt;strong&gt;Gorilla&lt;/strong&gt;（UC Berkeley, arXiv 2305.15334）开始微调模型学写 API 调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这几篇走的都是“提示词 + 解析”的路线——也就是上一话讲的那个脆办法。&lt;/p&gt;
&lt;p&gt;真正的转折在 2023 年 6 月 13 日：&lt;strong&gt;OpenAI 首次发布原生 Function Calling&lt;/strong&gt;，随 &lt;code&gt;gpt-4-0613&lt;/code&gt; 和 &lt;code&gt;gpt-3.5-turbo-0613&lt;/code&gt; 两个模型快照一起推出。从这一天起，“让模型提工具调用”不再靠提示词暗示，而是变成了 API 里的标准字段——就是我们第二节看到的 &lt;code&gt;tools&lt;/code&gt; 进、&lt;code&gt;tool_calls&lt;/code&gt; 出。&lt;/p&gt;
&lt;h2 id="四别看就一个-tool_calls背后做了很多工作"&gt;四、别看就一个 &lt;code&gt;tool_calls&lt;/code&gt;，背后做了很多工作
&lt;/h2&gt;&lt;p&gt;有个问题自然冒出来：同样是“让模型输出调用”，为什么提示词 hack 天天翻车，而协议一出来，模型就能稳定地输出合法参数、不再给你残缺 JSON？&lt;/p&gt;
&lt;p&gt;不是因为提示词写得好了。是厂商在背后做了工作——&lt;strong&gt;模型是被训练成“会正确提出调用”的&lt;/strong&gt;，这个能力不是运气，是训练出来的。&lt;/p&gt;
&lt;p&gt;这里只说结论，不展开原理（那是下一话的事）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;微调&lt;/strong&gt;：训练阶段喂了大量“工具调用对话”样本，让模型学会判断要不要调、选哪个工具、生成合法参数；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束解码&lt;/strong&gt;：生成的时候，在输出层卡住模型，让它只能吐出符合 schema 的内容，从根上杜绝“残缺 JSON”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两件事把“正确提出调用”从提示词的玄学，变成了模型本身的能力。至于模型内部到底是怎么学会的、这两件事的具体机制——那是第三话要专门讲的问题，这里先按下不表。&lt;/p&gt;
&lt;h2 id="五协议只负责提出不负责执行"&gt;五、协议只负责“提出”，不负责“执行”
&lt;/h2&gt;&lt;p&gt;回到那张申请单。&lt;code&gt;tool_calls&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;def&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;arguments_json&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;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&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;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&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;name&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 class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments_json&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="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&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="s2"&gt;&amp;#34;city&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;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;get_weather 参数必须且只能包含 city&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 class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&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="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&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="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&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;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city 必须是非空字符串&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 class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&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;25℃&lt;/code&gt; 来自 &lt;code&gt;get_weather()&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;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&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="n"&gt;weather&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;北京&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;多云，25℃，东北风3级&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="s2"&gt;&amp;#34;上海&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;小雨，22℃，东南风2级&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&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;weather&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&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;city&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议让模型“说清楚要干什么”，执行永远在模型外面——这是上一话那条结论的延续，在这里变成了可运行的代码。&lt;/p&gt;
&lt;h2 id="六把结果交回去也是协议的一部分"&gt;六、把结果交回去，也是协议的一部分
&lt;/h2&gt;&lt;p&gt;函数执行完，Python 手里有了天气文本，但用户还没看到一句话。模型上一次只停在“我想调什么工具”，所以要把两样东西放回消息历史：&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;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;assistant_message&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;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&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 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="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;tool_call_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 关联到上面那条 tool_calls&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;content&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&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 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;role=tool&lt;/code&gt; 这一步&lt;strong&gt;不是可选的，是协议强制要求的&lt;/strong&gt;。这里有个真实的坑：有人把 &lt;code&gt;role=tool&lt;/code&gt; 写成了 &lt;code&gt;role=user&lt;/code&gt;，API 直接报 HTTP 400——&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;An assistant message with &amp;#39;tool_calls&amp;#39; must be followed by
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tool messages responding to each &amp;#39;tool_call_id&amp;#39;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;意思很直白：你给模型看了一张 &lt;code&gt;tool_calls&lt;/code&gt; 申请单，就必须逐条配上一份对应的 &lt;code&gt;tool&lt;/code&gt; 结果，用 &lt;code&gt;tool_call_id&lt;/code&gt; 对上号。少一条、错一条，协议层面就拒绝，根本轮不到模型来猜。这个报错本身，就是“协议不是约定俗成、而是硬约束”的最好证据。&lt;/p&gt;
&lt;p&gt;补上结果后再问一次模型，它才能把“多云，25℃”组织成一句人话。事实来自函数，句子由模型生成，两边分得清清楚楚。&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;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;tools&lt;/code&gt; 菜单&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tool_calls&lt;/code&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;td&gt;不应把模型请求原样放行&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;role=tool&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;Function Calling 没有把函数“装进模型”。它做的，是把上一话那个脆弱的提示词 hack，变成了一套 &lt;code&gt;tools&lt;/code&gt; 进、&lt;code&gt;tool_calls&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-text" data-lang="text"&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;程序执行：多云，25℃，东北风3级
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;模型回答：北京现在多云，气温 25℃，东北风 3 级。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;配套 Python 与 Java 实现在 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c02-function-calling" target="_blank" rel="noopener"
 &gt;GYA 仓库&lt;/a&gt;与 &lt;a class="link" href="https://github.com/renxin2024/GYA-Java/tree/main/c02-function-calling" target="_blank" rel="noopener"
 &gt;GYA-Java 仓库&lt;/a&gt;的 &lt;code&gt;c02-function-calling&lt;/code&gt; 目录，按 README 的命令跑，不复制这篇文章里零散的代码块。&lt;/p&gt;
&lt;p&gt;协议跑通了，但一个更根本的问题还悬着：&lt;strong&gt;模型凭什么知道，该从菜单里挑 &lt;code&gt;get_weather&lt;/code&gt;、该填北京？&lt;/strong&gt; 这已经不是协议能回答的了——得回到模型训练本身。下一话。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek, &lt;a class="link" href="https://api-docs.deepseek.com/guides/tool_calls/" target="_blank" rel="noopener"
 &gt;Tool Calls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DeepSeek, &lt;a class="link" href="https://api-docs.deepseek.com/api/create-chat-completion" target="_blank" rel="noopener"
 &gt;Chat Completions API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yao et al., &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;Schick et al., &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;Patil et al., &lt;a class="link" href="https://arxiv.org/abs/2305.15334" target="_blank" rel="noopener"
 &gt;Gorilla: Large Language Model Connected with Massive APIs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第三话｜模型为什么能生成工具调用？</title><link>https://zh.renxinblog.cn/post/c03-function-calling-training/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c03-function-calling-training/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c03-function-calling-training-cover-v2.png" alt="Featured image of post 第三话｜模型为什么能生成工具调用？" /&gt;&lt;p&gt;上一话跑通了 Function Calling 协议，但留了个更根本的问题没收尾：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型凭什么知道该从 &lt;code&gt;tools&lt;/code&gt; 菜单里挑 &lt;code&gt;get_weather&lt;/code&gt;、又凭什么把“北京”填进 &lt;code&gt;city&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不是提示词写得好。协议只是给了模型一个“填在哪里”的位置，真正让模型知道“填什么”的，是它的训练。这一话就追这件事：模型是怎么学会“提出一次工具调用”的。&lt;/p&gt;
&lt;p&gt;先给结论，后面再拆：&lt;strong&gt;工具调用不是模型的内置能力，而是训练出来的“输出倾向”。训练改变的是它生成某些 token 序列的概率，不是给它装了一个真的会退款的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一模型学的仍然是下一个-token"&gt;一、模型学的，仍然是下一个 token
&lt;/h2&gt;&lt;p&gt;模型里没有一个叫“退款”的函数。它有的，只是“给定上文，预测下一个词”的能力。所谓“会调工具”，是它学会了：当上下文里出现“退款意图 + 工具定义”时，下一个该输出的，是 &lt;code&gt;refund_order&lt;/code&gt; 和 &lt;code&gt;order_id&lt;/code&gt; 这几个 token。&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;上下文：订单 O-100 已支付，用户要求退款；可用工具有 refund_order(order_id)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;目标：refund_order({&amp;#34;order_id&amp;#34;:&amp;#34;O-100&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;反复见这样的样本，模型参数里就长出一套倾向：&lt;strong&gt;“退款 + 已支付”这类上下文，会抬高 &lt;code&gt;refund_order&lt;/code&gt; 的概率；订单号 O-100，会抬高被填进 &lt;code&gt;order_id&lt;/code&gt; 的概率。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意，训练集还得有反例，否则模型会养成坏习惯：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户只问配送时间 → 直接回答，不调工具；&lt;/li&gt;
&lt;li&gt;订单状态不明 → 先追问，不调工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;少了这些负样本，模型就会变成“看见什么都想调工具”。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[训练样本&lt;br/&gt;意图、工具定义、目标调用] --&gt; B[模型参数&lt;br/&gt;形成输出倾向]
 C[本次请求&lt;br/&gt;用户消息、当前工具、订单状态] --&gt; D[模型生成候选 tool_calls]
 B --&gt; D
 D --&gt; E[Runtime&lt;br/&gt;校验、确认、执行或拒绝]&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;关键洞察：模型只是提出了候选调用。它没有因此获得订单事实、权限，也没有执行权。&lt;/strong&gt; 会“说”要干什么，和“真的干了什么”，是两码事。&lt;/p&gt;
&lt;h2 id="二这条倾向是靠什么训练出来的"&gt;二、这条“倾向”，是靠什么训练出来的？
&lt;/h2&gt;&lt;p&gt;问题来了：这些“上下文 → 正确调用”的样本，是怎么来的？人工一条条标注太贵，学界给了几条公开的路线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Toolformer&lt;/strong&gt; 解决的是“数据怎么规模化造出来”。它的做法很有意思——让模型自己当标注员：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在一段普通文本里，让模型猜哪些位置可能需要调 API，采样出一堆候选调用；&lt;/li&gt;
&lt;li&gt;真的去执行这些调用，拿到结果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键一步&lt;/strong&gt;：只保留那些“拿到结果后，模型预测后续文字更准了”的调用——具体说，就是比较“有结果”和“没结果”两种情况下，模型对后面 token 的预测损失，损失降得够多才留下；&lt;/li&gt;
&lt;li&gt;用过滤后的样本去微调模型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话：&lt;strong&gt;模型自己提出调用、自己执行、再自己判断“这次调用到底有没有用”，有用的才拿来当训练样本。&lt;/strong&gt; 全程不需要人工标注“这里该不该调工具”。&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;&lt;strong&gt;Gorilla&lt;/strong&gt; 解决的是另一个问题：API 会变。哪怕模型学过 &lt;code&gt;get_weather(city)&lt;/code&gt;，文档也可能改成 &lt;code&gt;get_weather(location, unit)&lt;/code&gt;。Gorilla 的做法是微调 + 文档检索结合：训练给模型调用能力，检索把“当前最新的接口契约”带进上下文，让它跟得上变化。&lt;a class="link" href="https://arxiv.org/abs/2305.15334" target="_blank" rel="noopener"
 &gt;Gorilla&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CoT&lt;/strong&gt; 常被顺带提起，但它其实是个对照，不是工具调用训练：它研究的是“在提示里给推理示例能不能改变输出”。它能改变当前这一轮的输出，但&lt;strong&gt;不改模型参数&lt;/strong&gt;——和 Toolformer/Gorilla 那种真正动参数的训练，是两回事。&lt;a class="link" href="https://arxiv.org/abs/2201.11903" target="_blank" rel="noopener"
 &gt;CoT&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="三一件必须分清的事哪些证据能信哪些不能"&gt;三、一件必须分清的事：哪些证据能信，哪些不能
&lt;/h2&gt;&lt;p&gt;讲到这里，得划一条诚实的边界。关于“模型为什么能调工具”，能拿到的证据分三类，&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;论文公开的方法（Toolformer/Gorilla）&lt;/td&gt;
					&lt;td&gt;“存在这些训练工具调用的公开方法”&lt;/td&gt;
					&lt;td&gt;不代表某个商业模型就用了它&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;厂商披露（OpenAI 公告）&lt;/td&gt;
					&lt;td&gt;0613 模型经过微调、Structured Outputs 靠训练+约束解码&lt;/td&gt;
					&lt;td&gt;只限定到那几家、那段时间，不能外推&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;黑盒 API 能观察到的&lt;/td&gt;
					&lt;td&gt;输入什么、输出什么（比如 description 改了就选错工具）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;看不到&lt;/strong&gt;训练语料、训练阶段、奖励方法&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最关键的一条是第三类：&lt;strong&gt;你从 API 拿到的结果再稳定，也反推不出厂商到底是怎么训练的。&lt;/strong&gt; 训练语料长什么样、分几个阶段、有没有用 RLHF——这些是黑盒，除非厂商自己说，否则只能标“未知”。&lt;/p&gt;
&lt;p&gt;所以别犯那个错：看到 &lt;code&gt;tool_calls&lt;/code&gt; 稳定输出，就说“这个模型用了 Toolformer”。稳定输出能证明“它训练得很好”，证明不了“它是怎么训练的”。&lt;/p&gt;
&lt;h2 id="四prompt-only-和-native-tools差的不只是格式"&gt;四、Prompt-only 和 Native tools，差的不只是“格式”
&lt;/h2&gt;&lt;p&gt;上一话讲了 Native tools 的协议形态，这里补一层它和训练/推理的关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt-only&lt;/strong&gt;：在 system prompt 里约定“需要退款时输出 JSON”。模型把 JSON 塞进普通 &lt;code&gt;content&lt;/code&gt; 里，Runtime 还得从正文里把命令抠出来——这里混着自然语言、Markdown、可能还有好几个 JSON。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Native tools&lt;/strong&gt;：在 API 的 &lt;code&gt;tools&lt;/code&gt; 字段传 schema，模型用独立的 &lt;code&gt;tool_calls&lt;/code&gt; 字段返回。Runtime 直接拿到工具名、参数和调用 id，不用猜哪里是命令。&lt;/p&gt;
&lt;p&gt;这个差异不是“格式好看点”，而是&lt;strong&gt;责任归属变了&lt;/strong&gt;：Prompt-only 把“保证输出长得像 JSON”的责任甩给了提示词和你的解析器；Native tools 把这层责任接了过去，一部分靠训练（让模型更会按 schema 输出），一部分靠推理阶段的约束解码（生成时卡住，只允许符合 schema 的 token 出来）。&lt;/p&gt;
&lt;p&gt;OpenAI 官方也明确说过：Function Calling 的可靠性，一部分来自模型的微调，一部分来自 Structured Outputs 的 constrained decoding。&lt;a class="link" href="https://openai.com/index/introducing-structured-outputs-in-the-api/" target="_blank" rel="noopener"
 &gt;Structured Outputs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里有个真实的坑能说明这层差异。我们 Java 版最初只写了“需要时输出 JSON”，没说清 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;arguments.order_id&lt;/code&gt; 的形状。结果 Native tools 模式仍能返回结构化调用，Prompt-only 模式却解析不出来。把提示词补成明确的 JSON 模板后，Java 的 8 个场景才全过。这不是“Java 不适合做 Agent”，而是&lt;strong&gt;Prompt-only 把输出协议的一部分责任，留在了你的提示词和解析器上&lt;/strong&gt;——而 Native tools 帮你卸掉了一部分。&lt;/p&gt;
&lt;h2 id="五动手看一次description-才是选哪个的开关"&gt;五、动手看一次：description 才是“选哪个”的开关
&lt;/h2&gt;&lt;p&gt;我们用一套虚构订单做实验，Python 和 Java 21 各跑一遍。Runtime 先注入已验证的订单状态，模型只做一次工具选择，程序只记录候选调用，&lt;strong&gt;绝不执行退款或取消&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;已支付 O-100，要求退款&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;refund_order(O-100)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;未支付 O-200，要求取消&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;cancel_order(O-200)&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;/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;基线下两种语言都过。然后做一个故障注入：&lt;strong&gt;只把 &lt;code&gt;refund_order&lt;/code&gt; 和 &lt;code&gt;cancel_order&lt;/code&gt; 的 description 对调&lt;/strong&gt;，其余全不变——工具名、模型、用户请求、参数 schema 都一样。&lt;/p&gt;
&lt;p&gt;结果很干净：已支付退款稳定变成 &lt;code&gt;cancel_order(O-100)&lt;/code&gt;，未支付取消稳定变成 &lt;code&gt;refund_order(O-200)&lt;/code&gt;，而&lt;strong&gt;订单号仍然是对的&lt;/strong&gt;。恢复 description 后，回归全过。&lt;/p&gt;
&lt;p&gt;这个实验说明两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具选择（选哪个）靠的是 description&lt;/strong&gt;——模型是读描述来判断“这个工具是干嘛的”，描述写错了，它就选错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数提取（填什么）可以和工具选择分开看&lt;/strong&gt;——订单号还是从用户消息和 Runtime 注入的上下文里来的，两个工具的参数 schema 没变，所以订单号没错。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但这能证明什么、不能证明什么，得说清楚：它证明了“description 影响路由”这个&lt;strong&gt;可观察行为&lt;/strong&gt;，&lt;strong&gt;证明不了&lt;/strong&gt;模型用了哪种训练法。description 写对了，也不代表参数永远填对——Runtime 照样得校验。&lt;/p&gt;
&lt;h2 id="六所以能信到什么程度"&gt;六、所以，能信到什么程度
&lt;/h2&gt;&lt;p&gt;绕了一圈，回到开头那个问题：模型为什么能生成工具调用？&lt;/p&gt;
&lt;p&gt;因为它被训练成了这样——训练样本让它形成了“意图 + 工具定义 → 结构化调用”的输出倾向。Toolformer、Gorilla 这些公开方法，展示了这条倾向可以怎么规模化地训练出来；厂商还叠加了微调和约束解码，让输出更稳。&lt;/p&gt;
&lt;p&gt;但“会提请求”不等于“可靠”。训练让模型更会“说”要干什么，不会让它“变聪明”——它照样可能选错工具、编造业务参数、请求一个无权执行的动作。&lt;strong&gt;tool_calls 永远是候选，不是命令。&lt;/strong&gt; 谁来判断“能不能执行、怎么执行”，是下一篇的事。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wei et al., &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;Schick et al., &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;Patil et al., &lt;a class="link" href="https://arxiv.org/abs/2305.15334" target="_blank" rel="noopener"
 &gt;Gorilla: Large Language Model Connected with Massive APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://openai.com/index/function-calling-and-other-api-updates/" target="_blank" rel="noopener"
 &gt;Function calling and other API updates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://openai.com/index/introducing-structured-outputs-in-the-api/" target="_blank" rel="noopener"
 &gt;Introducing Structured Outputs in the API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第四话｜模型说调 refund_order，Runtime 怎么不把事情搞砸？</title><link>https://zh.renxinblog.cn/post/c04-tool-registry/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c04-tool-registry/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c04-tool-registry-cover-v2.png" alt="Featured image of post 第四话｜模型说调 refund_order，Runtime 怎么不把事情搞砸？" /&gt;&lt;p&gt;第三话结束时，我们留下一个问题没解决：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;code&gt;tool_calls&lt;/code&gt; 永远是候选，不是命令。谁来判断&amp;quot;能不能执行、怎么执行&amp;quot;？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这就是第四话要回答的。模型提出的工具调用，哪怕 JSON 合法、参数齐全，也可能是错的、越权的、有副作用的。Runtime 不能照单全收——它得先校验，再执行，而且要在执行出问题时不把局面搞得更糟。&lt;/p&gt;
&lt;p&gt;这里最危险的，不是选错工具，而是&lt;strong&gt;一个有副作用、结果又说不清的操作&lt;/strong&gt;。比如模型要调 &lt;code&gt;refund_order&lt;/code&gt; 退款：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;handler 执行到一半超时了。钱到底退了没有？直接重试，可能重复退款；不重试，用户又以为没退成。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这一话就沿着这条退款主线，讲清楚 Runtime 怎么把&amp;quot;模型说要调用&amp;quot;变成&amp;quot;安全地执行&amp;quot;——以及最关键的，超时之后怎么根据真实状态做决定，而不是靠猜。&lt;/p&gt;
&lt;h2 id="一模型说调-refund_orderruntime-先干什么"&gt;一、模型说&amp;quot;调 refund_order&amp;quot;，Runtime 先干什么？
&lt;/h2&gt;&lt;p&gt;模型返回的 &lt;code&gt;tool_calls&lt;/code&gt; 里，是一段结构化申请单：工具名 &lt;code&gt;refund_order&lt;/code&gt;、参数 &lt;code&gt;order_id&lt;/code&gt; 和 &lt;code&gt;idempotency_key&lt;/code&gt;。但它是模型生成的，&lt;strong&gt;是外部输入，不是已经校验过的命令&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Runtime 拿到它，第一件事不是执行，是问三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个工具名，是允许执行的吗？&lt;/li&gt;
&lt;li&gt;参数齐全、类型对吗？&lt;/li&gt;
&lt;li&gt;这个工具现在可用吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任何一个不过，就不进 handler，而是返回一个&lt;strong&gt;结构化的错误&lt;/strong&gt;，让上层能稳定地分支处理。&lt;/p&gt;
&lt;p&gt;为什么要结构化，不能是一段&amp;quot;调用失败，请稍后再试&amp;quot;的文案？因为文案里没有可编程的信息——程序没法可靠地区分&amp;quot;工具不存在&amp;quot;和&amp;quot;参数缺了&amp;quot;和&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;&lt;code&gt;UNKNOWN_TOOL&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;INVALID_ARGUMENT&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;TOOL_UNAVAILABLE&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;EXECUTION_ERROR&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;handler 或下游执行失败&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;按恢复语义决定&lt;/strong&gt;，见下一节&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;前三个错误，handler 根本没执行——它们发生在执行之前。第四个才是真正进到了执行阶段，也是最麻烦的。&lt;/p&gt;
&lt;h2 id="二执行时出了问题最怕的是结果说不清"&gt;二、执行时出了问题，最怕的是&amp;quot;结果说不清&amp;quot;
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;refund_order&lt;/code&gt; 通过了校验，handler 真的去调订单服务了——然后超时。&lt;/p&gt;
&lt;p&gt;超时意味着什么？&lt;strong&gt;意味着&amp;quot;结果不确定&amp;quot;&lt;/strong&gt;。请求可能压根没到达订单服务，也可能已经到达、退款都成功了，只是响应没传回来。你分不清。&lt;/p&gt;
&lt;p&gt;这就是最危险的地方：如果 Runtime 把&amp;quot;超时&amp;quot;直接当成&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;code&gt;idempotency_key&lt;/code&gt;（幂等键）。它是发起退款时就带上的、这笔操作的唯一标识。领域服务按这个键记录&amp;quot;这笔退款到底执行了没有&amp;quot;，Runtime 超时后先去查它，而不是再发起一笔。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 A[refund_order 超时] --&gt; B[按 idempotency_key 查询真实状态]
 B --&gt;|SUCCEEDED| C[回放成功结果&lt;br/&gt;不再调 handler]
 B --&gt;|NOT_EXECUTED| D[确认没执行&lt;br/&gt;才重试一次]
 B --&gt;|UNKNOWN / 执行中| E[等待或人工核验]&lt;/pre&gt;&lt;p&gt;三条分支，对应三种真实状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SUCCEEDED&lt;/code&gt;&lt;/strong&gt;：钱已经退了。Runtime 把成功结果回放给用户，&lt;strong&gt;不再调 handler&lt;/strong&gt;。这是最容易犯错的场景——明明已经成功，却因为&amp;quot;没收到响应&amp;quot;而再退一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NOT_EXECUTED&lt;/code&gt;&lt;/strong&gt;：确认没执行过。这时重试一次是安全的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;UNKNOWN&lt;/code&gt; / 执行中&lt;/strong&gt;：状态也查不到。这时既不能重试，也不能谎报成功，只能等待对账或转人工核验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跑一遍主线 demo，你会看到这样一条 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;refund_order response=EXECUTION_ERROR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;idempotency_status=SUCCEEDED
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runtime_action=REPLAY_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;final=SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;refund_order handler_execution_count=1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最后一行是整条链路的关键证据：&lt;strong&gt;&lt;code&gt;handler_execution_count=1&lt;/code&gt;&lt;/strong&gt;。它证明 Runtime 虽然经历了&amp;quot;超时 → 查状态 → 回放&amp;quot;，但真正执行退款的 handler &lt;strong&gt;只跑了一次&lt;/strong&gt;。没有重复退款。&lt;/p&gt;
&lt;h2 id="三把谁负责什么钉死"&gt;三、把&amp;quot;谁负责什么&amp;quot;钉死
&lt;/h2&gt;&lt;p&gt;超时恢复这件事，看着是 Runtime 一个人的活，其实牵涉三个角色，职责不能混：&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;RefundDomain&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;记录和查询&amp;quot;退款到底发生没有&amp;quot;&lt;/td&gt;
					&lt;td&gt;不负责重试策略，也不知道 Runtime 怎么编排&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;模型&lt;/td&gt;
					&lt;td&gt;只提出候选调用&lt;/td&gt;
					&lt;td&gt;不拥有执行权，更不能拿它的话当退款状态&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话：&lt;strong&gt;有副作用工具的超时恢复，首先是领域状态问题，其次才是 Runtime 的重试策略问题。&lt;/strong&gt; 退款有没有发生，是领域事实，只能问领域服务；Runtime 能做的是照着这个事实去编排下一步，而不是自己猜。&lt;/p&gt;
&lt;p&gt;反过来看，如果领域服务不记录幂等状态，Runtime 再聪明也没用——它没有可以查询的真相，超时之后只能抓瞎。所以幂等这件事，&lt;strong&gt;得从工具设计那天就带上&lt;/strong&gt;，而不是等出了超时才补。&lt;/p&gt;
&lt;h2 id="四把这条链路跑起来"&gt;四、把这条链路跑起来
&lt;/h2&gt;&lt;p&gt;配套 demo 在 &lt;code&gt;code/agent-tutorial/c04-tool-registry/&lt;/code&gt;，Python 和 Java 各一份，语义一致。它不调用 LLM、订单或支付系统——所有 handler 都是本地确定性模拟，所以它验证的是 &lt;strong&gt;Runtime 的控制流&lt;/strong&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/c04-tool-registry/python
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 registry.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest test_registry.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="nb"&gt;cd&lt;/span&gt; ../java
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gradle run
&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;code&gt;UNKNOWN_TOOL&lt;/code&gt;，不进 handler&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;工具下线&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TOOL_UNAVAILABLE&lt;/code&gt;，不能硬调&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;参数缺失&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;，补参或拒绝&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正常调用&lt;/td&gt;
					&lt;td&gt;一次执行，&lt;code&gt;SUCCEEDED&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时但已成功&lt;/td&gt;
					&lt;td&gt;查幂等 → &lt;code&gt;SUCCEEDED&lt;/code&gt; → 回放，&lt;code&gt;handler_execution_count=1&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时且未执行&lt;/td&gt;
					&lt;td&gt;查幂等 → &lt;code&gt;NOT_EXECUTED&lt;/code&gt; → 重试一次&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中&amp;quot;超时但已成功&amp;quot;是最值得盯着看的一条：它模拟了 &lt;code&gt;handler&lt;/code&gt; 执行成功后、响应却丢失的场景——退款已经发生，&lt;code&gt;SUCCEEDED&lt;/code&gt;，但 Runtime 收到的是超时。正确的 Trace 必须是&amp;quot;回放成功结果&amp;quot;，而不是&amp;quot;再退一次&amp;quot;。&lt;/p&gt;
&lt;h2 id="五还差的执行成功--永远可靠"&gt;五、还差的：执行成功 ≠ 永远可靠
&lt;/h2&gt;&lt;p&gt;跑通这条链路，Runtime 能安全地执行一次有副作用的调用了。但这离&amp;quot;可靠&amp;quot;还有距离，几件事本篇点到为止，先立个边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一次超时不等于工具坏了&lt;/strong&gt;。订单服务偶发抖动，不能立刻把 &lt;code&gt;refund_order&lt;/code&gt; 永久下线；连续失败达到阈值才考虑熔断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;熔断后的探测，不能用一笔新退款去试&lt;/strong&gt;。要用无副作用的健康检查——否则&amp;quot;检查服务恢复没有&amp;quot;这件事本身，又制造了一笔业务动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime 不替代鉴权、审批和审计&lt;/strong&gt;。工具能不能调，除了契约校验，还涉及&amp;quot;这个用户有没有权限&amp;quot;&amp;ldquo;要不要二次确认&amp;quot;&amp;ldquo;留不留审计留底&amp;rdquo;——这些是 Runtime 与领域服务的责任，本篇没展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都属于&amp;quot;从单次安全执行到生产级可靠&amp;quot;的延伸，先把边界划在这，不抢后续章节的戏。&lt;/p&gt;
&lt;h2 id="到下一话问题才真正变难"&gt;到下一话，问题才真正变难
&lt;/h2&gt;&lt;p&gt;现在，Runtime 已经能把&lt;strong&gt;单个&lt;/strong&gt;候选调用安全地执行，并在超时后不搞砸。&lt;/p&gt;
&lt;p&gt;但它还不会根据第一次工具的结果，决定第二步做什么。&lt;/p&gt;
&lt;p&gt;第五话才进入真正的 ReAct 循环：第二个 Action 必须依赖第一个 Observation。那时，今天建立的 ToolResponse 和幂等恢复，会成为循环里可复用的执行底座。&lt;/p&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><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>第七话｜记忆：上下文、短期、长期</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>第八话｜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>第九话｜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>第十话｜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>第十一话｜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>第十六话｜SSE 是什么？为什么它会用在 AI Agent 开发中</title><link>https://zh.renxinblog.cn/post/sse-in-ai-agent-development/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/sse-in-ai-agent-development/</guid><description>&lt;p&gt;前面把 Agent 的循环、状态、记忆和工具接起来之后，用户仍可能面对一个黑盒。&lt;/p&gt;
&lt;p&gt;他发出“帮我检查这个仓库的测试为什么失败”，页面却在几十秒内没有任何变化。Agent 也许正在生成，也许正在调用工具，也许已经失败；用户无从分辨。&lt;/p&gt;
&lt;p&gt;问题出在传输方式上：普通 HTTP 是一问一答——客户端发一个请求，服务端算完最后一次性返回完整响应。Agent 干活要几十秒，响应就空白几十秒。要让 Agent 不再像黑盒，前端不只需要最终答案，还要持续接收文本增量、工具状态和 Run 的终态。&lt;/p&gt;
&lt;p&gt;SSE（Server-Sent Events）就是把这类&lt;strong&gt;服务端到浏览器的连续事件&lt;/strong&gt;放进 HTTP 的一种标准方式。结论先说：&lt;strong&gt;SSE 不负责让 Agent 变聪明；它负责把 Agent 已经发生的事，按事件边界可靠地呈现给 UI。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一旧方案为什么看不见"&gt;一、旧方案为什么看不见
&lt;/h2&gt;&lt;p&gt;“给我一个答案”用普通 HTTP 正合适：提交命令，取回结果，一次交互结束。但“让我看着你干活”不行——服务端在没有完整结果前不会返回响应，浏览器也就拿不到任何进度。&lt;/p&gt;
&lt;p&gt;要让用户看见进度，服务端必须&lt;strong&gt;在计算完成前就开始输出&lt;/strong&gt;，并且持续输出。这正是 SSE 站在 HTTP 上的理由：一个响应保持打开，服务端不断写入“有边界的小段文本”。&lt;/p&gt;
&lt;p&gt;这里有个经常被混在一起的概念要拆开说清楚。&lt;strong&gt;HTTP 持久连接&lt;/strong&gt;（persistent connection / Keep-Alive）指的是底层连接可以复用、承载多次请求响应，它降低建连成本，但服务端不会在没有新请求时主动推送业务消息。&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9112.html#section-9.3" target="_blank" rel="noopener"
 &gt;RFC 9112 §9.3&lt;/a&gt; &lt;strong&gt;长轮询&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;th&gt;通信方向&lt;/th&gt;
					&lt;th&gt;一次交互是否持续&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;普通 HTTP&lt;/td&gt;
					&lt;td&gt;提交命令、取得一次结果&lt;/td&gt;
					&lt;td&gt;双向，但一问一答&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
					&lt;td&gt;创建 Run、提交用户输入、取消任务、确认敏感工具调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTP 持久连接&lt;/td&gt;
					&lt;td&gt;复用底层连接，减少建连成本&lt;/td&gt;
					&lt;td&gt;仍是多次 HTTP 请求/响应&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
					&lt;td&gt;通常由客户端、网关和连接池处理，不是事件通道方案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SSE&lt;/td&gt;
					&lt;td&gt;服务端持续发送有边界的文本事件&lt;/td&gt;
					&lt;td&gt;服务端 → 浏览器&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;文本增量、工具进度、Run 状态、最终结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;WebSocket&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;/tbody&gt;
&lt;/table&gt;
&lt;figure class="gallery-image"&gt;
 &lt;a class="image-link" href="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-http-protocol-relationships.svg" data-pswp-width="746" data-pswp-height="622" target="_blank"&gt;
 &lt;img src="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-http-protocol-relationships.svg" width="746" height="622" alt="四种通信方式处在不同层次：HTTP 持久连接复用连接，SSE 持续响应，WebSocket 在 Upgrade 后使用双向帧"&gt;
 &lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;WebSocket 与 HTTP 的关系也要说准确：它用 HTTP Upgrade 完成开场握手，握手成功后双方交换的是 WebSocket 帧，而不是继续交换 HTTP 响应体。&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc6455#section-4" target="_blank" rel="noopener"
 &gt;RFC 6455 §4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：持久连接是连接复用策略；SSE 是 HTTP 上的事件流格式；WebSocket 是经 HTTP 握手切换出的双向协议。它们不在同一抽象层。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二亲眼看sse-在线上传了什么"&gt;二、亲眼看：SSE 在线上传了什么
&lt;/h2&gt;&lt;p&gt;浏览器用 &lt;code&gt;EventSource&lt;/code&gt; 发起请求，服务端以 &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; 返回响应，并持续写入按行组织的 UTF-8 文本。每个事件以空行结束。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2&lt;/a&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 42
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;delta&amp;#34;:&amp;#34;正在读取测试报告…&amp;#34;}
&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;th&gt;在 Agent 流中的用法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;data&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;事件数据；可以多行，浏览器会按规范合并&lt;/td&gt;
					&lt;td&gt;放 JSON payload，例如文本增量或工具结果摘要&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;event&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;事件类型；省略时是默认 &lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;区分 &lt;code&gt;text.delta&lt;/code&gt;、&lt;code&gt;tool.started&lt;/code&gt;、&lt;code&gt;run.completed&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;更新浏览器保存的最后事件 ID&lt;/td&gt;
					&lt;td&gt;用作断线续传的游标；服务端仍需校验 Run 归属&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;retry&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;建议浏览器下一次重连前等待的毫秒数&lt;/td&gt;
					&lt;td&gt;仅调整重连等待，不解决事件去重或业务恢复&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意最后一行的空行不是装饰。它是事件边界：如果连接在空行前结束，尚未完成的事件不会被分发。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.5–9.2.6&lt;/a&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-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/runs/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text.delta&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;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="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="nx"&gt;appendAssistantText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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&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="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;run.completed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;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="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;EventSource&lt;/code&gt; 对断开连接具备重连处理，并在已有事件 ID 时把 &lt;code&gt;Last-Event-ID&lt;/code&gt; 带回服务端。它提供的是恢复的协议钩子，&lt;strong&gt;不是“消息恰好一次送达”的保证&lt;/strong&gt;；应用仍要定义续传、去重和终态行为。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.3–9.2.4&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="三事件的形状由业务决定agent-run-事件契约"&gt;三、事件的形状由业务决定：Agent Run 事件契约
&lt;/h2&gt;&lt;p&gt;只把模型 token 一段段传到 UI，用户会看到“它在说话”，但仍不知道它是否在行动。&lt;/p&gt;
&lt;p&gt;对一个会调用工具的 Agent，更有用的做法是把 Run 生命周期投影成一组应用层事件：&lt;/p&gt;
&lt;figure class="gallery-image"&gt;
 &lt;a class="image-link" href="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-agent-run-event-flow.svg" data-pswp-width="864" data-pswp-height="288" target="_blank"&gt;
 &lt;img src="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-agent-run-event-flow.svg" width="864" height="288" alt="一次 Agent Run 中，普通 HTTP 用来创建任务，SSE 用来把模型与工具产生的下行事件送到浏览器"&gt;
 &lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;例如，“检查测试为什么失败”这个 Run 在客户端看来可以是这样一串事件：&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;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;delta&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;event: tool.started
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;tool&amp;#34;:&amp;#34;run_tests&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_7&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;event: tool.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_7&amp;#34;,&amp;#34;summary&amp;#34;:&amp;#34;3 个测试失败&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;event: run.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些名称不是 SSE 规定的字段值，而是本文建议的&lt;strong&gt;业务事件契约&lt;/strong&gt;。SSE 只规定如何分帧、如何指定事件类型、如何传递最后事件 ID；&lt;code&gt;tool.started&lt;/code&gt; 是否存在、数据结构是什么、是否向用户展示工具结果，都由你的 Agent 产品和安全策略决定。&lt;/p&gt;
&lt;p&gt;这和 Java 微服务中的“领域事件”和“Kafka 的传输记录”很像：Kafka 不替你定义订单状态机；SSE 也不替你定义 Agent Run 状态机。先定义业务状态和权限边界，再选择传输通道。&lt;/p&gt;
&lt;h2 id="四动手跑通一条本地-agent-run-事件流"&gt;四、动手：跑通一条本地 Agent Run 事件流
&lt;/h2&gt;&lt;p&gt;这一节的 demo 不调用真实模型，避免让 API Key、模型费用或网络问题遮住协议本身。服务端固定模拟一个 Run：它先发送文本增量，再报告工具开始、工具完成，最后发送 Run 完成事件。&lt;/p&gt;
&lt;p&gt;配套代码有两份等价实现：Python 位于 GYA 的 &lt;code&gt;c16-sse-agent-stream/&lt;/code&gt;，Java 位于 GYA-Java 的同名 Gradle 子模块。正文以 Python 为主，Java 版用于验证同一协议不依赖语言或 Spring 框架。&lt;/p&gt;
&lt;h3 id="python标准库即可启动"&gt;Python：标准库即可启动
&lt;/h3&gt;&lt;p&gt;前置环境：Python 3.9+ 与 &lt;code&gt;curl&lt;/code&gt;，无第三方依赖、无 API Key。&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;git clone git@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/c16-sse-agent-stream
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py --once
&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;curl -N http://127.0.0.1:8765/events
&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;id: 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;delta&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;id: 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.started
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;tool&amp;#34;:&amp;#34;run_tests&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_1&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;id: 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_1&amp;#34;,&amp;#34;summary&amp;#34;:&amp;#34;3 个测试失败&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;id: 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: run.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里最重要的不是 &lt;code&gt;sleep(0.1)&lt;/code&gt;，而是每次写完一帧后立刻 flush，并且用最后那个空行完成事件。把空行删掉，再用浏览器 &lt;code&gt;EventSource&lt;/code&gt; 接收时，读者会发现它不会把这一帧当作完成的事件。&lt;/p&gt;
&lt;h3 id="java同一事件流不换协议"&gt;Java：同一事件流，不换协议
&lt;/h3&gt;&lt;p&gt;前置环境：JDK 21 与 &lt;code&gt;curl&lt;/code&gt;。GYA-Java 使用仓库自带的 Gradle Wrapper；首次运行会按仓库配置下载 Gradle 和 Jackson。&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;git clone git@github.com:renxin2024/GYA-Java.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-Java
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./gradlew :c16-sse-agent-stream:run --args&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--once&amp;#34;&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;curl -N http://127.0.0.1:8766/events
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Java 版构建通过，并按相同顺序输出 &lt;code&gt;text.delta&lt;/code&gt;、&lt;code&gt;tool.started&lt;/code&gt;、&lt;code&gt;tool.completed&lt;/code&gt;、&lt;code&gt;run.completed&lt;/code&gt;。核心的分帧代码和 Python 同构，只是把字符串拼接换成了 Jackson 序列化 payload：&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;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IOException&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="c1"&gt;// SSE 是 UTF-8 的按行协议。最后的空行是事件边界；删除它会让&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="c1"&gt;// EventSource 持续等待，而不是把前面的字段作为一条完整事件分发出去。&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;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&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="s"&gt;&amp;#34;id: &amp;#34;&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()&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="s"&gt;&amp;#34;\n&amp;#34;&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="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;event: &amp;#34;&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()&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="s"&gt;&amp;#34;\n&amp;#34;&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="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data: &amp;#34;&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;())&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="s"&gt;&amp;#34;\n\n&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StandardCharsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&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="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;code&gt;Event&lt;/code&gt; 是一个只保存 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;type&lt;/code&gt;、&lt;code&gt;data&lt;/code&gt; 的 record；&lt;code&gt;EVENTS&lt;/code&gt; 里和 Python 一样放着四个固定事件。既然协议的“形状”完全由分帧代码决定，换语言、换框架都不会改变这条事件流的语义——这正是把协议层与业务层分开的价值。&lt;/p&gt;
&lt;p&gt;如果 Java 版本因 toolchain 无法启动，先运行 &lt;code&gt;java -version&lt;/code&gt; 确认实际环境是 JDK 21。这个 demo 故意保持 Java 21，不为了迁就本机默认 JDK 而降低系列约束。另外，不要把 Python 与 Java 输出的 JSON 键顺序当作正确性标准：JSON 字段顺序不属于 SSE 契约。&lt;/p&gt;
&lt;h2 id="五能连通--体验到好三个坑"&gt;五、能连通 ≠ 体验到好：三个坑
&lt;/h2&gt;&lt;p&gt;连得上、有输出，不代表体验是对的。下面是三个最常见的“能连通、但体验很差”的问题。&lt;/p&gt;
&lt;h3 id="1-服务端在发用户却最后一次性看到"&gt;1. 服务端在发，用户却最后一次性看到
&lt;/h3&gt;&lt;p&gt;SSE 需要端到端按事件及时传输。规范明确提醒：不恰当的块缓冲可能延迟事件分发。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;因此排查顺序不该从“模型为什么这么慢”开始，而应沿着浏览器 → CDN/网关 → 反向代理 → 应用服务器逐跳确认：响应类型是否正确、是否有缓冲、应用是否真的在事件产生时写出。&lt;code&gt;curl&lt;/code&gt; 先关掉客户端缓冲（&lt;code&gt;curl -N&lt;/code&gt;）；如果 curl 能一帧帧出来、浏览器不行，问题大概率在中间层。具体关闭何种缓冲取决于网关产品和部署方式，不能从一段通用配置照抄。&lt;/p&gt;
&lt;h3 id="2-断线后重复显示或者漏掉关键状态"&gt;2. 断线后重复显示，或者漏掉关键状态
&lt;/h3&gt;&lt;p&gt;浏览器会携带 &lt;code&gt;Last-Event-ID&lt;/code&gt; 重连，但服务端必须决定这个 ID 的语义：它是某个 Run 内单调递增的序号，还是全局事件 ID？事件保留多久？补发窗口过期时如何回退？&lt;/p&gt;
&lt;p&gt;一个可维护的最小约定是：&lt;code&gt;runId + eventId&lt;/code&gt; 唯一；客户端按该组合去重；服务端只允许已授权的主体按 &lt;code&gt;runId&lt;/code&gt; 查询或续传。这里的授权检查不能因为客户端带了一个事件 ID 就被跳过。&lt;/p&gt;
&lt;h3 id="3-run-已结束连接和资源还在等待"&gt;3. Run 已结束，连接和资源还在等待
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;run.completed&lt;/code&gt;、&lt;code&gt;run.failed&lt;/code&gt;、&lt;code&gt;run.cancelled&lt;/code&gt; 这类终态应有清晰语义：服务端发送终态事件后结束流，客户端收到后关闭 &lt;code&gt;EventSource&lt;/code&gt; 并更新界面。心跳注释可以帮助发现中间链路的空闲超时，但心跳不应伪装成业务进度。&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" target="_blank" rel="noopener"
 &gt;MDN SSE 指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：SSE 解决“事件怎样流到浏览器”；事件是否可恢复、可授权、可解释，仍是 Agent Run 模型的责任。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="六回到开场黑盒变成了可见的事件流"&gt;六、回到开场：黑盒变成了可见的事件流
&lt;/h2&gt;&lt;p&gt;现在回头看开头的那个 Run。“帮我检查这个仓库的测试为什么失败”在 demo 里变成了一条可见的时间线：先是“我先运行测试。”的文本增量，然后是工具开始、工具完成（&lt;code&gt;3 个测试失败&lt;/code&gt;），最后 Run 完成。用户不再对着空白页面猜 Agent 是不是卡死了——他能看到 Agent 正在做什么、做到哪了、结果如何。&lt;/p&gt;
&lt;p&gt;这就是这一话真正改变的东西：&lt;strong&gt;“它在动”从结论变成了过程。&lt;/strong&gt; SSE 不负责让 Agent 变聪明，它负责把 Agent 正在做和已经做的事，按事件边界可靠地送到浏览器；之后要不要展示工具结果、要不要拒绝敏感调用，仍然是产品与安全决策。&lt;/p&gt;
&lt;h2 id="七选型什么时候选-sse什么时候不选"&gt;七、选型：什么时候选 SSE，什么时候不选
&lt;/h2&gt;&lt;p&gt;如果 UI 的主要需求是“服务端持续告诉用户发生了什么”，SSE 是很自然的第一候选：浏览器有原生 &lt;code&gt;EventSource&lt;/code&gt;，协议有事件类型和重连语义，接入普通 HTTP 基础设施的成本通常较低。&lt;/p&gt;
&lt;p&gt;如果客户端也需要高频、低延迟地主动发消息，例如多人协同编辑、实时控制面板或双向交互游戏，再评估 WebSocket。若只是提交用户问题、取消 Run 或同意执行高风险工具，普通 HTTP 往往更直观，也更容易把请求、权限校验和审计记录放在清晰的命令边界上。&lt;/p&gt;
&lt;p&gt;不要用“WebSocket 更实时”替代选型分析。对 Agent 来说，最关键的问题是：消息主要朝哪个方向流？是否需要浏览器原生重连与事件 ID？你的 Run 是否有可恢复的状态模型？&lt;/p&gt;
&lt;h2 id="八下一步"&gt;八、下一步
&lt;/h2&gt;&lt;p&gt;这一话放在 GYA 的生产级补充位置：它不增加 Agent 的推理能力，却决定用户能否看见、理解和恢复一次正在运行的 Agent 任务。&lt;/p&gt;
&lt;p&gt;从开场那个 Run 继续往后走，下一步应把同样的事件契约接到真实的 Run 状态存储、鉴权与可观测性中——而不是继续往 SSE 帧里塞业务判断。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard — Server-sent events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9112.html#section-9.3" target="_blank" rel="noopener"
 &gt;RFC 9112 — HTTP/1.1 Persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc6455" target="_blank" rel="noopener"
 &gt;RFC 6455 — The WebSocket Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" target="_blank" rel="noopener"
 &gt;MDN — Using server-sent events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html#webmvc-ann-async-sse" target="_blank" rel="noopener"
 &gt;Spring Framework — MVC SSE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/return-types.html" target="_blank" rel="noopener"
 &gt;Spring Framework — WebFlux return values&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>