<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>LLM on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/llm/</link><description>Recent content in LLM on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Fri, 21 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/llm/index.xml" rel="self" type="application/rss+xml"/><item><title>第一话｜大模型会聊天，但怎么让它知道你的退货政策？</title><link>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gyr-c01-enterprise-rag-start-cover.png" alt="Featured image of post 第一话｜大模型会聊天，但怎么让它知道你的退货政策？" /&gt;&lt;!--
GYR 第一话：RAG 系列总览
演进主线：裸大模型 → Prompt → 最小 RAG → 企业级知识助手
下一篇：Embedding 与手写 Top-K 检索
--&gt;
&lt;p&gt;“NovaTrail X2 支持 7 天无理由退货吗？”&lt;/p&gt;
&lt;p&gt;把这个问题交给一个裸大模型，它大概率会给出一段很像客服的话：商品需要保持未使用、包装完整，并保留购买凭证。&lt;/p&gt;
&lt;p&gt;听起来没什么问题。&lt;/p&gt;
&lt;p&gt;但它不知道 NovaShop 的真实退货政策。它没有看过这份商品资料，不知道规则适用于哪个地区，也不知道政策是不是昨天刚刚改过。&lt;/p&gt;
&lt;p&gt;它只是生成了一段听起来合理的文字。&lt;/p&gt;
&lt;p&gt;这就是 RAG 系列要解决的起点：&lt;strong&gt;大模型虽然会聊天，但它不知道你的退货政策。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一裸大模型会生成答案但没有企业知识"&gt;一、裸大模型：会生成答案，但没有企业知识
&lt;/h2&gt;&lt;p&gt;先不谈 RAG，看看最原始的系统是什么样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户问题 → Chat 模型 → 文本回答
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这类模型可以翻译、总结、写代码，也能把客服话术写得很自然。但它的回答依赖两类输入：训练时学到的参数知识，以及本次请求中传入的上下文。&lt;/p&gt;
&lt;p&gt;企业自己的商品政策、内部流程、最新价格和租户数据，通常不在它能够直接访问的上下文里。模型并不会因为“你是 NovaShop 客服”这句话，就自动获得 NovaShop 的资料。&lt;/p&gt;
&lt;p&gt;所以裸模型的第一个边界很清楚：&lt;strong&gt;它可以生成企业知识问答的语言，却没有企业知识问答的证据。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二第一种补救把资料塞进-prompt"&gt;二、第一种补救：把资料塞进 Prompt
&lt;/h2&gt;&lt;p&gt;最简单的办法，是把政策直接放进提示词：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你是 NovaShop 客服。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;以下是退货政策：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NovaTrail X2 支持签收后 7 天内无理由退货，商品须未使用、包装完整，并保留配件和购买凭证。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;请根据这段政策回答用户的问题。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个办法确实有效。模型现在能看到资料，也能根据资料生成回答。&lt;/p&gt;
&lt;p&gt;问题是，企业资料不会永远只有一段。商品规格、物流规则、保修政策、不同地区的退货政策和新旧版本都会继续增加。最后，Prompt 会变成一份越来越长、越来越难维护的知识文档。&lt;/p&gt;
&lt;p&gt;当回答出错时，排查也很困难：资料没有放进去？放进去了但没有检索到？检索到了但模型没有使用？还是引用了已经过期的版本？&lt;/p&gt;
&lt;p&gt;Prompt 方案解决的是“把资料放到模型眼前”，却没有解决“资料如何管理、如何查找、如何证明回答有依据”。&lt;/p&gt;

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

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

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

 &lt;/blockquote&gt;
