从Function Calling到MCP协议:构建可扩展的AI Agent工具系统

📅 2026/8/26 6:38:03
从Function Calling到MCP协议:构建可扩展的AI Agent工具系统
1. 从 Function Calling 到 MCP为什么工具系统是 Agent 的“手”与“眼”如果你最近在折腾 AI Agent尤其是想让它帮你查个天气、发个邮件或者操作一下数据库那你大概率绕不开一个词Function Calling。这几乎是所有主流大模型比如 GPT、Claude对外提供工具能力的标准接口。你写一个函数描述清楚它的输入输出模型就能在合适的时机调用它看起来挺美好对吧但当你真的开始构建一个稍微复杂点的 Agent 应用尤其是作为一个后端开发者想把 Agent 能力集成到自己的业务系统里时Function Calling 带来的麻烦就开始了。最直接的痛点就是“硬编码”和“紧耦合”。你的工具函数列表Tool List得提前定义好然后一股脑塞给模型。每次新增、修改或下线一个工具都得重新调整这个列表甚至可能影响已有的提示词Prompt逻辑。这就像给你的机器人装上了一套固定的、不可拆卸的工具手想换个螺丝刀得把整条胳膊都拆了重装。更麻烦的是当你的 Agent 需要调用不同服务、不同团队维护的工具时这种集中式的管理方式会迅速变成协作的噩梦。这时候MCPModel Context Protocol协议进入了视野。它不是一个具体的工具而是一套设计理念和通信规范。你可以把它理解为给 Agent 世界制定的“USB 协议”或“插件标准”。MCP 的核心思想是“工具即服务动态可发现”。工具不再是一份静态的 JSON 列表而是一个个独立的、自描述的服务器MCP Server。Agent通过 MCP Client可以动态地发现、连接并使用这些工具就像你的电脑可以即插即用地识别新插入的 U 盘一样。所以这个系列走到第四篇我们聊工具系统设计其本质是在探讨如何为你的 Agent 构建一套灵活、可扩展、易于维护的“手”和“眼”。从 Function Calling 这种“预制工具包”模式进化到 MCP 这种“标准化工具插座”模式是 Agent 走向实用化、工程化的关键一步。这不仅仅是换一个 API 调用方式更是架构思维上的升级。接下来我会结合后端开发的常见场景拆解这里面的设计考量、实现细节以及我趟过的一些坑。2. Function Calling 的工程化困境当便利性遇上系统复杂度Function Calling 用起来确实简单。以 OpenAI 的 API 为例你只需要在对话请求的tools参数里传入一个描述工具的 JSON 数组。每个工具描述包括名字、描述和参数模式JSON Schema。模型会在认为需要时返回一个特殊的响应告诉你它想调用哪个工具以及传入什么参数。你执行完工具再把结果塞回对话上下文模型就能基于结果继续回答。// 一个典型的 Function Calling 工具定义 { type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气, parameters: { type: object, properties: { location: { type: string, description: 城市名例如北京上海 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位 } }, required: [location] } } }这个模式在 Demo 和小型原型中无往不利。但一旦进入稍具规模的工程实践它的局限性就暴露无遗。2.1 工具管理的“巨石应用”问题在 Function Calling 模式下所有工具的定义集中在一个地方通常是启动 Agent 的入口文件或某个配置中心。这带来了几个问题版本与部署耦合任何工具的逻辑变更哪怕只是修改描述文字以提升模型调用准确率都可能需要重新部署整个 Agent 服务。因为工具列表是启动时加载的静态配置。团队协作壁垒如果数据库工具由 DBA 团队维护邮件工具由运维团队提供支付工具由金融中台团队开发。在 Function Calling 模式下这些团队都需要向“中心化”的 Agent 维护者提交工具定义和更新沟通成本和流程复杂度激增。工具发现与注册死板工具列表是预定义的Agent 无法在运行时根据当前任务动态加载或卸载工具包。比如在处理客服对话时加载知识库检索工具在处理订单时加载物流查询工具这种场景实现起来很别扭。2.2 上下文窗口的无效消耗每个工具的定义包括详细的描述和参数模式都会占用宝贵的上下文窗口Token。当你有几十上百个工具时仅仅是把工具列表塞进提示词就可能消耗掉数千甚至上万个 Token。这不仅增加了成本更重要的是过长的上下文可能会干扰模型对核心对话历史的理解降低其选择正确工具的能力。模型需要从一大堆工具描述中“大海捞针”这本身就是一个挑战。2.3 安全与权限控制的粗粒度Function Calling 本身不提供工具级别的鉴权机制。通常的做法是在 Agent 后端收到模型的工具调用请求后在执行具体函数前进行权限校验。但这意味着权限逻辑和工具逻辑是分离的而且校验是基于调用者身份和工具名称的静态映射缺乏动态的、基于上下文的权限判断例如“这个用户在当前会话中是否有权限执行退款操作”。2.4 一个真实的踩坑案例工具描述“词不达意”我曾负责一个内部审批流程的 Agent。其中有一个工具叫approve_request参数是request_id。最初描述写的是“批准一个请求”。结果模型经常在用户只是“查询”审批状态时就错误地调用了这个工具造成了误操作。后来我们把描述改为“执行批准操作这将使流程进入下一环节。仅在用户明确表达‘同意’、‘批准’等指令时使用。” 同时我们增加了另一个工具get_request_status专门用于查询。这个微调显著提升了准确性但也让我们意识到工具描述的“Prompt 工程”同样重要且这部分逻辑和业务代码混在一起难以维护和 A/B 测试。这些困境共同指向一个需求我们需要一个解耦的、协议化的、动态的工具系统。而这正是 MCP 协议试图解决的问题。3. MCP 协议深度拆解架构、核心概念与通信模型MCP 协议由 Anthropic 提出其目标是为 AI 应用程序主要是 Agent与工具资源之间建立一套标准化的、双向的通信协议。它不是一个具体的库或框架而是一份规范。你可以基于任何语言Go, Python, Node.js 等来实现 MCP 的客户端Client和服务器Server。3.1 核心架构Client-Server 模型MCP 采用了清晰的客户端-服务器模型MCP Server工具提供方封装一个或多个具体的工具能力。例如一个“天气查询 Server”一个“数据库操作 Server”一个“内部 CRM 系统 Server”。每个 Server 独立运行管理自己的工具列表、实现具体的执行逻辑。MCP Client工具使用方通常是你的 Agent 应用核心。它负责连接一个或多个 MCP Server发现它们提供的工具并在需要时发起调用。Client 还管理着与 LLM 的交互决定何时调用哪个工具。这种架构天然实现了关注点分离。工具的实现者只需要关心如何做好自己的服务Server而 Agent 的构建者Client则专注于任务规划、对话管理和工具调度。3.2 关键概念与生命周期初始化InitializeClient 启动后与 Server 建立连接通常是 STDIO 或 WebSocket并交换初始化信息包括双方支持的协议版本、能力Capabilities等。工具列表List ToolsClient 可以向 Server 请求其提供的所有工具列表。Server 返回一个工具描述数组。关键点在于这个请求可以随时发生不限于启动阶段。这意味着 Client 可以动态地发现新接入的 Server。工具调用Call ToolClient 向 Server 发起工具调用请求包含工具名和参数。Server 执行后将结果或错误返回给 Client。结果可以是纯文本、JSON甚至是图片等结构化数据通过特定 Content Type 声明。资源Resources与提示词模板Prompts这是 MCP 比简单 Function Calling 更强大的地方。除了工具Server 还可以提供Resources静态或动态的内容资源如文档、配置文件、实时数据快照。Client 可以读取Read这些资源并将其作为上下文提供给 LLM。例如一个“系统状态 Server”可以提供一个/current-metrics资源Agent 在分析性能问题时可以读取它。Prompts预定义的提示词模板。Client 可以获取这些模板并填入变量后使用。这允许工具提供方同时提供“最佳实践”的提示词进一步降低集成复杂度。3.3 传输协议与消息格式MCP 通信基于 JSON-RPC 2.0这是一个轻量级的远程过程调用协议。消息是 JSON 格式的通过 STDIO标准输入输出或 WebSocket 传输。这使得它具有极好的语言无关性和进程隔离性。一个用 Python 写的 Agent Client可以轻松调用一个用 Go 写的数据库 Server。一个简化的callTool请求-响应示例// Client - Server 请求 { jsonrpc: 2.0, id: 1, method: tools/call, params: { name: get_current_weather, arguments: { location: San Francisco, unit: celsius } } } // Server - Client 响应成功 { jsonrpc: 2.0, id: 1, result: { content: [ { type: text, text: San Francisco 当前天气晴朗气温 18°C。 } ] } }3.4 与 Function Calling 的直观对比为了更清晰地看到差异我们可以从几个维度对比一下特性维度Function CallingMCP 协议架构模式中心化、静态集成分布式、动态插件化工具管理硬编码在 Client 侧变更需重启/重部署Server 侧自描述Client 动态发现耦合度紧耦合工具逻辑与 Agent 核心绑定松耦合通过标准协议通信扩展性较差新增工具需修改中心配置极佳新增一个 Server 即新增一类工具上下文消耗高所有工具描述需一次性送入上下文低可按需查询和送入相关工具描述权限与安全依赖 Client 统一实现粒度粗可在 Server 侧独立实现粒度更细适用场景工具数量少、变化不频繁的简单 Agent工具生态复杂、需要多团队协作、追求长期可维护性的生产级 Agent 系统从对比可以看出MCP 协议本质上是在用后端微服务的思想来重构 Agent 的工具层。每个工具或工具集都是一个独立的微服务MCP Server通过标准的协议MCP/JSON-RPC对外提供能力。这对于习惯设计分布式系统的后端开发者来说是一个非常自然和舒适的范式迁移。4. 实战从零设计一个基于 MCP 的 Agent 工具系统理论说得再多不如动手搭一个。假设我们要为一个“智能运维助手”构建工具系统它需要能查询服务器状态、查看日志、重启服务。我们将采用 MCP 架构。4.1 整体架构设计我们的系统将包含以下组件Agent Core (MCP Client)核心大脑用 Python 编写集成 LLM如 GPT-4负责对话理解和任务规划。它会连接多个 MCP Server。Server Status MCP Server用 Go 编写提供服务器状态查询工具如get_cpu_usage,get_memory_info。Log Query MCP Server用 Python 编写提供日志检索工具如search_log_by_keyword,get_recent_errors。Service Management MCP Server用 Node.js 编写提供服务管理工具如restart_service,check_service_health。出于安全考虑这个 Server 会实现严格的权限校验。所有 Server 与 Client 之间通过 STDIO 或 WebSocket 通信。在生产环境我们可能会使用一个轻量级的消息总线或 Sidecar 模式来管理这些连接以实现更好的可靠性和服务发现。4.2 实现一个简单的 MCP Server以 Python 为例虽然可以用底层 socket 实现但使用社区 SDK 更高效。这里以mcpPython 库为例实现日志查询 Server。首先安装基础库pip install mcp。# log_query_server.py import asyncio from typing import Any from mcp import ClientSession, StdioServerParameters from mcp.server import Server from mcp.server.models import TextContent import mcp.server.stdio # 创建 MCP Server 实例 server Server(log-query-server) # 1. 定义工具按关键词搜索日志 server.list_tools() async def handle_list_tools() - list: return [ { name: search_log_by_keyword, description: 在系统应用日志中搜索包含特定关键词的条目。, inputSchema: { type: object, properties: { keyword: {type: string, description: 要搜索的关键词}, lines: {type: integer, description: 返回的最大行数默认50, default: 50}, service: {type: string, description: 服务名称可选如 nginx, api-backend} }, required: [keyword] } }, { name: get_recent_errors, description: 获取最近一段时间内的错误日志。, inputSchema: { type: object, properties: { minutes: {type: integer, description: 回溯多少分钟内的日志默认30, default: 30} } } } ] # 2. 实现工具执行逻辑 server.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list: if name search_log_by_keyword: keyword arguments[keyword] lines arguments.get(lines, 50) service arguments.get(service) # 模拟日志查询逻辑 result f模拟搜索日志在服务 {service or 所有} 中找到包含 {keyword} 的最近 {lines} 行记录。\n示例行[ERROR] 2024-05-27 10:23:45 - 数据库连接超时关键词 {keyword} 触发告警。 return [TextContent(typetext, textresult)] elif name get_recent_errors: minutes arguments.get(minutes, 30) # 模拟查询错误日志 result f模拟获取最近 {minutes} 分钟的错误日志\n1. [ERROR] 2024-05-27 10:15:22 - API /user/login 响应超时。\n2. [WARN] 2024-05-27 10:10:05 - 内存使用率超过80%。 return [TextContent(typetext, textresult)] else: raise ValueError(f未知工具: {name}) # 3. 运行 Server通过 STDIO async def main(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, ServerParameters(namelog-query-server) # 可配置更多参数 ) if __name__ __main__: asyncio.run(main())这个 Server 启动后会监听标准输入输出。任何遵循 MCP 协议的 Client 都可以连接它获取工具列表并调用。4.3 实现 MCP Client 并集成 LLMClient 需要做几件事连接 Server、管理工具列表、与 LLM 交互、根据 LLM 决策调用工具。这里我们简化 LLM 交互聚焦 MCP 部分。# agent_core_client.py import asyncio import json from mcp import ClientSession, StdioServerParameters from openai import OpenAI # 假设使用 OpenAI class MCPAgentClient: def __init__(self, llm_client): self.llm llm_client self.sessions {} # 存放连接到不同 Server 的会话 async def connect_to_server(self, server_name, command, args): 连接到一个 MCP Server例如通过子进程启动 params StdioServerParameters(commandcommand, argsargs) session ClientSession(params) await session.__aenter__() # 建立连接 self.sessions[server_name] session # 初始化并获取工具列表 await session.initialize() tools await session.list_tools() print(f已连接 {server_name}, 获取到工具: {[t.name for t in tools]}) return tools async def get_all_tools(self): 汇总所有已连接 Server 的工具 all_tools [] for name, session in self.sessions.items(): tools await session.list_tools() for tool in tools: # 为工具来源打上标签方便LLM理解和后续路由 tool.metadata {server: name} all_tools.append(tool) return all_tools async def execute_agent_cycle(self, user_query): 执行一轮 Agent 循环LLM 思考 - 可能调用工具 - 返回结果 # 1. 获取当前所有可用工具描述用于构造 LLM 提示词 available_tools await self.get_all_tools() tool_descriptions_for_llm self._format_tools_for_prompt(available_tools) # 2. 构造包含工具信息的提示词发送给 LLM messages [ {role: system, content: f你是一个运维助手。你可以使用以下工具\n{tool_descriptions_for_llm}\n请根据用户问题决定是否调用工具。一次只调用一个工具。}, {role: user, content: user_query} ] response self.llm.chat.completions.create( modelgpt-4, messagesmessages, toolsself._convert_to_openai_tools(available_tools), # 将 MCP 工具格式转换为 OpenAI 的 Function Calling 格式 tool_choiceauto ) message response.choices[0].message # 3. 检查 LLM 是否决定调用工具 if message.tool_calls: tool_call message.tool_calls[0] # 简化处理假设一次只调用一个 tool_name tool_call.function.name arguments json.loads(tool_call.function.arguments) # 4. 路由并调用工具 result await self._route_and_call_tool(tool_name, arguments) # 5. 将工具结果返回给 LLM进行下一轮思考或生成最终回答 # ... (后续处理逻辑) return f调用了工具 {tool_name}结果{result} else: # LLM 直接回答 return message.content def _format_tools_for_prompt(self, tools): # 将工具信息格式化为自然语言描述供 LLM 参考 pass def _convert_to_openai_tools(self, tools): # 将 MCP 工具对象转换为 OpenAI API 所需的格式 pass async def _route_and_call_tool(self, tool_name, arguments): 根据工具名找到对应的 Server 会话并调用 # 这里需要实现一个路由逻辑例如通过工具名前缀或维护一个映射表 # 假设我们通过之前注入的 metadata 来路由 for session in self.sessions.values(): tools await session.list_tools() for tool in tools: if tool.name tool_name: # 找到工具所属的 session result await session.call_tool(tool_name, arguments) # 处理结果可能是 TextContent 或 ImageContent 等 if result and result.content: return result.content[0].text raise ValueError(f未找到工具: {tool_name}) async def main(): llm_client OpenAI(api_keyyour-key) agent MCPAgentClient(llm_client) # 连接不同的工具 Server这里假设 Server 已启动在指定命令路径 await agent.connect_to_server( log-server, python, [log_query_server.py] # 启动我们上面写的日志 Server ) # 可以继续连接 status-server, management-server... # 处理用户查询 answer await agent.execute_agent_cycle(帮我查一下最近有没有错误日志) print(answer) if __name__ __main__: asyncio.run(main())这个示例勾勒出了一个基于 MCP 的 Agent 核心骨架。关键在于_route_and_call_tool方法它负责将 LLM 想要调用的工具路由到正确的 MCP Server 上执行。这种设计使得工具的增加变得非常灵活。4.4 权限控制与安全增强设计在 MCP 架构下权限控制可以在两个层面实施Server 层面每个 MCP Server 可以也应该实现自己的认证和授权。例如Service Management Server在启动时可以要求传入一个 API 密钥或读取一个权限配置文件。当 Client 调用restart_service时Server 可以验证该 Client 连接是否具有相应的操作权限。这可以通过在初始化握手时传递身份信息或在每个工具调用请求中附带令牌来实现。Client 层面Agent Core 在收到用户请求后可以先根据用户身份和会话上下文决定哪些工具对该用户可见/可用。然后在向 LLM 提供的工具列表中进行过滤。同时在路由工具调用前可以进行二次权限校验。一个简单的权限校验思路是在 MCP 协议之上增加一个轻量的拦截层。例如Client 在调用工具前先查询一个“策略中心”询问“用户U是否允许在会话S中调用工具T”。注意安全无小事。对于重启服务、执行命令、访问敏感数据等高危工具务必实现多层校验、操作审计和二次确认机制例如要求 LLM 生成一个操作摘要由 Client 向用户确认后再执行。5. 生产环境下的挑战、优化与选型思考将 MCP 协议用于生产环境会面临一些新的挑战这也正是体现后端工程能力的地方。5.1 Server 的生命周期与连接管理MCP Server 是独立进程。如何管理它们的启动、停止、健康检查一种模式是“每个工具集一个常驻进程”但这可能浪费资源。另一种更优的模式是“按需启动”或使用“Server 池”。Agent Client 或一个独立的“MCP 路由器”可以维护一个 Server 注册中心。当需要某类工具时再启动或分配一个可用的 Server 进程。这需要处理进程间通信IPC、超时、崩溃重启等问题。5.2 性能与延迟每次工具调用都涉及跨进程通信甚至可能是网络通信和 JSON 序列化/反序列化其开销远大于直接的函数调用。对于延迟敏感的工具如简单的计算、内存缓存查询这可能成为瓶颈。优化策略包括批处理设计支持批量调用的工具接口。本地工具缓存对于非常高频的简单工具Client 可以将其“内化”即直接实现一个本地版本同时通过 MCP 协议对外提供保持架构统一但内部走优化路径。连接复用与长连接使用 WebSocket 保持长连接避免频繁建立连接的开销。5.3 工具描述的版本化与兼容性当 MCP Server 升级工具的描述名称、参数发生变化时如何保证旧的 Client 还能正常工作这需要引入版本协商机制。可以在初始化阶段Client 和 Server 交换支持的协议版本和工具版本。对于不兼容的变更可以考虑同时提供新旧版本的工具或让 Client 具备一定的适配能力。5.4 监控、可观测性与调试分布式系统离不开监控。你需要追踪工具调用链路一次用户请求触发了哪些工具调用每个调用的耗时、成功/失败率如何Server 健康状态各个 MCP Server 的进程状态、资源使用率。LLM 与工具的交互记录完整的对话历史、LLM 的思考过程如果支持、工具调用的请求和响应。这对于调试工具描述是否准确、LLM 决策是否合理至关重要。可以考虑在每个 MCP Server 和 Client 中集成 OpenTelemetry 等标准将追踪数据发送到统一的观测平台。5.5 社区生态与现有方案选型目前MCP 协议仍处于早期但生态在快速发展。除了自己从零实现还可以考虑以下方案Claude Code / Cursor 等 IDE 的 MCP 集成这些工具已经内置了 MCP Client你可以直接为它们编写 MCP Server 来扩展能力。这是快速体验 MCP 威力的好方法。开源 MCP SDK除了前面用的mcpPython 库社区也有其他语言的实现如 TypeScript 的modelcontextprotocol/sdk。评估其活跃度、文档和特性支持。云厂商的托管 Agent 服务一些云平台开始提供集成了工具调用能力的 Agent 托管服务。它们可能使用类似 MCP 的协议或自定义协议。评估其是否满足你的定制化需求。选型建议对于内部工具、需要深度定制、或工具逻辑复杂的场景基于开源 SDK 自建 MCP 体系是更可控的选择。对于快速验证想法、或主要面向特定 IDE 生态提供扩展则可以优先考虑兼容现有 MCP Client 的方案。从 Function Calling 到 MCP 协议工具系统的设计哲学从“集成”转向了“编排”。对于后端开发者而言这更像是在构建一个微服务化的“工具中台”而 Agent 则成为了这个中台上最智能的调度者和使用者。这个过程充满了架构设计的乐趣和挑战但一旦跑通你将获得一个真正可扩展、易维护的智能体基础设施。