从连上一个 MCP 服务到理解 AI 系统的工程本质
一次从"会用"到"理解原理"再到"能优化"的完整探索记录。
本文记录了我通过实际动手连接一个远程 MCP 服务(SIF —— 亚马逊卖家流量分析平台),一步步深入理解 MCP 协议机制、LLM 上下文管理、注意力资源分配、以及工具编排优化方案的全过程。
一、起点:连上一个真实的 MCP 服务
什么是 MCP?
MCP(Model Context Protocol)是 Anthropic 主导设计的一个开放协议,目的是标准化 AI 应用与外部工具/数据源之间的通信方式。你可以把它理解为"AI 世界的 USB 接口"—— 不管什么工具,只要实现了 MCP 协议,就能被任何支持 MCP 的 AI 客户端调用。
实战:探测 SIF MCP 服务
SIF(sif.com)是一个面向亚马逊卖家的流量分析和广告洞察工具平台,它提供了一个 MCP 服务端点:https://mcp.sif.com/mcp。
我写了一个 Python 脚本,用 MCP SDK 连上去探测它暴露了什么能力:
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async with streamablehttp_client(url=URL, headers=headers) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
result = await session.initialize()
tools_result = await session.list_tools()
resources_result = await session.list_resources()
几个发现:
-
传输方式是 Streamable HTTP,不是旧版的 SSE。一开始用
sse_client连接返回 405 错误,换成streamablehttp_client才成功。这说明 MCP 协议在演进,新版服务已经在用更新的传输方式。 -
服务只暴露了 Tools 能力,不支持 Resources 和 Prompts。这是一个纯工具型的 MCP 服务。
-
共 27 个工具,覆盖关键词分析、流量运营、广告洞察三大领域,按 ASIN → Campaign → Ad Group 三层结构组织。
-
有一个
sif_catalog工具,调用后返回官方的工具分类目录。这是一种自描述机制,让 AI 可以先看目录再决定深入哪个方向。
完整导出
我把所有工具的完整 schema(包括参数类型、必填标记、枚举值、x-output-hint 返回字段说明)全部导出到了 JSON 文件,并整理成了可读的 Markdown 文档。这份文档后来成了我理解后续所有问题的基础素材。
二、核心问题:MCP 是怎么把工具交给 LLM 的?
拿到 27 个工具的 schema 后,我产生了第一个疑问:这些东西是一次性全塞给 LLM 的吗?
答案是:分阶段、按需交互。
第一步:注册阶段(一次性)
客户端连上 MCP 服务后,调用 list_tools 拿到工具列表。拿到的是工具名、描述、参数定义这些 schema 信息。
这部分确实会塞进 LLM 的 system prompt 或上下文里。 相当于告诉 LLM:"你有这些工具可以用,每个工具长这样。"
第二步:对话阶段(按需)
用户提问后,LLM 根据问题自己判断要不要调工具、调哪个、传什么参数。客户端拿到 LLM 的调用请求,通过 MCP 协议发给服务端,拿到结果后再喂回 LLM。
实际的上下文消耗
固定成本:27 个工具的 schema 描述(几千 token,始终在上下文里)
+
按需成本:每次工具调用的返回数据(只有实际调了才会进上下文)
一句话总结:MCP 是给 LLM 装了一个"工具菜单",菜单常驻上下文,但菜(数据)是点了才上的。
三、上下文膨胀:一个不可忽视的工程问题
理解了机制之后,问题就来了:工具描述一直在上下文里,这个代价有多大?
SIF 的 27 个工具,schema 序列化后大概占 4000-6000 token。一个服务还好,但如果同时接了 5-6 个 MCP 服务,光工具描述就吃掉 2-3 万 token,还没开始干活呢。
对于 Claude 这种 200K 上下文的模型,2-3 万 token 还能接受。但对于 8K、16K 的模型,这就是灾难。
四、我的第一个优化想法:上帝 Agent
我最初的想法是搞一个"上帝 Agent"—— 它掌管着所有工具,不停审查上下文,当感觉某一步需要某个工具的时候就下发给执行 Agent。
这个思路的方向是对的,本质上是一个 Tool Router Agent:
用户 → 上帝 Agent(只持有工具目录摘要)
↓ 判断需要哪些工具
下发工具 schema → 执行 Agent(只拿到 2-3 个相关工具)
↓ 调用工具、生成回答
回传结果 → 上帝 Agent 审查、决定下一步
但深入思考后发现这个方案有几个严重的缺陷:
缺陷一:上帝 Agent 本身也消耗上下文
上帝 Agent 要"不停审查上下文",那它自己的上下文里至少要有:工具目录摘要 + 完整对话历史 + 执行 Agent 的中间结果。虽然它不持有完整的工具 schema,但对话越长,它自己的上下文也在膨胀。
本质上是用计算换空间 —— 省了执行 Agent 的上下文,但多了上帝 Agent 的推理开销。而且这个推理开销是每一步都要付出的。
缺陷二:路由准确率是命门
上帝 Agent 做判断的依据只有工具摘要(名字 + 一句话描述),没有完整 schema。如果摘要写得不够精确,路由就会出错。
比如用户问"这个 ASIN 广告效果怎么样",上帝 Agent 可能只下发了 ads_get_asin_ad_structure(广告结构),漏掉了 ads_get_asin_ad_feature_profile(广告特征画像)。执行 Agent 拿不到完整工具集,回答就是残缺的。
更糟糕的是,执行 Agent 不知道自己缺了什么 —— 它只能看到上帝 Agent 下发的工具,对于被过滤掉的工具完全无感知。这种"不知道自己不知道"的状态很危险。
缺陷三:"不停审查"的延迟成本
每一步都要上帝 Agent 介入意味着:用户问一个问题 → 上帝 Agent 推理一次(几百毫秒到几秒)→ 下发工具 → 执行 Agent 推理 → 可能还要回传给上帝 Agent 再审查。
一个原本 2 秒能完成的工具调用,加上上帝 Agent 的两次介入(下发 + 审查),可能变成 6-8 秒。对于需要多步推理的复杂问题,延迟会成倍放大。
缺陷四:状态管理的复杂性
上帝 Agent 要维护一个状态机:这轮对话已经调了哪些工具、拿到了什么数据、下一步可能需要什么、执行 Agent 当前的进度。这个状态管理本身就很复杂,而且状态信息也要占上下文。
更务实的改进方向
这个方案的方向没错,但"不停审查"这个设计太重了。更实际的做法是:
一次路由 + 按需追加。上帝 Agent 只在对话开始和明显话题切换时介入,选出相关工具子集交给执行 Agent。执行 Agent 发现工具不够用时,再向上帝 Agent 请求追加。不需要每一步都审查。
业界实际在做的几个变体:
- OpenAI 的方式 —— 不用 Agent 做路由,用 embedding 做工具检索(下一节详细展开)
- Anthropic 的 tool_choice 机制 —— 在 API 层面约束 LLM 只能从指定子集里选工具,路由逻辑放在客户端代码里而不是另一个 LLM 里
- Multi-Agent 框架(AutoGen、CrewAI) —— 不是"上帝监督"模式,而是"分工协作"模式。每个 Agent 只负责一个领域,自带该领域的工具。路由层只做意图分类,不做持续监督
五、更优雅的方案:用 Embedding 做工具检索
在思考 Agent 路由方案的过程中,我了解到了一个让我震撼的方案:用 embedding 向量检索来做工具筛选。
原理
工具描述是文本,用户问题也是文本,找最相关的 —— 这不就是向量检索天天干的事吗?
提前:27 个工具描述 → embedding 模型 → 27 个向量 → 存入向量库
运行时:
用户问题 → embedding 模型 → 1 个查询向量
↓
向量库做相似度检索 → 返回 top 3 最相关的工具
↓
只把这 3 个工具 schema 塞进 prompt → LLM 从 3 个里选 → 调用
跟原生方式的对比
| 原生 MCP 方式 | Embedding 检索方式 | |
|---|---|---|
| 谁做初筛 | 没有初筛,LLM 全看 | 向量检索(跟 LLM 无关) |
| 谁做最终选择 | LLM | 还是 LLM |
| 上下文占用 | 全部工具 | 只有命中的几个 |
| 额外依赖 | 无 | embedding 模型 + 向量库 |
| 出错风险 | LLM 可能选错 | 检索可能漏掉正确工具 |
为什么这个方案让我震撼
不是因为技术本身复杂,而是这个连接 —— 把一个我熟悉的技术(向量检索)用到了一个我没想到的场景(工具选择)。AI 工程很多时候就是这样,没有太多全新的东西,更多是把已有的积木换一种方式拼。
而且 27 个工具的向量库甚至不需要真正的数据库,内存里一个数组算余弦相似度就够了。检索延迟在个位数毫秒级别,快、无感知、直接降低上下文占用。
六、KV Cache 与 Prompt Caching:另一个维度的优化
KV Cache 是什么
LLM 推理时,Transformer 每一层都要算 Key 和 Value 矩阵。对于一段 prompt,第一次算完后可以缓存起来。下一次如果 prompt 前缀没变,直接复用,不用重新算。
打个比方:你每次去餐厅,服务员都要重新读一遍菜单给你听。KV Cache 就是服务员记住了"这个客人已经听过菜单了",直接从你点菜那一步开始。
跟 MCP 工具描述的关系
每轮对话发给 LLM 的 prompt 结构大概是:
[system prompt] ← 几乎不变
[27 个工具的 schema] ← 每轮都一样
[对话历史] ← 逐轮增长
[用户最新消息] ← 每轮变化
前两部分是稳定前缀。有 KV Cache 的话,这部分只在第一轮算一次,后续轮次直接复用。
Prompt Caching:API 层面的产品化
Anthropic 把 KV Cache 包装成了 API 功能。在工具定义上标记 cache_control,缓存命中的 token 费用降 90%。
{
"name": "tool_27",
"input_schema": {...},
"cache_control": {"type": "ephemeral"}
}
但缓存不解决注意力稀释问题
这是我在深入思考后抓到的一个关键点:
缓存解决的是计算成本和延迟的问题,不解决注意力稀释的问题。
KV Cache 命中后,那些 token 不需要重新算 K/V 矩阵了。但在 LLM 生成回答的时候,注意力机制依然要对上下文里所有 token 计算 attention score。缓存的 token 和新算的 token,在注意力层面是平等的。
| 问题 | Prompt Caching 能解决吗 | Embedding 筛选能解决吗 |
|---|---|---|
| 计算成本 | ✅ | ✅(更少 token) |
| 推理延迟 | ✅ | ✅ |
| 注意力稀释 | ❌ | ✅(无关工具根本不进上下文) |
这两个方案不是替代关系,是互补的。 最优解是两个都用:先用 embedding 筛掉无关工具,剩下的通过缓存降低重复计算成本。
七、Anthropic 的 Tool 管理设计详解
Anthropic 作为 MCP 协议的设计者,在自家 Claude API 中对工具管理有一套完整的设计。理解这套设计,能帮助我们看清"工业级的工具管理"是怎么做的。
7.1 工具定义:跟 MCP 统一的 Schema
调 Claude API 时,tools 是请求体里的一个数组:
{
"model": "claude-opus-4-0-20250514",
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的天气",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
],
"messages": [...]
}
这个结构跟我从 SIF MCP 服务 dump 出来的工具 schema 几乎一模一样。这不是巧合 —— MCP 就是 Anthropic 主导设计的,tool schema 格式本来就是一套。这意味着 MCP 服务暴露的工具定义可以无缝传递给 Claude API,中间不需要任何格式转换。
7.2 tool_choice:四种控制模式
这是 Anthropic 做工具管理的核心手段。通过 tool_choice 参数,客户端可以精确控制 LLM 的工具选择行为:
模式一:auto(默认)—— LLM 全权决定
"tool_choice": {"type": "auto"}
LLM 自己判断要不要用工具、用哪个。这是最灵活的模式,也是大多数场景的默认选择。LLM 可能决定不用任何工具直接回答,也可能连续调用多个工具。
模式二:tool —— 强制指定某个工具
"tool_choice": {"type": "tool", "name": "get_weather"}
你替 LLM 做了选择,它只需要负责填参数。这个模式在跟 embedding 检索配合时特别有用 —— 如果检索结果高度确定只需要一个工具,直接指定,LLM 连选都不用选,减少了一层决策的不确定性。
模式三:any —— 必须用工具,但哪个由 LLM 选
"tool_choice": {"type": "any"}
强制 LLM 必须调用至少一个工具,不允许直接文本回答。适用于你明确知道这个问题一定需要外部数据的场景,避免 LLM "偷懒"直接编造答案。
模式四:none —— 禁止用工具
"tool_choice": {"type": "none"}
即使定义了工具也不让用。适用于你只想让 LLM 基于已有上下文(包括之前工具调用的结果)做总结和推理的场景。
四种模式的工程意义: 它们让客户端可以根据不同阶段动态调整 LLM 的行为。比如一个复杂的分析任务,前几轮用 any 强制收集数据,最后一轮用 none 强制做总结。这种精细控制是纯 MCP 协议层面做不到的。
7.3 Prompt Caching:工具描述的成本优化
Anthropic 允许在工具定义上标记缓存控制:
{
"tools": [
{
"name": "tool_1",
"description": "...",
"input_schema": {...}
},
{
"name": "tool_27",
"description": "...",
"input_schema": {...},
"cache_control": {"type": "ephemeral"}
}
]
}
在最后一个工具上打 cache_control 标记,整个 tools 数组都会被缓存。下一轮对话如果工具列表没变,缓存命中,这部分输入 token 费用降 90%。
这个设计的前提是工具列表在多轮对话中保持稳定 —— 而这恰恰是 MCP 场景的常态。你连上一个 MCP 服务后,工具列表在整个会话期间通常不会变。
缓存命中的条件很严格: 工具列表的顺序不能变、描述不能改、连一个字都不能动。这也是为什么很多 MCP 客户端实现会把工具定义放在 system prompt 最前面、保持固定顺序 —— 就是为了最大化 cache 命中率。
7.4 多轮工具调用的完整流程
Claude API 的工具调用不是一次性的,是一个客户端驱动的循环:
第 1 轮:
客户端 → Claude: 用户问题 + 工具定义
Claude → 客户端: stop_reason="tool_use", 返回要调用的工具和参数
第 2 轮:
客户端执行工具(通过 MCP 协议调用远程服务)
客户端 → Claude: 工具执行结果(role="tool", tool_use_id=xxx)
Claude → 客户端: 可能再次返回 tool_use(需要调另一个工具)
第 3 轮:
客户端再次执行工具
客户端 → Claude: 第二个工具的结果
Claude → 客户端: stop_reason="end_turn", 返回最终文本回答
每一轮都是一个完整的 API 请求。MCP 客户端就是在这个循环里充当翻译官 —— 把 Claude 的 tool_use 响应翻译成 MCP 的 call_tool 请求,把 MCP 服务的响应翻译成 Claude 的 tool_result 消息。
关键细节: 每一轮 API 请求都要带上完整的对话历史(包括之前的工具调用和结果)。这意味着随着工具调用轮次增加,上下文在持续膨胀。这也是为什么工具返回的数据量需要控制 —— 一个工具返回 10KB 的 JSON,三轮调用下来就是 30KB 额外的上下文。
7.5 没有的东西:服务端工具注册
Anthropic API 目前没有服务端侧的工具管理。每次请求你都要把完整的 tools 数组传过去。没有"注册一次,后续自动带上"的机制。也没有"根据对话内容自动加载相关工具"的智能路由。
这意味着工具管理的全部责任在客户端。客户端决定每次请求带哪些工具、带多少个、用什么 tool_choice 模式。这也是为什么 embedding 筛选、Agent 路由这些方案有价值 —— 它们都是在客户端侧填补这个空白。
7.6 Anthropic 的设计哲学总结
| 层面 | Anthropic 的做法 | 设计意图 |
|---|---|---|
| 工具定义 | 跟 MCP 统一 schema | 生态无缝对接 |
| 选择控制 | tool_choice 四种模式 | 给客户端精细控制权 |
| 成本优化 | Prompt Caching | 降低重复工具描述的费用 |
| 执行流程 | 多轮循环,客户端驱动 | 保持协议简单,复杂逻辑外推 |
| 动态管理 | 不管 | 交给客户端/社区自己搞 |
一句话概括 Anthropic 的策略:协议标准和推理能力我提供,工具编排的自由度和责任都交给你。 这种设计给了开发者最大的灵活性,但也意味着要做出好的体验,客户端的工程能力至关重要。
八、最终认知:MCP 只是起点,Agent 才是关键
经过这一整轮的探索,我得出了一个重要结论:
只搞 MCP 不搞 Agent,做不出好的体验。
MCP 只是一个管道,它解决的是"工具怎么接进来"的问题。但用户体验取决于管道两头:一头是工具本身的质量,另一头是 Agent 怎么编排这些工具。
拿 SIF 举例,用户说"帮我分析一下这个 ASIN 最近流量为什么跌了",一个好的 Agent 应该:
- 先调
ops_get_asin_traffic_trend看整体趋势 - 发现某周下跌,调
ops_get_asin_traffic_trend_detail下钻到关键词级别 - 发现是广告流量跌了,调
ads_get_asin_ad_traffic_trend看广告侧 - 综合给出结论
这是一个 4 步的推理链,每一步的决策都依赖上一步的结果。这个编排逻辑,MCP 协议本身一个字都不管。
MCP 大概占整个工作量的 20%,Agent 编排占 80%。
九、总结:这次探索的收获
| 阶段 | 收获 |
|---|---|
| 动手连接 | MCP 协议的实际工作方式(Streamable HTTP、list_tools、call_tool) |
| 追问机制 | 工具 schema 常驻上下文,数据按需加载 |
| 发现问题 | 上下文膨胀、注意力稀释 |
| 自己想方案 | Agent 路由(方向对,但太重) |
| 学到更优方案 | Embedding 工具检索(快、无感知、彻底减少无关信息) |
| 深入底层 | KV Cache 解决计算成本,但不解决注意力稀释;两者互补 |
| 全局认知 | MCP 是管道,Agent 编排才是体验的关键 |
这条路径的价值不在于记住了多少知识点,而在于建立了一个**从"会用"到"理解原理"再到"能优化"**的思考框架。这个框架可以迁移到任何新技术的学习上。
本文基于实际动手探索和深度对话整理而成。所有代码和数据导出均为真实操作记录。
陕公网安备61011302002223号