🛠️ 第8章 Skill、Subagent 与 MCP:把能力和上下文装进系统
📄 一句话概括

MCP 解决的是「工具太多」:把工具描述从提示词里挪到协议里,按需列出来。 三步就是全部:initialize 握手、tools/list 列工具、tools/call 调用。 但工具描述是常驻上下文——接十个服务器就是六千 token,不管用不用得上。

📝 本章和前面几章的关系

这一篇是这一章最「协议」的一篇,但结论很朴素:先问要不要接。 Demo 真起了一个 MCP 服务器进程,用标准输入输出跟它对话。 三步报文一来一回,全部真跑——包括最后一次调用返回的「开心」。 沙箱里没有现成的 MCP 服务器,所以这一个是手写的, 但报文形状照规范来,客户端也是真的按行读写。

一、MCP 解决的是工具太多

没有 MCP 的时候,工具怎么给模型?写在提示词里。 十个工具,提示词里就有十段描述加参数说明。 工具一多,这件事就崩了: 五十个工具的描述能占掉大半窗口,而这次任务可能只用一个。 MCP 的做法是把工具描述挪出去,变成「可查询的」。 先握手问「你有什么工具」,它回一份列表;要用哪个再调。 这样描述仍然是上下文,但至少是「列出来之后才知道要占多少」。 注意它没有真正解决「占上下文」这件事。 它解决的是「不用在提示词里硬编码」。 这两件事的差别,决定了「接不接」的判断标准。

二、一次握手的报文长什么样

三步,每一对都是一行 JSON 出去、一行 JSON 回来。

方法 谁发 回什么
initialize 客户端 协议版本、能力、服务器名
tools/list 客户端 工具列表与参数模式
tools/call 客户端 调用结果或错误码

Demo 里三步全跑通了。 第一次回来的是 protocolVersion 和 serverInfo;第二次回来的是工具列表; 第三次回来的是「开心」两个字。 报文里有两个字段要认得: id 用来配对请求和响应(因为可以并发发多个);jsonrpc 固定是 2.0。 还有一个细节:没有 id 的消息是通知,不需要回。 这个设计让「单向告知」和「请求响应」用同一套管道。

三、什么时候不该接

情形 该不该接 为什么
一个脚本能干的事 不接 脚本不进上下文
只用一次的外部服务 不接 握手加描述比直接用还贵
团队都在用的系统 接 一次配置,所有人省事
需要鉴权和权限的 接 凭据集中管,别散在脚本里

第一行最关键:一个脚本能干的事,不要接 MCP。 因为脚本的描述不占上下文—— 你只要在 AGENTS.md 里写「跑 python tools/fmt.py」,这十几个字就是全部成本。 而接一个 MCP 服务器,工具描述是常驻的。 Demo 里算了:接十个服务器、每个五个工具,常驻六千 token。 六千 token 大概等于十个中等文件的量。 所以判断标准不是「这个能力好不好」,是「它会不会被反复用、而且反复用的方式一样」。 会,值得接;不会,写个脚本。

四、动手 Demo:MCP:先问是否值得接,再问怎么接

先算一笔账:接几个服务器,常驻上下文多少。 然后真起一个服务器进程,把三步报文跑一遍。 服务器是手写的二十行 Python,用标准输入输出收发 JSON-RPC。 客户端也是真的按行读写,不是假装。

p = subprocess.Popen([PY, u"mcp_server.py"], cwd=base,
                     stdin=subprocess.PIPE, stdout=subprocess.PIPE)

for m in msgs:
    line = json.dumps(m, ensure_ascii=False, separators=(",", ":"))
    p.stdin.write((line + u"\n").encode(u"utf-8"))
    p.stdin.flush()
    back = p.stdout.readline().decode(u"utf-8").strip()
    P(u"  <- " + short(back))

实跑输出:

--------------------------------------------------------------------
MCP:先问是否值得接,再问怎么接
--------------------------------------------------------------------


  ① 工具太多是什么问题
  --------------------------------------------------------------------
  假设每个工具的描述加参数模式占一百二十 token。
  接几个服务器、每个服务器几个工具,直接乘出「常驻上下文」。