&lt;h2 id="六gyr-系列接下来会补齐什么"&gt;六、GYR 系列接下来会补齐什么
&lt;/h2&gt;&lt;p&gt;现在可以回头看整个系列的路线。每一篇不是凭空增加一个组件，而是在解决上一层暴露出来的具体问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;阶段&lt;/th&gt;
					&lt;th&gt;要解决的问题&lt;/th&gt;
					&lt;th&gt;主题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;第一话&lt;/td&gt;
					&lt;td&gt;整条链路到底是什么&lt;/td&gt;
					&lt;td&gt;RAG 全景与演进路线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第二话&lt;/td&gt;
					&lt;td&gt;问题和文本为什么可以比较&lt;/td&gt;
					&lt;td&gt;Embedding、余弦相似度、Top-K&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第三话&lt;/td&gt;
					&lt;td&gt;一篇长文应该如何检索&lt;/td&gt;
					&lt;td&gt;文档分块与 Chunk 设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第四话&lt;/td&gt;
					&lt;td&gt;找到资料后如何可靠回答&lt;/td&gt;
					&lt;td&gt;上下文、引用与拒答&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第五话–第六话&lt;/td&gt;
					&lt;td&gt;如何保存和评价检索结果&lt;/td&gt;
					&lt;td&gt;PGVector、版本与评测基线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第七话–第八话&lt;/td&gt;
					&lt;td&gt;关键词和向量如何配合&lt;/td&gt;
					&lt;td&gt;检索优化与混合检索&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第九话–第十二话&lt;/td&gt;
					&lt;td&gt;企业资料如何进入系统并保持边界&lt;/td&gt;
					&lt;td&gt;解析、租户权限、任务恢复与观测&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第十三话–第十四话&lt;/td&gt;
					&lt;td&gt;如何收敛为可运行的服务&lt;/td&gt;
					&lt;td&gt;API、模块化原型与框架对照&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这条路线有一个刻意的约束：先手写最小机制，再引入数据库、服务和框架。否则读者很容易得到一个“能启动的 RAG”，却不知道每个组件为什么存在。&lt;/p&gt;
&lt;h2 id="下一篇embedding-到底做了什么"&gt;下一篇：Embedding 到底做了什么
&lt;/h2&gt;&lt;p&gt;现在我们知道 RAG 的整体链路，也看到一个最小版本确实可以把问题和资料交给模型。但“检索”仍然像一个黑盒：为什么退货政策排在前面？相似度分数是什么？SKU 这种精确编号也适合这样检索吗？&lt;/p&gt;
&lt;p&gt;下一篇只回答一个问题：&lt;strong&gt;一句用户问题，为什么可以和一段商品政策计算相似度？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们会检查 Embedding 的真实输出，手写余弦相似度和 Top-K 排序，把第一话图里的“检索”拆开来看。&lt;/p&gt;
&lt;p&gt;第一篇只需要记住一句话：&lt;/p&gt;

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

 &lt;/blockquote&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Patrick Lewis 等，&lt;a class="link" href="https://arxiv.org/abs/2005.11401" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;&lt;/a&gt;，2020。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/en/model-studio/list-models" target="_blank" rel="noopener"
 &gt;&lt;em&gt;List models&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/deepseek-api" target="_blank" rel="noopener"
 &gt;&lt;em&gt;DeepSeek API&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Elasticsearch，&lt;a class="link" href="https://www.elastic.co/docs/solutions/search/search-approaches" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Search approaches&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>RAG 分块策略：为什么你的知识库答非所问，问题多半出在这里</title><link>https://zh.renxinblog.cn/post/rag-chunking-strategies/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/rag-chunking-strategies/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/rag-chunking-strategies-cover.png" alt="Featured image of post RAG 分块策略：为什么你的知识库答非所问，问题多半出在这里" /&gt;&lt;p&gt;你搭了一个知识库问答系统：上传文档、建好索引、跑通 demo——然后 LLM 开始答非所问。&lt;/p&gt;
