MCP协议

MCP 协议:屏蔽了LLM和Tool之间的差异

大家都感受到了,最近两年 AI 简直火得一塌糊涂。而且现在的 AI 越来越好用,不再只是陪你干聊,而是能实打实帮你写代码、查数据库、跑自动化的Agent了。Agent想要干活,就必须得学会“使用工具”。那么问题来了:AI是怎么学会用工具的?使用工具为什么需要MCP协议?MCP协议解决了什么问题?是怎么解决的?以及一个why if问题。
今天这篇博客,主要分享一下我自己对于MCP协议的浅显理解。主要围绕上述问题展开,并在最后实现一个简单的MCP Server。


AI如何调用工具?

AI本质上就是LLM,大语言模型,目前AI不具备自我思考的能力,一旦训练好,只能去预测,所有的输出都是预测的。
我们想知道今天天气,ai肯定不知道的,他知识是静态的,他不能去查询天气,他能预测token。
可以通过prompt约束,例如:

# Role
你是一个智能助理。你目前的知识库是静态的,你无法得知任何实时信息(如天气、股票、新闻)。

# Execution Protocol
当用户向你提问时,你必须在大脑中评估:你现有的静态知识能否百分之百准确地回答。
1. 如果可以,请直接回答用户。
2. 如果需要实时信息(例如问天气),你【绝对不允许】编造,必须【立刻停止】正常文本输出,仅输出以下约定的【工具指令暗号】。

# Tool Format (你的唯一暗号)
[TOOL_QUERY: 接口名, 参数键值对]

## 可用接口:
- 接口名: fetch_weather
  功能: 查询指定城市的实时天气
  参数格式: city="城市名"

# Hard Constraints (硬性死命令)
- 如果决定调用工具,你的整个回复中【只能】包含该暗号,不能带有任何“好的”、“没问题”、“以下是”等废话,否则系统会崩溃。
- 严禁向用户暴露这个暗号格式。

后台代码收到这样特定的格式之后,再去调用工具。把结果填充到当前上下文中。这样模型就可以通过prompt约束来实现调用工具的能力了。早期模型不支持function calling的时候确实是这样做的,但是这样有很多问题,主要是约束性问题,模型的输出都是预测的,有时候我们想调用工具,他的输出不一定完全符合我们的约束。第二个问题是工具调用和正常的回答都是混在一起的,一个输出通道,后台代码不知道到底是工具调用还是用户回答。

为了解决上面的问题,出现了Function Calling,Function Calling(工具调用/函数调用)并不是什么魔法,他是通过“特定数据微调”让大模型具备的一种专项能力。就是通过监督微调,让LLM具备这个工具调用的特定,需要工具调用的时候输出特殊的响应。约束性更强,并且工具调用和预测是两种不同的输出格式。

工具调用输出
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_1a2b3c",
            "type": "function",
            "function": {
              "name": "fetch_weather",
              "arguments": "{\"city\": \"上海\"}"
            }
          }
        ]
      }
    }
  ]
}
回答用户问题
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "你文章里写的 `[TOOL_QUERY: fetch_weather, city=\"上海\"]` 这个例子非常好,读者能看懂。",
        "tool_calls": null
      }
    }
  ]
}

有了Function Calling之后,我们询问AI上海的天气怎么样,到最终得到结果的过程是这样的:

  • 用户询问:“上海的天气怎么样?”
    • 模型返回特殊的tool_use响应
    • 后台代码拦截、解析并调用真正的api得到上海的天气,比如:{"temperature": "15°C", "weather": "阴"}。
    • 后台代码将这个结果响应给ai,ai看到这个完整的上下文后,才会吐出最终的用户可见文本:“上海今天阴天,气温 15°C。”
  • AI回答:“上海今天阴天,气温 15°C。”
// 后台发给大模型的最终上下文
[
  {"role": "user", "content": "上海今天天气怎么样?"},
  {"role": "assistant", "tool_calls": [...]}, // 刚才模型自己生成的
  {"role": "tool", "tool_call_id": "call_1a2b3c", "content": "{\"temperature\": \"15°C\", \"weather\": \"阴\"}"} // 后台塞给它的结果
]

需要注意的是大模型本身永远无法直接访问互联网或运行本地代码。无论是早期的Prompt方案还是现在的Function Calling,大模型扮演的角色始终是“决策者”和“参数提取器”,而不是“执行者”。他只是看到问题->判断需要工具->输出标准的JSON参数->停下来,等待后台代码去真正发起网络请求->后台把结果喂回给大模型->大模型总结回答。


为什么需要 MCP?

MCP(Model Context Protocol),即模型上下文协议,是由 Anthropic 提出的一种开放标准。

