从连上一个 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()

几个发现:

  1. 传输方式是 Streamable HTTP,不是旧版的 SSE。一开始用 sse_client 连接返回 405 错误,换成 streamablehttp_client 才成功。这说明 MCP 协议在演进,新版服务已经在用更新的传输方式。

  2. 服务只暴露了 Tools 能力,不支持 Resources 和 Prompts。这是一个纯工具型的 MCP 服务。

  3. 共 27 个工具,覆盖关键词分析、流量运营、广告洞察三大领域,按 ASIN → Campaign → Ad Group 三层结构组织。

  4. 有一个 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 应该:

  1. 先调 ops_get_asin_traffic_trend 看整体趋势
  2. 发现某周下跌,调 ops_get_asin_traffic_trend_detail 下钻到关键词级别
  3. 发现是广告流量跌了,调 ads_get_asin_ad_traffic_trend 看广告侧
  4. 综合给出结论

这是一个 4 步的推理链,每一步的决策都依赖上一步的结果。这个编排逻辑,MCP 协议本身一个字都不管。

MCP 大概占整个工作量的 20%,Agent 编排占 80%。


九、总结:这次探索的收获

阶段 收获
动手连接 MCP 协议的实际工作方式(Streamable HTTP、list_tools、call_tool)
追问机制 工具 schema 常驻上下文,数据按需加载
发现问题 上下文膨胀、注意力稀释
自己想方案 Agent 路由(方向对,但太重)
学到更优方案 Embedding 工具检索(快、无感知、彻底减少无关信息)
深入底层 KV Cache 解决计算成本,但不解决注意力稀释;两者互补
全局认知 MCP 是管道,Agent 编排才是体验的关键

这条路径的价值不在于记住了多少知识点,而在于建立了一个**从"会用"到"理解原理"再到"能优化"**的思考框架。这个框架可以迁移到任何新技术的学习上。


本文基于实际动手探索和深度对话整理而成。所有代码和数据导出均为真实操作记录。

Read more

MCP 服务端的隐藏设计:结论性数据如何改变

Agent 的工作方式 我们以为 MCP 服务只是查数据的管道,拆开一看,发现服务端已经把分析结论都算好了。这个发现改变了我对 Agent 架构的理解。 起因:一次对 MCP 服务的逆向探索 最近在研究 MCP(Model Context Protocol)的实际应用,我选了一个真实的商业 MCP 服务 —— 某电商卖家流量分析平台作为研究对象。该服务提供了 27 个工具,覆盖关键词分析、流量运营、广告洞察等领域。 最初的预期很简单:MCP 服务就是一个数据接口,Agent(LLM)调用它拿到原始数据,然后自己分析、得出结论、给用户建议。 实际拆开一看,完全不是这么回事。 第一个发现:返回数据里藏着完整的分析结论 我写了一个 Python 脚本,绕过所有 AI 客户端,直接用

By ladydd

FastAPI 异步任务服务的并发设计演进:从单进程轮询到多 Worker 协程直处理

本文记录了一个 FastAPI 异步任务服务在并发架构上的思考和演进过程。这个服务的本质很简单:接收客户端请求,转发给下游 AI API,把结果存起来供客户端轮询。它不做复杂的业务计算,不做数据聚合,就是一个纯转发层——接活、派活、存结果。正因为场景足够简单,我们才有机会做一次化繁为简的架构妥协,把原本"看起来该用任务队列"的设计砍到只剩三行核心配置。 一、先说清楚场景:我们到底在干什么 这个服务做的事情可以用一句话概括: 客户端提交参数 → 服务转发给下游 AI API → 等结果 → 存 Redis → 客户端来取。 关键特征: * 纯 IO 转发:服务本身不做任何 CPU 密集计算,所有耗时都花在等下游 API 返回。一次调用几秒到几十秒不等,全是网络等待。 * 异步模式:客户端提交任务后立即拿到 task_id,

By ladydd

OpenCLI 学习 08:现实约束与兼容层思路

1. 我当前面对的现实约束 虽然我现在越来越倾向于: * 上层做 Agent * 下层做 Harness 但现实里调用我的人,很多时候只会通过 API 的形式来调用能力。 这意味着: * 我未必能决定上层最终长成什么样 * 外部接入形式可能仍然是 HTTP、函数调用或者一次性接口 2. 我当前的重要判断 我现在认为,这并不和 Agent + Harness 的方向冲突。 更合理的理解是: * Agent + Harness 是内部核心结构 * API、函数调用、HTTP 等形式是外部兼容层 也就是说,我真正需要先做好的是: * Agent 的能力设计 * Harness 的抽象与落地 * Agent 和 Harness 之间的接口关系 而不是一开始就被外部接入形式绑死。 3. 一个重要认识:不是 API 和 Agent 二选一 我当前更认可的分层是:

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号