&lt;p&gt;你以为是模型不够聪明。换更强的模型，还是不对。&lt;/p&gt;
&lt;p&gt;问题很可能出在一个不起眼的环节：&lt;strong&gt;文档入库时的分块&lt;/strong&gt;。LLM 根本没读到该读的内容，模型再强也白搭。&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;/p&gt;
&lt;h3 id="原因一上下文窗口是有限的"&gt;原因一：上下文窗口是有限的
&lt;/h3&gt;&lt;p&gt;LLM 的上下文窗口再大也有上限。就算现在有 128K、1M 上下文的模型，检索也不是把整个文档库塞进 Prompt——那样成本直接爆炸。&lt;/p&gt;
&lt;p&gt;更重要的是，窗口越大，模型越难从中找到准确的那一段。这就是著名的&amp;quot;大海捞针&amp;quot;问题：上下文拉长，检索精度反而下降。&lt;/p&gt;
&lt;h3 id="原因二检索信噪比"&gt;原因二：检索信噪比
&lt;/h3&gt;&lt;p&gt;用户问的是文档里的一个小问题，但整篇文档有几千字。&lt;/p&gt;
&lt;p&gt;整篇匹配会发生什么？不相关的内容把相关内容的信号稀释了。向量检索返回的 Top-K 结果里，噪音占了大多数，相关的那一小段被埋没。&lt;/p&gt;
&lt;p&gt;块太大 → 无关信息稀释信号。这是信噪比的第一面。&lt;/p&gt;
&lt;h3 id="原因三embedding-对长文本的平均化"&gt;原因三：Embedding 对长文本的&amp;quot;平均化&amp;quot;
&lt;/h3&gt;&lt;p&gt;Embedding 模型擅长表示短文本——一两句话的语义最清晰。&lt;/p&gt;
&lt;p&gt;文本一长，向量就被&amp;quot;平均&amp;quot;了：整段的语义混在一起，变成一个模糊的方向。检索时，这个模糊向量既匹配不上问题，也匹配不上文档里的任何精确信息。&lt;/p&gt;
&lt;p&gt;Embedding 模型对短文本的语义表示最好，长文本会被&amp;quot;平均&amp;quot;成模糊向量。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;三个原因叠加，结论只有一个：&lt;strong&gt;必须把文档切成合适大小的块&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["① 上下文窗口有限"] --&gt; D["必须分块"]
 B["② 检索信噪比"] --&gt; D
 C["③ Embedding 平均化"] --&gt; D
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style D fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;但怎么切，才是真正的问题。切小了语境不够，切大了检索不精——这就是分块的核心矛盾：&lt;strong&gt;检索精度 vs 上下文完整性&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="分块的两个基础参数"&gt;分块的两个基础参数
&lt;/h2&gt;&lt;p&gt;在讨论策略之前，先理解所有分块策略都绕不开的两个参数。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;参数&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;经验值（中文场景）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;chunk_size&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每块的大小&lt;/td&gt;
					&lt;td&gt;300-800 字（约 500-1200 tokens）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;chunk_overlap&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;相邻块之间的重叠量&lt;/td&gt;
					&lt;td&gt;chunk_size 的 10%-20%&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;为什么需要 overlap？看一个例子。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["文档&lt;br/&gt;AAAABBBBCCCCDDDD"] --&gt; B["chunk_size=8, overlap=2&lt;br/&gt;块1: AAAABBBB&lt;br/&gt;块2: BBCCCCDD&lt;br/&gt;BB 被两块共享"]
 A --&gt; C["chunk_size=8, overlap=0&lt;br/&gt;块1: AAAABBBB&lt;br/&gt;块2: CCCCDD&lt;br/&gt;BB-CC 之间语义断裂"]
 B --&gt; D["✅ 跨块内容仍可检索"]
 C --&gt; E["❌ 关键句恰好被切断时丢失"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#d1fae5,stroke:#059669,color:#064e3b
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#d1fae5,stroke:#059669,color:#064e3b
 style E fill:#fee2e2,stroke:#dc2626,color:#7f1d1d&lt;/pre&gt;&lt;p&gt;overlap=0 时，如果&amp;quot;BBCC&amp;quot;恰好是完整语义（比如一个概念的完整描述），它被拦腰切断，两块都不完整。检索时哪块都匹配不精准，LLM 拿到的都是残句。&lt;/p&gt;
&lt;p&gt;overlap 就是给跨块语义留的缓冲带。&lt;/p&gt;
&lt;h2 id="五种分块策略逐个拆解"&gt;五种分块策略，逐个拆解
&lt;/h2&gt;&lt;p&gt;理解了参数，进入正题。分块策略从简单到复杂，正好是一条问题驱动的问题链。&lt;/p&gt;
&lt;h3 id="策略一固定大小分块最简单也最粗暴"&gt;策略一：固定大小分块——最简单，也最粗暴
&lt;/h3&gt;&lt;p&gt;做法：按预设的字符数或 token 数直接切。&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="c1"&gt;# LangChain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;text_splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharacterTextSplitter&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;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&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;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&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;/p&gt;
&lt;p&gt;缺点却很致命：&lt;strong&gt;完全无视语义边界&lt;/strong&gt;。可能在句子中间、甚至词语中间切断。&lt;/p&gt;
&lt;p&gt;切出来的块，很多是&amp;quot;半个意思&amp;quot;。检索时匹配到半句话，LLM 看着上下文猜——这正是答非所问的来源。&lt;/p&gt;
&lt;p&gt;适合日志、代码等结构弱、语义依赖不强的文本。自然语言文档用它，基本等于摆烂。&lt;/p&gt;
&lt;h3 id="策略二递归分块默认策略性价比之王"&gt;策略二：递归分块——默认策略，性价比之王
&lt;/h3&gt;&lt;p&gt;做法：按优先级尝试分隔符，从大到小逐级切。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# LangChain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;text_splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&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;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&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;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&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;separators&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;&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&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="s2"&gt;&amp;#34;，&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="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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["超长段落"] --&gt; B{"按优先级逐级切分&lt;br/&gt;段落 → 换行 → 句子 → 词"}
 B --&gt;|"每块 ≤ 上限"| C["✅ 在语义边界成块"]
 B --&gt;|"仍超限"| D["降级到更细的分隔符"]
 D --&gt; B
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&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;h3 id="策略三语义分块让-embedding-参与决策"&gt;策略三：语义分块——让 Embedding 参与决策
&lt;/h3&gt;&lt;p&gt;递归分块是按&amp;quot;形式&amp;quot;切（分隔符）。语义分块按&amp;quot;意思&amp;quot;切。&lt;/p&gt;
&lt;p&gt;做法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把文本拆成句子&lt;/li&gt;
&lt;li&gt;计算每个句子的 Embedding 向量&lt;/li&gt;
&lt;li&gt;计算相邻句子的语义相似度&lt;/li&gt;
&lt;li&gt;在相似度&amp;quot;断崖&amp;quot;处切分&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["句子1: 聚类算法把样本分组"] --&gt; B["句子2: K-Means 是经典算法"]
 B --&gt; C["句子3: 明天北京多云转晴"]
 C --&gt; D["语义断崖&lt;br/&gt;句子2→3 相似度骤降&lt;br/&gt;在这里切"]
 A -.-&gt;|"高相似"| B
 B -.-&gt;|"低相似"| C
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&gt;&lt;p&gt;核心算法是新颖度检测：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;novelty(s_i) = 1 - cosine_sim(embed(s_i), embed(context_window))
当 novelty &amp;gt; 均值 + 0.8 * 标准差 → 切分
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;优点：语义连贯性最好，检索精度最高。这也是为什么生产环境在&amp;quot;疑难块&amp;quot;上会启用它。&lt;/p&gt;
&lt;p&gt;缺点：每个句子都要算一次 Embedding，&lt;strong&gt;计算成本翻倍&lt;/strong&gt;。全量用不划算，适合作为精修手段。&lt;/p&gt;
&lt;p&gt;工具：semchunk、LlamaIndex SemanticSplitterNodeParser。官方 benchmark 里，semchunk 的 AI 分块模式在 Legal RAG QA 上正确率 37.7%，比 LangChain 递归分块的 34.8% 高约 2.9 个百分点。&lt;/p&gt;
&lt;h3 id="策略四结构感知分块利用文档自身的结构"&gt;策略四：结构感知分块——利用文档自身的结构
&lt;/h3&gt;&lt;p&gt;如果文档本身有结构，为什么不直接用？&lt;/p&gt;
&lt;p&gt;做法：按文档的固有边界切——Markdown 按标题层级、HTML 按标签、代码按函数类定义、对话按轮次。&lt;/p&gt;
&lt;p&gt;关键技巧是&lt;strong&gt;注入结构元数据&lt;/strong&gt;：每个 chunk 附带它在文档中的位置路径。&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;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="nt"&gt;&amp;#34;metadata&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;header_path&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;第三章 &amp;gt; 3.2 无监督学习 &amp;gt; 3.2.1 K-Means&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;section_level&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&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;/li&gt;
&lt;li&gt;&lt;strong&gt;检索结果自带上下文&lt;/strong&gt;——LLM 不光看到这段内容，还知道它来自哪一章哪一节&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["第三章 无监督学习"] --&gt; B["3.2 无监督学习"]
 B --&gt; C["3.2.1 K-Means&lt;br/&gt;chunk 携带 header_path"]
 C --&gt; D["检索命中 3.2.1 时&lt;br/&gt;LLM 知道完整上下文位置"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;缺点：依赖文档格式规范。纯文本、扫描件这类无结构文档，它无从下手。&lt;/p&gt;
&lt;h3 id="策略五父子分块生产环境的首选"&gt;策略五：父子分块——生产环境的首选
&lt;/h3&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;：段落/小节级别（500-1000 tokens），提供完整上下文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子块&lt;/strong&gt;：句子级别（100-200 tokens），负责精确检索&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;检索流程：用小的找，用大的答。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["用户提问"] --&gt; B["在子块中检索&lt;br/&gt;句子级别，高精度"]
 B --&gt; C["命中子块：&lt;br/&gt;K-Means 是经典算法"]
 C --&gt; D["通过 parent_id 找到父块"]
 D --&gt; E["返回父块完整内容&lt;br/&gt;上下文完整，LLM 能理解"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#d1fae5,stroke:#059669,color:#064e3b
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style E fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 subgraph P["父块：3.2.1 完整小节"]
 C1["子块1: 算法定义"]
 C2["子块2: 参数说明"]
 C3["子块3: 适用场景"]
 end
 Q["问题命中子块2"] --&gt; C2
 C2 --&gt; P
 P --&gt; LLM["LLM 收到父块全文"]
 style P fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C1 fill:#d1fae5,stroke:#059669,color:#064e3b
 style C2 fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style C3 fill:#d1fae5,stroke:#059669,color:#064e3b
 style Q fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style LLM fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;优点：&lt;strong&gt;检索精度与上下文完整性兼得&lt;/strong&gt;——化解了分块最核心的矛盾。小粒度保证命中精准，大粒度保证 LLM 拿到完整语境，避免&amp;quot;找到了关键句但缺前因后果&amp;quot;。&lt;/p&gt;
&lt;p&gt;缺点：存储开销翻倍（父子块都要存向量）、实现复杂度较高。&lt;/p&gt;
&lt;p&gt;工具：LlamaIndex 的 auto-merging retriever / recursive retriever 就是这种思路的官方实现。&lt;/p&gt;
&lt;h2 id="生产环境怎么选"&gt;生产环境怎么选
&lt;/h2&gt;&lt;p&gt;五种策略各有适用场景，先看总对比。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;原理&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;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;按字符/token 数硬切&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;递归分块&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;strong&gt;通用默认&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;语义分块&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;用 Embedding 找语义断崖&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;结构感知&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;Markdown/HTML/代码文档&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;父子分块&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;strong&gt;生产环境首选&lt;/strong&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;。单一策略覆盖不了所有文档类型——你的知识库里既有 Markdown 规范文档，也有扫描 PDF，还有代码片段。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 B["① 结构感知粗切&lt;br/&gt;按标题/章节"] --&gt; C{"块大小判断"}
 C --&gt;|"过小"| D["合并相邻块"]
 C --&gt;|"合适"| E["直接使用"]
 C --&gt;|"过大"| F["递归/语义细分"]
 D --&gt; G["② 父子分块映射&lt;br/&gt;+ 元数据注入"]
 E --&gt; G
 F --&gt; G
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#d1fae5,stroke:#059669,color:#064e3b
 style E fill:#d1fae5,stroke:#059669,color:#064e3b
 style F fill:#d1fae5,stroke:#059669,color:#064e3b
 style G fill:#dbeafe,stroke:#2563eb,color:#1e3a5f&lt;/pre&gt;&lt;p&gt;混合分块的思路：结构感知先粗切出框架，块大小判断决定后续处理（过小合并、合适直接用、过大递归/语义细分），最后用父子分块建立层级，注入元数据。&lt;/p&gt;
&lt;h2 id="落地建议"&gt;落地建议
&lt;/h2&gt;&lt;p&gt;回到开头的场景——知识库答非所问，怎么排查？&lt;/p&gt;
&lt;p&gt;按这个顺序自查，90% 的问题能定位：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先看分块&lt;/strong&gt;：打开一个 chunk 看看，句子是不是被拦腰切断？上下文是不是残缺？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认从递归分块起步&lt;/strong&gt;：chunk_size 500-800、overlap 10%-20%，把基线跑通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化文档换结构感知&lt;/strong&gt;：你的文档有标题层级，就别浪费这个信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检索精度不够再上父子分块&lt;/strong&gt;：让子块找、父块答，通常能显著提升&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;疑难块用语义分块精修&lt;/strong&gt;：只对检索差的高频问题块启用，控制成本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分块不是一个&amp;quot;选一次就完事&amp;quot;的环节。文档类型变了、检索效果变了，分块策略就要跟着调。这是 RAG 系统里最值得花时间的调优点之一——因为它是整个检索质量的起点，也是你最能控制的一环。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;本文基于个人对 RAG 工程实践的梳理与官方文档验证（MinerU OmniDocBench v1.6 得分 95.39 来自其官方 README；semchunk 基准数据来自其官方 README 及 isaacus.com 博客）。&lt;/em&gt;&lt;/p&gt;</description></item><item><title>RAG 是什么？从一条文档的完整旅程说起</title><link>https://zh.renxinblog.cn/post/rag-overview/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/rag-overview/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/rag-overview-cover.png" alt="Featured image of post RAG 是什么？从一条文档的完整旅程说起" /&gt;&lt;p&gt;你有没有想过一个问题——&lt;/p&gt;
&lt;p&gt;LLM 训练时读了几万亿字的互联网数据，几乎什么都知道。但它不知道你的业务数据——你的产品手册、售后政策、内部文档、客服对话记录。这些它没读过。&lt;/p&gt;
&lt;p&gt;怎么让它知道该知道的？&lt;/p&gt;
&lt;p&gt;这就是 RAG 要解决的问题。这篇文章从为什么需要 RAG 开始，一直讲到它的完整架构和最容易翻车的地方。&lt;/p&gt;
&lt;h2 id="llm-什么都知道但不知道你的业务"&gt;LLM 什么都知道，但不知道你的业务
&lt;/h2&gt;&lt;p&gt;LLM 有两个先天缺陷，在实际落地中很难绕过去。&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;strong&gt;知识截止&lt;/strong&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;不知道私有数据&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;没读过你的内部文档、产品手册、客服记录&lt;/td&gt;
					&lt;td&gt;业务场景下基本不可用&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;怎么解决？目前有三条路可以走。&lt;/p&gt;
&lt;h3 id="方案一微调fine-tuning"&gt;方案一：微调（Fine-tuning）
&lt;/h3&gt;&lt;p&gt;拿业务数据继续训练模型，让它把新知识学进去。&lt;/p&gt;
&lt;p&gt;听起来最直接，但实际操作成本很高。你需要准备高质量的训练数据、处理算力资源，而且每次业务文档更新了，又得重新训一遍。更麻烦的是，微调可能会破坏模型原有的通用能力——模型学会了你的业务术语，但写邮件的能力反而变差了。&lt;/p&gt;
&lt;h3 id="方案二塞长上下文long-context"&gt;方案二：塞长上下文（Long Context）
&lt;/h3&gt;&lt;p&gt;把所有资料都塞进 Prompt 里，让模型自己翻。&lt;/p&gt;
&lt;p&gt;实现最简单，不用动模型。现在有些模型支持 128K、1M 甚至更长的上下文，看起来够用。但实际效果没那么理想：一是 Token 成本会随着内容量线性增长；二是内容太多之后，模型很难从海量信息中找到准确的那一段——这就是著名的&amp;quot;大海捞针&amp;quot;问题。上下文再长，检索精度是会下降的。&lt;/p&gt;
&lt;h3 id="方案三rag检索增强生成"&gt;方案三：RAG（检索增强生成）
&lt;/h3&gt;&lt;p&gt;每次提问的时候，先去知识库里检索相关内容，拼到 Prompt 里，再让 LLM 基于这些材料回答。&lt;/p&gt;
&lt;p&gt;RAG 是目前最主流的方案。原因很实在：&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;：知识库加个文档就行，不需要重训&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可追溯&lt;/strong&gt;：LLM 是看着你给的资料回答的，能知道它引用的是哪份文档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幻觉少&lt;/strong&gt;：基于具体材料生成，比凭空回答可靠得多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，RAG 也有自己的问题——它依赖检索质量。如果检索到的内容不对或者不完整，LLM 材料再好也回答不对。后面会详细讲这个。&lt;/p&gt;
&lt;h2 id="rag-不是搜索引擎它和搜索引擎的区别"&gt;RAG 不是搜索引擎——它和搜索引擎的区别
&lt;/h2&gt;&lt;p&gt;很多人第一次接触 RAG 的反应是：这不就是搜索引擎吗？用户搜关键词，系统返回匹配的文档——有什么新鲜的？&lt;/p&gt;
&lt;p&gt;这个理解对了一半，错的一半恰好是 RAG 的核心。&lt;/p&gt;
&lt;h3 id="搜索引擎esbm25怎么工作"&gt;搜索引擎（ES/BM25）怎么工作
&lt;/h3&gt;&lt;p&gt;你搜一个关键词，搜索引擎在你的文档库里找到包含这个词的所有文档，按匹配度排序返回。它做的是&lt;strong&gt;关键词精确匹配&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如说你搜&amp;quot;2025年Q3营收&amp;quot;，它能找到包含这几个词的那份财报。但如果你问&amp;quot;去年第三季度营收怎么样？&amp;quot;，它可能找不到——因为文档里写的是&amp;quot;2025年Q3营收同比增长15.3%&amp;quot;，关键词对不上。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["搜：去年第三季度营收怎么样？"] --&gt; B[ES 关键词匹配]
 B --&gt; C["找到「去年」「第三」「季度」「营收」"]
 C --&gt; D[❌ 文档写的是「Q3营收同比增长15.3%」&lt;br/&gt;关键词对不上，找不到]&lt;/pre&gt;&lt;p&gt;这是一直以来搜索引擎的运作方式。在&amp;quot;搜文档、找文件&amp;quot;的场景下非常好用，但在&amp;quot;回答问题&amp;quot;的场景下就显得力不从心。&lt;/p&gt;
&lt;h3 id="rag向量检索怎么工作"&gt;RAG（向量检索）怎么工作
&lt;/h3&gt;&lt;p&gt;RAG 不一样。它先把你的文档库转成一组向量（可以理解成一组数字坐标），用户提问时也把问题转成向量，然后计算问题向量和文档向量之间的距离。距离越近，说明内容越相关。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["同样的问题：去年第三季度营收怎么样？"] --&gt; B[RAG 向量检索]
 B --&gt; C[把问题和文档都转成向量&lt;br/&gt;计算语义距离]
 C --&gt; D[✅ 匹配到「Q3营收同比增长15.3%」&lt;br/&gt;语义相近，找到了]&lt;/pre&gt;&lt;p&gt;&amp;ldquo;去年第三季度营收怎么样？&amp;ldquo;和&amp;quot;2025年Q3营收同比增长15.3%&amp;ldquo;在字面上没有共同的关键词，但在语义上是同一件事。向量检索能捕捉到这种语义上的相近。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;搜索引擎（ES/BM25）&lt;/th&gt;
					&lt;th&gt;RAG（向量检索）&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;关键词精确匹配&lt;/td&gt;
					&lt;td&gt;语义匹配&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;搜&amp;quot;苹果&amp;rdquo;&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;返回含&amp;quot;苹果&amp;quot;二字的所有文档&lt;/td&gt;
					&lt;td&gt;能区分你在问水果还是手机品牌&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;输出&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;文档列表&lt;/td&gt;
					&lt;td&gt;相关内容 + LLM 生成的自然语言回答&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;适合场景&lt;/strong&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;局限性&lt;/strong&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;所以搜索引擎和 RAG 不是替代关系，而是互补关系。生产环境最常用的方案是&lt;strong&gt;混合检索&lt;/strong&gt;：向量检索负责语义匹配，BM25 负责精确匹配，两者互补。&lt;/p&gt;
&lt;p&gt;但语义检索有一个前提条件：内容必须被正确地理解和切分。这就引出了下一个问题。&lt;/p&gt;
&lt;h2 id="一条文档在-rag-系统中的完整旅程"&gt;一条文档在 RAG 系统中的完整旅程
&lt;/h2&gt;&lt;p&gt;语义匹配的效果，很大程度上取决于文档在入库时被处理成了什么样子。一条文档从上传到最终被 LLM 用来回答问题，中间经历了一整套加工管道。&lt;/p&gt;
&lt;a href="https://zh.renxinblog.cn/images/rag-pipeline.svg" target="_blank"&gt;
 &lt;figure&gt;&lt;img src="https://zh.renxinblog.cn/images/rag-pipeline.svg"
 			alt="RAG系统完整管道图" width="100%"&gt;&lt;figcaption&gt;
 			&lt;p&gt;RAG 系统完整管道：入库阶段 → 检索阶段（点击图片查看大图）&lt;/p&gt;
 		&lt;/figcaption&gt;
 &lt;/figure&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

=== Step 2 ===
[Thought] 我有了两个结果，现在总结...
[Final Answer] 北京今天多云... 123×456=56088
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;也就是说，这个循环里**&amp;ldquo;下一步做什么&amp;quot;是由模型推理决定的**：模型说&amp;quot;我要调工具&amp;rdquo;，代码就去调；模型说&amp;quot;我该总结了&amp;quot;，循环就结束。这很自由——但代价是，&lt;strong&gt;每一步的路由决策都花掉一次模型推理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在问一个问题：如果这个流程是固定的——先理解意图、再调工具、最后总结——每次让模型重新&amp;quot;想&amp;quot;一遍下一步去哪，是不是有点浪费？更麻烦的是，如果任务是&amp;quot;查完笔记，有冲突就提醒，没冲突就生成报告&amp;quot;，&amp;ldquo;去哪&amp;quot;的判断写在模型脑子里，它偶尔会想歪，而且每一步都在消耗 token 做路由而不是做任务。&lt;/p&gt;
&lt;p&gt;我先说结论：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;复杂任务的 Agent，需要一个显式的状态管理：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;：共享的状态对象（所有节点读写同一份）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node&lt;/strong&gt;：一个执行单元（函数：读 State → 做事 → 返回更新）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge&lt;/strong&gt;：节点间的路由（确定性代码，不走 LLM）
这三个东西组在一起，就是 &lt;strong&gt;StateGraph&lt;/strong&gt;——&lt;strong&gt;图管流程（确定性的传送带），LLM 管内容（只在需要语义的节点被调用）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

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

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

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

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

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

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

 &lt;/blockquote&gt;</description></item><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>第二话｜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>第一话｜大模型只会补全文字，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></channel></rss>