<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans">
  <title>史鹏涛的博客</title>
  <subtitle>史鹏涛的博客，关注 AI 应用、产品出海与一人公司。</subtitle>
  <id>https://shipengtao.com/zh/</id>
  <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/"/>
  <link rel="self" type="application/atom+xml" href="https://shipengtao.com/zh/atom.xml"/>
  <updated>2026-07-01T00:00:00+08:00</updated>
  <author><name>Shi Pengtao</name></author>
  <entry>
    <title>一文读懂 Memory：让 AI 记住你</title>
    <id>https://shipengtao.com/zh/ai-memory/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/ai-memory/"/>
    <published>2026-07-01T00:00:00+08:00</published>
    <updated>2026-07-01T00:00:00+08:00</updated>
    <summary type="text">一文读懂 Memory：让 AI 记住你 AI 的记忆（Memory）到底是什么？为什么它不等于&quot;更大的上下文窗口&quot;？这篇讲透记忆的四种类型、一个记忆系统真正在做的四件事，拆解五套经典架构（Generative Agents 的记忆流、MemGPT 的&quot;LLM…</summary>
    <content type="html"><![CDATA[<h1 id="一文读懂-memory让-ai-记住你">一文读懂 Memory：让 AI 记住你</h1>
<blockquote>
<p>AI 的记忆（Memory）到底是什么？为什么它不等于"更大的上下文窗口"？这篇讲透记忆的四种类型、一个记忆系统真正在做的四件事，拆解五套经典架构（Generative Agents 的记忆流、MemGPT 的"LLM 操作系统"，以及 Voyager、MemoryBank、HippoRAG），对比 ChatGPT / Claude / Mem0 的真实实现，最后回答最难的两个问题：记什么、忘什么。附自查清单。</p>
</blockquote>
<p>大模型有个容易被忽略的事实：<strong>它天生没有记忆。</strong> 每次调用，模型都从一张白纸开始，你这次说的话、上次聊的内容，它一概不记得——除非你把这些内容再一次塞进上下文窗口。你在 ChatGPT 里感受到的"它记得我"，不是模型的能力，而是外面套了一层<strong>记忆系统</strong>在替它记、替它取。</p>
<p>这篇讲的就是这层系统：它由什么构成、怎么工作、业界到底怎么做的。面向工程师和技术决策者，从概念到架构到实战，附照抄清单。它可以看作《上下文工程》那篇的续集——如果说上下文工程管的是"这一步窗口里放什么"，那记忆管的就是"跨越时间，什么值得被记住、又该在何时被想起"。</p>
<hr>
<h2 id="一先厘清记忆--更大的上下文窗口">一、先厘清：记忆 ≠ 更大的上下文窗口</h2>
<p>这是最常见的误解。很多人以为"上下文窗口从 8K 涨到 1M，记忆问题就解决了"。不对。</p>
<p>上下文窗口是<strong>工作台</strong>，记忆是<strong>仓库</strong>。区别有三：</p>
<ul>
<li><strong>持久性</strong>：上下文窗口是易失的，一次对话结束就清空；记忆要跨会话、跨天、跨月存在。</li>
<li><strong>容量</strong>：窗口再大也有上限，而且越塞越满、信噪比越低效果越差（context rot，见《上下文工程》）；仓库理论上无限。</li>
<li><strong>成本</strong>：窗口里的每个 token 每一步都要重新计算、重新收费；仓库里的信息静静躺着，只在需要时取一小部分进窗口。</li>
</ul>
<p>所以记忆系统的核心任务，不是"把所有历史都留在窗口里"，而恰恰相反：<strong>把信息挪到窗口之外存起来，只在恰当的时机取回恰当的一小块。</strong> 一句话——</p>
<blockquote>
<p>记忆的本质，是"用窗口外的存储，换窗口内的稀缺注意力"。</p>
</blockquote>
<p>这也解释了为什么"无限长上下文"永远替代不了记忆：就算窗口能装下你一年的对话，模型也读不好那么长，成本更是天文数字。仓库和工作台，是两种东西。</p>
<hr>
<h2 id="二人脑给的地图记忆的几种类型">二、人脑给的地图：记忆的几种类型</h2>
<p>AI 记忆的分类，几乎照搬了认知科学对人类记忆的划分。记住这张图，后面所有架构都能对号入座。</p>
<p><strong>短期记忆（Short-term / Working Memory）</strong>：当下任务用的"工作内存"，相当于电脑的 RAM。在 LLM 里，它基本就等于<strong>当前上下文窗口</strong>——正在进行的这轮对话、刚拿到的工具返回。易失、容量有限、速度快。</p>
<p><strong>长期记忆（Long-term Memory）</strong>：跨会话保留的信息，相当于硬盘。它又细分为三种，这个区分很重要：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>存什么</th>
<th>例子</th>
<th>类比</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>情景记忆</strong>（Episodic）</td>
<td>具体发生过的事件、经历</td>
<td>"上周你帮我订了去伦敦开会的机票"</td>
<td>个人日记</td>
</tr>
<tr>
<td><strong>语义记忆</strong>（Semantic）</td>
<td>抽象的事实、概念、偏好</td>
<td>"用户是软件工程师，偏好简洁回答"</td>
<td>知识库</td>
</tr>
<tr>
<td><strong>程序记忆</strong>（Procedural）</td>
<td>学会的技能、固定流程</td>
<td>"处理退款要先查订单再走审批"</td>
<td>肌肉记忆 / SOP</td>
</tr>
</tbody>
</table>
<p>一个好用的判断法：<strong>情景记忆回答"发生过什么"，语义记忆回答"什么是真的"，程序记忆回答"该怎么做"。</strong> 大多数产品今天做得最扎实的是语义记忆（记住你的偏好和事实），情景记忆次之，程序记忆最难、也最前沿——它意味着 Agent 能从经验里"学会一套做法"并固化下来。</p>
<hr>
<h2 id="三一个记忆系统真正在做的四件事">三、一个记忆系统真正在做的四件事</h2>
<p>抛开花哨的名词，任何记忆系统都在循环做四件事。这四个动词，是你评估任何方案的骨架：</p>
<p><strong>1）写入 / 编码（Encode）</strong>——从这次交互里，判断"哪些值得记"，并把它整理成一条可存储的记忆。难点是<strong>取舍</strong>：全记会污染未来，漏记会失忆。</p>
<p><strong>2）存储（Store）</strong>——把记忆放进窗口外的载体：数据库、向量库、知识图谱或纯文件。这一步决定了后面怎么检索。</p>
<p><strong>3）检索（Retrieve）</strong>——下次交互时，判断"这一次该想起哪几条"，把它们取进上下文窗口。取多了是噪声，取少了不够用。</p>
<p><strong>4）更新 / 遗忘（Update / Forget）</strong>——记忆会过时、会冲突（"他去年住上海，今年搬北京了"）。系统要能修正旧记忆、淘汰无用记忆，而不是无脑堆积。</p>
<p>如果你读过《上下文工程》那篇会发现，这正是其中"<strong>写出（Write）</strong>"和"<strong>选取（Select）</strong>"两个操作在时间维度上的展开——写入=Write，检索=Select。记忆不是一个新东西，而是上下文工程沿着"时间轴"长出来的一条分支。</p>
<blockquote>
<p>判断一个记忆系统好不好，别看它"能存多少"，看它这四步做得准不准——尤其是<strong>写入的取舍</strong>和<strong>遗忘的更新</strong>，这两步最难，也最能拉开差距。</p>
</blockquote>
<hr>
<h2 id="四几套必须知道的经典架构">四、几套必须知道的经典架构</h2>
<p>记忆的经典设计不止一种。下面五个是理解一切记忆系统的骨架，<strong>每一个都钉死了一个不同的关键能力</strong>，看完你就有了一张完整的"能力地图"。</p>
<h3 id="1-generative-agents记忆流--反思">1. Generative Agents：记忆流 + 反思</h3>
<p>2023 年斯坦福那篇著名的《Generative Agents》（让 25 个 AI 小人在虚拟小镇里自主生活）第一次把记忆架构讲得清清楚楚，至今仍是入门必读。它由三部分组成：</p>
<ul>
<li><strong>记忆流（Memory Stream）</strong>：一个不断追加的、用自然语言记录的经历清单。小人看到的、听到的、说过的、做过的，全都按时间顺序记下来。</li>
<li><strong>检索（Retrieval）</strong>：不可能把整条流都塞进窗口，所以每次按当前情境<strong>打分挑选</strong>——分数是三个维度的加权和：<strong>相关性</strong>（与当前处境的语义相似度）、<strong>新近性</strong>（越久没想起分越低，按时间指数衰减）、<strong>重要性</strong>（写入时打的分，"吃了顿早饭"低、"和恋人分手"高）。</li>
<li><strong>反思（Reflection）</strong>：最点睛的一步。Agent 定期回看最近的记忆，<strong>综合出更高层的结论</strong>（从零散互动里总结出"我好像挺喜欢隔壁的 Klaus"），再作为新记忆写回流里。反思让记忆从"流水账"升级为"洞察"。</li>
</ul>
<blockquote>
<p>记住这三个词——<strong>记忆流、按相关性/新近性/重要性检索、反思</strong>。后来几乎所有记忆系统，都是在这个骨架上做加减法。</p>
</blockquote>
<h3 id="2-memgpt把-llm-当操作系统分层--自编辑">2. MemGPT：把 LLM 当操作系统（分层 + 自编辑）</h3>
<p>如果说 Generative Agents 给了"检索"的范式，那 2023 年的 <strong>MemGPT</strong>（论文副标题就叫 <em>Towards LLMs as Operating Systems</em>，现已并入开源框架 <strong>Letta</strong>）给了"分层管理"的范式。它的洞察很妙：<strong>上下文窗口就像操作系统的物理内存（RAM），窗口外的存储就像磁盘；操作系统靠"分页"换入换出制造"内存无限大"的错觉——LLM 也可以。</strong></p>
<table>
<thead>
<tr>
<th>层级</th>
<th>位置</th>
<th>类比</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>核心记忆</strong>（Core）</td>
<td>窗口内</td>
<td>RAM</td>
<td>最关键、必须随时可见的信息（用户身份、当前任务、关键偏好）。常驻上下文，容量小、最金贵，模型可<strong>自己改写</strong>增删——相当于它"始终记在脑子里"的东西</td>
</tr>
<tr>
<td><strong>回溯记忆</strong>（Recall）</td>
<td>窗口外</td>
<td>磁盘缓存</td>
<td><strong>系统自动记录</strong>的完整对话历史，存在窗口外、可按语义或关键词搜索。平时不占实时上下文，需要时才把相关片段调回来——回答"我们<strong>之前聊过什么</strong>"</td>
</tr>
<tr>
<td><strong>归档记忆</strong>（Archival）</td>
<td>窗口外</td>
<td>冷存储</td>
<td>Agent <strong>主动写入</strong>的任意长期知识（提炼出的事实、外部文档、笔记，<strong>不限于对话</strong>），容量近乎无限，靠工具调用检索——回答"关于这个用户/项目，我<strong>沉淀了哪些知识</strong>"</td>
</tr>
</tbody>
</table>
<p>最关键的设计是：<strong>模型拥有"编辑自己记忆"的工具</strong>——核心记忆快满时，它自己决定把哪条挪去归档、把哪条调回核心。这就把"记忆管理"从人写死的规则，变成了模型自己的能力。这套"分层 + 自编辑"思路直接影响了产品级实现：比如 <strong>Anthropic 在 2025 年 9 月给 Claude 加的"记忆工具（memory tool）"</strong>——给模型一个专属文件目录，让它自己增删改查、跨会话沉淀知识；配合"上下文编辑（自动清理窗口里过期的工具调用）"，官方数据显示 100 轮网页搜索评测里<strong>削减 84% 的 token、比基线提升 39%</strong>。同一个骨架，产品化的样子。</p>
<h3 id="3-另外三个各钉一个关键点voyagermemorybankhipporag">3. 另外三个各钉一个关键点：Voyager、MemoryBank、HippoRAG</h3>
<ul>
<li><strong>Voyager——技能库就是"程序记忆"。</strong> 2023 年这个在《我的世界》里自主探索的智能体，让模型为每个新任务<strong>生成可执行代码</strong>，把跑通的程序按自然语言描述<strong>存进不断增长的"技能库"</strong>；下次遇到相似任务直接检索、组合旧技能，而不从头再想。这正是第二节说的<strong>程序记忆</strong>——存的不是"事实"，而是"会做的本事"。</li>
<li><strong>MemoryBank——给记忆装上"遗忘曲线"。</strong> 它把心理学的<strong>艾宾浩斯遗忘曲线</strong>搬进 AI：每条记忆有"强度"，被想起就加强、长期不碰就衰减，检索时综合语义相似度和这个"留存分"。它给第七节"忘什么"提供了优雅的机器答案——<strong>不是一刀切删除，而是像人一样自然淡忘、又能被唤醒。</strong></li>
<li><strong>HippoRAG——照着海马体来建图。</strong> 它借用神经科学的"海马体索引理论"：把 LLM 当"新皮层"，另建一张知识图谱当"海马体索引"，检索时用类似 PageRank 的算法在图上联想。它是<strong>图记忆</strong>这条路线里最有理论野心的一支——擅长把散落的线索<strong>串联</strong>起来回答问题。</li>
</ul>
<blockquote>
<p>把五个放一起，就是一张记忆设计的"能力地图"：<strong>Generative Agents 管检索与反思、MemGPT 管分层调度、Voyager 管技能积累、MemoryBank 管遗忘、HippoRAG 管联想。</strong> 你缺哪块能力，就去看押注那块的设计。</p>
</blockquote>
<hr>
<h2 id="五产品里的记忆长什么样">五、产品里的记忆，长什么样</h2>
<p>把架构落到你天天在用的产品上：</p>
<p><strong>ChatGPT</strong>：两层记忆。一层是<strong>保存的记忆（Saved Memories）</strong>——一份你能查看、能删的显式事实清单（"我是素食主义者"），模型自动往里写；另一层是 2025 年 4 月上线的<strong>引用聊天历史（Reference Chat History）</strong>——不列清单，而是隐式地从你过往所有对话里抓取模式，让回答更贴合你。两层都能在设置里单独关掉，临时对话则不读也不写记忆。2026 年 OpenAI 又给它加了后台自动整理记忆的机制，让记忆随时间越来越准。</p>
<p><strong>Claude</strong>：走的是上一节说的"文件式记忆工具 + 上下文编辑"路线，更偏开发者可控——记忆存在你自己的基础设施里，你完全掌握数据。</p>
<p><strong>Gemini</strong>：Google 也给 Gemini 加了记忆——既能记住你在设置里保存的个人信息，也能选择性地参考你过往的对话来个性化回答，同样支持查看、编辑与关闭。</p>
<p><strong>共同的产品哲学</strong>：记忆不是黑箱。让用户<strong>看得见、改得动、关得掉</strong>，正在成为标配——因为记忆一旦记错、记了不该记的，体验和信任的崩塌是双倍的。</p>
<hr>
<h2 id="六工程落地三条主流技术路线">六、工程落地：三条主流技术路线</h2>
<p>真要自己做一套记忆系统，存储和检索这块，业界基本是三条路线（常常混用）：</p>
<p><strong>路线一：全量 / 摘要（Full-context / Summary）</strong>
最朴素：把历史全部或摘要后塞回窗口。简单，但长了就撞上 context rot，又慢又贵。适合短周期、轻量场景。</p>
<p><strong>路线二：向量检索（Vector / RAG-style）</strong>
把每条记忆转成向量存进向量库，用时按语义相似度检索最相关的几条。这是目前<strong>最主流</strong>的做法。开源框架 <strong>Mem0</strong> 是代表：它不存原始对话，而是用 LLM 从对话里<strong>抽取关键事实</strong>再存，检索时只取相关记忆。在业界常用的长程对话记忆基准 <strong>LOCOMO</strong> 上，Mem0 官方报告称其准确率比 OpenAI 的记忆方案高约 26%、延迟低约 91%、token 成本省约 90%——核心就赢在"只喂相关的一小撮，而不是全量历史"。</p>
<p><strong>路线三：知识图谱（Graph）</strong>
把记忆存成"实体—关系"的图（张三—就职于—某公司）。擅长处理<strong>多跳推理</strong>和<strong>关系随时间变化</strong>（"他跳槽了"只需改一条边）。表达力强，但构建和维护成本高。</p>
<table>
<thead>
<tr>
<th>路线</th>
<th>优势</th>
<th>软肋</th>
<th>适合</th>
</tr>
</thead>
<tbody>
<tr>
<td>全量/摘要</td>
<td>简单、无信息损失</td>
<td>长了就崩、贵</td>
<td>短会话</td>
</tr>
<tr>
<td>向量检索</td>
<td>主流、平衡、易上手</td>
<td>弱于关系/时序推理</td>
<td>大多数产品</td>
</tr>
<tr>
<td>知识图谱</td>
<td>关系与时序推理强</td>
<td>构建维护重</td>
<td>复杂领域、强关系场景</td>
</tr>
</tbody>
</table>
<p>实务上没有银弹，成熟系统往往<strong>向量为主、图谱为辅、摘要兜底</strong>。</p>
<hr>
<h2 id="七最难的两件事记什么忘什么">七、最难的两件事：记什么，忘什么</h2>
<p>存储和检索是工程，真正的难点是两个判断题：</p>
<p><strong>难题一：写什么？</strong>
不是每句话都值得记。乱记会带来两种灾难：一是<strong>噪声</strong>——把无关琐事都记下来，未来检索时全是干扰；二是<strong>记忆污染（memory poisoning）</strong>——一个早期的错误结论被写进长期记忆，之后每次都被取回、一路将错就错，甚至成为被攻击的入口（有人专门研究往记忆里"投毒"）。所以写入必须有取舍、去重、和<strong>冲突检测</strong>。</p>
<p><strong>难题二：忘什么？</strong>
记忆会过时、会矛盾。用户去年说"我在读研"，今年毕业了；上个月喜欢的东西这个月厌了。一个只增不减的记忆库，迟早自相矛盾。好的系统需要<strong>更新</strong>（用新记忆覆盖旧的）、<strong>衰减</strong>（长期不用的自然淡出）、乃至<strong>主动遗忘</strong>（用户要求删除，这也是合规刚需）。</p>
<blockquote>
<p>做记忆系统，"存"是入门，"取"是及格，<strong>"忘"才是高手</strong>。会遗忘的记忆才是活的记忆——否则你得到的不是一个越来越懂你的助手，而是一个背着一身过期偏见、越来越固执的包袱。</p>
</blockquote>
<hr>
<h2 id="八开发者想加记忆别自己造轮子先看这几个方案">八、开发者想加记忆：别自己造轮子，先看这几个方案</h2>
<p>好消息是，到 2026 年，"给 Agent 加记忆"已经不用从零写起——一批成熟的开源框架把上面的路线都封装好了。想动手的开发者，主流选择大致是这几个（都可自托管）：</p>
<table>
<thead>
<tr>
<th>方案</th>
<th>一句话定位</th>
<th>底层路线</th>
<th>适合</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Mem0</strong></td>
<td>最快上手、最流行</td>
<td>向量 + 图 + KV 混合</td>
<td>通用个性化、想快速见效</td>
</tr>
<tr>
<td><strong>Zep / Graphiti</strong></td>
<td>时序推理最强</td>
<td>时序知识图谱</td>
<td>事实随时间变化的场景</td>
</tr>
<tr>
<td><strong>Letta</strong>（原 MemGPT）</td>
<td>有状态 Agent 运行时</td>
<td>分层（core/recall/archival）</td>
<td>需要"会自管记忆"的 Agent</td>
</tr>
<tr>
<td><strong>Cognee</strong></td>
<td>检索模式最丰富、可自改进</td>
<td>图 + 向量 + 关系混合</td>
<td>复杂领域、企业知识抽取</td>
</tr>
<tr>
<td><strong>LangMem</strong></td>
<td>LangGraph 原生</td>
<td>依托 LangGraph 存储</td>
<td>已在用 LangGraph 的项目</td>
</tr>
<tr>
<td><strong>各平台内置</strong>（如 Claude memory tool）</td>
<td>不想自己搭</td>
<td>文件式 / 托管</td>
<td>只想开箱即用</td>
</tr>
</tbody>
</table>
<p>选型别记参数，记这条主线：<strong>你最缺的是什么能力，就选押注那个能力的框架</strong>——要快就 Mem0，要处理"张三跳槽了"这种时序变化就 Zep/Graphiti，要一个能自己管理记忆的有状态 Agent 就 Letta。</p>
<p>而 2026 年最出圈的一个新项目，恰恰是对第七节"记什么、忘什么"给出了一个反向的答案——<strong>MemPalace</strong>。它由《生化危机》女主角 <strong>米拉·乔沃维奇（Milla Jovovich）</strong> 和工程师 Ben Sigman 合作、用 Claude Code 开发，MIT 开源，2026 年 4 月发布后两天就冲上 2 万多星。它的出发点是一句抱怨：<strong>"我受够了 AI 替我决定该记住什么、然后把我真正需要的上下文丢掉。"</strong></p>
<p>于是 MemPalace 反其道而行，借用古老的"记忆宫殿"记忆术：把对话<strong>逐字、不做摘要地</strong>存下来，再用空间隐喻组织成<strong>厅（wings，按人和项目）→ 房间（rooms，按主题）→ 抽屉（drawers，放原文）</strong>，检索时能"分区"精确查找，而不是在一大团向量里瞎捞。它用混合检索（关键词加权 + 时间邻近），在 LongMemEval 基准上纯检索就拿到 96.6% 的 recall@5。存储后端可插拔（默认 ChromaDB，也支持 SQLite/Qdrant/pgvector），能对接 Claude Code、Cursor 等。</p>
<blockquote>
<p>MemPalace 的价值不只在于"一个女明星写了个爆款开源项目"，而在于它把第七节那个哲学分歧摆上了台面：<strong>记忆到底该由 AI 替你自动裁剪，还是由你自己保留全部、掌控结构？</strong> 前者省心，后者可控——这没有标准答案，但它提醒你：<strong>"让 AI 决定记什么"本身，就是一个需要你亲自拍板的设计决策。</strong></p>
</blockquote>
<hr>
<h2 id="九照抄即用记忆系统自查清单">九、照抄即用：记忆系统自查清单</h2>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 我分清了**短期记忆（窗口内）<strong>和</strong>长期记忆（窗口外）**吗？没把"更大的窗口"当成记忆的答案吧？</li>
<li class="task-list-item"><input type="checkbox" disabled> 长期记忆里，<strong>语义 / 情景 / 程序</strong>三类，我需要哪几种？各用什么存？</li>
<li class="task-list-item"><input type="checkbox" disabled> 写入有<strong>取舍</strong>吗？还是把所有对话无脑全存？有没有去重和冲突检测？</li>
<li class="task-list-item"><input type="checkbox" disabled> 检索是<strong>按相关性/新近性/重要性打分挑选</strong>，还是全量塞回窗口？</li>
<li class="task-list-item"><input type="checkbox" disabled> 有没有<strong>更新和遗忘</strong>机制？旧的、错的、过期的记忆会被修正或淘汰吗？</li>
<li class="task-list-item"><input type="checkbox" disabled> 用户能<strong>看见、修改、删除、关闭</strong>自己的记忆吗（体验 + 合规）？</li>
<li class="task-list-item"><input type="checkbox" disabled> 存储选型对了吗——<strong>向量</strong>够不够，需不需要<strong>图谱</strong>处理关系与时序？</li>
<li class="task-list-item"><input type="checkbox" disabled> 有没有防<strong>记忆污染</strong>？错误结论被写进长期记忆后会不会一路带偏？</li>
<li class="task-list-item"><input type="checkbox" disabled> 有没有像 MemGPT 那样考虑<strong>分层</strong>，或像 Generative Agents 那样加<strong>反思</strong>，让记忆从流水账升级为洞察？</li>
</ul>
<blockquote>
<p>模型会越来越聪明，窗口会越来越大。但只要"注意力有限、信息会过时"这两条还成立，<strong>能不能记住你、并且在对的时候想起对的事，就永远是一套需要认真设计的系统，而不是模型白送的能力。</strong></p>
</blockquote>
<hr>
<h2 id="参考">参考</h2>
<ul>
<li>Packer et al.，<a href="https://arxiv.org/abs/2310.08560">MemGPT: Towards LLMs as Operating Systems</a>（2023；现为开源框架 <a href="https://www.letta.com/blog/memgpt-and-letta/">Letta</a>）</li>
<li>Park et al.，<a href="https://arxiv.org/abs/2304.03442">Generative Agents: Interactive Simulacra of Human Behavior</a>（2023，记忆流 / 检索 / 反思）</li>
<li>Wang et al.，<a href="https://arxiv.org/abs/2305.16291">Voyager: An Open-Ended Embodied Agent with Large Language Models</a>（2023，技能库 / 程序记忆）</li>
<li>Zhong et al.，<a href="https://arxiv.org/abs/2305.10250">MemoryBank: Enhancing LLMs with Long-Term Memory</a>（2023，艾宾浩斯遗忘曲线）</li>
<li>Gutiérrez et al.，<a href="https://arxiv.org/abs/2405.14831">HippoRAG: Neurobiologically Inspired Long-Term Memory for LLMs</a>（2024，海马体索引 / 图记忆）</li>
<li>Mem0，<a href="https://arxiv.org/abs/2504.19413">Building Production-Ready AI Agents with Scalable Long-Term Memory</a>（2025，LOCOMO 基准）；开源仓库 <a href="https://github.com/mem0ai/mem0">mem0ai/mem0</a></li>
<li>开源记忆框架：<a href="https://github.com/getzep/graphiti">Zep / Graphiti</a>、<a href="https://github.com/letta-ai/letta">Letta</a>、<a href="https://github.com/topoteretes/cognee">Cognee</a>、<a href="https://github.com/langchain-ai/langmem">LangMem</a></li>
<li>MemPalace，<a href="https://github.com/milla-jovovich/mempalace">GitHub 仓库</a>（2026，米拉·乔沃维奇 + Ben Sigman，"记忆宫殿"式）；<a href="https://www.forbes.com/sites/joshpearce/2026/04/09/milla-jovovich-goes-open-source-guns-blazing-with--top-ai-memory-code/">Forbes 报道</a></li>
<li>Anthropic / Claude，<a href="https://claude.com/blog/context-management">Managing context on the Claude Developer Platform</a>（2025，memory tool + context editing）</li>
<li>OpenAI，<a href="https://openai.com/index/memory-and-new-controls-for-chatgpt/">Memory and new controls for ChatGPT</a> 与 <a href="https://help.openai.com/en/articles/8590148-memory-faq">Reference chat history</a>（2025）</li>
<li><a href="https://github.com/Shichun-Liu/Agent-Memory-Paper-List">Memory in the Age of AI Agents: A Survey</a>（2025，综述与论文清单）</li>
</ul>]]></content>
  </entry>
  <entry>
    <title>上下文工程：喂给模型什么，决定它的上限</title>
    <id>https://shipengtao.com/zh/context-engineering/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/context-engineering/"/>
    <published>2026-06-25T00:00:00+08:00</published>
    <updated>2026-06-25T00:00:00+08:00</updated>
    <summary type="text">上下文工程：喂给模型什么，决定它的上限 上下文工程（Context Engineering）：决定 AI Agent 上限的不是模型，而是你喂给它的上下文。讲透四大操作——写出/选取/压缩/隔离，RAG 还是长上下文、记忆、KV-cache…</summary>
    <content type="html"><![CDATA[<h1 id="上下文工程喂给模型什么决定它的上限">上下文工程：喂给模型什么，决定它的上限</h1>
<blockquote>
<p>上下文工程（Context Engineering）：决定 AI Agent 上限的不是模型，而是你喂给它的上下文。讲透四大操作——写出/选取/压缩/隔离，RAG 还是长上下文、记忆、KV-cache 优化，附实战与自查清单。</p>
</blockquote>
<p>做 Agent 的人迟早会撞上这堵墙：prompt 已经调到极致，效果还是不稳。问题往往不在那句话，而在模型每一步到底看到了什么。这篇讲的就是这件事——<strong>怎么管理模型的"视野"</strong>。面向工程师与技术决策者，从概念到实战，附照抄清单。</p>
<hr>
<h2 id="一一个反直觉的事实">一、一个反直觉的事实</h2>
<p>很多人调 Agent 的路径是这样的：效果不好 → 改 prompt → 还是不好 → 继续堆 prompt → 把所有可能用到的资料、历史、工具说明一股脑塞进去 → 效果反而更差。</p>
<p>问题往往不在 prompt 写得好不好，而在<strong>模型这一步看到的那段上下文是否对</strong>。</p>
<p>2025 年开始，业界有了一个共识性的词来命名这件事。Karpathy 把它讲得最直白：</p>
<blockquote>
<p>"上下文工程"胜过"提示工程"。人们一提到 prompt，想到的往往是日常里随手给模型的一句简短指令；可在任何工业级的 LLM 应用里，上下文工程才是真功夫——<strong>它是一门精细的艺术与科学：在上下文窗口里，为下一步恰到好处地填入正确的信息</strong>。</p>
<p><em>「+1 for "context engineering" over "prompt engineering". People associate prompts with short task descriptions you'd give an LLM in your day-to-day use. When in every industrial-strength LLM app, context engineering is the delicate art and science of filling the context window with just the right information for the next step.」</em></p>
</blockquote>
<p>Shopify 的 CEO Tobi Lütke 也公开表达过同样的偏好——他更喜欢"上下文工程"胜过"提示工程"，因为它更准确地描述了那项核心技能："为任务提供全部上下文，让大模型能合理地把事做成"（<em>"the art of providing all the context for the task to be plausibly solvable by the LLM"</em>）。</p>
<p>一句话区分：</p>
<blockquote>
<p><strong>提示工程</strong>：写好一段相对静态的指令。
<strong>上下文工程</strong>：在一个多步骤、不断变化的 Agent 循环里，动态地决定<strong>每一步往有限的上下文窗口里放什么、不放什么</strong>。</p>
</blockquote>
<p>提示工程是上下文工程的一个子集。当任务从"一问一答"变成"自主跑几十步、调一堆工具、读一堆返回结果"时，主战场就从"那句话"转移到了"那整个窗口"。</p>
<hr>
<h2 id="二为什么上下文是稀缺资源">二、为什么上下文是稀缺资源</h2>
<p>直觉上，上下文窗口越大越好，1M token 听起来什么都装得下。但实践里，<strong>上下文是最稀缺的资源</strong>，原因有三：</p>
<p><strong>1）注意力是有限的，且会"中间失忆"。</strong> 经典研究《Lost in the Middle》早就发现：把关键信息放在长上下文的中间，模型经常视而不见——它对开头和结尾敏感，对中段迟钝。后来 Chroma 等团队用更系统的实验把这个现象命名为 <strong>context rot（上下文腐坏）</strong>：随着上下文变长，模型在同一任务上的表现会持续、平滑地下降，哪怕"大海捞针"测试还显示它"找得到"。<strong>能找到 ≠ 能用好。</strong></p>
<p><strong>2）无关信息会主动干扰。</strong> 上下文里多塞的每一段无关内容，都是在和正确答案抢注意力。常见三种病症：</p>
<ul>
<li><strong>干扰（distraction）</strong>：被无关历史带偏；</li>
<li><strong>污染（poisoning）</strong>：一个早期的错误结论留在上下文里，后面一路将错就错；</li>
<li><strong>冲突（clash）</strong>：塞进去的资料彼此矛盾，模型无所适从。</li>
</ul>
<p><strong>3）成本和延迟随 token 线性增长。</strong> 每多一个 token，都要花钱、花时间。一个把上下文堆到满的 Agent，又慢又贵又不准。</p>
<p>结论很反直觉，但极其重要：</p>
<blockquote>
<p><strong>上下文不是越多越好，而是"信噪比"越高越好。上下文工程的本质，是在每一步只留下"刚好够用"的高信号信息。</strong></p>
</blockquote>
<hr>
<h2 id="三上下文里到底有什么">三、上下文里到底有什么</h2>
<p>先把"上下文"拆开。模型推理时看到的，远不止用户那句话，而是一整个被拼装出来的窗口：</p>
<table>
<thead>
<tr>
<th>成分</th>
<th>说明</th>
<th>谁来控制</th>
</tr>
</thead>
<tbody>
<tr>
<td>系统提示 / 角色设定</td>
<td>身份、规则、输出格式</td>
<td>工程师，相对静态</td>
</tr>
<tr>
<td>工具定义</td>
<td>有哪些工具、怎么调</td>
<td>工程师 + 动态筛选</td>
</tr>
<tr>
<td>用户输入</td>
<td>当前这一轮的请求</td>
<td>用户</td>
</tr>
<tr>
<td>对话历史</td>
<td>之前几轮的来回</td>
<td>需要管理（压缩/裁剪）</td>
</tr>
<tr>
<td>工具返回结果</td>
<td>检索到的文档、API 结果、报错</td>
<td>高度动态，最易爆炸</td>
</tr>
<tr>
<td>记忆</td>
<td>跨会话的长期信息</td>
<td>需要写入/取回</td>
</tr>
</tbody>
</table>
<p>上下文工程，就是<strong>对这张表里每一格做"加什么、减什么、留多久"的决策</strong>。其中最容易失控的是"工具返回结果"——一次网页抓取、一次数据库查询，就可能糊上几千 token。</p>
<hr>
<h2 id="四四种基本操作写出选取压缩隔离">四、四种基本操作：写出、选取、压缩、隔离</h2>
<p>LangChain 把上下文工程的全部手段，归纳成四类操作。这是目前最好用的一张心智地图：</p>
<h3 id="1-write--写出把信息存在窗口之外">1. Write —— 写出（把信息存在窗口之外）</h3>
<p>不是所有东西都得待在上下文里。把中间结论、计划、笔记<strong>写到窗口外部</strong>，需要时再取回：</p>
<ul>
<li><strong>草稿区（scratchpad）</strong>：当前任务内的临时笔记，让模型记住"我已经做到第几步、得出了什么"。</li>
<li><strong>记忆（memory）</strong>：跨任务、跨会话的持久信息（用户偏好、项目约定）。</li>
</ul>
<h3 id="2-select--选取只在需要时取回相关的那一点">2. Select —— 选取（只在需要时取回相关的那一点）</h3>
<p>这是 <strong>RAG（检索增强）</strong> 的本质，也是工具选择、示例选择的本质：</p>
<ul>
<li>不把整个知识库塞进去，而是按当前问题<strong>检索出最相关的几段</strong>；</li>
<li>工具太多时，先用一层检索筛出"这一步可能用到的几个工具"，而不是把上百个工具定义全摆上；</li>
<li>连 few-shot 示例都可以按相似度动态挑选。</li>
</ul>
<h3 id="3-compress--压缩用更少的-token-表达同样的信息">3. Compress —— 压缩（用更少的 token 表达同样的信息）</h3>
<p>上下文快满时，不是粗暴截断，而是<strong>压缩</strong>：</p>
<ul>
<li><strong>摘要 / compaction</strong>：把前面几十轮对话总结成一段"目前进展纪要"，丢掉原始过程、保留结论和未决事项；</li>
<li><strong>裁剪</strong>：按规则删掉最老、最不相关的部分。</li>
</ul>
<h3 id="4-isolate--隔离把上下文拆到多个独立空间">4. Isolate —— 隔离（把上下文拆到多个独立空间）</h3>
<p>把一个大任务拆给多个<strong>子 Agent</strong>，每个子 Agent 拿一个<strong>干净的、专属的上下文窗口</strong>去做子任务，只把"浓缩后的结论"返回给主 Agent。主 Agent 的窗口因此始终清爽。沙箱、独立运行环境也属于这一类——把可能产生海量输出的操作隔离在外。</p>
<blockquote>
<p>记住这四个动词——<strong>写出、选取、压缩、隔离</strong>——你就有了应对几乎所有上下文问题的工具箱。</p>
</blockquote>
<p>接下来几节，挑出这套框架里最常被追问、也最容易做错的几处，逐个深入。</p>
<hr>
<h2 id="五rag-还是长上下文2026-年的答案">五、RAG 还是长上下文？2026 年的答案</h2>
<p>长上下文窗口刚出现时，很多人断言"RAG 要死了——既然能把整本书塞进去，何必检索？"两年过去，结论清晰了：<strong>长上下文没有杀死 RAG，反而让两者各归其位。</strong></p>
<ul>
<li><strong>长上下文的软肋</strong>就是第二节说的 context rot：塞得越满，越容易中间失忆、越慢越贵。把 50 万 token 全灌进去，不等于模型真的"读懂"了 50 万 token。</li>
<li><strong>RAG 的价值</strong>恰恰是把"50 万里相关的 5 千"挑出来，喂高信号、低噪声的上下文——这正是上下文工程想要的。</li>
</ul>
<p>实务上的选择：</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>倾向</th>
</tr>
</thead>
<tbody>
<tr>
<td>知识库巨大、且持续更新</td>
<td>RAG（检索 + 引用来源）</td>
</tr>
<tr>
<td>单份文档、需要全局理解（如审一份长合同）</td>
<td>长上下文直接读</td>
</tr>
<tr>
<td>既大又要精</td>
<td>混合：先检索粗筛，再把候选段落放进长上下文精读</td>
</tr>
</tbody>
</table>
<p>更前沿的做法是 <strong>agentic retrieval（智能体式检索）</strong>：不再是"一次检索、拿结果就走"，而是让 Agent 自己多轮搜索、自己判断"够不够、要不要再查一次"，把检索变成一个有反馈的循环。</p>
<hr>
<h2 id="六记忆让-agent-跨会话变聪明">六、记忆：让 Agent 跨会话变聪明</h2>
<p>记忆是上下文工程里专门处理"时间"的部分，分两层：</p>
<ul>
<li><strong>短期记忆</strong>：单次会话/任务内的连续性，主要靠第四节的 compaction 和 scratchpad 维持。</li>
<li><strong>长期记忆</strong>：跨会话保留的信息——用户是谁、偏好什么、项目有哪些约定。你在 ChatGPT、Claude 里看到的"记住了你的偏好"，背后就是一套<strong>写入（什么值得记）+ 取回（这次该想起什么）</strong> 的机制。</li>
</ul>
<p>记忆的难点从来不是"存"，而是两个判断：</p>
<ol>
<li><strong>写什么</strong>：不是所有对话都值得记，乱记会污染未来的上下文；</li>
<li><strong>取回什么</strong>：这一次该想起哪几条，取多了又是噪声。</li>
</ol>
<p>说到底，记忆系统就是"Write + Select"这两个操作在时间维度上的应用。</p>
<hr>
<h2 id="七一条最容易被忽略的实务让上下文对缓存友好">七、一条最容易被忽略的实务：让上下文对缓存友好</h2>
<p>前面几节都在谈"放什么、不放什么"，还有一条几乎直接决定成本的路径常被忽略——<strong>KV-cache（提示缓存）</strong>。</p>
<p>模型处理上下文时，会把前缀算成一份可复用的中间状态（KV-cache）。只要这一轮的上下文<strong>前缀和上一轮完全一致</strong>，这部分就能直接命中缓存、不必重算——又快又省（命中的 token 通常只按原价的几分之一计费）。在多步 Agent 里，每一步都带着几乎相同的系统提示和历史，<strong>缓存命中率几乎直接决定了账单和延迟</strong>。业界甚至有一句话："KV-cache 命中率，是生产级 Agent 最重要的单项指标。"</p>
<p>由此引出几条和"信噪比"同等重要的构造原则：</p>
<ul>
<li><strong>稳定的放前面，易变的放后面。</strong> 系统提示、工具定义这些每轮不变的内容固定在最前，保证前缀稳定。</li>
<li><strong>只追加，不改写。</strong> 新信息往尾部追加；一旦回头去改前面的内容（哪怕只动一个时间戳），其后的缓存会全部失效。</li>
<li><strong>压缩要挑时机。</strong> compaction、裁剪会改写前缀、使缓存失效，所以别太频繁触发，挑划算的点一次性做。</li>
</ul>
<p>一句话：<strong>上下文工程不只决定"模型看得准不准"，也直接决定"这套系统跑得贵不贵"。</strong></p>
<hr>
<h2 id="八实战两个最常用的模式">八、实战：两个最常用的模式</h2>
<p><strong>模式一：Compaction（上下文压缩续跑）。</strong> 这是长任务的命脉。以 Claude Code 为例，当上下文窗口快满时，它不会直接断档，而是<strong>自动把前面的工作总结成一段紧凑纪要</strong>（改了哪些文件、当前状态、下一步要做什么），然后用这段纪要"接着跑"。要点是：<strong>总结要保留"未决事项和关键决策"，丢掉"原始过程"</strong>。压缩没做好，Agent 就会"失忆"，反复做已经做过的事。</p>
<p><strong>模式二：Sub-agent 上下文隔离。</strong> 主 Agent 负责编排，遇到一个重活（比如"把这 20 个文件逐个读一遍找出 bug"），就派一个子 Agent 去做。子 Agent 在自己干净的窗口里读完 20 个文件、得出结论，<strong>只把一句话结论返回</strong>给主 Agent。主 Agent 的窗口完全不会被那 20 个文件的内容淹没。这正是"隔离"的威力——也是多 Agent 系统最被低估的好处：它本质上是一种<strong>上下文管理手段</strong>，而不只是"并行干活"。</p>
<hr>
<h2 id="九它和这些概念是什么关系">九、它和这些概念是什么关系</h2>
<p>上下文工程周围围着一圈容易混淆的词。不必死记，按"关系的类型"理一遍，边界自然就清楚了：</p>
<p><strong>① 上下游——MCP 在下面供料，Harness 在外面兜底。</strong></p>
<ul>
<li><strong>MCP（模型上下文协议）</strong>：解决"<strong>能接什么</strong>"，用一套统一协议把外部工具和数据源标准化地接进来，是上下文的"来源"。</li>
<li><strong>Harness（智能体的工程外壳）</strong>：模型权重之外、决定智能能否稳定落地的全部工程（工具、状态、验证、上下文管理……）。上下文管理只是其中一个子系统，却往往是<strong>最先翻车</strong>的那一个。</li>
</ul>
<p><strong>② 子集——提示工程和 RAG，都是它的一部分，不是它本身。</strong></p>
<ul>
<li><strong>提示工程</strong>：上下文工程的<strong>前身和子集</strong>。任务从"一问一答"变成"自主跑几十步"，主战场就从"写好那句话"扩大到了"管好整个窗口"。</li>
<li><strong>RAG（检索增强）</strong>：只是"<strong>选取</strong>"这一类操作里的一个具体手段，负责"按需把相关知识取进来"。<strong>它是上下文工程的子集，不是同义词</strong>——把两者画等号，就漏掉了写出、压缩、隔离另外三大类。</li>
</ul>
<p><strong>③ 替代路线——上下文工程 vs 微调。</strong></p>
<p>给模型注入知识和能力，有两条根本不同的路：把它<strong>喂进上下文</strong>（in-context，本文讲的全部），还是<strong>训进权重</strong>（in-weights，即微调 fine-tuning）。</p>
<ul>
<li>知识更新快、要带来源、要随时增删 → 走上下文（RAG / 长上下文）；</li>
<li>要固化一种稳定的风格、格式或专有能力，且数据足够 → 考虑微调。</li>
</ul>
<p>多数成熟系统两者并用：<strong>微调定"底色"，上下文工程喂"当下这件事"。</strong></p>
<p>一句话收束：<strong>上下文工程决定模型每一步的"视野"——提示工程、RAG 是它的局部，微调是它的另一条路，MCP 在下面供料，Harness 在外面兜底。</strong></p>
<hr>
<h2 id="十照抄即用上下文自查清单">十、照抄即用：上下文自查清单</h2>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 我能说清模型在<strong>每一步</strong>看到的上下文里，到底有哪些成分吗？</li>
<li class="task-list-item"><input type="checkbox" disabled> 工具返回结果有没有做"瘦身"（只留结论、截断超长输出）？还是原样塞回去？</li>
<li class="task-list-item"><input type="checkbox" disabled> 上下文里有没有"已经没用的历史"？有没有定期压缩/裁剪？</li>
<li class="task-list-item"><input type="checkbox" disabled> 长任务有没有 compaction 机制？压缩时<strong>保留了未决事项和关键决策</strong>吗？</li>
<li class="task-list-item"><input type="checkbox" disabled> 知识检索是"全量塞入"还是"按需检索"？检索结果带不带来源？</li>
<li class="task-list-item"><input type="checkbox" disabled> 工具/示例是不是"全量摆上"？能不能按当前步骤动态筛选？</li>
<li class="task-list-item"><input type="checkbox" disabled> 有没有把重活拆给子 Agent，用独立窗口隔离它产生的海量中间信息？</li>
<li class="task-list-item"><input type="checkbox" disabled> 上下文是不是"稳定内容在前、只在尾部追加"，以尽量命中 KV-cache、压住成本？</li>
<li class="task-list-item"><input type="checkbox" disabled> 我有没有在"上下文越长越好"和"信噪比越高越好"之间，站对了队？</li>
</ul>
<blockquote>
<p>模型会越来越聪明，上下文窗口会越来越大。但只要"注意力有限、信息有噪声"这两条还成立，<strong>决定 Agent 上限的，就永远不只是模型本身，而是你喂给它的那段上下文。</strong></p>
</blockquote>
<hr>
<h2 id="参考">参考</h2>
<ul>
<li>Andrej Karpathy，<a href="https://x.com/karpathy/status/1937902205765607626">关于 "context engineering" 的推文</a>（2025）</li>
<li>LangChain，<a href="https://www.langchain.com/blog/context-engineering-for-agents">Context Engineering for Agents</a>（Write / Select / Compress / Isolate 框架）</li>
<li>Anthropic，<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Effective context engineering for AI agents</a></li>
<li>Chroma，<a href="https://www.trychroma.com/research/context-rot">Context Rot: How Increasing Input Tokens Impacts LLM Performance</a>（2025）</li>
<li>Liu et al.(arxiv)，<a href="https://arxiv.org/abs/2307.03172">Lost in the Middle: How Language Models Use Long Contexts</a>（2023）</li>
</ul>]]></content>
  </entry>
  <entry>
    <title>思维的工具箱：从提问到复盘的跨学科思考法</title>
    <id>https://shipengtao.com/zh/thinking-toolbox/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/thinking-toolbox/"/>
    <published>2026-06-11T00:00:00+08:00</published>
    <updated>2026-06-11T00:00:00+08:00</updated>
    <summary type="text">…</summary>
    <content type="html"><![CDATA[<h1 id="思维的工具箱从提问到复盘的跨学科思考法">思维的工具箱：从提问到复盘的跨学科思考法</h1>
<blockquote>
<p>"如果你手里只有一把锤子，就会把所有东西都看成钉子。" —— 亚伯拉罕·马斯洛（查理·芒格爱引此语，并称之为"拿锤人综合症"）</p>
</blockquote>
<p>人类历史上最聪明的一批大脑，分散在不同的学科里：哲学家锤炼了两千多年的提问技术，数学家发明了最严密的推理工具，物理学家最擅长把复杂世界简化成可计算的模型，心理学家则专门研究我们的大脑会在哪里出错。</p>
<p>问题在于，这些方法散落在各自的领域里，很少有人把它们放进同一个工具箱。芒格把这叫做"<strong>多元思维模型</strong>"（Latticework of Mental Models）：你不需要成为每个领域的专家，但需要掌握每个领域最重要的那几个模型，并知道在什么时候用哪一个。</p>
<p>这篇文章做的就是这件事——从哲学、数学、物理学、心理学、经济学、生物学、工程学里，各取最重要的几个思考方法，然后按照<strong>一次完整思考的六个阶段</strong>把它们串起来：</p>
<p><strong>定义问题 → 拆解建模 → 推理探索 → 验证纠错 → 决策行动 → 复盘迭代</strong></p>
<p>任何一次认真的思考——无论是做技术选型、写一篇文章，还是决定要不要搬去另一座城市——大体都会经过这六步。每一步，都有某个学科替你打磨好了趁手的工具。</p>
<p>文章先交代这套阶段划分的家谱（它不是我发明的，它有一百多年的传承），然后逐一介绍每个阶段的工具——每件都附<strong>谱系注</strong>：它从哪里来、地基有多牢，其中好几件的地基至今还是哲学上的未解之题，知道这一点会让你用得更清醒。</p>
<p>在出发前，先回到开头那把锤子。锤子问题的解法从来不是拥有更多的锤子——工具越多，慌乱时退回熟悉那把的概率反而越高——而是一张<strong>索引</strong>，告诉你什么时候该伸手拿哪一件。六个阶段，就是那张索引。</p>
<hr>
<h2 id="第零章这套框架本身从哪里来">第零章：这套框架本身从哪里来</h2>
<p>诚实地交代家谱：六阶段不是思维的自然规律，而是一个<strong>规范性模型</strong>——它描述的是"应该怎么想"，不是大脑实际怎么运作（大脑是并行的、跳跃的、被情绪驱动的）。这个模型有一条清晰的传承线：</p>
<ul>
<li>**杜威《我们如何思维》（1910）**提出反思性思维五步：感到困难 → 界定问题 → 提出假设 → 推理 → 检验。这是本文阶段一到四的直接祖先。杜威最重要的洞察是把"感到困难"和"界定问题"拆成两步——你最初感到的从来不是问题本身，只是症状。</li>
<li>**波利亚《怎样解题》（1945）**给数学解题的四步：理解题目 → 制订计划 → 执行 → 回顾。"回顾"是阶段六的来源之一。</li>
<li><strong>PDCA 循环、OODA 循环、设计思维</strong>等二十世纪的工程与管理变体，共同贡献了一件事：把"行动"纳入思考的循环，而不是当作思考的终点。</li>
</ul>
<p>把这些方案全部蒸馏到底，内核只剩一个算法：<strong>生成—检验—保留</strong>。波普尔称之为"猜想与反驳"，心理学家唐纳德·坎贝尔称之为"盲变异与选择性保留"，进化论是它的无意识版本——后文你会看到，第三、四、六阶段就是这个内核本身，第一、二阶段是杜威加上的问题构造前端，第五阶段是决策论加上的行动后端。</p>
<p>第五阶段的插入处，藏着整个框架里最硬的一条分界线，它的依据来自休谟：<strong>实然推不出应然</strong>。从阶段四到阶段五，思考发生了范畴切换——前面四个阶段问"什么是真的"（认知理性），第五阶段问"在稀缺约束下该做什么"（实践理性）。验证做得再完美，也不会自动告诉你该选哪个，因为"该"必须引入价值和约束。其他几条阶段边界都是渐变的，唯独这一条是断崖。</p>
<p>最后一句提醒：阶段是螺旋而非直线，走到后面发现前面错了，就退回去重走——文末的使用说明会再回到这一点。</p>
<hr>
<h2 id="第一阶段定义问题--哲学的领地">第一阶段：定义问题 —— 哲学的领地</h2>
<p>有一句被广泛归到爱因斯坦名下、但其实查无实据的话：如果给我一小时拯救世界，我会花五十五分钟弄清楚问题是什么。无论它出自谁口，道理都不假——绝大多数低质量的思考，败在第一步：解了一个错误的问题。</p>
<h3 id="1-苏格拉底诘问法把模糊的词钉死">1. 苏格拉底诘问法：把模糊的词钉死</h3>
<p>哲学贡献的第一个工具，是苏格拉底在雅典街头用了一辈子的方法：<strong>对每一个关键概念连续追问"你说的 X 到底是什么意思？"</strong></p>
<p>"我想做一个更好的产品"——什么叫"更好"？对谁更好？用什么衡量？"团队效率太低了"——"效率"指交付速度、单位产出还是返工率？你会发现，很多争论吵了三小时，其实双方说的根本不是同一个东西。<strong>概念没钉死之前，一切讨论都是空转。</strong></p>
<p><em>谱系：柏拉图对话录中的"诘问法"（elenchus），公元前五世纪。它暗含一个预设——概念都有可定义的本质；两千多年后维特根斯坦用"家族相似"反驳：多数日常概念（比如"游戏"）根本没有统一定义。所以钉概念时求的不是完美定义，而是"本次讨论内大家用同一个定义"。</em></p>
<h3 id="2-第一性原理把问题拆回不可再拆的事实">2. 第一性原理：把问题拆回不可再拆的事实</h3>
<p>这个词源自亚里士多德，因马斯克而流行：<strong>不要从"大家都这么做"出发推理（类比思维），而要从"哪些事实是物理上确定为真的"出发，重新往上搭。</strong></p>
<p>经典案例是电池：类比思维说"电池历史上一直很贵，所以未来也贵"；第一性原理问"电池由哪些原料构成？这些原料在市场上值多少钱？"——答案是材料成本只占售价的一小部分，剩下的都是可以被重新设计的环节。</p>
<p><em>谱系：亚里士多德《形而上学》中的 ἀρχή（本原）；《后分析篇》主张一切证明最终要落在不证自明的起点上——这是知识论中"基础主义"的源头，笛卡尔的普遍怀疑是同一个动作。但地基并不如听起来牢固：古希腊怀疑论的"阿格里帕三难"指出，任何辩护链要么无穷回溯、要么循环、要么武断地停下。实践中我们选择第三种，所以要记住：你的"事实清单"本身也可错。</em></p>
<h3 id="3-问题的转译你真正要解决的是什么">3. 问题的转译：你真正要解决的是什么？</h3>
<p>哲学训练里还有一个朴素但锋利的习惯：区分<strong>表层问题</strong>和<strong>底层问题</strong>。电梯太慢，表层方案是换电梯；但底层问题可能是"等待让人烦躁"，于是在电梯旁装一面镜子，抱怨就消失了。在动手之前，多问一句："这个问题，是什么更大问题的症状？"</p>
<p><em>谱系：杜威《我们如何思维》把"感到困难"与"界定问题"分为两个独立步骤，正是承认两者经常不一致；设计思维把这一步制度化为"问题重构"（reframing）。</em></p>
<blockquote>
<p><strong>本阶段输出：一句话写清楚"我要解决的问题是 <strong>_，判断解决与否的标准是 _</strong>"。写不出来，就不要进入下一阶段。</strong></p>
</blockquote>
<hr>
<h2 id="第二阶段拆解与建模--数学和物理的看家本领">第二阶段：拆解与建模 —— 数学和物理的看家本领</h2>
<p>问题定义清楚后，它通常还是太大、太复杂。第二阶段的任务是：把它变小、变简单、变得可以下手。</p>
<h3 id="4-抽象与分治数学家的两板斧">4. 抽象与分治：数学家的两板斧</h3>
<p><strong>抽象</strong>，是丢掉无关细节、只保留问题骨架的能力。数学家不关心是七个苹果还是七头牛，只关心"7"。面对复杂问题时先问：去掉所有表面信息后，这个问题的结构是什么？它像不像某个我已经会解的问题？</p>
<p><strong>分治</strong>（Divide and Conquer），是把一个解不动的大问题切成几个能解的小问题。关键在于切口要"正交"——子问题之间尽量互不纠缠，否则切了等于没切。</p>
<p><em>谱系：抽象自欧几里得以来就是数学的本体。分治的经典表述是笛卡尔《谈谈方法》（1637）第二条规则——"把所考察的每个难题分成尽可能多的小部分，直到可以妥善解决为止"，三百年后成为算法设计的支柱。</em></p>
<h3 id="5-理想化模型物理学家的无摩擦斜面">5. 理想化模型：物理学家的"无摩擦斜面"</h3>
<p>物理学的核心方法不是数学，而是<strong>敢于简化</strong>。伽利略研究落体时假设没有空气阻力，力学里全是"质点""刚体""理想气体"——全是现实中不存在的东西，但正是这些"错误"的模型让问题变得可解。</p>
<p>实践版本是：<strong>先建一个粗糙但能算的模型，再逐步加回被忽略的因素。</strong> 先假设用户都是理性的、网络永远不抖动、需求不会变，把主干逻辑跑通；然后一项一项放回现实的复杂性，看哪一项真正改变了结论。</p>
<p><em>谱系：伽利略的理想斜面是近代科学的起点——他第一个意识到"先研究不存在的简化世界"比直接面对混乱的现实更有效。统计学家乔治·博克斯的总结后来成了行规："所有模型都是错的，但有些是有用的。"</em></p>
<h3 id="6-费米估算与量纲检查快速逼近数量级">6. 费米估算与量纲检查：快速逼近数量级</h3>
<p>费米能用"芝加哥有多少调音师"这类问题训练学生：把一个无从下手的量，拆成几个可以粗略估计的因子相乘。误差会互相抵消，结果往往能对到数量级。<strong>很多决策不需要精确答案，只需要知道是 10 还是 10000。</strong></p>
<p>配套的工具是<strong>量纲分析</strong>：算完之后检查单位对不对、数量级合不合理。一个"每天新增 300 万用户"的估算，乘上 365 天就会暴露荒谬。这是成本几乎为零的纠错手段。</p>
<p><em>谱系：估算法以费米命名；量纲分析的源头是傅里叶，经瑞利发扬，由白金汉 π 定理（1914）系统化——物理方程两边量纲必须一致，这条朴素的约束强到有时能直接猜出公式的形状。</em></p>
<blockquote>
<p><strong>本阶段输出：一个简化的模型或拆解图——哪些因素被保留，哪些被暂时忽略，心里有数。</strong></p>
</blockquote>
<hr>
<h2 id="第三阶段推理与探索--演绎归纳与思想实验">第三阶段：推理与探索 —— 演绎、归纳与思想实验</h2>
<p>模型建好了，现在要在模型上推理，生成候选答案。</p>
<h3 id="7-演绎归纳与溯因三种推理引擎">7. 演绎、归纳与溯因：三种推理引擎</h3>
<p>逻辑学区分了三种推理：<strong>演绎</strong>（从一般到特殊，前提为真则结论必真）、<strong>归纳</strong>（从样本总结规律，结论只是大概率）、<strong>溯因</strong>（从结果倒推最可能的原因——医生诊断、工程师排查 bug 用的都是它；候选解释打架时，先举<strong>奥卡姆剃刀</strong>：优先采信假设最少的那个）。</p>
<p>关键不是会用哪一种，而是<strong>清楚自己此刻在用哪一种</strong>。把归纳出来的经验当成演绎般的铁律（"前三次都是缓存问题，这次一定也是"），是排查问题时最常见的陷阱。</p>
<p><em>谱系：演绎源自亚里士多德的三段论，1879 年由弗雷格彻底形式化；溯因由皮尔士在十九世纪末命名，哈曼后来称之为"最佳解释推理"——但"最佳解释凭什么更可能为真"至今没有公认答案；奥卡姆剃刀以十四世纪经院哲学家奥卡姆的威廉命名，"如无必要，勿增实体"这句流行表述其实是后人的提炼。归纳的地基则是哲学史上最著名的一次塌方：<strong>休谟（1748）证明，"过去如此，所以未来也将如此"无法被非循环地证成</strong>——你只能用归纳本身来为归纳辩护。这就是"归纳问题"。其后的主要应答——康德的先验范畴、波普尔的证伪主义（第 11 节）、贝叶斯主义（第 15 节）——后文都会遇到，而古德曼 1955 年的"绿蓝悖论"证明问题比休谟说的还深。连演绎也不能幸免：刘易斯·卡罗尔的《乌龟对阿喀琉斯说了什么》（1895）表明，推理规则本身无法靠推理证成。我们使用这三台引擎，但没有一台的地基是焊死的。</em></p>
<h3 id="8-反证法与极端情形数学家的探针">8. 反证法与极端情形：数学家的探针</h3>
<p><strong>反证法</strong>：先假设结论不成立，看会推出什么荒谬。日常版本是"假设这个方案是错的，那它最可能错在哪？"——这比正面论证更容易暴露盲点。</p>
<p><strong>极端情形检验</strong>：把参数推到 0、推到无穷大，看结论还成立吗。"如果用户量是现在的 1000 倍，这个架构会先在哪里崩？""如果只剩一个人维护，这套流程还能转吗？"极端值是最便宜的压力测试。</p>
<p><em>谱系：已知最早的反证法杰作，是毕达哥拉斯学派对"√2 是无理数"的证明；它依赖的矛盾律被亚里士多德称为"一切原理中最确实的"。一个值得知道的例外：直觉主义数学家（布劳威尔）拒绝其中一类用法——不允许从"不存在会导出矛盾"直接断言"存在"。日常思考用不到这条戒律，但它提醒我们：连反证法的边界都曾被认真争论过。</em></p>
<h3 id="9-思想实验与对称性物理学家的想象力">9. 思想实验与对称性：物理学家的想象力</h3>
<p>爱因斯坦追着光跑，得出相对论；麦克斯韦养了一只分拣分子的"妖"。<strong>思想实验是在头脑中搭建实验室，用想象力运行现实中做不了的实验。</strong> 决策中的对应物是哲学家罗尔斯的"无知之幕"：假如你不知道自己会是这个规则下的哪一方，你还会制定这个规则吗？</p>
<p>物理学还贡献了<strong>对称与守恒</strong>的直觉：变化中寻找不变量。系统再复杂，总有些东西是守恒的——预算总量、总时间、信任。"这个方案号称三方共赢，那成本守恒地转移到谁头上了？"</p>
<p><em>谱系：思想实验的第一个杰作就出自伽利略——"重物落得更快"可以纯靠想象推出矛盾（把大小两块石头绑在一起，按亚里士多德的理论会同时推出更快和更慢）；"思想实验"（Gedankenexperiment）一词由物理学家奥斯特创造，经马赫之手才成为自觉的方法论；它的认识论地位至今有争论，诺顿认为思想实验只是化了妆的论证。"无知之幕"出自罗尔斯《正义论》（1971）。守恒直觉的地基则是全文工具里最硬的——<strong>诺特定理（1918）：每一个连续对称性严格对应一个守恒量</strong>（时间平移对称 ⇒ 能量守恒）。它是被证明的定理，不是启发式；把守恒用到预算和信任上当然只是类比，但这个类比的母版极其可靠。</em></p>
<h3 id="10-逆向思维反过来想总是反过来想">10. 逆向思维：反过来想，总是反过来想</h3>
<p>数学家雅可比的名言"反过来想，总是反过来想"（Invert, always invert），被芒格奉为终身信条。与其问"怎样让项目成功"，不如先问"<strong>怎样能确保它失败</strong>"——然后把清单上的每一条都避开。失败的路径远比成功的路径清晰可枚举。</p>
<p>芒格自己最著名的一次示范，是 1986 年在哈佛西湖学校的毕业演讲：别人讲如何获得幸福，他通篇讲"如何保证自己过上悲惨的一生"——嫉妒、怨恨、反复无常、不从别人的教训中学习、遭遇挫折就一蹶不振。把每一条反过来执行，就是答案。</p>
<p><em>谱系：雅可比说的本是数学实践——许多问题从反面看结构更简单，这背后是数学中无处不在的对偶性；芒格把它从数学搬进了生活与投资。</em></p>
<blockquote>
<p><strong>本阶段输出：不止一个候选方案。只有一个选项的时候，你其实没有在思考，只是在服从。</strong></p>
</blockquote>
<hr>
<h2 id="第四阶段验证与纠错--科学方法与心理学的主场">第四阶段：验证与纠错 —— 科学方法与心理学的主场</h2>
<p>到这里你手上已经有了几个看起来不错的候选答案。第四阶段的全部要义是：<strong>你的大脑此刻最想做的事是证明自己是对的，而你要逼它做相反的事。</strong></p>
<h3 id="11-可证伪性波普尔的分界线">11. 可证伪性：波普尔的分界线</h3>
<p>科学哲学家波普尔指出，科学与非科学的分界不在于"能否被证实"，而在于"<strong>能否被证伪</strong>"。一个怎么样都对的理论（"这都是命运的安排"）不传递任何信息。</p>
<p>实践方法：为你的结论写下<strong>证伪条件</strong>——"如果观察到 X，我就承认这个判断错了"。写不出 X 的判断，不叫判断，叫信仰。然后，主动去找 X，而不是等它撞上来。</p>
<p>波普尔本人的顿悟就来自一次真实对照：1919 年爱丁顿率队远征观测日全食，检验广义相对论预言的星光偏折——爱因斯坦把脖子伸进了铡刀，数据不符理论就死；而同时代的弗洛伊德学说，在波普尔看来无论病人怎么表现都能自圆其说。一边敢于被杀死，一边永远正确，分界线由此画下。</p>
<p><em>谱系：波普尔《科学发现的逻辑》（1934）。值得知道它的动机：波普尔全盘接受了休谟对归纳的判决（见第 7 节），然后釜底抽薪——科学根本不靠归纳，理论永远只是猜想，科学的全部理性就在于高效地淘汰错误猜想。后续的重要修正是迪昂—蒯因论旨：单个假设永远无法被孤立证伪，你总可以把锅甩给辅助假设（"是仪器坏了"）；拉卡托斯由此区分"进步的"与"退化的"研究纲领——一个靠不断打补丁才活着的信念，就是退化的。给自己的信念做这个检查，相当残忍，也相当有效。</em></p>
<h3 id="12-确认偏误与系统-1系统-2知道大脑会在哪里骗你">12. 确认偏误与系统 1/系统 2：知道大脑会在哪里骗你</h3>
<p>心理学家卡尼曼把思维分成两套系统：<strong>系统 1</strong> 快速、自动、靠直觉；<strong>系统 2</strong> 缓慢、费力、讲逻辑。麻烦在于系统 1 永远在线，并且擅长伪装成理性——你以为自己在推理，其实在为直觉找借口。</p>
<p>最危险的几个偏误值得点名：<strong>确认偏误</strong>（只看见支持自己的证据）、<strong>锚定效应</strong>（第一个数字绑架后续判断）、<strong>损失厌恶</strong>（损失的痛苦约是同等收益快乐的两倍，所以人会死守沉没成本）、<strong>幸存者偏差</strong>（二战时军方想给返航轰炸机上弹孔密集的部位加装甲，统计学家沃尔德指出该加固的恰恰是没有弹孔的部位——那些地方中弹的飞机没能飞回来）。纠错的第一步不是"更努力地客观"，而是<strong>承认自己此刻大概率正在偏，然后用流程对冲</strong>——比如强制写下反方观点，或者找一个真敢说话的人当红队。</p>
<p><em>谱系：卡尼曼与特沃斯基 1970 年代开创"启发式与偏差"研究纲领（《思考，快与慢》是其大众版总结）；再深一层是西蒙 1950 年代的"有限理性"：偏差不是大脑的 bug，是有限算力下的工程妥协。要听反方：吉仁泽论证许多启发式在真实环境中是"生态理性"的，比完整计算更快也常常更准；"损失厌恶约两倍"的普适性在近年文献中也有争议。结论：偏差清单是路标，不是判决书。</em></p>
<h3 id="13-相关不等于因果一起出现不代表谁导致谁">13. 相关不等于因果：一起出现，不代表谁导致谁</h3>
<p>上一节的偏误是大脑主动骗你，这一节的陷阱藏在数据里：<strong>两个变量总是一起涨落，不代表其中一个导致了另一个。</strong> 冰淇淋销量和溺水人数高度正相关，但不是冰淇淋让人淹死——背后是"夏天"这个共同原因（混淆变量）。看到相关，至少有四种可能并存：A 导致 B、B 导致 A、第三个因素 C 同时推动两者、纯属巧合。</p>
<p>实践纪律：行动之前先找混淆变量，问一句"有没有一个 C 同时驱动了两边"；能做随机对照实验（把样本随机分成两组）就做，做不了就把结论的可信度降一档。一个隐蔽的变种是<strong>辛普森悖论</strong>——同一份数据，分组看和合并看可以得出完全相反的结论。</p>
<p><em>谱系：休谟早就指出我们从未"看见"因果，只看见恒常的先后相继（又是第 7 节那个归纳问题的源头）；把因果推断变成可操作工具的是统计学——费希尔的随机对照实验（1920 年代）与朱迪亚·珀尔的因果图和 do-演算（1990 年代）。"相关不蕴含因果"人人会背，难的从来是识别出那个具体的混淆变量。</em></p>
<h3 id="14-外部视角与基础概率先看同类再看自己">14. 外部视角与基础概率：先看同类，再看自己</h3>
<p>预测一件事会怎么收场，有两条路。<strong>内部视角</strong>盯着这件事本身的细节推演："我们团队强、计划周密，三个月能上线。" <strong>外部视角</strong>先问一句："同类的事，通常是什么结局？"——类似规模的项目，有多少按时上线？延期的中位数是多少？人天然偏爱内部视角，而它系统性地过度乐观，这就是<strong>规划谬误</strong>：几乎每个大工程都超期超预算，因为每个团队都觉得"我们不一样"。</p>
<p>纠错动作只有一个：任何预测，先给它找一个<strong>参照类</strong>，拿这群同类的真实分布当锚，再根据本案的特殊性做有限调整。<strong>基础概率是你的起点，不是可以略过的背景板。</strong></p>
<p>卡尼曼讲过自己的亲历：他带队编一本教材，组内乐观估计两年完成；他追问一位资深成员"你见过的同类项目平均花多久、有多少半途夭折"，答案是七到十年、约四成流产。他们没把这个外部数据当回事，结果用了八年——明知基础概率，仍败给了内部视角。</p>
<p><em>谱系：卡尼曼与洛瓦洛 1993 年区分"外部视角 vs 内部视角"；其操作化是参照类预测（reference class forecasting），弗吕夫别格用它系统改进大型基建的成本预估，已被多国政府写进规范。它和下一节的贝叶斯是一体两面：外部视角给你一个像样的先验，贝叶斯告诉你拿到新证据后该把它移动多少。</em></p>
<h3 id="15-贝叶斯更新把信念当概率来维护">15. 贝叶斯更新：把信念当概率来维护</h3>
<p>贝叶斯定理的日常版本是：<strong>信念不是非黑即白的开关，而是随证据滑动的概率。</strong> 拿到新证据时问三个问题：我原来的把握是多少（先验）？如果我是对的，看到这个证据的可能性多大？如果我是错的呢？</p>
<p>凯恩斯（据传）说过："事实变了，我的想法就跟着变。你呢，先生？"贝叶斯主义者的美德不是立场坚定，而是<strong>更新得快、且更新的幅度恰如其分</strong>——不因一个反例全盘推翻，也不无视十个反例岿然不动。</p>
<p>这套朴素的算法找到过飞机残骸：2009 年法航 447 航班坠入大西洋，两年搜索一无所获；2011 年搜索方请来贝叶斯搜索专家，把此前每一次失败都当作证据，更新海底各区域藏有残骸的概率分布，新一轮搜索开始一周内就找到了机身。同样的方法在 1968 年定位过沉没的"天蝎号"核潜艇。</p>
<p><em>谱系：贝叶斯（1763 年遗作）与拉普拉斯。它有两个罕见地硬的证成：Cox 定理证明，满足几条合理性公理的"可信度"运算必然就是概率论；德·菲内蒂的"荷兰赌"论证则证明，不按概率公理行事的人，可以被构造出一组稳赚不赔的赌局收割。换句话说：拒绝贝叶斯更新不只是固执，是可以被定价的固执。</em></p>
<h3 id="16-事前验尸在失败发生前开追悼会">16. 事前验尸：在失败发生前开追悼会</h3>
<p>心理学家加里·克莱因发明的 <strong>Premortem（事前验尸）</strong>：在方案启动前，全员假设"现在是一年后，这个项目已经惨败"，然后每个人写下失败的原因。这个简单的换框，能把"提出担忧"从扫兴变成任务，挖出平时没人敢说的风险。一个人也能用：给一年后的自己写一封失败信。</p>
<p>克莱因在原文里记了一例：一家财富 50 强级别的公司启动一个十亿美元级的可持续发展项目，事前验尸会上，一位高管写下的死因是——力挺它的 CEO 一退休，项目就会失去靠山。这种话在动员会上没人敢说，在"追悼会"上脱口而出。</p>
<p><em>谱系：克莱因 2007 年发表于《哈佛商业评论》；其实验基础是 1989 年的"前瞻性后见之明"研究——让人假设结果已经发生，对原因的想象会具体生动得多。</em></p>
<blockquote>
<p><strong>本阶段输出：一份证伪条件清单 + 一份事前验尸报告。你的方案被自己攻击过之后还站得住，才有资格进入决策。</strong></p>
</blockquote>
<hr>
<h2 id="第五阶段决策与行动--经济学与工程学接管">第五阶段：决策与行动 —— 经济学与工程学接管</h2>
<p>验证过的方案可能仍有好几个，而资源只有一份。第五阶段从"求真"切换到"<strong>权衡</strong>"——还记得第零章里休谟那条断崖吗？就是从这里跨过去的。</p>
<h3 id="17-机会成本与边际思维经济学的两块基石">17. 机会成本与边际思维：经济学的两块基石</h3>
<p><strong>机会成本</strong>：一个选择的真实成本，不是你付出了什么，而是<strong>你因此放弃的最好的那个选项</strong>。"这件事值不值得做"是个伪问题，真问题是"这件事比我能做的其他事更值得吗"。</p>
<p><strong>边际思维</strong>：决策永远看增量，不看总量。不要问"要不要做营销"，要问"<strong>多投这一块钱</strong>营销，能多换回多少"。与之配套的是对<strong>沉没成本</strong>的铁律：已经花掉的钱、投入的感情、写完的代码，与未来的决策无关——尽管损失厌恶会拼命让你觉得有关。</p>
<p>斩断沉没成本最著名的一刀发生在 1985 年的英特尔。存储器是公司起家的业务，被日本厂商打得节节败退，却谁也下不了手。格鲁夫问摩尔："如果董事会把我们换掉，新来的 CEO 会怎么做？"摩尔答："退出存储器。"格鲁夫说："那为什么我们不自己走出这扇门，再走回来，亲手去做？"新 CEO 不背旧账——这个思想动作的全部内容，就是把沉没成本清零。英特尔从此转向处理器。</p>
<p><em>谱系：1870 年代的"边际革命"——杰文斯、门格尔、瓦尔拉斯几乎同时独立提出，经济学从此从"价值由什么决定"转向"增量如何比较"；机会成本由维塞尔命名。地基是稀缺性公理：只要资源有限，选择就必然意味着放弃。</em></p>
<h3 id="18-期望值与不对称性和不确定性共处">18. 期望值与不对称性：和不确定性共处</h3>
<p>理性决策的骨架是<strong>期望值</strong>：收益 × 概率。但真正的高手还看<strong>分布的形状</strong>——塔勒布称之为不对称性：下行有限、上行巨大的事（写作、开源、社交中的善意）值得反复做；上行有限、下行致命的事（加杠杆、单点依赖、灰色地带）一次都嫌多。<strong>永远不要冒会被踢出局的风险，不管期望值多漂亮。</strong></p>
<p>反面教材是长期资本管理公司（LTCM）：合伙人名单上有两位诺贝尔经济学奖得主，每笔套利的期望值都为正，于是加上约 25 倍杠杆反复下注。1998 年俄罗斯国债违约，一次尾部事件就把它彻底踢出了局，最后由美联储出面组织救助。每一步的期望值都对，错的是他们在玩一个不允许输一次的游戏。</p>
<p><em>谱系：1654 年帕斯卡与费马为分赌金问题通信，概率论由此诞生；冯·诺伊曼与摩根斯坦 1944 年完成期望效用的公理化。裸期望值的毛病很早就暴露了——伯努利 1738 年的圣彼得堡悖论；而"别冒出局风险"的现代数学依据是遍历性问题（物理学家彼得斯）：<strong>时间平均不等于集合平均</strong>，一个期望值为正的赌局，对必须连续下注的个体可以是毁灭性的。塔勒布说的就是这个。</em></p>
<h3 id="19-激励与博弈对面也有人">19. 激励与博弈：对面也有人</h3>
<p>机会成本和期望值都默认你在和"自然"打交道——成本和概率不会因为你的选择而改变。但多数真实决策的对面，坐着会预判你、回应你的人。这时要加两道检查。一是<strong>激励</strong>：判断一个人或机构会怎么做，看它的激励结构，远比听它的承诺可靠——芒格的版本是"给我看激励，我就给你看结果"。二是<strong>对手的最优应对</strong>：把别人的反应算进方案里——"我降价，对手会不会跟？跟了之后我还赚吗？"一个不考虑回应的计划，是写给静止世界的。</p>
<p>激励压倒口号的教科书案例是 2016 年的富国银行：总部给柜员定下"每位客户交叉销售八个产品"的硬指标并与薪酬挂钩，于是员工在客户不知情的情况下开出了约三百五十万个虚假账户。每个人都在对激励做理性应对，加总起来是一场灾难——口号说的是服务客户，激励奖的是开户数量，员工听激励的。这就是<strong>古德哈特定律</strong>：一个指标一旦被当成考核目标，它就会被人们冲着数字去优化，从此不再是它原本想衡量的那件事。</p>
<p><em>谱系：博弈论与第 18 节的期望效用出自同一本书——冯·诺伊曼与摩根斯坦《博弈论与经济行为》（1944）；纳什 1950 年的均衡概念把它推广到非零和情形。"看激励"的学术版是机制设计理论（赫维茨、马斯金、迈尔森，2007 年诺贝尔经济学奖）：干脆把激励当成可以被设计的对象。</em></p>
<h3 id="20-可逆与不可逆贝索斯的两扇门">20. 可逆与不可逆：贝索斯的两扇门</h3>
<p>贝索斯把决策分成两类：<strong>双向门</strong>（错了可以退回来）和<strong>单向门</strong>（走过去就回不了头）。双向门决策应该快、便宜、授权给最近的人——此时拖延的成本远高于犯错；单向门决策才值得慢下来，把前四个阶段的工具全部用一遍。<strong>多数人的问题是用单向门的谨慎对待双向门，又用双向门的草率走过单向门。</strong></p>
<p>亚马逊自己的单向门样本是 2005 年的 Prime：财务模型怎么算都算不平，而"免费两日达"一旦送出就几乎收不回来——从用户手里拿走既有的福利，代价远大于从未给过——贝索斯让团队反复推演后才拍板。与之对照，亚马逊网站上每天运行的几百个界面实验是标准的双向门：数据难看，当天下线。</p>
<p><em>谱系：贝索斯 2015 年度致股东信。学术对应物是西蒙的"满意化"（satisficing）：有限理性者的最优策略不是事事求最优，而是按决策的重要性分配认知预算。</em></p>
<h3 id="21-工程师的妥协艺术没有最优只有取舍">21. 工程师的妥协艺术：没有最优，只有取舍</h3>
<p>工程学贡献的核心世界观是：<strong>所有设计都是取舍</strong>（trade-off），快、好、便宜只能选两个。配套的方法是<strong>最小可行产品</strong>（MVP）：当分析无法继续降低不确定性时，停止分析，用最小的成本把方案推进现实，让现实接管验证——做，本身就是一种更高带宽的思考。</p>
<p>两个教科书案例都收录在《精益创业》里。Dropbox 的产品还没法公开使用时，创始人休斯顿先放出一段三分钟的演示视频，一夜之间候补名单从五千人涨到七万五千人——一行代码没多写，需求已经验证。Zappos 的创始人则先跑去附近鞋店把鞋拍照上架，有人下单他再原价买来寄出，用零库存证明了"有人愿意在网上买鞋"。</p>
<p><em>谱系：MVP 由埃里克·莱斯《精益创业》（2011）普及，但它的哲学根源要老得多——皮尔士和杜威的实用主义：观念的意义就在它的实际效果里，行动不是探究的结束，而是探究的一部分。</em></p>
<blockquote>
<p><strong>本阶段输出：一个明确的决定 + 它的机会成本 + 它是哪种门 + 最小的第一步。</strong></p>
</blockquote>
<hr>
<h2 id="第六阶段复盘与迭代--控制论与进化论收尾">第六阶段：复盘与迭代 —— 控制论与进化论收尾</h2>
<p>行动不是思考的终点，而是下一轮思考的输入。</p>
<h3 id="22-反馈回路控制论的核心">22. 反馈回路：控制论的核心</h3>
<p>维纳的控制论把一切智能行为归结为<strong>反馈</strong>：输出被测量、与目标比较、误差被送回去修正输入。导弹是这么追上飞机的，恒温器是这么稳住温度的，人是这么学会骑车的。</p>
<p>推论很实际：<strong>系统的进步速度，取决于反馈回路的速度和保真度。</strong> 反馈周期一年（年度绩效）的系统，进化速度注定不如反馈周期一天（每日复盘）的系统。改进一件事最有杠杆的方式，往往不是更努力，而是缩短它的反馈回路。</p>
<p><em>谱系：维纳《控制论》（1948）；更早的数学化是麦克斯韦 1868 年的《论调速器》——为分析瓦特蒸汽机的离心调速器而写，被视为控制理论的第一篇论文。</em></p>
<h3 id="23-变异选择保留进化论的算法">23. 变异—选择—保留：进化论的算法</h3>
<p>达尔文给出的或许是有史以来最强大的问题求解算法，它不需要任何智能就能造出眼睛和大脑：<strong>产生变异 → 环境选择 → 保留胜者 → 重复</strong>。</p>
<p>应用到个人和组织：保持小成本的多样化尝试（变异），用真实世界的反馈而非内部意见来筛选（选择），把验证过的做法固化成习惯和流程（保留）。注意三者缺一不可——只变异不保留是瞎折腾，只保留不变异是僵化。</p>
<p>商业史上最完整的一次三拍合奏是便利贴：3M 的化学家席尔弗发明了一种"失败"的弱胶水，怎么都粘不牢（变异）；几年后同事弗莱用它给唱诗班的赞美诗集做书签，发现"粘得住又撕得下来不留痕"正是无数人需要的（选择）；3M 把它固化成常设产品线，畅销至今（保留）。变异从哪来也有答案——3M 允许员工拿 15% 的工时做自选项目：组织无法设计出变异本身，但可以设计让变异得以发生的环境。</p>
<p><em>谱系：达尔文《物种起源》（1859）；坎贝尔 1960 年将其推广为"盲变异与选择性保留"——一切知识增长的普适算法。注意它与第 11 节波普尔的"猜想与反驳"是同构的：猜想即变异，反驳即选择。这不是巧合——这就是第零章说的那台发动机，整个六阶段框架深处运转的是同一个算法。</em></p>
<h3 id="24-元认知与刻意练习对思考本身进行思考">24. 元认知与刻意练习：对思考本身进行思考</h3>
<p>心理学的收尾礼物是<strong>元认知</strong>：监控自己思考过程的能力。复盘时不仅问"结果对不对"，更要问"<strong>我当时的思考过程对不对</strong>"——好决策可能有坏运气，坏决策也可能有好结果，扑克牌手安妮·杜克称混淆这两者为 "resulting"，这是复盘中最隐蔽的错误。杜克的书开篇就是真实判例：2015 年超级碗最后 26 秒，海鹰队在一码线上选择传球而非地面冲锋，被对方拦截，痛失冠军，主教练卡罗尔被骂作"史上最蠢决策"；但按当时的时间、剩余暂停和该类传球约 1%—2% 的被拦截率算，传球是站得住的选择——人们只是用结果给过程定了罪。与它孪生的陷阱是<strong>均值回归</strong>：极端的结果之后大概率跟着更平庸的结果，这是纯粹的统计现象——把爆发后的回落归因于"自己松懈了"，把低谷后的回升归因于"整改有效"，复盘就成了给噪声编故事。</p>
<p>刻意练习的研究则提醒：单纯的重复不产生进步，<strong>带着明确目标、聚焦弱点、获取即时反馈的重复</strong>才会。思考方法本身也是技能，同样适用这条定律。</p>
<p><em>谱系：元认知由弗拉维尔 1976 年命名；均值回归由高尔顿（1886）在身高遗传研究中发现；刻意练习是埃里克松 1993 年的研究纲领——"一万小时定律"是格拉德威尔的简化转述，埃里克松本人并不认账；后续元分析（麦克纳马拉，2014）表明刻意练习能解释的成绩差异比流行说法小得多。方向有效，幅度别神化。"resulting"出自安妮·杜克《对赌》（2018）。</em></p>
<blockquote>
<p><strong>本阶段输出：写下来。没有写下来的复盘，两周后就会被记忆篡改成"我早就料到了"（后见之明偏差，心理学的最后一击）。</strong></p>
</blockquote>
<hr>
<h2 id="全图一张速查表">全图：一张速查表</h2>
<table>
<thead>
<tr>
<th>阶段</th>
<th>核心问题</th>
<th>主力学科</th>
<th>关键工具</th>
</tr>
</thead>
<tbody>
<tr>
<td>1. 定义问题</td>
<td>我到底要解决什么？</td>
<td>哲学</td>
<td>苏格拉底诘问、第一性原理、问题转译</td>
</tr>
<tr>
<td>2. 拆解建模</td>
<td>怎么把它变得可下手？</td>
<td>数学、物理</td>
<td>抽象、分治、理想化模型、费米估算</td>
</tr>
<tr>
<td>3. 推理探索</td>
<td>答案可能是什么？</td>
<td>逻辑学、数学、物理</td>
<td>三种推理、奥卡姆剃刀、反证法、思想实验、逆向思维</td>
</tr>
<tr>
<td>4. 验证纠错</td>
<td>我怎么知道自己错了？</td>
<td>科学方法、心理学</td>
<td>可证伪性、认知偏差清单、相关 ≠ 因果、外部视角、贝叶斯更新、事前验尸</td>
</tr>
<tr>
<td>5. 决策行动</td>
<td>在约束下选哪个？</td>
<td>经济学、工程学</td>
<td>机会成本、边际、期望值、激励与博弈、双向门、MVP</td>
</tr>
<tr>
<td>6. 复盘迭代</td>
<td>下一轮怎么更好？</td>
<td>控制论、进化论、心理学</td>
<td>反馈回路、变异—选择—保留、元认知</td>
</tr>
</tbody>
</table>
<p>三点使用说明：</p>
<p><strong>第一，阶段是螺旋而非直线。</strong> 验证阶段发现问题定义错了，就退回第一阶段——这不是失败，这恰恰是流程在起作用。真实的思考是在六个阶段之间反复横跳的。</p>
<p><strong>第二，不是每次都要走全程。</strong> 双向门的小决策，用直觉加一个快速的"反过来想"就够了；单向门的大决策，才值得把六个阶段完整走一遍。而面对单向门时，最有杠杆的一步往往是先把它<strong>转译成一个双向门问题</strong>——"要不要花一年写一本书"，先变成"把其中一章写成文章发出去，有没有陌生人愿意读、愿意转"，后者用一个周末的最小实验就能开始回答。对工具的选择本身，就是一种判断力。</p>
<p><strong>第三，工具不会自动生效。</strong> 知道确认偏误的人照样确认偏误，背熟沉没成本的人照样舍不得割肉。这些方法只有变成<strong>写下来的清单和固定的流程</strong>——决策前强制写证伪条件、项目前强制开事前验尸会——才能在你最需要它们、也最不想用它们的时刻起作用。</p>
<p>芒格说，他这一生不过是"手里拿着几个大观念，耐心等待"。这个工具箱里的二十四件工具，每一件背后都站着一个学科上百年甚至上千年的打磨——有些工具的地基至今还在哲学家手里返修，但这不妨碍它们好用，正如你不需要等量子引力完工才使用牛顿力学。你不需要发明新的思考方法，你只需要在对的阶段，伸手拿对的那一件。</p>]]></content>
  </entry>
  <entry>
    <title>Harness Engineering：决定 Agent 成败的，是模型之外的那圈工程</title>
    <id>https://shipengtao.com/zh/harness-engineering/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/harness-engineering/"/>
    <published>2026-06-05T00:00:00+08:00</published>
    <updated>2026-06-05T00:00:00+08:00</updated>
    <summary type="text">Harness Engineering：决定 Agent 成败的，是模型之外的那圈工程 模型越来越聪明，为什么 Agent 还是动不动就翻车？答案不在模型里，而在模型外面那一圈工程系统——harness…</summary>
    <content type="html"><![CDATA[<h1 id="harness-engineering决定-agent-成败的是模型之外的那圈工程">Harness Engineering：决定 Agent 成败的，是模型之外的那圈工程</h1>
<blockquote>
<p>模型越来越聪明，为什么 Agent 还是动不动就翻车？答案不在模型里，而在模型外面那一圈工程系统——harness。本文从概念讲到实战，附带可直接照抄的检查清单。</p>
</blockquote>
<hr>
<h2 id="一先看一个反直觉的事实">一、先看一个反直觉的事实</h2>
<p>同一个 Claude / Codex 模型，同一道任务，换两套"外壳"，成功率能从 <strong>20% 跳到接近 100%</strong>——模型一个字没改。</p>
<p>这是 <code class="language-text">learn-harness-engineering</code> 课程（见底部参考文档）里反复出现的实测结论：一个 TypeScript/React 项目，只给模型一个 README 时成功率约 20%；把"规则、状态、验证"这套外壳补齐后，成功率逼近 100%。<strong>没有换模型，只换了 harness。</strong></p>
<p>OpenAI 的案例更极端（2026 年 2 月，Ryan Lopopolo）：一支小队从 2025 年 8 月底的空仓库起步，约五个月里 <strong>0 行人工代码，每一行都由 Codex 生成</strong>，最终产出约 100 万行代码（含应用逻辑、基础设施、工具链与文档）、1500 个合并的 PR，团队从 3 人涨到 7 人、人均吞吐反而上升到 <strong>每人每天 3.5 个 PR</strong>。这群人几乎没碰业务代码，他们在干一件事——<strong>设计让模型可靠产出代码的环境</strong>。据他们估算，整体只花了手写代码约 1/10 的时间。</p>
<p>这件事现在有了名字：<strong>Harness Engineering（脚手架工程 / 框架工程）</strong>。</p>
<p>一句话定义：</p>
<blockquote>
<p><strong>Agent = Model + Harness。</strong>
模型负责"聪明"，harness 负责"可靠"。</p>
</blockquote>
<hr>
<h2 id="二什么是-harness">二、什么是 Harness？</h2>
<p>最常见的误解：以为 harness 就是一个写得好的 system prompt，或者一个 <code class="language-text">CLAUDE.md</code> 文件。<strong>错。</strong></p>
<p>各家给的定义高度一致，核心都是同一句话：</p>
<blockquote>
<p>Harness = <strong>模型权重之外，所有决定"智能能落地多少"的工程基础设施。</strong>
（LangChain 的说法："every piece of code, configuration, and execution logic that isn't the model itself."）</p>
</blockquote>
<p>它包括但远不止 prompt：工具的定义与执行、文件系统与沙箱、状态持久化、验证机制、上下文管理、子智能体编排、<strong>确定性的钩子/中间件</strong>（compaction、续跑、lint 拦截等不靠模型自觉、由代码强制执行的环节）、可观测性……</p>
<p>打个最贴切的比方：</p>
<blockquote>
<p><strong>把 Agent 当成一个今天入职的资深工程师。</strong> 他很聪明，但对你的代码库一无所知。他需要：项目文档、跑得起来的环境、能用的工具、知道"完成"的标准、以及交班记录。
这些东西的总和，就是 harness。你给的脚手架越完整，这个"新人"发挥出的能力就越接近他的真实水平。</p>
</blockquote>
<p>关键洞察：<strong>今天 Agent 失败，多数不是"不够聪明"，而是"看不到 / 跑不动 / 不知道何时算完"——这些全是 harness 问题，不是模型问题。</strong> 这就是所谓的 <strong>capability–execution gap（能力与执行的鸿沟）</strong>。</p>
<hr>
<h2 id="三harness-的五个子系统">三、Harness 的五个子系统</h2>
<p>把一个完整的 harness 拆开，可以收敛成五个子系统。下面这套五分法综合自三处：WalkingLabs 的 <code class="language-text">learn-harness-engineering</code> 课程、LangChain 的组件全景、以及 Anthropic 的长任务实践。建议按下面这张表逐项自查：</p>
<blockquote>
<p><strong>一点说明（各家划分并不完全一致）</strong>：<code class="language-text">learn-harness-engineering</code> 课程原本的五件套是 <strong>Instructions / State / Verification / Scope / Session Lifecycle</strong>——把"会话生命周期"单列为一项。本文改用更贴近 LangChain/Anthropic 的切法：把"工具与环境"（两家都重点强调，且最直接决定 Agent 能否跑得起来）提为核心一项，而把"会话生命周期"并入第四节的长任务方法（初始化/执行分离、干净重启）。<strong>子系统怎么命名、归到几项都不重要，别漏项才重要。</strong></p>
</blockquote>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">                ┌─ Instructions  指令：项目是什么、约束是什么
                ├─ Tools/Env     工具与环境：能做什么、跑得起来
   Harness ─────┼─ State         状态：上次进行到哪了
                ├─ Scope         边界：这次只做一件事，做到什么算完
                └─ Verification  验证：怎么自证做对了</code></pre></div>
<table>
<thead>
<tr>
<th>子系统</th>
<th>解决的问题</th>
<th>典型载体</th>
<th>投入产出比</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Instructions（指令）</strong></td>
<td>Agent 不知道项目背景与红线</td>
<td><code class="language-text">AGENTS.md</code> / <code class="language-text">CLAUDE.md</code>，约 100 行</td>
<td>高</td>
</tr>
<tr>
<td><strong>Tools / Environment（工具与环境）</strong></td>
<td>能力够不够、环境跑不跑得起来</td>
<td>工具集、<code class="language-text">pyproject.toml</code>/<code class="language-text">package.json</code>、<code class="language-text">.nvmrc</code>、Docker/devcontainer</td>
<td>高</td>
</tr>
<tr>
<td><strong>State（状态）</strong></td>
<td>跨会话失忆，重复劳动</td>
<td><code class="language-text">PROGRESS.md</code> / <code class="language-text">claude-progress.txt</code>、git 提交历史</td>
<td>中高</td>
</tr>
<tr>
<td><strong>Scope（边界）</strong></td>
<td>Agent 一次想干太多，半途而废</td>
<td>机器可读的 feature list（JSON）</td>
<td>中高</td>
</tr>
<tr>
<td><strong>Verification（验证）</strong></td>
<td>自我感觉良好，假装做完了</td>
<td>测试、lint、type-check、E2E</td>
<td><strong>最高</strong></td>
</tr>
</tbody>
</table>
<p>下面逐个拆。</p>
<h3 id="1-instructions把新人须知写进仓库">1. Instructions：把"新人须知"写进仓库</h3>
<p>原则不是"写得越多越好"，而是 <strong>progressive disclosure（渐进式披露）</strong>：一个 100 行左右的核心指令文件，说清楚项目是什么、技术栈、首次运行命令、不可违反的约束、文档入口。细节按需链接出去，而不是一次性糊一篇百科全书给模型——后者只会稀释注意力。</p>
<p>核心铁律：<strong>仓库即唯一事实来源（repo as the system of record）。</strong> Agent 不能像人一样转头问同事，凡是它需要的东西，必须在仓库里看得到。</p>
<h3 id="2-tools--environment少给约束多给能力">2. Tools / Environment：少给约束，多给能力</h3>
<ul>
<li>工具权限遵循 <strong>最小权限</strong>，而不是一律禁掉——限制过严，Agent 就只能靠猜测。</li>
<li>环境要<strong>自描述、可复现</strong>：依赖文件、运行时版本、容器配置都齐了，Agent 才能一键起服务。Anthropic 的实践里专门有个 <code class="language-text">init.sh</code> 负责拉起开发环境。</li>
<li>一个被低估的能力：<strong>给 Agent 一个能跑 bash / 执行代码的沙箱</strong>。比起预置一堆窄工具，"能自己写代码并运行"才是通用自治的关键——它能自己验证、自己排错。</li>
</ul>
<h3 id="3-state像倒班工人一样交接">3. State：像倒班工人一样交接</h3>
<p>长任务跨越多个上下文窗口，<strong>每个新会话都是一张白纸</strong>。Anthropic 把这比作"轮班的工程师之间没有交接文档"。</p>
<p>解法是把状态落盘：</p>
<ul>
<li>进度文件（<code class="language-text">claude-progress.txt</code> / <code class="language-text">PROGRESS.md</code>）记录：做完了什么、正在做什么、卡在哪。</li>
<li>用 <strong>git 提交</strong>和提交信息留下可追溯的痕迹。</li>
<li>每个会话<strong>开始先读进度和 git log，结束前更新进度</strong>。</li>
</ul>
<p>效果：有 harness 时，Agent 能精确地"从上次断点接着干"；没有时，它要么重做，要么每次从头开始。</p>
<h3 id="4-scope一次只做一件事">4. Scope：一次只做一件事</h3>
<p>Agent 的通病是<strong>贪多</strong>——一次想实现过多功能，结果哪个都没做完。</p>
<p>Anthropic 的做法很直接：把任务拆成 <strong>200+ 条颗粒度极细的 feature list</strong>，每条标注 pass/fail，<strong>用 JSON 而不是 Markdown</strong>（JSON 结构化，模型更不容易擅自改写）。每个会话<strong>只做一个 feature</strong>，做完、测过、提交、更新进度，再进下一个。</p>
<h3 id="5-verification把好不好变成过没过">5. Verification：把"好不好"变成"过没过"</h3>
<p>这是<strong>投入最低、回报最高</strong>的子系统，也是最容易被忽略的。</p>
<p>模型有两个致命倾向：</p>
<ol>
<li><strong>过早宣布胜利</strong>——没测就说"做完了"。</li>
<li><strong>自我表扬</strong>——让它评价自己的产出，它几乎总说"很棒"，哪怕在人看来一塌糊涂。</li>
</ol>
<p>对策：</p>
<ul>
<li>在文档里<strong>显式列出验证命令</strong>（test / lint / typecheck），让 Agent 每次都能跑。</li>
<li>对 Web 应用，要求用<strong>浏览器自动化（如 Puppeteer MCP）做端到端验证</strong>，而不是"看起来对就行"。Anthropic 实测这一条让"功能真的可用"的准确率大幅提升。</li>
<li>把主观质量<strong>变成可打分的客观标准</strong>。Anthropic 做前端时，把"好不好看"这种主观判断拆成四个可评维度：design quality、originality、craft、functionality，再用 few-shot 例子校准评审者。</li>
</ul>
<hr>
<h2 id="四长任务的进阶方法">四、长任务的进阶方法</h2>
<p>短任务把上面五件事做好就够了。但当任务要跑几个小时、跨几十个上下文窗口时，还需要三个进阶模式。</p>
<h3 id="1-初始化-agent-与执行-agent-分离">1. 初始化 Agent 与执行 Agent 分离</h3>
<p>Anthropic 的长任务 harness 是双角色架构：</p>
<ul>
<li><strong>Initializer Agent（初始化）</strong>：第一个会话搭地基——写 <code class="language-text">init.sh</code>、建进度文件、做首次 git 提交、生成那份 200+ 条的 feature list。</li>
<li><strong>Coding Agent（执行）</strong>：后续每个会话遵循固定流程——读进度 → 先跑 E2E 冒烟测试 → 完成一个 feature → 提交 → 更新进度，<strong>离场时必须留下可合并的干净状态</strong>。</li>
</ul>
<h3 id="2-生成与评估分离generator--evaluator">2. 生成与评估分离（Generator / Evaluator）</h3>
<p>不要让同一个 Agent 既当运动员又当裁判。<strong>独立的生成者和评估者</strong>能破解"自我表扬"。评估者只在任务<strong>超出"单个模型独自能可靠完成"的范围</strong>时才值得加——任务很简单时，这套开销反而是浪费。</p>
<blockquote>
<p>Anthropic 的经典对照实验：一句话 prompt 让做个复古游戏。</p>
<ul>
<li><strong>单个 Agent 独自跑（solo run）</strong>：$9、20 分钟，产出一个跑不起来的应用。</li>
<li><strong>三 Agent harness（规划+生成+评估）</strong>：$200、6 小时，产出一个能玩、UI 精致的游戏。</li>
</ul>
<p>质量差距撑得起这笔投入——<strong>前提是任务确实难。</strong></p>
</blockquote>
<h3 id="3-上下文重置-优于-上下文压缩">3. 上下文重置 优于 上下文压缩</h3>
<p>当模型感到"token 快用完了"会出现 <strong>context anxiety（上下文焦虑）</strong>——提前匆忙收尾。直觉做法是把历史**压缩（compaction）**塞进新窗口；但 Anthropic 发现，<strong>直接清空上下文、配合结构化的交接产物（progress 文件 + feature list）重新开始</strong>，效果反而更好。这也呼应 LangChain 说的 <strong>"Ralph Loop"</strong>：在干净的新窗口里重新注入 prompt，对抗 <strong>context rot（上下文腐烂）</strong>。</p>
<hr>
<h2 id="五案例深读openai-的-100-万行代码是怎么撑住的">五、案例深读：OpenAI 的 100 万行代码是怎么撑住的</h2>
<p>OpenAI 这篇《Harness Engineering: Leveraging Codex in an Agent-First World》是目前最翔实的实战记录。把它拆开看，几乎每条经验都能对应回前面的五个子系统——但有几个点足够反直觉，值得单独讲。</p>
<h3 id="1-工程师的角色变了不写代码造环境">1. 工程师的角色变了：不写代码，造环境</h3>
<p>早期进展比预期慢，原因不是 Codex 不行，而是<strong>环境欠规约</strong>——Agent 缺工具、缺抽象、缺内部结构。于是工程师的核心工作变成一句话：<strong>"缺了什么能力，怎么把它做得对 Agent 既可见又可强制？"</strong></p>
<p>出问题时，修法<strong>几乎从不是"让模型再努力一点"</strong>，而是回头补 harness。人只在更高的抽象层工作：排优先级、把用户反馈翻译成验收标准、验证结果。连修复也是让 Codex 自己写。</p>
<h3 id="2-给地图别给一千页手册">2. 给地图，别给一千页手册</h3>
<p>他们一开始也试过"一个大 AGENTS.md"，<strong>失败得很典型</strong>：</p>
<ul>
<li><strong>上下文是稀缺资源</strong>——大文件把任务、代码、相关文档全挤出去了；</li>
<li><strong>全是重点 = 没有重点</strong>——Agent 退化成局部模式匹配；</li>
<li><strong>瞬间腐烂</strong>——很快堆满过期规则、无人维护；</li>
<li><strong>难以校验</strong>——一整块大文件没法做机械检查（覆盖率、新鲜度、交叉链接）。</li>
</ul>
<p>改法：<strong>把 AGENTS.md 当目录（table of contents），不当百科全书。</strong> 一个约 100 行的 AGENTS.md 只做"地图"，真正的事实来源放在结构化的 <code class="language-text">docs/</code> 目录里——设计文档、执行计划（active/completed/技术债追踪）、生成的 DB schema、产品规格、<code class="language-text">*-llms.txt</code> 参考资料等。<strong>计划是一等公民</strong>：小改动用临时轻量计划，复杂工作落成签入仓库的执行计划，带进度和决策日志。</p>
<p>这就是 <strong>渐进式披露</strong>：Agent 从一个小而稳的入口进入，再被"教"去哪里找下一步。而且<strong>机械强制</strong>——专门的 linter 和 CI 校验知识库是否最新、是否交叉链接；还有个**"doc-gardening"（文档园丁）Agent** 定期扫描过期文档、自动提修复 PR。</p>
<h3 id="3-一句最该记住的话agent-看不见的就等于不存在">3. 一句最该记住的话：Agent 看不见的，就等于不存在</h3>
<blockquote>
<p>From the agent's point of view, anything it can't access in-context while running effectively doesn't exist.</p>
</blockquote>
<p>那条对齐了团队架构的 Slack 讨论？如果 Agent 检索不到，它就跟"三个月后入职的新人不知道"一样<strong>不存在</strong>。结论：<strong>不断把上下文塞进仓库</strong>——代码、markdown、schema、可执行计划，这些才是 Agent 唯一看得见的东西。</p>
<p>由此推出一组反直觉取舍：<strong>优先选 Agent 能完全内化的依赖和抽象</strong>。所谓"无聊"的技术因为 API 稳定、可组合、在训练集里出现多，反而更好被模型建模。有时候<strong>自己重写一个子集，比绕开一个不透明的上游库更划算</strong>——他们没用通用的 <code class="language-text">p-limit</code>，而是手写了个 map-with-concurrency，深度集成自家 OpenTelemetry、100% 覆盖、行为完全可控。</p>
<h3 id="4-让应用本身对-agent-可读这是吞吐的真瓶颈">4. 让应用本身对 Agent 可读（这是吞吐的真瓶颈）</h3>
<p>代码吞吐上来后，瓶颈变成<strong>人的 QA 能力</strong>。对策是把 UI、日志、指标<strong>直接做得对 Codex 可见</strong>：</p>
<ul>
<li><strong>每个 git worktree 能独立启动一份应用实例</strong>，一个改动配一个实例；</li>
<li>把 <strong>Chrome DevTools Protocol 接进 Agent 运行时</strong>，做了 DOM 快照、截图、导航的 skill——Codex 能自己复现 bug、验证修复、对 UI 行为推理；</li>
<li>给每个 worktree 配<strong>临时的可观测性栈</strong>，Codex 用 LogQL 查日志、PromQL 查指标。于是 <strong>"保证服务启动 800ms 内完成"、"这四条关键链路里没有 span 超过 2 秒"</strong> 这种 prompt 变得可执行。</li>
</ul>
<p>效果：<strong>单个 Codex run 经常连续干 6 小时以上</strong>（通常在人睡觉的时候）。</p>
<h3 id="5-强制不变量而非微观管理">5. 强制不变量，而非微观管理</h3>
<p>文档撑不住一个全自动生成的代码库的连贯性，<strong>架构约束</strong>才行。他们的口号是 <strong>"enforce invariants, not micromanage implementations"</strong>：</p>
<ul>
<li>要求"<strong>在边界处解析数据形状</strong>"，但不规定怎么做（模型自己爱用 Zod，没人指定）；</li>
<li>整个应用建在<strong>刚性分层架构</strong>上：每个业务域固定分层（Types → Config → Repo → Service → Runtime → UI），依赖方向严格校验，横切关注点（鉴权、连接器、遥测、特性开关）只能从单一的 <strong>Providers</strong> 接口进入；</li>
<li>全部用 <strong>自定义 linter（当然也是 Codex 写的）和结构化测试机械强制</strong>；自定义 lint 的报错信息<strong>直接把修复指引注入 Agent 上下文</strong>。</li>
</ul>
<p>一句点睛：<strong>这种架构本来是公司几百个工程师才需要的；有了 Agent，它成了一开始就得有的前提——约束正是"快而不失序"的前提。</strong> 原则是 <strong>"中心强制边界，局部允许自治"</strong>：严格守住边界、正确性、可复现；边界之内，给 Agent 充分自由。代码不总符合人类审美，<strong>只要正确、可维护、对未来的 Agent 可读，就达标。</strong></p>
<h3 id="6-吞吐量改变了合并哲学">6. 吞吐量改变了合并哲学</h3>
<p>当 Agent 吞吐远超人的注意力时，很多传统工程规范反而<strong>有害</strong>：仓库<strong>几乎没有阻塞性的合并门禁</strong>，PR 短命，测试 flake 用"重跑一次"解决而非无限期阻塞。因为在这种系统里，<strong>纠错很便宜，等待很贵</strong>。——这在低吞吐环境下是不负责任的，在这里却往往是对的。</p>
<p>往深一层，这背后是整套工程实践的经济学内核：<strong>token、算力、Agent 工时都不再稀缺，唯一真正稀缺的，是"需要人同步介入"的那点注意力。</strong> 于是几乎所有工程决策都收敛到同一个目标——<strong>把人的同步注意力省到极致</strong>。合并门禁的取舍、QA 瓶颈的破解、后台自动清理，本质上都是在省这一项。看懂了这条，前面那些反直觉的取舍就都顺理成章了。</p>
<h3 id="7-熵与垃圾回收把人的品味编码一次永久强制">7. 熵与垃圾回收：把人的品味"编码一次，永久强制"</h3>
<p>全自动也带来新问题：<strong>Codex 会复刻仓库里已有的模式，包括其中不好的</strong>，于是必然漂移。</p>
<p>他们一开始靠人手清——<strong>每周五（20% 的工时）专门清理 "AI slop"，根本不 scale。</strong> 真正的解法是把 <strong>golden principles（黄金法则）</strong> 编码进仓库，配一套<strong>周期性清理流程</strong>：</p>
<ul>
<li>优先用<strong>共享工具包</strong>，把不变量收敛在一处，而不是到处手写零散的 helper；</li>
<li><strong>不"YOLO-style"地探数据</strong>——而是在边界处校验、或用带类型的 SDK，避免 Agent 建在猜出来的数据形状上；</li>
<li>一组<strong>后台 Codex 任务</strong>定期扫描偏差、更新质量评分、提定向重构 PR——大多能<strong>一分钟内 review 完、自动合并</strong>。</li>
</ul>
<blockquote>
<p>这套机制像<strong>垃圾回收</strong>。技术债是高息贷款：<strong>持续小额偿还，永远好过让它复利滚大再痛苦集中还。</strong> 人的品味只需被捕捉一次，之后在每一行代码上被持续强制。</p>
</blockquote>
<h3 id="8-自治的临界点">8. 自治的临界点</h3>
<p>随着测试、验证、review、反馈处理、恢复都被编码进系统，这个仓库<strong>最近跨过了一道门槛</strong>：给一句话 prompt，Codex 能端到端地——校验当前状态 → 复现 bug → 录一段失败视频 → 实现修复 → 驱动应用验证 → 再录一段修复后的视频 → 开 PR → 回应人和 Agent 的反馈 → 检测并修复构建失败 → <strong>只在需要判断时才升级给人</strong> → 合并。</p>
<blockquote>
<p>但他们明确提醒：<strong>这高度依赖该仓库特定的结构与工具投入，不要假设能直接泛化——至少现在还不能。</strong></p>
</blockquote>
<p><strong>一句话收束这一节</strong>：人的纪律没有消失，只是<strong>从代码本身转移到了脚手架上</strong>。</p>
<hr>
<h2 id="六方案对比到底要做到哪一档">六、方案对比：到底要做到哪一档？</h2>
<p>按投入由轻到重，分三档：</p>
<ul>
<li><strong>A. 裸 prompt</strong> —— 只给一个 README，什么都不补。</li>
<li><strong>B. 单 Agent + 五子系统</strong> —— 补齐第三节那五项；一个 Agent，可跨会话。</li>
<li><strong>C. 多 Agent 全套</strong> —— B 之上再叠第四节的进阶方法（初始化/执行分离、生成/评估分离、上下文重置）。</li>
</ul>
<p>怎么选，看一个问题：<strong>这个任务，一个 Agent 在一个会话里能不能干完？</strong></p>
<ul>
<li>能，且做完即弃 → <strong>A</strong></li>
<li>要持续迭代、跨多会话 → <strong>B</strong>（绝大多数情况）</li>
<li>一个 Agent 独自搞不定（几小时、跨几十个窗口、质量要求高）→ <strong>C</strong></li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th align="center">A</th>
<th align="center">B</th>
<th align="center">C</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>▲ 得到</strong></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>任务成功率</td>
<td align="center">低</td>
<td align="center">高</td>
<td align="center">很高</td>
</tr>
<tr>
<td>跨会话连续性</td>
<td align="center">无</td>
<td align="center">强</td>
<td align="center">强</td>
</tr>
<tr>
<td>质量可控性</td>
<td align="center">低</td>
<td align="center">高</td>
<td align="center">很高</td>
</tr>
<tr>
<td><strong>▼ 付出</strong></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>搭建成本</td>
<td align="center">几乎为零</td>
<td align="center">中</td>
<td align="center">高</td>
</tr>
<tr>
<td>单位任务花费</td>
<td align="center">低</td>
<td align="center">中</td>
<td align="center">高</td>
</tr>
<tr>
<td>维护负担</td>
<td align="center">低</td>
<td align="center">中</td>
<td align="center">中高</td>
</tr>
<tr>
<td><strong>→ 选它的场景</strong></td>
<td align="center">一次性探索</td>
<td align="center"><strong>默认</strong>：要持续迭代的活</td>
<td align="center">超出单模型能力的硬任务</td>
</tr>
</tbody>
</table>
<p><strong>结论：</strong></p>
<ul>
<li><strong>绝大多数团队的最优解是 B</strong>——投入产出比最高，是性价比最佳点。<strong>不要一上来就叠加多 Agent。</strong></li>
<li><strong>C 只在任务"超出单个模型独自能力"时才划算</strong>：几小时以上、跨多窗口、质量要求高（如完整应用、复杂重构）。它总评不比 B 高，是因为简单任务里那套开销纯属浪费——<strong>复杂度要匹配任务难度</strong>。</li>
<li><strong>A 只适合一次性、用完即弃的探索。</strong> 任何要持续迭代的东西，停在 A 都会反复付出返工代价。</li>
</ul>
<p>一个反复被验证的纪律：<strong>模型升级后，回头把 harness 逐件做消融测试（ablation）。</strong> Anthropic 在 Opus 4.6 上就发现，模型变强后原来的"sprint 契约"机制可以整个删掉，质量不降、token 大降。<strong>Harness 不是只增不减——模型补上的能力，harness 就该减负。</strong></p>
<hr>
<h2 id="七照抄即用harness-自查清单">七、照抄即用：Harness 自查清单</h2>
<p>落地时，对着这张表勾一遍：</p>
<ul>
<li><strong>Instructions</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 有一个 ≤100 行的 <code class="language-text">AGENTS.md</code> / <code class="language-text">CLAUDE.md</code>，写清项目、技术栈、首次运行命令、红线约束</li>
<li class="task-list-item"><input type="checkbox" disabled> 细节走链接，不堆成百科全书</li>
<li class="task-list-item"><input type="checkbox" disabled> Agent 需要的一切都在仓库里（repo = 唯一事实来源）</li>
</ul>
</li>
<li><strong>Tools / Environment</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 依赖、运行时版本、容器配置齐全，可一键起环境（<code class="language-text">init.sh</code>）</li>
<li class="task-list-item"><input type="checkbox" disabled> 给了能跑 bash / 执行代码的沙箱</li>
<li class="task-list-item"><input type="checkbox" disabled> 权限按最小权限给，而不是一刀切禁</li>
</ul>
</li>
<li><strong>State</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 有进度文件，记录"做完/在做/卡住"</li>
<li class="task-list-item"><input type="checkbox" disabled> 会话开始读进度+git log，结束前更新</li>
<li class="task-list-item"><input type="checkbox" disabled> 用 git 提交留痕</li>
</ul>
</li>
<li><strong>Scope</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 任务拆成机器可读的 feature list（建议 JSON）</li>
<li class="task-list-item"><input type="checkbox" disabled> 每会话只做一个 feature</li>
</ul>
</li>
<li><strong>Verification</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 文档里显式列出 test / lint / typecheck 命令</li>
<li class="task-list-item"><input type="checkbox" disabled> Web 应用有端到端验证（浏览器自动化）</li>
<li class="task-list-item"><input type="checkbox" disabled> 主观质量已拆成可打分的客观维度</li>
</ul>
</li>
<li><strong>长任务额外项</strong>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 初始化与执行 Agent 分离</li>
<li class="task-list-item"><input type="checkbox" disabled> 生成与评估分离（任务够难才上）</li>
<li class="task-list-item"><input type="checkbox" disabled> 用上下文重置 + 交接产物，而非一味压缩</li>
</ul>
</li>
</ul>
<hr>
<h2 id="八为什么这件事会长期重要">八、为什么这件事会长期重要</h2>
<p>有人会问：模型不是越来越强吗，等它足够聪明，harness 不就没用了？</p>
<p>恰恰相反。LangChain 和 OpenAI 的判断一致：<strong>模型越强，harness 的重心只是从"打补丁填模型的坑"转向"为智能优化系统"——但它不会消失。</strong> 就像今天模型早已不傻，prompt engineering 依然重要一样。</p>
<p>还有一层更硬的耦合常被忽略：<strong>今天的强模型，是和 harness 一起被训练出来的。</strong> LangChain 指出，Claude Code 这类产品在后训练阶段就把"模型 + harness"放进同一个回路联调，模型被专门优化以适配它训练时的那套外壳。这也解释了一个反直觉现象——<strong>把一个强模型塞进一个陌生的 harness，往往跑不出它的真实水平。</strong> 模型与 harness 是共同进化的关系，不是谁迟早取代谁。</p>
<p>OpenAI 那句话点破了行业的转向：</p>
<blockquote>
<p>在 agent-first 的世界里，工程师的<strong>主要工作不再是写代码，而是设计让 Agent 可靠产出的环境。</strong></p>
</blockquote>
<p>模型是发动机，harness 是底盘、传动和仪表盘。<strong>光有发动机，跑不远，也不安全。</strong></p>
<hr>
<h2 id="附参考文档">附：参考文档</h2>
<ul>
<li>LangChain — <a href="https://www.langchain.com/blog/the-anatomy-of-an-agent-harness">The Anatomy of an Agent Harness</a>：<code class="language-text">Agent = Model + Harness</code> 的组件全景。</li>
<li>Anthropic — <a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents">Effective harnesses for long-running agents</a>：初始化 Agent、feature list、自验证的实战。</li>
<li>Anthropic — <a href="https://www.anthropic.com/engineering/harness-design-long-running-apps">Harness design for long-running apps</a>：生成/评估分离、上下文重置、可打分质量。</li>
<li>OpenAI — <a href="https://openai.com/index/harness-engineering/">Harness Engineering: Leveraging Codex in an Agent-First World</a>：100 万行代码与黄金法则。</li>
<li>WalkingLabs — <a href="https://github.com/walkinglabs/learn-harness-engineering">learn-harness-engineering</a>（<a href="https://walkinglabs.github.io/learn-harness-engineering/">在线讲义</a>）：12 讲 + 6 个动手项目的系统课程（五子系统框架的来源）。</li>
<li>WalkingLabs — <a href="https://github.com/walkinglabs/awesome-harness-engineering">awesome-harness-engineering</a>：资源合集（上下文、评估、护栏、编排、Benchmark）。</li>
</ul>]]></content>
  </entry>
  <entry>
    <title>一文读懂 MCP（Model Context Protocol）</title>
    <id>https://shipengtao.com/zh/mcp-guide/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/mcp-guide/"/>
    <published>2026-05-28T00:00:00+08:00</published>
    <updated>2026-05-28T00:00:00+08:00</updated>
    <summary type="text">面向工程师与技术决策者的入门指南。从概念到实战，配套 Python SDK 示例。 一、为什么会有 MCP？ 在 MCP 出现之前，把一个 AI 应用接入外部系统大致是这样一种状态： 想让模型查数据库？写一套 function calling 的 schema、写 handler…</summary>
    <content type="html"><![CDATA[<blockquote>
<p>面向工程师与技术决策者的入门指南。从概念到实战，配套 Python SDK 示例。</p>
</blockquote>
<hr>
<h2 id="一为什么会有-mcp">一、为什么会有 MCP？</h2>
<p>在 MCP 出现之前，把一个 AI 应用接入外部系统大致是这样一种状态：</p>
<ul>
<li>想让模型查数据库？写一套 function calling 的 schema、写 handler、写权限校验。</li>
<li>想让模型读文件？再写一遍。</li>
<li>同一个"查 GitHub Issue"的能力，Claude 桌面端、Cursor、VS Code、自研 Agent，各写一遍。</li>
<li>工具多了之后，prompt 维护成本急剧上升，跨应用复用几乎为零。</li>
</ul>
<p>这就是典型的 <strong>M × N 集成问题</strong>：M 个 AI 应用 × N 个数据源/工具 = M × N 套对接代码。</p>
<p><strong>MCP（Model Context Protocol，模型上下文协议）</strong> 是 Anthropic 牵头、目前已成为事实标准的开放协议，目标是把这个 M × N 收敛为 <strong>M + N</strong>：</p>
<ul>
<li>数据源/工具方只需要实现一次 MCP <strong>服务端</strong>；</li>
<li>AI 应用方只需要实现一次 MCP <strong>客户端</strong>；</li>
<li>任意客户端都能即插即用地连接任意服务端。</li>
</ul>
<p>官方一句话比喻：<strong>MCP 之于 AI 应用，相当于 USB-C 之于电子设备</strong>——一个统一的接口，连接万物。</p>
<hr>
<h2 id="二一图总览">二、一图总览</h2>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">                          ┌─ Tools       (模型调用，有副作用)
              Server ─────┼─ Resources   (应用读取，只读上下文)
             /            └─ Prompts     (用户触发，工作流模板)
            /
        Client
       /
  Host
       \                  ┌─ Sampling    (Server 反向请求 LLM)
        Client ───────────┼─ Elicitation (Server 反向问用户)
             \            ├─ Roots       (Client 告知工作目录)
              \           └─ Logging     (Server 输出日志)
               Server

         数据层：JSON-RPC 2.0
         传输层：stdio（本地） / Streamable HTTP（远程，支持 OAuth）</code></pre></div>
<ul>
<li><strong>传输层</strong>：Client 与 Server 之间用 JSON-RPC 2.0 通信，承载在 stdio 或 Streamable HTTP 之上。</li>
<li><strong>Server 原语</strong>：Server 主动暴露给 Host 使用的能力，分别面向<strong>模型</strong>（Tools）、<strong>应用</strong>（Resources）、<strong>用户</strong>（Prompts）三类不同的"决策者"。</li>
<li><strong>Client 原语</strong>：Client 暴露给 Server 反向调用的能力，让 Server 在不直接依赖 LLM/UI 的前提下也能完成 agentic 工作流。</li>
</ul>
<hr>
<h2 id="三三个最容易混淆的角色host--client--server">三、三个最容易混淆的角色：Host / Client / Server</h2>
<p>这是入门 MCP 时最容易绕晕的地方。它们三者的关系是这样的：</p>
<p><svg id="mermaid-0" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 946.3359375px;" viewBox="0 0 946.3359375 296" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#mermaid-0{font-family:arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-0 .error-icon{fill:#552222;}#mermaid-0 .error-text{fill:#552222;stroke:#552222;}#mermaid-0 .edge-thickness-normal{stroke-width:1px;}#mermaid-0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-0 .marker{fill:#333333;stroke:#333333;}#mermaid-0 .marker.cross{stroke:#333333;}#mermaid-0 svg{font-family:arial,sans-serif;font-size:16px;}#mermaid-0 p{margin:0;}#mermaid-0 .label{font-family:arial,sans-serif;color:#333;}#mermaid-0 .cluster-label text{fill:#333;}#mermaid-0 .cluster-label span{color:#333;}#mermaid-0 .cluster-label span p{background-color:transparent;}#mermaid-0 .label text,#mermaid-0 span{fill:#333;color:#333;}#mermaid-0 .node rect,#mermaid-0 .node circle,#mermaid-0 .node ellipse,#mermaid-0 .node polygon,#mermaid-0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-0 .rough-node .label text,#mermaid-0 .node .label text,#mermaid-0 .image-shape .label,#mermaid-0 .icon-shape .label{text-anchor:middle;}#mermaid-0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-0 .rough-node .label,#mermaid-0 .node .label,#mermaid-0 .image-shape .label,#mermaid-0 .icon-shape .label{text-align:center;}#mermaid-0 .node.clickable{cursor:pointer;}#mermaid-0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-0 .arrowheadPath{fill:#333333;}#mermaid-0 .edgePath .path{stroke:#333333;stroke-width:1px;}#mermaid-0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-0 .cluster text{fill:#333;}#mermaid-0 .cluster span{color:#333;}#mermaid-0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-0 rect.text{fill:none;stroke-width:0;}#mermaid-0 .icon-shape,#mermaid-0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-0 .icon-shape p,#mermaid-0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-0 .icon-shape .label rect,#mermaid-0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-0 .node .neo-node{stroke:#9370DB;}#mermaid-0 [data-look="neo"].node rect,#mermaid-0 [data-look="neo"].cluster rect,#mermaid-0 [data-look="neo"].node polygon{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-0 [data-look="neo"].node path{stroke:#9370DB;stroke-width:1px;}#mermaid-0 [data-look="neo"].node .outer-path{filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-0 [data-look="neo"].node .neo-line path{stroke:#9370DB;filter:none;}#mermaid-0 [data-look="neo"].node circle{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-0 [data-look="neo"].node circle .state-start{fill:#000000;}#mermaid-0 [data-look="neo"].icon-shape .icon{fill:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-0 [data-look="neo"].icon-shape .icon-neo path{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-0 :root{--mermaid-font-family:arial,sans-serif;}</style><g><marker id="mermaid-0_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="mermaid-0_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-0_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-0_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"></path></marker><marker id="mermaid-0_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g class="root"><g class="clusters"><g class="cluster" id="mermaid-0-subGraph0" data-look="classic"><rect style="" x="8" y="8" width="930.3359375" height="104"></rect><g class="cluster-label" transform="translate(380.83203125, 8)"><foreignObject width="184.671875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>MCP Host (AI Application)</p></span></div></foreignObject></g></g></g><g class="edgePaths"><path d="M119.984,87L119.984,91.167C119.984,95.333,119.984,103.667,119.984,116C119.984,128.333,119.984,144.667,119.984,161C119.984,177.333,119.984,193.667,119.984,201.833L119.984,210" id="mermaid-0-L_Client1_ServerA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Client1_ServerA_0" data-points="W3sieCI6MTE5Ljk4NDM3NSwieSI6ODd9LHsieCI6MTE5Ljk4NDM3NSwieSI6MTEyfSx7IngiOjExOS45ODQzNzUsInkiOjE2MX0seyJ4IjoxMTkuOTg0Mzc1LCJ5IjoyMTB9XQ==" data-look="classic"></path><path d="M383.523,87L383.523,91.167C383.523,95.333,383.523,103.667,383.523,116C383.523,128.333,383.523,144.667,383.523,161C383.523,177.333,383.523,193.667,383.523,201.833L383.523,210" id="mermaid-0-L_Client2_ServerB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Client2_ServerB_0" data-points="W3sieCI6MzgzLjUyMzQzNzUsInkiOjg3fSx7IngiOjM4My41MjM0Mzc1LCJ5IjoxMTJ9LHsieCI6MzgzLjUyMzQzNzUsInkiOjE2MX0seyJ4IjozODMuNTIzNDM3NSwieSI6MjEwfV0=" data-look="classic"></path><path d="M622.383,87L622.383,91.167C622.383,95.333,622.383,103.667,622.383,116C622.383,128.333,622.383,144.667,631.847,161C641.312,177.333,660.241,193.667,669.705,201.833L679.17,210" id="mermaid-0-L_Client3_ServerC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Client3_ServerC_0" data-points="W3sieCI6NjIyLjM4MjgxMjUsInkiOjg3fSx7IngiOjYyMi4zODI4MTI1LCJ5IjoxMTJ9LHsieCI6NjIyLjM4MjgxMjUsInkiOjE2MX0seyJ4Ijo2NzkuMTY5NTY2NzYxMzYzNiwieSI6MjEwfV0=" data-look="classic"></path><path d="M826.352,87L826.352,91.167C826.352,95.333,826.352,103.667,826.352,116C826.352,128.333,826.352,144.667,816.887,161C807.423,177.333,788.494,193.667,779.029,201.833L769.565,210" id="mermaid-0-L_Client4_ServerC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Client4_ServerC_0" data-points="W3sieCI6ODI2LjM1MTU2MjUsInkiOjg3fSx7IngiOjgyNi4zNTE1NjI1LCJ5IjoxMTJ9LHsieCI6ODI2LjM1MTU2MjUsInkiOjE2MX0seyJ4Ijo3NjkuNTY0ODA4MjM4NjM2NCwieSI6MjEwfV0=" data-look="classic"></path></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(119.984375, 161)"><g class="label" data-id="L_Client1_ServerA_0" transform="translate(-38.6953125, -24)"><foreignObject width="77.390625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Dedicated<br></br>connection</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(383.5234375, 161)"><g class="label" data-id="L_Client2_ServerB_0" transform="translate(-38.6953125, -24)"><foreignObject width="77.390625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Dedicated<br></br>connection</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(622.3828125, 161)"><g class="label" data-id="L_Client3_ServerC_0" transform="translate(-38.6953125, -24)"><foreignObject width="77.390625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Dedicated<br></br>connection</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(826.3515625, 161)"><g class="label" data-id="L_Client4_ServerC_0" transform="translate(-38.6953125, -24)"><foreignObject width="77.390625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Dedicated<br></br>connection</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="mermaid-0-flowchart-Client1-0" data-look="classic" transform="translate(119.984375, 60)"><rect class="basic label-container" style="" x="-76.984375" y="-27" width="153.96875" height="54"></rect><g class="label" style="" transform="translate(-46.984375, -12)"><rect></rect><foreignObject width="93.96875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Client 1</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-Client2-1" data-look="classic" transform="translate(383.5234375, 60)"><rect class="basic label-container" style="" x="-76.984375" y="-27" width="153.96875" height="54"></rect><g class="label" style="" transform="translate(-46.984375, -12)"><rect></rect><foreignObject width="93.96875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Client 2</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-Client3-2" data-look="classic" transform="translate(622.3828125, 60)"><rect class="basic label-container" style="" x="-76.984375" y="-27" width="153.96875" height="54"></rect><g class="label" style="" transform="translate(-46.984375, -12)"><rect></rect><foreignObject width="93.96875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Client 3</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-Client4-3" data-look="classic" transform="translate(826.3515625, 60)"><rect class="basic label-container" style="" x="-76.984375" y="-27" width="153.96875" height="54"></rect><g class="label" style="" transform="translate(-46.984375, -12)"><rect></rect><foreignObject width="93.96875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Client 4</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-ServerA-4" data-look="classic" transform="translate(119.984375, 249)"><rect class="basic label-container" style="" x="-106.328125" y="-39" width="212.65625" height="78"></rect><g class="label" style="" transform="translate(-76.328125, -24)"><rect></rect><foreignObject width="152.65625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Server A - Local<br></br>(e.g. Filesystem)</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-ServerB-5" data-look="classic" transform="translate(383.5234375, 249)"><rect class="basic label-container" style="" x="-107.2109375" y="-39" width="214.421875" height="78"></rect><g class="label" style="" transform="translate(-77.2109375, -24)"><rect></rect><foreignObject width="154.421875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Server B - Local<br></br>(e.g. Database)</p></span></div></foreignObject></g></g><g class="node default" id="mermaid-0-flowchart-ServerC-6" data-look="classic" transform="translate(724.3671875, 249)"><rect class="basic label-container" style="" x="-116.5390625" y="-39" width="233.078125" height="78"></rect><g class="label" style="" transform="translate(-86.5390625, -24)"><rect></rect><foreignObject width="173.078125" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>MCP Server C - Remote<br></br>(e.g. Sentry)</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="mermaid-0-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></feDropShadow></filter></defs><defs><filter id="mermaid-0-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></feDropShadow></filter></defs></svg></p>
<p>注意右下角 Server C 同时连接了两个 Client——<strong>远程 Server 通常服务多个 Client</strong>，而本地 stdio Server 一般是 1:1。</p>
<table>
<thead>
<tr>
<th>角色</th>
<th>角色定位</th>
<th>举例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Host（宿主）</strong></td>
<td>用户直接交互的 AI 应用，负责协调多个 Client</td>
<td>Claude Desktop、Cursor、VS Code、ChatGPT、你自研的 Agent</td>
</tr>
<tr>
<td><strong>Client（客户端）</strong></td>
<td>Host 内部的一个组件，每个 Client 与一个 Server 维持 1:1 的专用连接</td>
<td>Host 启动时为每个配置的 Server 各创建一个 Client</td>
</tr>
<tr>
<td><strong>Server（服务端）</strong></td>
<td>提供工具/数据/模板的程序，本地或远程均可</td>
<td>filesystem、postgres、github、sentry、Slack 等</td>
</tr>
</tbody>
</table>
<blockquote>
<p>一个常见误解：以为 "Server" 一定是远程服务。错。<strong>Server 指的是协议角色，不是部署形态。</strong> 通过 stdio 启动的本地子进程同样是 Server。</p>
</blockquote>
<hr>
<h2 id="四协议的两层数据层--传输层">四、协议的两层：数据层 + 传输层</h2>
<p>MCP 的协议设计干净地分成了两层，便于在不同场景复用。</p>
<h3 id="1-数据层data-layer">1. 数据层（Data Layer）</h3>
<p>基于 <strong>JSON-RPC 2.0</strong>，定义消息的结构与语义。包含：</p>
<ul>
<li><strong>生命周期管理</strong>：连接建立、能力协商、连接关闭</li>
<li><strong>服务端能力</strong>：tools、resources、prompts</li>
<li><strong>客户端能力</strong>：sampling、elicitation、logging、roots</li>
<li><strong>通知机制</strong>：列表变更、进度更新等</li>
</ul>
<h3 id="2-传输层transport-layer">2. 传输层（Transport Layer）</h3>
<p>负责把数据层的 JSON 消息从一端送到另一端。MCP 目前定义了两种官方传输：</p>
<table>
<thead>
<tr>
<th>传输方式</th>
<th>适用场景</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>stdio</strong></td>
<td>本地 Server</td>
<td>Host 把 Server 作为子进程启动，通过标准输入/输出通信。零网络开销，最适合本地工具（文件、Git、本地 DB）。</td>
</tr>
<tr>
<td><strong>Streamable HTTP</strong></td>
<td>远程 Server</td>
<td>客户端用 HTTP POST 发请求，服务端可选用 Server-Sent Events (SSE) 推送流式响应。支持标准 HTTP 鉴权（Bearer Token、OAuth），适合 SaaS 化的 MCP Server。</td>
</tr>
</tbody>
</table>
<blockquote>
<p>旧版协议中还有一个独立的 SSE 传输，<strong>新规范已合并到 Streamable HTTP</strong>，新写代码请直接选 Streamable HTTP。</p>
</blockquote>
<hr>
<h2 id="五服务端三大原语tools--resources--prompts">五、服务端三大原语：Tools / Resources / Prompts</h2>
<p>这是日常开发中接触最频繁的部分。三者非常容易混淆，但它们的设计意图完全不同——<strong>关键差异在于"谁来决定调用"</strong>。</p>
<table>
<thead>
<tr>
<th>原语</th>
<th>控制权</th>
<th>类比</th>
<th>典型用途</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Tools（工具）</strong></td>
<td><strong>模型</strong>控制</td>
<td>类似 REST 的 <code class="language-text">POST</code></td>
<td>模型自主决定调用，会产生副作用：写数据库、发消息、调 API</td>
</tr>
<tr>
<td><strong>Resources（资源）</strong></td>
<td><strong>应用</strong>控制</td>
<td>类似 REST 的 <code class="language-text">GET</code></td>
<td>被动数据源，只读。由宿主应用选择什么时机把哪些上下文塞给模型</td>
</tr>
<tr>
<td><strong>Prompts（提示）</strong></td>
<td><strong>用户</strong>控制</td>
<td>类似 Slash Command</td>
<td>预制的工作流模板，用户主动选择调用（如 <code class="language-text">/plan-vacation</code>）</td>
</tr>
</tbody>
</table>
<p>我喜欢这样记忆：</p>
<blockquote>
<p><strong>Tools 是动词，Resources 是名词，Prompts 是工作流。</strong></p>
</blockquote>
<h3 id="tools模型主动调用">Tools：模型主动调用</h3>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token comment"># 模型看到用户说"查下旧金山天气"，自己决定调用这个工具</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">get_weather</span><span class="token punctuation">(</span>city<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""获取某个城市的当前天气"""</span>
    <span class="token keyword">return</span> fetch_weather_api<span class="token punctuation">(</span>city<span class="token punctuation">)</span></code></pre></div>
<p>相关方法：<code class="language-text">tools/list</code>（发现）、<code class="language-text">tools/call</code>（执行）。</p>
<h3 id="resources应用按需读取">Resources：应用按需读取</h3>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token comment"># 应用决定是否要把这段 README 塞给模型作为上下文</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>resource</span><span class="token punctuation">(</span><span class="token string">"file:///{path}"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">read_file</span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token keyword">return</span> <span class="token builtin">open</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span><span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre></div>
<p>每个资源有一个 URI（如 <code class="language-text">file:///README.md</code>、<code class="language-text">postgres://mydb/schema</code>），支持模板化（<code class="language-text">weather://forecast/{city}</code>）。相关方法：<code class="language-text">resources/list</code>、<code class="language-text">resources/templates/list</code>、<code class="language-text">resources/read</code>、<code class="language-text">resources/subscribe</code>。</p>
<h3 id="tools-vs-resources完整链路与选型">Tools vs Resources：完整链路与选型</h3>
<p>光看"控制权"不够直观，对比一下两者的完整数据流：</p>
<p><strong>Tools 的链路</strong>（模型回合内触发）：</p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">1. Host 启动 → 调 tools/list 拿 schema
2. Host 把 schema 注入 LLM 调用的 tools 字段
3. 用户发消息 → LLM 推理 → 输出 tool_use 块
4. Host 截获 → 路由到对应 Server → 发 tools/call
5. Server 执行（跑 SQL、调 API）→ 返回 content
6. Host 把 content 作为 tool_result 拼回对话
7. LLM 继续生成（可能再触发下一轮，循环 3-6）</code></pre></div>
<p><strong>Resources 的链路</strong>（对话回合之外注入）：</p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">1. Host 启动 → 调 resources/list 和 resources/templates/list
2. Host 把资源在 UI 上呈现（文件树 / @ 补全 / 自动建议）
3. 资源进入对话上下文的三种触发方式：
   a. 用户手动 @ 引用
   b. 应用启发式自动塞入
   c. 桥接为 Tool 让模型自主调用（下面详述）
4. Host 调 resources/read(uri) → Server 返回 contents
5. Host 把 contents 作为 user 消息的一部分发给 LLM
6. LLM 收到时是"已经在上下文里"的事实</code></pre></div>
<p><strong>一句话区分</strong>：</p>
<blockquote>
<p><strong>Tools 在 LLM 回合内被模型主动拉取；Resources 在 LLM 回合外被应用主动推入。</strong></p>
</blockquote>
<p><strong>"数据但需要计算"选哪个？</strong> 判断顺序：</p>
<ol>
<li><strong>有副作用？</strong> 有 → 必须 Tool（哪怕只是埋点）。</li>
<li><strong>要让模型自主决定调用？</strong> → Tool。</li>
<li><strong>要让用户/应用预先选好作为上下文塞入？</strong> → Resource Template。</li>
<li><strong>客户端生态考量</strong>：现状是 Tools 在所有 Host 上都支持，Resources 的 UI 支持差异较大。<strong>很多 Server 会把同一能力同时暴露为 Tool 和 Resource</strong>——Tool 兜底兼容，Resource 给支持的 Host 用更优雅的呈现。</li>
</ol>
<p><strong>Resources 也能由 LLM 驱动吗？</strong> 协议层并未禁止——"应用控制"是设计意图而非技术约束。常见的做法是 <strong>Resource-as-Tool 桥接</strong>：Host 在 LLM 的工具列表里额外注入两个合成工具：</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"list_resources"</span><span class="token punctuation">,</span> <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"列出所有可用 MCP 资源"</span><span class="token punctuation">,</span> ... <span class="token punctuation">}</span>
<span class="token punctuation">{</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"read_resource"</span><span class="token punctuation">,</span>  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"按 URI 读取资源"</span><span class="token punctuation">,</span>        ... <span class="token punctuation">}</span></code></pre></div>
<p>LLM 决定调用 <code class="language-text">read_resource(uri="postgres://schema/users")</code> → Host 转发为 <code class="language-text">resources/read</code>。<strong>形式是 Tool，本质是 LLM 在驱动 Resource</strong>。Claude Code 默认就这么做。</p>
<p><strong>"应用启发式塞入"具体是什么？</strong> 几个真实场景：</p>
<ul>
<li><strong>IDE 类 Host 的上下文注入</strong>：每次发消息时自动塞入当前光标所在文件、最近编辑过的几个文件、<code class="language-text">.cursorrules</code> / <code class="language-text">CLAUDE.md</code> 等约定配置。</li>
<li><strong>项目初始化自动加载</strong>：打开项目时自动读 <code class="language-text">README.md</code>、<code class="language-text">package.json</code>、<code class="language-text">.env.example</code> 作为 system context。</li>
<li><strong>语义检索自动召回</strong>：对 user message 做 embedding，对所有 resources 做向量检索，Top-K 命中自动拼入。</li>
<li><strong>订阅式实时同步</strong>：用户改了文件 → Server 推 <code class="language-text">notifications/resources/updated</code> → Host 把新内容刷进下一轮上下文。</li>
</ul>
<p>这些都不需要用户和模型显式发起，由 Host 用一套规则决定。</p>
<h3 id="prompts用户显式触发">Prompts：用户显式触发</h3>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>prompt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">plan_vacation</span><span class="token punctuation">(</span>destination<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> days<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""生成一次度假规划"""</span>
    <span class="token keyword">return</span> <span class="token string-interpolation"><span class="token string">f"请帮我规划一次为期 </span><span class="token interpolation"><span class="token punctuation">{</span>days<span class="token punctuation">}</span></span><span class="token string"> 天的 </span><span class="token interpolation"><span class="token punctuation">{</span>destination<span class="token punctuation">}</span></span><span class="token string"> 之旅..."</span></span></code></pre></div>
<p>在 Claude Desktop、Claude Code、Cursor 这类应用中，Prompt 通常表现为 <code class="language-text">/</code> 命令或命令面板里的快捷入口。</p>
<p>关于 Prompts 有三个常被混淆的点需要澄清：</p>
<p><strong>1. "只能由用户触发"是 UX 约定，不是协议铁律。</strong> 协议层没限制谁调 <code class="language-text">prompts/get</code>，任何持有 session 的一方都能触发。"user-controlled" 是规范给 Host 的 UX 建议，意在让用户感知到自己在主动发起，避免模型偷偷调用模板。自研 Agent 完全可以在某个 workflow 节点上自动 <code class="language-text">prompts/get</code>，技术上没人会拦。</p>
<p><strong>2. 返回值不只是字符串。</strong> <code class="language-text">prompts/get</code> 真正返回的是一个 <code class="language-text">messages</code> 数组：</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"messages"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
    <span class="token punctuation">{</span> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span>      <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"text"</span><span class="token punctuation">,</span>  <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"..."</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">{</span> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token string">"assistant"</span><span class="token punctuation">,</span> <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"text"</span><span class="token punctuation">,</span>  <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"..."</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">{</span> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span>      <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"image"</span><span class="token punctuation">,</span> <span class="token property">"data"</span><span class="token operator">:</span> <span class="token string">"base64..."</span><span class="token punctuation">,</span> <span class="token property">"mimeType"</span><span class="token operator">:</span> <span class="token string">"image/png"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">{</span> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span>      <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"resource"</span><span class="token punctuation">,</span> <span class="token property">"resource"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"uri"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span> <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"..."</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span>
  <span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre></div>
<p>可以构造<strong>多轮 few-shot 示例</strong>、<strong>包含图片/音频的多模态 prompt</strong>、<strong>嵌入资源引用</strong>。FastMCP 里 <code class="language-text">@mcp.prompt()</code> 返回字符串只是便利封装——它会被自动包装成单条 user 消息。要更复杂的结构，显式返回 <code class="language-text">list[Message]</code>：</p>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp<span class="token punctuation">.</span>prompts <span class="token keyword">import</span> base

<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>prompt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">debug_session</span><span class="token punctuation">(</span>error<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">list</span><span class="token punctuation">[</span>base<span class="token punctuation">.</span>Message<span class="token punctuation">]</span><span class="token punctuation">:</span>
    <span class="token keyword">return</span> <span class="token punctuation">[</span>
        base<span class="token punctuation">.</span>UserMessage<span class="token punctuation">(</span><span class="token string">"我遇到了这个错误："</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        base<span class="token punctuation">.</span>UserMessage<span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">,</span>
        base<span class="token punctuation">.</span>AssistantMessage<span class="token punctuation">(</span><span class="token string">"我来帮你分析。先确认几个问题..."</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">]</span></code></pre></div>
<p><strong>3. Prompts ≠ 简化版 Skills。</strong> 这两者经常被混为一谈，但解决的是完全不同维度的问题：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th><strong>MCP Prompts</strong></th>
<th><strong>Claude Skills</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>核心问题</td>
<td>让用户把<strong>已知任务</strong>用模板化方式发起</td>
<td>让模型在合适的时候<strong>自主获得一项能力</strong></td>
</tr>
<tr>
<td>触发者</td>
<td>用户（UI 主动选）</td>
<td>模型（基于 <code class="language-text">SKILL.md</code> frontmatter 自主判断）</td>
</tr>
<tr>
<td>形态</td>
<td>协议消息，通过 JSON-RPC 取</td>
<td>文件系统上的 markdown bundle（含可选脚本）</td>
</tr>
<tr>
<td>内容</td>
<td>返回 messages 数组喂给 LLM</td>
<td>markdown 指令 + 可执行代码 + 资源文件</td>
</tr>
<tr>
<td>执行</td>
<td>纯模板替换，不执行代码</td>
<td>Agent 可执行其中的脚本</td>
</tr>
<tr>
<td>控制平面</td>
<td>服务端原语</td>
<td>模型侧能力</td>
</tr>
</tbody>
</table>
<p>更准确的类比：</p>
<blockquote>
<p><strong>Prompts 像 IDE 的 Code Snippet 或 Notion 的 Template</strong>：用户选 → 自动填模板。
<strong>Skills 像 Unix 的 man page + 可执行脚本</strong>：模型自己读说明书 → 决定要不要用 → 可能还执行其中代码。</p>
</blockquote>
<p>一句话：<strong>Prompts 是"用户能用的模板"，Skills 是"模型能学的技能"。</strong></p>
<hr>
<h2 id="六客户端原语让-server-反过来调用client">六、客户端原语：让 Server 反过来"调用"Client</h2>
<p>服务端不只是被动响应，它也可以主动向客户端请求某些能力。客户端原语有四个：</p>
<table>
<thead>
<tr>
<th>客户端原语</th>
<th>用途</th>
<th>经典场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Sampling</strong></td>
<td>Server 请求 Host 的 LLM 帮自己做一次推理</td>
<td>Server 想分析 47 个航班选项但不想内置 LLM SDK，于是让 Client 代为完成</td>
</tr>
<tr>
<td><strong>Elicitation</strong></td>
<td>Server 请求用户提供信息或确认操作</td>
<td>"请确认这笔 $3000 的订单"，配 JSON Schema 描述需要哪些字段</td>
</tr>
<tr>
<td><strong>Roots</strong></td>
<td>Client 告诉 Server "你只能在这些目录下工作"</td>
<td>IDE 把当前打开的项目目录暴露给 Server</td>
</tr>
<tr>
<td><strong>Logging</strong></td>
<td>Server 向 Client 发送日志，便于调试与可观测</td>
<td>tool 执行中打印 debug 信息</td>
</tr>
</tbody>
</table>
<p><strong>Sampling 的精妙之处</strong>：Server 想用 LLM 但不想自己花钱、不想绑定模型供应商，于是把"调用 LLM"的工作反向托管给 Client。Client 已经有 LLM 访问能力（用户已经付费/授权了），所以让 Client 出钱出力，并且用户可在中途审核 prompt 和返回内容——这天然就是 human-in-the-loop。</p>
<hr>
<h2 id="七生命周期一次完整的握手">七、生命周期：一次完整的握手</h2>
<p>理解了角色和原语，我们用 JSON-RPC 看一次最小完整流程，便于将来抓包调试时不抓瞎。</p>
<h3 id="step-1-初始化握手">Step 1. 初始化握手</h3>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Client → Server</span>
<span class="token punctuation">{</span>
  <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token property">"method"</span><span class="token operator">:</span> <span class="token string">"initialize"</span><span class="token punctuation">,</span>
  <span class="token property">"params"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"protocolVersion"</span><span class="token operator">:</span> <span class="token string">"2025-06-18"</span><span class="token punctuation">,</span>
    <span class="token property">"capabilities"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"elicitation"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"clientInfo"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"example-client"</span><span class="token punctuation">,</span> <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Server → Client</span>
<span class="token punctuation">{</span>
  <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span>
  <span class="token property">"result"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"protocolVersion"</span><span class="token operator">:</span> <span class="token string">"2025-06-18"</span><span class="token punctuation">,</span>
    <span class="token property">"capabilities"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"tools"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"listChanged"</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
      <span class="token property">"resources"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"serverInfo"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"example-server"</span><span class="token punctuation">,</span> <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Client → Server（通知，无 id 无返回值）</span>
<span class="token punctuation">{</span> <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"method"</span><span class="token operator">:</span> <span class="token string">"notifications/initialized"</span> <span class="token punctuation">}</span></code></pre></div>
<p>这里有几个关键点：</p>
<ol>
<li><strong>协议版本协商</strong>：双方必须使用兼容的版本，否则连接终止。</li>
<li><strong>能力声明</strong>：Server 在 <code class="language-text">capabilities</code> 里<strong>显式列出自己支持的原语</strong>——本例中只声明了 <code class="language-text">tools</code> 和 <code class="language-text">resources</code>，意味着这个 Server 不支持 prompts，Client 也就不会调用 <code class="language-text">prompts/*</code> 相关方法。声明了 <code class="language-text">tools.listChanged: true</code> 的服务端，将来工具列表变更时会主动发 <code class="language-text">notifications/tools/list_changed</code>。</li>
<li><strong><code class="language-text">{}</code> 不是空对象</strong>：它表示"我支持这个能力，但没有可配置选项"——是声明的最小形式。</li>
</ol>
<h3 id="step-2-发现工具">Step 2. 发现工具</h3>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Client → Server</span>
<span class="token punctuation">{</span> <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token property">"method"</span><span class="token operator">:</span> <span class="token string">"tools/list"</span> <span class="token punctuation">}</span></code></pre></div>
<h3 id="step-3-调用工具">Step 3. 调用工具</h3>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Client → Server</span>
<span class="token punctuation">{</span>
  <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token property">"method"</span><span class="token operator">:</span> <span class="token string">"tools/call"</span><span class="token punctuation">,</span>
  <span class="token property">"params"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"weather_current"</span><span class="token punctuation">,</span>
    <span class="token property">"arguments"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"location"</span><span class="token operator">:</span> <span class="token string">"San Francisco"</span><span class="token punctuation">,</span> <span class="token property">"units"</span><span class="token operator">:</span> <span class="token string">"imperial"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div>
<h3 id="step-4-服务端主动通知">Step 4. 服务端主动通知</h3>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token comment">// Server → Client（工具列表变更，无需响应）</span>
<span class="token punctuation">{</span> <span class="token property">"jsonrpc"</span><span class="token operator">:</span> <span class="token string">"2.0"</span><span class="token punctuation">,</span> <span class="token property">"method"</span><span class="token operator">:</span> <span class="token string">"notifications/tools/list_changed"</span> <span class="token punctuation">}</span></code></pre></div>
<hr>
<h2 id="八python-sdk-实战">八、Python SDK 实战</h2>
<p>接下来全部用 Python 官方 SDK（<code class="language-text">mcp</code>）演示。</p>
<h3 id="1-安装">1. 安装</h3>
<p>推荐使用 <a href="https://docs.astral.sh/uv/">uv</a>：</p>
<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash">uv init mcp-demo
<span class="token builtin class-name">cd</span> mcp-demo
uv <span class="token function">add</span> <span class="token string">"mcp[cli]"</span></code></pre></div>
<p>也可以用 pip：</p>
<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash">pip <span class="token function">install</span> <span class="token string">"mcp[cli]"</span></code></pre></div>
<h3 id="2-最简服务端tools--resources--prompts-全家桶">2. 最简服务端：Tools + Resources + Prompts 全家桶</h3>
<p><code class="language-text">server.py</code>：</p>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp <span class="token keyword">import</span> FastMCP

mcp <span class="token operator">=</span> FastMCP<span class="token punctuation">(</span><span class="token string">"Demo"</span><span class="token punctuation">)</span>

<span class="token comment"># ---- Tool：模型主动调用，会产生副作用 ----</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">add</span><span class="token punctuation">(</span>a<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> b<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">int</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""两数相加"""</span>
    <span class="token keyword">return</span> a <span class="token operator">+</span> b

<span class="token comment"># ---- Resource：应用按需读取 ----</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>resource</span><span class="token punctuation">(</span><span class="token string">"greeting://{name}"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">get_greeting</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""根据名字生成个性化问候"""</span>
    <span class="token keyword">return</span> <span class="token string-interpolation"><span class="token string">f"Hello, </span><span class="token interpolation"><span class="token punctuation">{</span>name<span class="token punctuation">}</span></span><span class="token string">!"</span></span>

<span class="token comment"># ---- Prompt：用户显式触发 ----</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>prompt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">greet_user</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> style<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">=</span> <span class="token string">"friendly"</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""生成不同风格的问候 prompt 模板"""</span>
    styles <span class="token operator">=</span> <span class="token punctuation">{</span>
        <span class="token string">"friendly"</span><span class="token punctuation">:</span> <span class="token string">"请写一段温暖、友好的问候"</span><span class="token punctuation">,</span>
        <span class="token string">"formal"</span><span class="token punctuation">:</span>   <span class="token string">"请写一段正式的问候"</span><span class="token punctuation">,</span>
        <span class="token string">"casual"</span><span class="token punctuation">:</span>   <span class="token string">"请写一段轻松随意的问候"</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>styles<span class="token punctuation">[</span>style<span class="token punctuation">]</span><span class="token punctuation">}</span></span><span class="token string">，对象是名叫 </span><span class="token interpolation"><span class="token punctuation">{</span>name<span class="token punctuation">}</span></span><span class="token string"> 的人。"</span></span>

<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span>
    mcp<span class="token punctuation">.</span>run<span class="token punctuation">(</span><span class="token punctuation">)</span>  <span class="token comment"># 默认 stdio 传输</span></code></pre></div>
<p>启动方式三选一：</p>
<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token comment"># 调试：MCP Inspector 提供 Web UI</span>
uv run mcp dev server.py

<span class="token comment"># 安装进 Claude Desktop</span>
uv run mcp <span class="token function">install</span> server.py

<span class="token comment"># 切换为 HTTP 传输（生产推荐）</span>
<span class="token comment"># 在代码里改成 mcp.run(transport="streamable-http")</span></code></pre></div>
<h3 id="3-结构化输出用-pydantic-自动生成-schema">3. 结构化输出：用 Pydantic 自动生成 Schema</h3>
<p>FastMCP 会从类型注解自动生成 JSON Schema，模型侧会更容易理解返回数据：</p>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">from</span> pydantic <span class="token keyword">import</span> BaseModel<span class="token punctuation">,</span> Field
<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp <span class="token keyword">import</span> FastMCP

mcp <span class="token operator">=</span> FastMCP<span class="token punctuation">(</span><span class="token string">"Weather"</span><span class="token punctuation">)</span>

<span class="token keyword">class</span> <span class="token class-name">WeatherData</span><span class="token punctuation">(</span>BaseModel<span class="token punctuation">)</span><span class="token punctuation">:</span>
    temperature<span class="token punctuation">:</span> <span class="token builtin">float</span> <span class="token operator">=</span> Field<span class="token punctuation">(</span>description<span class="token operator">=</span><span class="token string">"摄氏度"</span><span class="token punctuation">)</span>
    humidity<span class="token punctuation">:</span> <span class="token builtin">float</span> <span class="token operator">=</span> Field<span class="token punctuation">(</span>description<span class="token operator">=</span><span class="token string">"百分比"</span><span class="token punctuation">)</span>
    condition<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">=</span> Field<span class="token punctuation">(</span>description<span class="token operator">=</span><span class="token string">"天气状况，如 sunny / cloudy"</span><span class="token punctuation">)</span>

<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">get_weather</span><span class="token punctuation">(</span>city<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> WeatherData<span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""获取指定城市的天气信息（结构化返回）"""</span>
    <span class="token keyword">return</span> WeatherData<span class="token punctuation">(</span>temperature<span class="token operator">=</span><span class="token number">22.5</span><span class="token punctuation">,</span> humidity<span class="token operator">=</span><span class="token number">45.0</span><span class="token punctuation">,</span> condition<span class="token operator">=</span><span class="token string">"sunny"</span><span class="token punctuation">)</span></code></pre></div>
<h3 id="4-context-注入日志进度samplingelicitation">4. Context 注入：日志、进度、Sampling、Elicitation</h3>
<p>向 tool 加一个 <code class="language-text">Context</code> 参数，FastMCP 会自动注入。借助它可以做日志、进度、反向调用 LLM、向用户索取信息：</p>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp <span class="token keyword">import</span> Context<span class="token punctuation">,</span> FastMCP
<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>types <span class="token keyword">import</span> SamplingMessage<span class="token punctuation">,</span> TextContent

mcp <span class="token operator">=</span> FastMCP<span class="token punctuation">(</span><span class="token string">"Advanced"</span><span class="token punctuation">)</span>

<span class="token comment"># 进度上报 + 日志</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">long_task</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> ctx<span class="token punctuation">:</span> Context<span class="token punctuation">,</span> steps<span class="token punctuation">:</span> <span class="token builtin">int</span> <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token keyword">await</span> ctx<span class="token punctuation">.</span>info<span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"开始执行任务：</span><span class="token interpolation"><span class="token punctuation">{</span>name<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
    <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>steps<span class="token punctuation">)</span><span class="token punctuation">:</span>
        <span class="token keyword">await</span> ctx<span class="token punctuation">.</span>report_progress<span class="token punctuation">(</span>
            progress<span class="token operator">=</span><span class="token punctuation">(</span>i <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">/</span> steps<span class="token punctuation">,</span>
            total<span class="token operator">=</span><span class="token number">1.0</span><span class="token punctuation">,</span>
            message<span class="token operator">=</span><span class="token string-interpolation"><span class="token string">f"步骤 </span><span class="token interpolation"><span class="token punctuation">{</span>i<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token punctuation">{</span>steps<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span>
    <span class="token keyword">return</span> <span class="token string-interpolation"><span class="token string">f"任务 </span><span class="token interpolation"><span class="token punctuation">{</span>name<span class="token punctuation">}</span></span><span class="token string"> 完成"</span></span>

<span class="token comment"># Sampling：反向请求 Client 的 LLM</span>
<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">summarize</span><span class="token punctuation">(</span>topic<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> ctx<span class="token punctuation">:</span> Context<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""让 Host 的 LLM 帮我总结一下"""</span>
    result <span class="token operator">=</span> <span class="token keyword">await</span> ctx<span class="token punctuation">.</span>session<span class="token punctuation">.</span>create_message<span class="token punctuation">(</span>
        messages<span class="token operator">=</span><span class="token punctuation">[</span>
            SamplingMessage<span class="token punctuation">(</span>
                role<span class="token operator">=</span><span class="token string">"user"</span><span class="token punctuation">,</span>
                content<span class="token operator">=</span>TextContent<span class="token punctuation">(</span><span class="token builtin">type</span><span class="token operator">=</span><span class="token string">"text"</span><span class="token punctuation">,</span> text<span class="token operator">=</span><span class="token string-interpolation"><span class="token string">f"请用一句话总结 </span><span class="token interpolation"><span class="token punctuation">{</span>topic<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token punctuation">)</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
        max_tokens<span class="token operator">=</span><span class="token number">100</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span>
    <span class="token keyword">return</span> result<span class="token punctuation">.</span>content<span class="token punctuation">.</span>text <span class="token keyword">if</span> result<span class="token punctuation">.</span>content<span class="token punctuation">.</span><span class="token builtin">type</span> <span class="token operator">==</span> <span class="token string">"text"</span> <span class="token keyword">else</span> <span class="token builtin">str</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span>content<span class="token punctuation">)</span></code></pre></div>
<h3 id="5-生命周期管理启动时建连接关闭时清理">5. 生命周期管理：启动时建连接，关闭时清理</h3>
<p>需要在启动时初始化数据库连接池等长生命周期资源时，使用 <code class="language-text">lifespan</code>：</p>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">from</span> dataclasses <span class="token keyword">import</span> dataclass
<span class="token keyword">from</span> contextlib <span class="token keyword">import</span> asynccontextmanager
<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp <span class="token keyword">import</span> Context<span class="token punctuation">,</span> FastMCP

<span class="token decorator annotation punctuation">@dataclass</span>
<span class="token keyword">class</span> <span class="token class-name">AppContext</span><span class="token punctuation">:</span>
    db<span class="token punctuation">:</span> <span class="token string">"Database"</span>

<span class="token decorator annotation punctuation">@asynccontextmanager</span>
<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">app_lifespan</span><span class="token punctuation">(</span>server<span class="token punctuation">:</span> FastMCP<span class="token punctuation">)</span><span class="token punctuation">:</span>
    db <span class="token operator">=</span> <span class="token keyword">await</span> Database<span class="token punctuation">.</span>connect<span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">try</span><span class="token punctuation">:</span>
        <span class="token keyword">yield</span> AppContext<span class="token punctuation">(</span>db<span class="token operator">=</span>db<span class="token punctuation">)</span>
    <span class="token keyword">finally</span><span class="token punctuation">:</span>
        <span class="token keyword">await</span> db<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>

mcp <span class="token operator">=</span> FastMCP<span class="token punctuation">(</span><span class="token string">"MyApp"</span><span class="token punctuation">,</span> lifespan<span class="token operator">=</span>app_lifespan<span class="token punctuation">)</span>

<span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">query</span><span class="token punctuation">(</span>sql<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> ctx<span class="token punctuation">:</span> Context<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    app_ctx<span class="token punctuation">:</span> AppContext <span class="token operator">=</span> ctx<span class="token punctuation">.</span>request_context<span class="token punctuation">.</span>lifespan_context
    <span class="token keyword">return</span> app_ctx<span class="token punctuation">.</span>db<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>sql<span class="token punctuation">)</span></code></pre></div>
<h3 id="6-客户端stdio-连接本地-server">6. 客户端：stdio 连接本地 Server</h3>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">import</span> asyncio
<span class="token keyword">from</span> mcp <span class="token keyword">import</span> ClientSession<span class="token punctuation">,</span> StdioServerParameters
<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>client<span class="token punctuation">.</span>stdio <span class="token keyword">import</span> stdio_client

<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    params <span class="token operator">=</span> StdioServerParameters<span class="token punctuation">(</span>
        command<span class="token operator">=</span><span class="token string">"uv"</span><span class="token punctuation">,</span>
        args<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"run"</span><span class="token punctuation">,</span> <span class="token string">"server.py"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span>
    <span class="token keyword">async</span> <span class="token keyword">with</span> stdio_client<span class="token punctuation">(</span>params<span class="token punctuation">)</span> <span class="token keyword">as</span> <span class="token punctuation">(</span>read<span class="token punctuation">,</span> write<span class="token punctuation">)</span><span class="token punctuation">:</span>
        <span class="token keyword">async</span> <span class="token keyword">with</span> ClientSession<span class="token punctuation">(</span>read<span class="token punctuation">,</span> write<span class="token punctuation">)</span> <span class="token keyword">as</span> session<span class="token punctuation">:</span>
            <span class="token keyword">await</span> session<span class="token punctuation">.</span>initialize<span class="token punctuation">(</span><span class="token punctuation">)</span>

            tools <span class="token operator">=</span> <span class="token keyword">await</span> session<span class="token punctuation">.</span>list_tools<span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Tools:"</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>t<span class="token punctuation">.</span>name <span class="token keyword">for</span> t <span class="token keyword">in</span> tools<span class="token punctuation">.</span>tools<span class="token punctuation">]</span><span class="token punctuation">)</span>

            result <span class="token operator">=</span> <span class="token keyword">await</span> session<span class="token punctuation">.</span>call_tool<span class="token punctuation">(</span><span class="token string">"add"</span><span class="token punctuation">,</span> arguments<span class="token operator">=</span><span class="token punctuation">{</span><span class="token string">"a"</span><span class="token punctuation">:</span> <span class="token number">5</span><span class="token punctuation">,</span> <span class="token string">"b"</span><span class="token punctuation">:</span> <span class="token number">3</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Result:"</span><span class="token punctuation">,</span> result<span class="token punctuation">.</span>content<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>text<span class="token punctuation">)</span>

asyncio<span class="token punctuation">.</span>run<span class="token punctuation">(</span>main<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div>
<h3 id="7-客户端streamable-http-连接远程-server">7. 客户端：Streamable HTTP 连接远程 Server</h3>
<div class="gatsby-highlight" data-language="python"><pre class="language-python"><code class="language-python"><span class="token keyword">import</span> asyncio
<span class="token keyword">from</span> mcp <span class="token keyword">import</span> ClientSession
<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>client<span class="token punctuation">.</span>streamable_http <span class="token keyword">import</span> streamablehttp_client

<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">async</span> <span class="token keyword">with</span> streamablehttp_client<span class="token punctuation">(</span><span class="token string">"http://localhost:8000/mcp"</span><span class="token punctuation">)</span> <span class="token keyword">as</span> <span class="token punctuation">(</span>read<span class="token punctuation">,</span> write<span class="token punctuation">,</span> _<span class="token punctuation">)</span><span class="token punctuation">:</span>
        <span class="token keyword">async</span> <span class="token keyword">with</span> ClientSession<span class="token punctuation">(</span>read<span class="token punctuation">,</span> write<span class="token punctuation">)</span> <span class="token keyword">as</span> session<span class="token punctuation">:</span>
            <span class="token keyword">await</span> session<span class="token punctuation">.</span>initialize<span class="token punctuation">(</span><span class="token punctuation">)</span>
            tools <span class="token operator">=</span> <span class="token keyword">await</span> session<span class="token punctuation">.</span>list_tools<span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Tools:"</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>t<span class="token punctuation">.</span>name <span class="token keyword">for</span> t <span class="token keyword">in</span> tools<span class="token punctuation">.</span>tools<span class="token punctuation">]</span><span class="token punctuation">)</span>

asyncio<span class="token punctuation">.</span>run<span class="token punctuation">(</span>main<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div>
<hr>
<h2 id="九在-claude-code-中的具体映射">九、在 Claude Code 中的具体映射</h2>
<p>理论讲完了，看一下三大原语在真实 Host 里到底长什么样。以 Claude Code 为例：</p>
<h3 id="tools--mcp__server__tool-工具">Tools → <code class="language-text">mcp__&lt;server>__&lt;tool></code> 工具</h3>
<p>服务端的每个 tool 都被改名后注入模型的 tool 列表。例如装了 <code class="language-text">github</code> MCP server，模型看到的工具叫 <code class="language-text">mcp__github__create_issue</code>。</p>
<p>特殊机制 <strong>Tool Search</strong>（默认开启）：tool 定义不是一次性塞进 context window，模型先看到一个 <code class="language-text">ToolSearch</code> 工具，按需查找。所以装 20 个 Server 也不会撑爆上下文。可以用 <code class="language-text">alwaysLoad: true</code> 把高频 Server 强制常驻。</p>
<h3 id="resources---引用--桥接-tool-双路并存">Resources → <code class="language-text">@</code> 引用 + 桥接 Tool 双路并存</h3>
<p>Claude Code 对 Resources 的支持正好印证上一节的讨论——<strong>应用驱动和模型驱动同时存在</strong>：</p>
<p><strong>路径 A：用户主动 <code class="language-text">@</code> 引用</strong></p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Can you analyze @github:issue://123 and suggest a fix?
Compare @postgres:schema://users with @docs:file://database/user-model</code></pre></div>
<p>输入 <code class="language-text">@</code> 触发自动补全，从所有连接的 Server 列出可用资源，支持模糊搜索。被引用的资源自动作为 attachment 拉进上下文。</p>
<p><strong>路径 B：模型主动调用（桥接为 Tool）</strong></p>
<p>官方文档原话：</p>
<blockquote>
<p>"Claude Code automatically provides tools to list and read MCP resources when servers support them"</p>
</blockquote>
<p>Claude Code 自动把 <code class="language-text">resources/list</code> 和 <code class="language-text">resources/read</code> 包装成内置工具暴露给模型——模型可以自主决定要不要去翻资源。</p>
<h3 id="prompts--mcp__server__prompt-slash-命令">Prompts → <code class="language-text">/mcp__&lt;server>__&lt;prompt></code> Slash 命令</h3>
<p>Server 定义的每个 prompt 都映射成一个 slash 命令：</p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/mcp__github__list_prs
/mcp__github__pr_review 456
/mcp__jira__create_issue "Bug in login flow" high</code></pre></div>
<ul>
<li>参数按 prompt 的 schema 解析，空格分隔；</li>
<li>服务端和 prompt 名里的空格被规范化为下划线；</li>
<li>Prompt 返回的 messages 数组直接拼进对话。</li>
</ul>
<h3 id="客户端原语支持情况">客户端原语支持情况</h3>
<ul>
<li><strong>Elicitation</strong>：自动弹交互对话框（form 模式或 URL 模式），可用 hook 自动应答。</li>
<li><strong>Roots</strong>：Server 启动时设置 <code class="language-text">CLAUDE_PROJECT_DIR</code> 环境变量，Server 也可以调 <code class="language-text">roots/list</code> 查询当前工作目录。</li>
</ul>
<h3 id="对-server-作者的启示">对 Server 作者的启示</h3>
<p>不同客户端对 MCP 原语的支持程度差异很大。如果你的 Server 想兼容性最大化：</p>
<ul>
<li><strong>必须支持 Tools</strong>——这是目前所有 MCP 客户端都支持的最大公约数。</li>
<li><strong>重要能力建议双重暴露</strong>：既写成 Tool 兜底，也写成 Resource Template 给支持的 Host 用更优雅的方式呈现。比如查询类的接口，既提供 <code class="language-text">query_user(id)</code> Tool，又暴露 <code class="language-text">user://{id}</code> Resource Template。</li>
<li><strong>Prompts 锦上添花</strong>：对于支持 Prompts 的 Host，它能显著提升用户在常见工作流上的体验。</li>
</ul>
<hr>
<h2 id="十mcp-vs-function-calling-vs-自研插件协议">十、MCP vs Function Calling vs 自研插件协议</h2>
<p>这是公司内部分享时最常被问的问题之一。</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>Function Calling</th>
<th>自研插件协议</th>
<th><strong>MCP</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>标准化程度</td>
<td>各家 LLM 厂商各有定义</td>
<td>公司内部一套</td>
<td>跨厂商开放标准</td>
</tr>
<tr>
<td>生态复用</td>
<td>每个模型一套 schema</td>
<td>几乎为零</td>
<td>一次实现，跨 Host 通用</td>
</tr>
<tr>
<td>上下文资源</td>
<td>通常只有"函数调用"</td>
<td>自定义</td>
<td>Tools / Resources / Prompts 三类原语</td>
</tr>
<tr>
<td>反向能力</td>
<td>不支持</td>
<td>一般不支持</td>
<td>Sampling / Elicitation / Roots</td>
</tr>
<tr>
<td>传输与鉴权</td>
<td>内嵌 API</td>
<td>自定</td>
<td>stdio / Streamable HTTP + OAuth</td>
</tr>
<tr>
<td>适合场景</td>
<td>单一应用内部小范围</td>
<td>私有强定制</td>
<td>工具/数据要被多端复用、要标准化</td>
</tr>
</tbody>
</table>
<p>一个粗略的判断准则：</p>
<ul>
<li><strong>能力只服务于一个 AI 应用、几乎不会被复用</strong> → Function Calling 足够。</li>
<li><strong>能力会被多个 AI 应用消费</strong>（团队多个产品 + 第三方工具如 Claude Desktop / Cursor）→ <strong>优先选 MCP</strong>。</li>
<li><strong>需要给用户/模型暴露资源和工作流模板，而不只是函数</strong> → <strong>MCP 的 Resources / Prompts 是 Function Calling 没有的</strong>。</li>
</ul>
<hr>
<h2 id="十一什么时候不用-mcp">十一、什么时候<strong>不</strong>用 MCP</h2>
<p>凡是建议，必有反例。下面几种情况，强行套 MCP 反而增加复杂度：</p>
<ol>
<li><strong>只是 Prompt 工程</strong>：没有外部系统交互，纯粹是 prompt 调优，不需要 MCP。</li>
<li><strong>极致低延迟的同进程调用</strong>：MCP 即使是 stdio，也有 JSON-RPC 序列化开销，对延迟敏感的场景直接函数调用更划算。</li>
<li><strong>能力跟应用强耦合、永远不会复用</strong>：自己造一套 function calling 更轻量。</li>
<li><strong>跨语言、跨进程的 RPC 通用诉求</strong>：那不是 MCP 的目标，请用 gRPC / OpenAPI。</li>
</ol>
<hr>
<h2 id="十二生产实践中的几点建议">十二、生产实践中的几点建议</h2>
<p>实际使用 MCP 时，下面几条是踩过坑后总结的：</p>
<h3 id="1-工具名要带命名空间">1. 工具名要带命名空间</h3>
<p>不要叫 <code class="language-text">search</code>，叫 <code class="language-text">github_search_issues</code>、<code class="language-text">jira_search_tickets</code>。多个 Server 同时连接时，模型才能准确路由。</p>
<h3 id="2-description-写给模型看不是写给人看">2. <code class="language-text">description</code> 写给模型看，不是写给人看</h3>
<p>工具描述会直接进入模型上下文，措辞会显著影响调用准确率。要写清楚：<strong>做什么、何时该用、何时不该用、输入约束</strong>。</p>
<h3 id="3-危险操作必须-elicitation">3. 危险操作必须 Elicitation</h3>
<p>写库、发消息、扣款……这类有副作用的操作，<strong>主动调用 <code class="language-text">elicitation/create</code> 让用户确认</strong>，而不是相信"模型不会乱来"。</p>
<h3 id="4-streamable-http-配合-oauth">4. Streamable HTTP 配合 OAuth</h3>
<p>远程 Server 不要再用 Bearer Token 写死在配置里。OAuth 是规范推荐的鉴权方式，配合 <code class="language-text">roots</code> 限定 Server 的工作范围。</p>
<h3 id="5-善用-listchanged-通知">5. 善用 <code class="language-text">listChanged</code> 通知</h3>
<p>工具集随业务动态变化（如新接入一个数据源）时，发 <code class="language-text">notifications/tools/list_changed</code>，Client 会立即刷新，无需重启。</p>
<h3 id="6-开发期用-mcp-inspector">6. 开发期用 MCP Inspector</h3>
<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash">uv run mcp dev server.py</code></pre></div>
<p>会启一个 Web UI，可视化地查看 tools/resources/prompts、手动调用、看 JSON-RPC 报文，比靠 print 调试快十倍。</p>
<hr>
<h2 id="十三参考文档">十三、参考文档</h2>
<ul>
<li>官方文档首页：<a href="https://modelcontextprotocol.io/docs/">https://modelcontextprotocol.io/docs/</a></li>
<li>架构概览：<a href="https://modelcontextprotocol.io/docs/learn/architecture">https://modelcontextprotocol.io/docs/learn/architecture</a></li>
<li>服务端概念（Tools / Resources / Prompts）：<a href="https://modelcontextprotocol.io/docs/learn/server-concepts">https://modelcontextprotocol.io/docs/learn/server-concepts</a></li>
<li>客户端概念（Sampling / Elicitation / Roots）：<a href="https://modelcontextprotocol.io/docs/learn/client-concepts">https://modelcontextprotocol.io/docs/learn/client-concepts</a></li>
<li>最新规范：<a href="https://modelcontextprotocol.io/specification/latest">https://modelcontextprotocol.io/specification/latest</a></li>
<li>Python SDK：<a href="https://github.com/modelcontextprotocol/python-sdk">https://github.com/modelcontextprotocol/python-sdk</a></li>
<li>官方参考 Server 仓库：<a href="https://github.com/modelcontextprotocol/servers">https://github.com/modelcontextprotocol/servers</a></li>
<li>MCP Inspector（调试利器）：<a href="https://github.com/modelcontextprotocol/inspector">https://github.com/modelcontextprotocol/inspector</a></li>
</ul>
<hr>
<h2 id="结语">结语</h2>
<p>MCP 在 2024 年底问世，到今天已经被 Claude、ChatGPT、VS Code、Cursor 等主流 AI 应用普遍支持，成为 AI 工具生态的事实标准。</p>
<p>它的设计哲学很简单：</p>
<blockquote>
<p><strong>协议归协议，模型归模型。</strong>
MCP 不规定你怎么用 LLM、怎么管理上下文，它只规定"AI 应用如何与外部系统通信"这一件事，并把它做到了足够通用与足够干净。</p>
</blockquote>
<p>对工程师而言，理解 MCP 的回报很高：写一次 Server，便能被生态里的所有 Host 复用；写一次 Client，便能即插即用任意社区 Server。在 AI 应用碎片化加剧的今天，<strong>MCP 是少数能跨厂商、跨产品沉淀工程价值的协议层</strong>。</p>]]></content>
  </entry>
  <entry>
    <title>Hello World</title>
    <id>https://shipengtao.com/zh/hello-world/</id>
    <link rel="alternate" type="text/html" href="https://shipengtao.com/zh/hello-world/"/>
    <published>2024-05-23T00:00:00+08:00</published>
    <updated>2024-05-23T00:00:00+08:00</updated>
    <summary type="text">你好，世界！</summary>
    <content type="html"><![CDATA[<p>你好，世界！</p>]]></content>
  </entry>
</feed>