简单来说,它就像是 AI 领域的 USB 接口标准,它为大语言模型(LLM)与外部数据源、工具(Tools)之间的通信制定了一个统一的规范。屏蔽了不同大模型和工具之间的差异。

在 MCP 出现之前,我们调用某个查询天气的工具是通过 【各家模型原生API的tools字段参数】和【应用层手写的硬编码解析逻辑】完成的。如果你要在一个复杂的系统里接入十几个工具,你的架构会变成一张混乱的蜘蛛网(即M×N的对接灾难):

  • 对接 OpenAI,你需要按照OpenAI的Schema规范去写一套工具描述
  • 对接Gemini,你得把同样的工具描述换成Google的数据结构重新写一遍
  • 对接Claude,又得换成Anthropic的tool_use协议重新适配

如果我们针对某个模型写了很多工具调用的 Prompt、定义和参数的编码解码逻辑,一旦换 LLM,这些全部都需要推倒重来。因为不同 LLM 原生调用 API 的格式完全不同,本质上是各家大厂在对模型进行监督微调(SFT)时使用的工具交互数据集和协议标准各不相同。

MCP 在 LLM(大模型)和 Tool(外部工具)之间抽象出了一层标准的 Client-Server 架构。
在这种架构下,我们(大模型应用)要调用一个工具,不再是直接去写代码对接API,而是需要通过以下三个标准的协议动作来完成:

  • 工具发现:MCP Client(应用端)向 MCP Server(工具端)发送一个标准的tools/list请求。Server会立刻吐出当前可用的所有工具清单(包含工具名、功能描述、参数 JSON Schema)。Client 负责把这个清单作为上下文(Context)喂给LLM
  • 工具调用:LLM 读完上下文后,通过语义理解做出决策,吐出标准的调用指令。MCP Client 收到后,将其转化为一个JSON-RPC 2.0标准请求发送给Server:
{
  "method": "tools/call",
  "params": { "name": "工具名", "arguments": { "参数名": "参数值" } }
}
  • 结果返回:MCP Server收到请求后,在本地执行对应的代码(如查数据库、调API),并将结果包裹在标准的content数组对象中(明确标注是文本还是图片)返回给 Client,Client再喂回给模型完成闭环

所以,MCP 本质上就是在 LLM 与外部世界之间建立的一个“应用层网关协议”。只要我们的工具实现了MCP协议,那么我们只需要开发一次,任何LLM都可以通过MCP Client来调用。

Snipaste_2026-06-03_16-48-25

可以看到MCP把LLM和Tool之间的M×N点对点适配,转化为M+N的即插即用。

假设所有LLM监督微调的数据都一样,也就是说所有LLM调用api的格式都一样,那么就没有差异了,也就不需要MCP了。

在软件工程中,任何架构的演化都是一场权衡。MCP虽然通过抽象一层,终结了多对多的适配问题,但也引出了三个问题:

  • 安全性:统的工具调用是开发者自己手写的,安全完全可控;而MCP鼓励引入第三方的、开源的MCP Server,这直接让安全防御陷入了“零信任”黑洞
  • 如何抉择:当工具箱里只有3把扳手时,AI很难选错;但当MCP为AI送来100把形态各异的螺丝刀时,AI就会陷入“抉择困难”
  • 上下文膨胀:一旦引入一个 MCP Server,MCP Cient就会去调一次tools/list,而这个接口吐出来的不是几个简单的工具名字,而是把该Server下所有工具的“长篇大论说明书”打包全吐出来。Client又把这一大坨说明书无脑塞进了上下文。这才导致了 Token 的恐怖膨胀。

如何实现一个 MCP Server?

实现一个 MCP Server 其实非常简单,官方提供了 Python 和 TypeScript 的 SDK。以下是一个使用 TypeScript 实现的简单伪代码示例:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// 1. 初始化 MCP 服务端
const server = new Server({
  name: "my-weather-server",
  version: "1.0.0"
}, {
  capabilities: { tools: {} }
});

// 2. 注册一个工具(Tool)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "get_weather",
    description: "获取指定城市的实时天气",
    inputSchema: {
      type: "object",
      properties: { city: { type: "string" } },
      required: ["city"]
    }
  }]
}));

// 3. 处理 AI 的调用逻辑
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "get_weather") {
    const city = request.params.arguments?.city;
    // 在这里写你实际的业务逻辑(比如请求天气API)
    return { content: [{ type: "text", text: `${city}今天晴,25℃` }] };
  }
  throw new Error("Tool not found");
});

// 4. 启动服务(通过标准输入输出通信)
const transport = new StdioServerTransport();
await server.connect(transport);
posted @ 2026-06-03 16:50  布鲁si  阅读(88)  评论(0)    收藏  举报