ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Function Calling到MCP:AI工具集成的协议化演进与实战

从Function Calling到MCP:AI工具集成的协议化演进与实战 1. 项目概述从“黑盒”调用到“协议”对话如果你在过去一年里折腾过AI应用开发尤其是基于大语言模型LLM构建能“动手做事”的智能体Agent那么“Function Calling”这个词你一定不陌生。它就像给一个博学但“手无缚鸡之力”的哲学家配上了一双可以操作现实世界的手让LLM能够调用外部工具比如查询天气、发送邮件、操作数据库。然而随着我们构建的Agent越来越复杂需要调用的工具成百上千来自不同的开发者、不同的团队时传统的Function Calling模式开始显得捉襟见肘。这时一个名为**MCPModel Context Protocol**的协议开始进入视野它试图从根本上重新定义AI与工具之间的“对话”方式。今天我们就来彻底拆解一下从Function Calling到MCP这背后究竟是如何从一种“一次性指令”演进为一种“标准化通信机制”的。简单来说Function Calling是LLM如GPT-4提供的一种请求-响应式的接口规范。你告诉模型有哪些函数可用名称、描述、参数模型在认为需要时会输出一个结构化的JSON请求你的程序解析这个JSON然后去执行对应的本地函数最后把结果再塞回给模型。这个过程高度耦合在你的应用代码里。而MCP则是由Anthropic提出并开源的一种标准化协议。它定义了一套AI模型客户端与外部工具、数据源服务器之间如何进行发现、调用和流式通信的通用语言。你可以把它想象成AI世界的“USB协议”或“HTTP协议”——只要工具方按照MCP协议实现一个“服务器”MCPServer任何支持MCP协议的AI模型或客户端如Claude Desktop、Cursor IDE就能即插即用地发现并使用它无需为每个工具单独编写集成代码。这个演进的核心是从“中心化集成”转向“去中心化互联”。对于开发者而言这意味着你开发的工具可以一次编写处处运行对于AI应用构建者这意味着你可以从一个丰富的、不断增长的“工具市场”中随意组合能力快速搭建强大的Agent。理解这两者的底层通信机制不仅能帮你更好地使用现有框架更能让你在设计自己的AI系统时做出更面向未来的架构决策。2. 核心机制深度对比Function Calling vs. MCP要理解为什么需要MCP我们必须先看清Function Calling在复杂场景下的局限性。这并不是说Function Calling不好相反它是让LLM具备实用性的关键一步。但当我们站在构建复杂AI Agent系统的角度它的设计哲学和实现机制就成了一种约束。2.1 Function Calling紧密耦合的“预编译”集成Function Calling的工作流程本质上是一个围绕单一LLM会话的闭环。其通信机制可以分解为以下几个步骤定义阶段开发时你在代码中硬编码一个工具函数列表。每个函数包括name函数名、description给模型看的自然语言描述、parameters遵循JSON Schema的参数定义。这个列表是静态的在应用启动时就确定了。# 一个典型的Function Calling工具定义示例 tools [ { type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气, parameters: { type: object, properties: { location: {type: string, description: 城市名例如北京}, unit: {type: string, enum: [celsius, fahrenheit], default: celsius} }, required: [location] } } } ]会话与提示阶段运行时当你将用户查询如“北京天气怎么样”和tools列表一起发送给LLM API时API内部会将工具列表作为系统提示的一部分隐式地指导模型“你可以使用这些工具”。模型决策与结构化输出LLM理解用户意图后如果判断需要调用工具它不会直接说“调用get_current_weather”而是输出一个严格的、预定义格式的JSON对象。这个对象通常包含tool_call_id本次调用的唯一ID用于匹配后续结果和具体的函数调用参数。{ role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: get_current_weather, arguments: {\location\: \北京\, \unit\: \celsius\} } } ] }应用层执行与回调你的应用程序收到这个JSON后解析它找到本地对应的get_current_weather函数传入参数执行获得结果如{“temperature”: 22, “condition”: “晴朗”}。然后你必须将这个结果以特定格式包含tool_call_id作为新一轮消息追加到对话历史中传回给LLM。# 将工具执行结果返回给LLM的格式 messages.append({ role: tool, content: {\temperature\: 22, \condition\: \晴朗\}, tool_call_id: call_abc123 # 必须匹配之前的ID })这个机制的“通信”本质是什么它其实是LLM与你的应用程序之间的一种私有、临时、会话内的约定。工具列表是静态注入的调用是同步的请求-响应所有逻辑都绑定在你的代码进程里。这就带来了几个核心问题工具发现僵化工具集在会话开始时固定无法动态增删。如果一个工具需要临时启动或连接这套机制无法处理。集成成本高每个新工具都需要你修改应用代码重新定义tools列表并实现调用逻辑。想用社区里别人写的好工具得先把他的代码扒下来改成符合你框架的格式。无法处理复杂工具对于需要长时间运行、产生流式输出如tail -f log、或需要双向通信如需要用户中途授权的工具Function Calling的单次请求-响应模式显得力不从心。上下文局限工具的描述和参数Schema是唯一的“说明书”模型无法在调用前进行更丰富的交互式探索比如先列出数据库有哪些表。2.2 MCP松散耦合的“协议化”通信MCP协议则采用了一种完全不同的思路。它模拟了人类使用计算机的方式我们通过一个统一的界面如Shell、RPC框架去发现和调用各种独立运行的服务。MCP的通信建立在客户端-服务器Client-Server模型之上通常使用标准输入输出stdio或HTTP作为传输层并通过JSON-RPC作为消息协议。其核心通信机制如下连接与初始化MCP客户端如Claude Desktop启动时会根据配置启动一个或多个MCP服务器进程每个工具或工具集是一个独立的Server。它们通过stdio或网络Socket建立连接。连接建立后双方会交换初始化信息协商协议版本。工具发现动态、实时连接成功后客户端会主动向服务器发送一个tools/list请求。服务器则响应一个动态的工具列表。这意味着工具列表不是在客户端硬编码的而是由服务器在运行时决定的。服务器可以根据当前状态、配置、权限等因素决定对外提供哪些工具。// 客户端请求 {jsonrpc: 2.0, method: tools/list, id: 1} // 服务器响应 { jsonrpc: 2.0, id: 1, result: { tools: [ { name: search_web, description: 在互联网上搜索信息, inputSchema: { type: object, properties: { query: {type: string} }, required: [query] } }, { name: query_database, description: 执行SQL查询, inputSchema: {...} } ] } }资源发现超越工具这是MCP比Function Calling更强大的一个概念。除了工具可执行的操作MCP服务器还可以暴露资源Resources——即一些可读的数据片段如文件列表、数据库表结构、API文档。客户端可以通过resources/list和resources/read来浏览和读取这些资源让AI在调用工具前先“了解”环境。例如一个SQL服务器可以先让AI读取数据库的schema再让AI生成查询语句。工具调用与流式响应当AI决定调用一个工具时客户端会向服务器发送tools/call请求。这里的一个关键增强是支持流式响应。对于耗时长或需要持续输出的操作如执行一个需要10秒的Shell命令服务器可以分多次返回partial结果最后返回complete。客户端可以实时地将这些更新呈现给用户或传递给AI进行中间思考。// 服务器流式响应示例 {jsonrpc: 2.0, method: tools/call/update, params: {callId: call_1, content: [{type: text, text: 正在搜索...}]}} {jsonrpc: 2.0, method: tools/call/update, params: {callId: call_1, content: [{type: text, text: 找到10条结果。}]}} {jsonrpc: 2.0, result: {callId: call_1, content: [{type: text, text: 完整结果...}]}, id: 2}协议化通信所有上述交互都通过标准的JSON-RPC 2.0消息进行。这意味着任何实现了JSON-RPC和MCP语义的客户端和服务器都可以互操作实现了真正的解耦。MCP通信机制的优势动态性工具和资源可以随时被添加、移除或更新无需重启客户端或修改AI应用代码。可组合性你可以同时运行多个MCP服务器一个管文件一个管数据库一个管网络搜索客户端自动聚合所有工具形成一个强大的工具集。能力增强资源发现和流式响应支持使得AI能进行更复杂、更交互式的任务。生态友好工具开发者只需关注实现MCP服务器就可以让工具接入所有兼容MCP的AI平台。用户像安装插件一样配置即可使用。注意Function Calling和MCP并非取代关系而是适用于不同层次。Function Calling是LLM提供商如OpenAI在API层面定义的、与模型推理紧密相关的交互格式。而MCP是应用架构层面用于连接AI与外部系统的通信协议。一个复杂的Agent系统内部可能依然使用Function Calling的格式与核心LLM交互但背后执行具体操作的“工具层”完全可以通过MCP协议来动态管理和调用。3. 实战解析构建一个简单的MCP服务器理解了理论最好的巩固方式就是动手。我们来构建一个最简单的MCP服务器它提供一个工具calculate能进行加减乘除运算。我们将使用Node.js和官方modelcontextprotocol/sdk来实现。3.1 环境准备与项目初始化首先确保你安装了Node.js建议18.x或以上版本。然后创建一个新目录并初始化项目mkdir simple-mcp-server cd simple-mcp-server npm init -y接下来安装MCP SDK。这个SDK提供了构建服务器和客户端所需的所有类型和工具函数。npm install modelcontextprotocol/sdk同时我们安装zod库用于更方便地进行参数验证虽然SDK内部已集成类似功能但zod能让我们写得更清晰。npm install zod现在创建一个名为server.js的文件作为我们服务器的入口。3.2 服务器核心代码实现我们将一步步构建服务器。MCP SDK的核心是创建一个Server实例并为其注册各种“能力”Capabilities的处理函数。// server.js const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { z } require(zod); // 1. 创建Server实例 // 第一个参数是服务器元信息第二个参数是能力声明。我们先声明支持tools能力。 const server new Server( { name: simple-calculator, version: 0.1.0, }, { capabilities: { tools: {}, // 声明本服务器提供工具 // 未来还可以添加 resources: {}, prompts: {} 等 }, } ); // 2. 定义工具的参数模式Schema // 使用Zod定义清晰的输入验证规则这比手写JSON Schema更易读、更安全。 const CalculatorArgsSchema z.object({ a: z.number().describe(第一个运算数), b: z.number().describe(第二个运算数), op: z.enum([add, subtract, multiply, divide]).describe(运算符: add(), subtract(-), multiply(*), divide(/)), }); // 3. 实现工具的处理逻辑 const calculateToolHandler async (request, extra) { // request.params 包含了客户端调用时传入的参数 const args request.params.arguments; try { // 使用Zod验证并解析参数 const { a, b, op } CalculatorArgsSchema.parse(args); let result; switch (op) { case add: result a b; break; case subtract: result a - b; break; case multiply: result a * b; break; case divide: if (b 0) { throw new Error(Division by zero is not allowed.); } result a / b; break; default: throw new Error(Unsupported operation: ${op}); } // 返回成功的响应content是一个数组可以包含多种类型文本、图像等 return { content: [ { type: text, text: The result of ${a} ${op} ${b} is: ${result}, }, ], }; } catch (error) { // 如果参数验证失败或计算出错返回错误信息 // 在实际生产中错误处理应更细致区分验证错误和运行时错误。 return { content: [ { type: text, text: Error: ${error.message}, }, ], isError: true, // 这是一个关键字段告知客户端此次调用失败了 }; } }; // 4. 将工具注册到服务器上 // 当客户端发起tools/list请求时服务器会返回这里定义的工具列表。 server.setRequestHandler(tools/list, async () { return { tools: [ { name: calculate, // 工具的唯一标识 description: Perform basic arithmetic calculations (addition, subtraction, multiplication, division)., inputSchema: { type: object, properties: { a: { type: number, description: The first operand }, b: { type: number, description: The second operand }, op: { type: string, enum: [add, subtract, multiply, divide], description: The arithmetic operation to perform }, }, required: [a, b, op], }, }, ], }; }); // 5. 设置工具调用的请求处理器 // 当客户端调用tools/call且工具名为calculate时执行上面的处理函数。 server.setRequestHandler(tools/call, async (request) { if (request.params.name calculate) { return await calculateToolHandler(request); } // 如果请求的工具名未注册应返回一个明确的错误。这里简单处理。 return { content: [{ type: text, text: Unknown tool: ${request.params.name} }], isError: true, }; }); // 6. 启动服务器使用stdio传输层 // 这是最常见的用法客户端如Claude Desktop会以子进程形式启动本脚本通过标准输入输出通信。 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Simple Calculator MCP Server running on stdio...); } runServer().catch((error) { console.error(Server error:, error); process.exit(1); });3.3 配置与测试要让MCP客户端如Claude Desktop识别并使用我们的服务器需要一个配置文件。不同客户端的配置方式不同这里以Claude Desktop为例。在Claude Desktop的配置目录macOS通常在~/Library/Application Support/Claude/claude_desktop_config.jsonWindows在%APPDATA%\Claude\claude_desktop_config.json中添加如下配置{ mcpServers: { simple-calculator: { command: node, args: [/ABSOLUTE/PATH/TO/YOUR/simple-mcp-server/server.js], env: {} } } }关键点command启动服务器的命令这里是node。args命令的参数第一个是JavaScript文件的绝对路径。使用相对路径很可能失败因为Claude Desktop的工作目录不确定。env可以设置环境变量这里为空。保存配置并重启Claude Desktop。重启后在聊天界面你应该能看到模型如Claude 3获得了新的能力。你可以尝试提问“请使用计算器工具计算 15 乘以 28。” 模型应该会识别并调用calculate工具并返回结果。实操心得在开发MCP服务器时最常遇到的启动失败问题就是路径错误或环境问题。务必使用绝对路径并确保node命令在系统PATH中。调试初期可以暂时在server.js的开头用console.error打印一些日志这些日志会输出到Claude Desktop的日志文件中对于排查连接问题非常有帮助。4. 高级特性与架构设计考量当你掌握了基础MCP服务器的构建后就可以探索更强大的特性并思考如何将其用于设计复杂的Agent系统。4.1 资源Resources暴露让AI先“看见”再“操作”工具是让AI“做事”资源是让AI“知情”。这对于需要上下文的操作至关重要。例如一个文件系统MCP服务器除了提供read_file、write_file工具更应该暴露file:///或directory:///这样的资源。AI可以先list某个目录下的文件读取资源列表再决定读取哪个文件的内容调用read_file工具。在服务器代码中你需要声明resources能力并实现resources/list和resources/read的请求处理器。resources/list返回一个资源URI列表及其简要描述resources/read则根据URI返回具体内容如文件内容、数据库表结构JSON等。这相当于为AI提供了一个可浏览的“文件系统”或“数据目录”。4.2 流式响应Streaming与长任务处理不是所有工具调用都能瞬间完成。执行一个复杂的Shell脚本、监控一个日志文件、训练一个小模型都可能需要很长时间。MCP的tools/call支持流式响应。在工具处理函数中你可以返回一个AsyncIterable。SDK提供了Callback和Promise两种形式的流式支持。你可以分多次yield部分结果客户端会收到一系列的tools/call/update通知最后以一个tools/call/result结束。这极大地改善了用户体验也让AI可以在任务执行中途就获取到部分信息进行思考甚至做出中断等决策。4.3 权限与安全模型这是生产环境部署MCP服务器时必须严肃对待的问题。一个不受限制的MCP服务器可能拥有执行任意命令、访问任意文件的权限。在设计时需要考虑最小权限原则服务器进程应该以尽可能低的系统权限运行。输入验证与净化对客户端传入的所有参数进行严格的验证和转义防止命令注入、路径遍历等攻击。我们上面使用zod就是很好的实践。操作范围限制在服务器代码内部显式定义可访问的目录、可执行的命令白名单。用户确认对于高风险操作如删除文件、重启服务服务器可以实现一个需要用户交互确认的流程。虽然MCP协议本身不直接定义GUI确认但可以通过返回一个需要用户“确认”的特殊结果由客户端如IDE弹窗处理。4.4 在复杂Agent系统中的定位在一个完整的AI Agent框架如LangChain、LlamaIndex、AutoGen中MCP可以扮演什么角色我认为它是一个优秀的**“工具总线”** 或“外部能力适配层”。传统架构Agent框架 - 自定义工具类 - 直接调用API/库函数。引入MCP的架构Agent框架 -MCP客户端- (通过协议) -多个MCP服务器- 实际能力。这样做的好处是解耦工具的实现与Agent框架彻底分离可以用任何语言编写。标准化所有工具提供统一的发现、调用接口。动态性工具可以热插拔无需修改Agent核心代码。可观测性由于所有通信都通过标准协议可以很方便地在中间层加入日志、监控、审计等功能。你可以构建一个轻量的“MCP工具执行器”作为Agent框架的一个特殊工具。这个执行器负责管理所有MCP服务器的连接、路由工具调用请求。这样现有的基于Function Calling的Agent就能无缝获得接入整个MCP生态的能力。5. 常见问题与排查技巧实录在实际开发和集成MCP的过程中你会遇到各种“坑”。以下是我从实践中总结的一些典型问题及其解决方法。5.1 服务器连接失败这是最常见的问题现象是客户端如Claude Desktop启动后模型完全没有获得新工具。检查配置文件路径99%的问题出在这里。确保配置文件中args里的JavaScript文件路径是绝对路径。在终端中使用pwd和ls命令确认文件真实存在。检查命令可执行性确保command如node在客户端进程的PATH环境变量中。有时GUI应用的环境变量与终端不同。一个笨办法但有效的方法是在args中直接使用命令的绝对路径如/usr/local/bin/node。查看客户端日志Claude Desktop等客户端通常有日志文件。在macOS上可以在~/Library/Logs/Claude/找到在Windows上查看%APPDATA%\Claude\logs。日志中会详细记录启动子进程的错误信息如“找不到文件”或“权限被拒绝”。服务器启动自检在server.js最开始添加console.error(‘MCP Server starting...‘)如果能在客户端日志中看到这行输出说明进程启动了问题可能出在协议通信上。5.2 工具列表不显示或调用无反应服务器连接成功了但AI不提及有新工具或者调用工具时没反应。验证协议实现首先用最简单的“echo”服务器测试。网上有现成的示例确保你的基础环境没问题。检查能力声明在new Server()时capabilities对象里必须明确声明你提供的功能如{ tools: {} }。如果提供了资源也要声明resources: {}。声明不对客户端不会发送相应的list请求。审查tools/list响应格式确保返回的JSON结构完全符合MCP协议。特别是inputSchema必须是一个有效的JSON Schema对象。使用在线JSON Schema验证器检查你的schema。一个常见的错误是properties字段写错。处理未捕获的异常在tools/call的请求处理器中一定要用try...catch包裹所有逻辑并返回格式正确的错误响应包含isError: true。一个未捕获的异常会导致整个连接中断客户端会认为服务器崩溃了。5.3 性能与稳定性问题当工具调用涉及网络IO、复杂计算或流式响应时。设置超时在客户端配置或服务器实现中为工具调用设置合理的超时时间。防止一个长时间挂起的调用阻塞整个会话。流式响应优化对于长任务务必使用流式响应。即使只是每秒发送一个“.”作为心跳也能让客户端和用户知道任务还在进行中而不是卡死了。资源管理MCP服务器通常是常驻进程。注意管理内存泄漏比如避免在全局变量中累积数据。对于数据库连接、HTTP客户端等资源要实现合理的连接池和重用机制。5.4 与其他系统的集成困惑“我的工具已经有一个REST API了还需要MCP吗” 这是一个很好的问题。通常你不需要重写整个后端。可以编写一个轻量的“MCP适配器服务器”。这个服务器的tools/call处理器里只是去调用你现有的REST API然后将结果包装成MCP格式返回。这样你既保留了现有的系统架构又让AI生态能通过标准协议访问你的服务。最后调试MCP通信的终极技巧是使用MCP Inspector这样的工具。它是一个独立的调试客户端可以连接到你的MCP服务器让你直观地看到所有的JSON-RPC请求和响应精确到每个字段对于排查协议层面的问题 invaluable。当你觉得“明明代码没错就是不通”时用它看一眼通信过程往往能立刻找到问题所在。
返回列表