| 接多少 | 工具总数 | 常驻上下文 |
|:---:|:---:|:---:|
| 1 个服务器 | 5 个工具 | 600 |
| 3 个服务器 | 15 个工具 | 1,800 |
| 5 个服务器 | 25 个工具 | 3,000 |
| 10 个服务器 | 50 个工具 | 6,000 |

  ! 这些描述是常驻的——不管这次任务用不用得上,它都占着窗口。所以「要不要接」比「怎么接」更重要。

  ② 一次握手的报文长什么样
  --------------------------------------------------------------------
  -> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion"…
  <- {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabil…
  -> {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
  <- {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"judge_emotion","descr…
  -> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"judge_emo…
  <- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"开心"}]…

  ③ 三步:握手、列工具、调用
  --------------------------------------------------------------------
| 方法 | 谁发 | 回什么 |
|---|---|---|
| initialize | 客户端 | 协议版本、能力、服务器名 |
| tools/list | 客户端 | 工具列表与参数模式 |
| tools/call | 客户端 | 调用结果或错误码 |

  三步都是同一套形状:一行 JSON 出去,一行 JSON 回来。
  没有长连接、没有二进制、没有握手状态机以外的东西。

  ④ 什么时候不该接
  --------------------------------------------------------------------
| 情形 | 该不该接 | 为什么 |
|---|---|---|
| 一个脚本能干的事 | 不接 | 脚本不进上下文 |
| 只用一次的外部服务 | 不接 | 握手加描述比直接用还贵 |
| 团队都在用的系统 | 接 | 一次配置,所有人省事 |
| 需要鉴权和权限的 | 接 | 凭据集中管,别散在脚本里 |

  ! 判断标准:这个能力会不会被反复用、而且反复用的方式一样。会,值得接;不会,写个脚本。

第三步回来的 "text":"开心" 是真算出来的—— 服务器收到「我今天很开心」,按关键词判断返回了「开心」。 整个链路是真跑的:主进程写管道、子进程读、子进程写回、主进程读。 报文被截断了,但形状看得清:jsonrpc 加 id 加 result。 请求里多一个 method 和 params。 就这些,没有别的。 ① 那张表是这一篇的重点。 十个服务器六千 token 常驻——这个数跟「接不接」的判断直接相关,比报文形状重要。 最后那句判断标准要记住:会不会被反复用、而且用法一样。 两个条件都满足才接,缺一个就用脚本。

30 秒实验

  1. 数一下你每天调的外部服务有几个。 两个以下的,写脚本;五个以上的,考虑接。
  2. 找一个你接了的 MCP,看它的工具描述占多少 token。 算一下「这次任务用到了几个」。
  3. 手写一个最小的 MCP 服务器,只实现三步。 跑通一次,以后再看到「接一下这个 MCP」就不慌了。

常见误区

  1. 「MCP 能省上下文。」 它不省。工具描述还是常驻的。 它省的是「不用在提示词里硬编码」。
  2. 「工具越多越强。」 工具多到一定程度,模型选错的概率上升,而且描述一直占着窗口。 能力不是越多越好,是越准越好。
  3. 「有现成的 MCP 就接上。」 先算常驻成本。 一个只用一次的服务器,握手加描述比直接用还贵。
自测三问(点开对答案) 1. MCP 到底解决了什么问题? —— **把工具描述从提示词里挪到协议里,变成可查询的。** **它不省上下文,省的是「不用硬编码」。** 2. 三步报文分别是什么? —— **initialize(握手,换协议版本与能力)、tools/list(列工具与参数模式)、** **tools/call(调用并拿结果)。** 3. 什么时候不该接 MCP? —— **一个脚本能干的、只用一次的。** **判断标准是「会不会被反复用、而且用法一样」——两个都满足才接。**

下一篇 8.4 收这一章:三者怎么协同,以及复杂度预算怎么算。

相关笔记