首发于公众号 code进化论,欢迎关注。
理论知识
随着大语言模型(LLM, Large Language Model)如 GPT、Claude、DeepSeek 等在自然语言处理任务中取得显著突破,越来越多的系统开始尝试将其作为核心能力集成进各类智能应用中。然而,在实际应用过程中,我们逐渐发现,大语言模型在设计理念和实现方式上存在一些结构性缺陷,这些缺陷直接限制了其在高可靠性、高实时性场景中的表现,也成为推动“增强型语言系统”兴起的重要动因。
缺陷
缺陷一:知识封闭,难以实施更新
大语言模型的训练方式决定了其知识边界是静态的,模型训练往往使用某一时间点前收集的大规模语料,在此基础上通过长时间离线训练获得语言生成能力。一旦训练完成,其内部参数便不再具备学习能力,也无法通过外部交互进行知识更新。就像是一个知识丰富但是被困在屋子里的人,只能依靠自己已有的知识回答问题。
举个例子:GPT-3 是 2020 年训练的,即使你问它 2023 年 NBA 总冠军是谁,它也答不出来——它不知道!
缺陷二:无法调用外部工具
大模型本质上是一个语言生成器,而不是任务执行器,简单来说它就是一个聊天机器人,连最简单的天气查询都做不到,例如当用户说:
“请帮我查一下明天下午北京的天气。”
普通的大语言模型并不会真的去查天气数据,而是可能这样回答:
“明天下午北京可能会是晴天,气温在20度左右。”
这听起来像是个回答,但它完全是模型根据过去训练数据中出现过的天气描述“猜”的,并不来自于任何真实的天气数据源。
而要构建一个简单的天气预报 agent 就需要大模型能够调用类似 OpenWeather 这样的天气查询工具。
方案
RAG的提出:解决知识滞后
检索增强生成(RAG,Retrieval-Augmented Generation) 是一种结合信息检索和生成模型的技术框架。它通过将大语言模型(LLM)的生成能力与外部知识库的检索能力结合,使 AI 应用在回答问题或生成内容时更准确。
简单来理解 RAG 系统所做的事情是帮助大模型临时性地获得他所不具备的外部知识,允许它在回答问题之前先找答案。
Function Calling的提出:赋予模型行动力
解决知识问题只是第一步,如何让大模型具备调用外部工具的能力,才是其向智能体(Agent)演进的关键。为此,Function Calling(函数调用)机制应运而生。
Function Calling 是 Open AI 于 2023 年 6 月份首次提出的方案,可查看原文链接。
由于底层的技术限制,大模型本身是无法和外部工具进行交互的,因此 Function Calling 的思路就是创建一个外部函数作为中介:
这样大模型就能间接的去调用外部工具,下面是 OpenAi 官方给出的一个示例,展示了整个 Function Calling 的执行流程:
-
当用户输入 query 后,agent 首先会将已有的工具函数及其描述&query送给大模型。
from openai import OpenAI import json client = OpenAI() tools = [{ "type": "function", "name": "get_weather", "description": "Get current temperature for provided coordinates in celsius.", "parameters": { "type": "object", "properties": { "latitude": {"type": "number"}, "longitude": {"type": "number"} }, "required": ["latitude", "longitude"], "additionalProperties": False }, "strict": True }] input_messages = [{"role": "user", "content": "What's the weather like in Paris today?"}] response = client.responses.create( model="gpt-4o", input=input_messages, tools=tools, ) -
大模型会根据用户 query 的意图分析调用哪一个工具,并将工具函数名及其需要的参数输出。
[{ "type": "function_call", "id": "fc_12345xyz", "call_id": "call_12345xyz", "name": "get_weather", "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" }] -
agent 拿到工具函数及其参数后,会执行代码调用函数,拿到工具函数返回的参数。
-
agent 再将之前的信息和工具函数的返回值送给大模型,让大模型进行总结。
-
大模型返回总结后的结果输出给 agent,最终展示给用户。
Function Calling 这个能力确实是挺好的,给了大模型赋予了更多的能力,目前像 coze 的插件系统、千帆 Appbuilder 里的组件系统都是基于 Function Calling 的思路来封装的。
但是它有一个比较大的缺点,就是实现成本太高了,主要体现在几方面。
-
编写外部函数的成本高
一个简单的外部函数往往就要上百行代码,同时为了让大模型能够认识这些外部函数,需要开发者为这些外部函数编写 JSON Schema 格式的功能说明。
-
微调大模型以支持 Function calling 能力
通过微调大模型让他能够认识工具函数,并构造出函数所需要的参数。
-
协议碎片化(重点)
OPEN AI最开始提出这项技术的时候,并没有想让它成为一项标准,所以虽然后续很多模型也支持了Function Call的调用,但是各自实现的方式都不太一样。这也就意味着,如果我们要发开一个
Function Call工具,需要对不同的模型进行适配,比如参数格式、触发逻辑、返回结构等等,这个成本是非常高的。所以对与开发者来说,大部分情况还需要借助像 Dify、coze、Appbuilder 等平台来开发 Ai Agent。
-
设计一套提示词模板
为了提高 Function Calling 的准确率,可能还需要开发者去设计一套提示词模板,例如千帆 Appbuilder 提供的角色指令功能。
详细的函数定义说明可查看 OpenAI 文档。
什么是MCP?
MCP(Model Context Protocol,模型上下文协议)是一种由 Anthropic 公司推出的一个开放标准协议,旨在标准化大型语言模型(LLM)与外部数据源和工具的集成,其核心目标是解决“MxN”问题,即将 M 个不同的 LLM 与 N 个不同的工具集成的组合复杂性。通过提供一个通用的开放标准,进而取代了 Function Calling 碎片化的定制集成。
MCP整体架构
MCP组成部分
MCP Host
Host 指的是内置了 MCP Client 的应用程序,例如 Claude Desktop、Cursor,内部包含了对大模型的调用,调用 MCP Client 连接 MCP Server 等。在 MCP 的官方文档中已经列举出了目前支持 MCP 协议的 MCP Host:
MCP Client
Client 位于 Host 内部,负责通过标准的 MCP 协议与 MCP Server 建立连接和交互。
MCP Server
server 是一个轻量级应用程序,负责实现各种和三方资源交互的逻辑,比如访问数据库、浏览器、本地文件,然后通过标准化模型上下文协议公开其功能,client 连接到 server 之后能够访问其公开的功能。MCP Server 主要提供三种能力:
- 资源(Resources)类似文件的数据,可以被客户端读取(如 API 响应、文件内容)。
- 工具(Tools):LLM 可以调用的函数(需要用户授权)。
- 提示词(Prompts):预先编写的模板,帮助用户完成特定任务。
这三个组成部分的关系如下图所示:
有了 MCP 协议之后,开发者只需要为每个工具按照 MCP 协议开发一个 MCP Server,不同的大模型只需要通过 MCP Client 和 MCP Server 建立连接就能使用 Server 中提供的功能,进而间接的调用外部工具,这可以大大减少开发者的工作量。
目前大部分第三方工具都提供了对应的 MCP Server,例如百度地图、高德地图、Github等,想查找更多 MCP Server 可以到下面几个网站:
- MCP Server github(github.com/modelcontex…)
- MCP Market(mcpmarket.cn/)
- MCP.so(mcp.so/)
使用案例
现在我们成热打铁,使用一个案例来带大家快速上手 MCP,这里我们选用 VsCode + Cline 来演示。
在这之前需要先保证电脑上已经安装 nodejs环境,因为下面演示的 MCP Server 需要在 node 环境运行。
在 vscode 中安装 Clien
模型配置
这里要配置两部分:
- 模型提供商选择 OpenRouter,因为它提供了免费的 deepseek v3
- 选择 deepseek-chat:free 模型
安装MCP Server
在 cline 的 MCP Server 商场中选择 github 对应的 MCP Server 并进行安装。
最终 cline 会提示如果要访问你的 github 仓库需要输入你的 API Key
这里我们只需要按照提示点击链接去生成一个 API Key,并给这个 API Key 赋予相应的操作权限,比如仓库内容的访问权限、仓库的创建、删除权限等:
查看是否连接
在MCP Server 页面选择已安装的 MCP Server,如果显示绿的按钮,就表示已经连接,并可以查看 MCP Server 提供的工具。
调用MCP Server
接下来就可以直接调用 MCP Server 中的工具来操作 github 仓库,比如在输入框中输入查询 query:
我的github名字是xxx,我有哪些仓库
最终大模型会返回 github 下所有的仓库。
MCP流程解析
看到这里其实大家基本对 MCP 有了一个初步的了解,但是大多数人对整个 MCP 运行的流程肯定还存在疑问,比如
- MCP Clinet 是怎么知道 MCP Server 都提供了哪些工具列表的。
- 大模型怎么从这些工具中选择出合适的工具。
- MCP 是如何解决 Function Calling 的弊端。
接下来就会带大家使用上面的案例来一步步解析 MCP 完整的流程,在这个过程中会使用抓包工具 Charles 对 Cline 进行抓包,抓包的信息主要分为两部分:
-
MCP Host 和大模型之间的通信。
-
MCP Client 和 MCP Server 之间的通信。
由于上面的使用案例使用的是 stdio 的通信方式,我们会在后面的章节通过手动开发一个使用 SSE 作为通信方式的 MCP Server,这样就能进行抓包分析。
配置抓包工具
由于 Charles 并不能直接抓取到 VsCode 的请求,我们需要在 VsCode 下配置请求代理,在 Vscode 的配置页搜索 http.proxy,然后在其中填写 http://127.0.0.1:8888,这样可以将 Vscode 的请求都代理到 Charles 下:
这样后续 openRouter 的请求就能在 charles 看到:
这里要注意的是请求的返回结果都是流式的(sse请求),所以返回的信息都是一段一段的,我们可以直接把这些消息全部复制出来保存到一个 txt 文件,让 chatgpt 帮忙合并一下:
请求分析
从 Charles 的抓包结果看,Cline 调用了两次 LLM API,下面对这两次请求进行分析:
-
第一次请求
第一次请求的请求体包含两部分内容,第一部分是一段很长的系统提示词,告诉大模型他的角色信息、可用工具等,第二部分是用户的 query 信息。
系统提示词主要包含五部分:
-
角色指定
-
工具使用介绍
-
MCP Server 信息介绍
-
约束信息
-
目标工作流程
因此只要大模型能读懂系统提示词就能读懂 MCP Server 提供的工具及其使用方式,这也就解决了 Function Calling 中存在的 n * m 的问题。
-
-
第一次请求的响应
这里的返回和系统提示此中要求的是一致的,在 中展示大模型对用户需求的分析及满足客户需求要调用的工具。在 <use_mcp_tool> 标签中展示了调用的工具及其传入的参数。
-
第二次请求
第二次请求的请求体内容在第一次请求的基础上加了两条:
- 第一次请求的返回结果,即大模型思考和需要调用的工具信息。
- 调用工具后的返回结果
将这些信息传给大模型进行最终的答案总结。
-
第二次请求响应
大模型根据输入的信息产出最终回答给用户的内容,下面是合并 sse 消息之后的结果。
流程总结
MCP Server原理
mcp server 本质上就是一个服务,通过标准化模型上下文协议公开特定功能,MCP Client 与 MCP Server 建立连接之后可以通过特定的方式进行通信,在这个过程涉及了两个问题:
- 如何以及何时启动 MCP Server。
- MCP Client 如何和 MCP Server 进行通信。
通信方式
MCP Server 支持两种通信方式:STDIO 和 SSE。
-
STDIO(标准输入输出)
MCP Client 将 MCP Server 作为一个子进程启动,运行在本地环境,它们通过标准输入/输出流进行通信,消息格式为 JSON-RPC ,整体流程如下:
例如 Github-MCP-Server、Filesystem-MCP-Server 都是 STDIO 类型的。如果想将 STDIO 的通信方式转成 SSE,可以使用 supergateway 进行转换**。**
-
SSE(服务器推送事件)
MCP Server 会部署为一个远程服务,MCP Client 通过 HTTP 协议连接远程服务器,服务器可以主动推送数据。
启动 MCP Server
不同通信方式的 MCP Server 启动方式会有所不同,使用 STDIO 的 MCP Server 需要在本地环境启动一个单独的进行运行,而使用 SSE 的 MCP Server 是一个已部署的远程服务,接入简单,下面通过分析两者的配置文件来介绍。
-
STDIO(标准输入输出)
这里以 Github-MCP-Server 为例:
{ "mcpServers": { "github.com/modelcontextprotocol/servers/tree/main/src/github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "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" }, "disabled": false, "autoApprove": [] } } }-
command
指定如何启动该 MCP Server,这里使用
npx。npx是Node.js生态系统中的一个命令行工具,它的主要功能就是帮助我们快速运行一些通过npm安装的工具,而不需要我们手动去下载安装这些工具到全局环境。除了
npx我们也可以通过node、python等工具来启动 MCP Server,这个具体要看 MCP Server 的实现。 -
args
传递给
npx命令的参数,"-y"其实就等同于 --yes,其作用是在执行命令时自动同意所有提示信息,可以避免交互式的确认步骤,第二个参数其实就是这个npm包的名字。 -
env
定义了环境变量,用于传递配置或授权信息给 MCP Server。
-
-
SSE(服务器推送事件)
{ "mcpServers": { "baidu-maps": { "url": "https://mcp.map.baidu.com/sse?ak=您的AK" } } }基于 SSE 通信的 MCP Server 是一个远程的服务,所以只需要知道这个远程服务的 url 就能连接 MCP Server,MCP Server 所需要的鉴权参数通过 SearchParams 传入。
如何开发一个MCP Server
在官方文档中可以找到 MCP Server 的开发方式,并且官方提供了各个语言的 SDK 的示例代码:
整个开发流程写的非常清晰,跟着官方的流程就能快速开发一个 MCP Server,但是官方文档中只给出了 STDIO 类型的 MCP Server 开发示例,如果想学习 SSE 类型的 MCP Server 开发流程可以到 SDK 的 github 仓库中找到对应的示例:
创建MCP Server实例
使用官方提供的 SDK @modelcontextprotocol/sdk/server/mcp.js ,创建一个 McpServer 实例:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
/**
* 创建 MCP 服务器实例
*/
const server = new McpServer({
name: "calculate", // 服务器名称
version: "1.0.0", // 服务器版本
});
定义工具
import {z} from "zod";
/** 增加一个计算两数之和的工具 */
server.tool(
"calculate_sum",
"计算两个数的和",
{
a: z.number().describe('一个数字'),
b: z.number().describe('一个数字')
},
async ({a, b}) => ({
content: [{type: "text", text: String(a + b)}]
})
);
/** 增加一个计算两数之积的工具 */
server.tool(
"calculate_multiply",
"计算两个数的乘积",
{
a: z.number().describe('一个数字'),
b: z.number().describe('一个数字')
},
async ({a, b}) => ({
content: [{type: "text", text: String(a * b)}]
})
);
使用 server.tool 定义工具,包括工具方法的名称、工具方法的描述、工具方法的参数、工具方法的具体实现逻辑。
启动 Server
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
const startServer = async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");
}
startServer().catch((error) => {
console.error("Fatal error in startServer():", error);
process.exit(1);
});
启动 Server 时需要传入使用的通信工具,这里使用的 StdioServerTransport 通信协议,启动后等待外部的标准输入,并且把工具的执行结果转化为标准输出反馈到外部。
Cline接入 MCP Server
在 Cline 的 MCP Server 配置中加入刚刚创建的 MCP Server,这里需要注意的是由于我们创建的 MCP Server 是一个 STDIO 的 Server,所以需要在配置中配置他的启动命令,这里使用的是 node ,因为我们的产出就是一个本地的 js 文件,同时将产物的地址作为参数传给 node 命令。这样就能运行这个 js 文件启动服务。
参考资料
【1】Function Calling: openai.com/index/funct… 【2】标准输入输出:en.wikipedia.org/wiki/Standa… 【3】JSON-RPC:en.wikipedia.org/wiki/JSON-R… 【4】supergateway: github.com/coding-alt/… 【5】服务器推送事件: developer.mozilla.org/en-US/docs/… 【6】mcp官方文档: modelcontextprotocol.io/quickstart/…