做Agent开发的人,大概率都绕不开一个问题:怎么让AI调用你的工具?
这个问题听起来简单,但过去三年,整个行业的答案换了三轮。从最早硬编码在prompt里的function call,到自己搞一套HTTP接口动态注册,再到现在几乎成为事实标准的MCP协议。
每一轮切换都不是因为新技术多酷,而是旧方案在实际项目中真的扛不住了。
这篇文章把这三种方案的设计思路、技术取舍和适用场景都讲清楚。
第一代:function call——能用,但耦合得要命
2023年6月,OpenAI在GPT-4里首次放出function calling能力。简单说就是:你在请求里告诉模型"我有这些函数,每个函数叫什么、要什么参数",模型决定调哪个、填什么参数,返回一个结构化的调用指令,你自己执行完再把结果喂回去。
大概长这样:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"},
"unit": {"type": "string", "enum": ["c", "f"]}
},
"required": ["city"]
}
}
}
]
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools
)
模型返回:
{
"tool_calls": [{
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"西安\", \"unit\": \"c\"}"
}
}]
}
拿到这个指令后,你自己去调天气API,把结果塞回messages,再来一轮。模型不直接执行任何东西,它只负责"决定调什么"。
这个方案的优点很直接:
- 简单,请求里带个JSON Schema就行
- 模型原生支持,不需要额外基础设施
- 调试方便,调用链路清晰,每一步都在你的代码里
但问题在项目稍微大一点就全暴露了:
第一,工具定义和业务代码强耦合。每加一个工具,就要改调用模型的代码——加Schema、加执行逻辑、加参数校验。工具多了之后,维护一个巨大的tools列表,每次请求都全量传给模型,token浪费严重。20个工具的Schema加起来可能就几千token,不管这轮对话用不用得到,每次都得传。
第二,跨模型不兼容。OpenAI的function call格式、Anthropic的tool_use格式、Google的function_calling格式,长得都不一样。想换模型?工具定义层重写一遍。虽然各家SDK做了些封装,但底层格式差异始终存在。
第三,工具发现是静态的。必须在请求前就知道有哪些工具。如果Agent需要根据用户输入动态加载不同的工具集(比如不同客户用不同的工具、不同场景激活不同能力),function call搞起来非常别扭——你得自己维护一套工具激活/屏蔽逻辑,然后在每次请求时重新拼tools数组。
第四,没有标准的工具执行层。模型只告诉你"调什么、参数是什么",怎么执行、出错怎么办、超时怎么处理、结果怎么格式化,全靠自己写。每个项目都在重复造这个轮子。
第五,多轮工具调用编排全靠手写。模型可能需要先调A工具,拿到结果再调B工具,中间还要做条件判断。这个循环逻辑(agent loop)每个框架都有自己的实现,没有统一标准。
function call适合工具少、场景固定的简单应用。一旦工具数量上了两位数,或者需要对接多个模型、多个平台,维护成本会急剧上升。
第二代:HTTP接口动态注册——灵活,但各自为战
为了解决function call的耦合问题,行业开始搞自己的"工具注册中心"。
思路很朴素:把工具定义从代码里抽出来,做成独立的HTTP服务。Agent启动时从注册中心拉取工具列表,运行时根据需要动态加载。调用工具时,Agent通过HTTP请求发到工具服务,拿回结果。
架构大概是这样:
┌──────────┐ 1. GET /tools ┌──────────────┐
│ Agent │ ──────────────────→ │ Tool Registry│
│ Core │ ←────────────────── │ (工具注册中心)│
└──────────┘ 2. 返回工具列表 └──────────────┘
│
│ 3. POST /invoke {tool: "query_order", args: {...}}
↓
┌──────────────┐
│ Tool Server │
│ (工具执行层) │
└──────────────┘
工具服务启动时自己注册:
# 工具服务启动时向注册中心注册
registry.register(
name="query_order",
description="查询订单状态",
parameters={
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"]
},
endpoint="https://tool-server.internal/query_order"
)
Agent这边就轻量了:
# 启动时拉取工具列表
tools = await registry.fetch_tools()
# 运行时,模型选了某个工具,Agent转发请求
result = await http.post(
tool.endpoint,
json=tool_arguments,
headers={"Authorization": f"Bearer {token}"}
)
这比function call进了一步:
- 工具和Agent核心解耦了,加新工具不用改Agent代码,工具服务独立部署
- 工具可以独立扩展,哪个工具调用量大就单独加实例
- 不同团队可以各自维护自己的工具服务,通过注册中心统一暴露
- 工具列表可以动态更新,不用重启Agent
- 工具的实现语言无关——Python写的Agent可以调Go写的工具服务
但核心问题是——没有标准。
每个公司搞的注册中心都不一样:
- 工具描述格式不一样(有的用JSON Schema,有的用自定义描述格式)
- 调用协议不一样(有的RESTful,有的GraphQL,有的直接JSON-RPC)
- 认证方式不一样(有的API Key,有的OAuth,有的JWT,有的mTLS)
- 错误处理和状态码定义不一样
- 工具版本管理策略不一样,有的URL里带版本号,有的靠Header,有的根本没版本概念
这就像2010年的手机充电口——每家都有自己的接口,线不通用。A公司写的GitHub工具,B公司拿过去用不了,得重新对接。
更麻烦的是,这种自建方案在安全性、可观测性、版本管理上基本是裸奔:
- 认证授权:工具服务之间怎么认证?某个Agent能调哪些工具?谁审批的?大部分自建方案就是一个共享API Key,粗粒度到"能调"或"不能调",没有细粒度权限。
- 审计日志:谁在什么时候调了什么工具、传了什么参数、返回了什么?出了安全事件怎么追溯?自建系统很少有完整的审计链路。
- 工具发现:注册中心虽然能列出工具,但工具描述的质量参差不齐,模型在选择工具时全靠description写得好不好。没有统一的描述规范,工具选择准确率波动很大。
- 流式调用:工具执行时间长(比如跑一个大数据查询),怎么返回进度?大部分自建方案只支持同步请求-响应,不支持服务端推送进度。
- 人工确认:工具执行前如果需要用户确认(比如删除数据、发起付款),怎么中断流程、等用户确认后再继续?这个在自建方案里实现起来很别扭,通常需要自己搞一套回调机制。
Dify、Coze、LangChain这些框架各自实现了一套工具系统,彼此之间工具不通用。企业如果同时用多个平台,同一个工具要封装多份。
第三代:MCP协议——终于有了标准
2024年11月,Anthropic发布了MCP(Model Context Protocol)。2025年底移交Linux基金会旗下的Agentic AI Foundation治理。到2026年中,MCP SDK月下载量接近5亿,TypeScript和Python SDK总下载量双双破10亿。Stripe、Linear、Notion、Cloudflare、AWS、Atlassian都已发布官方MCP Server。Cursor、Zed、JetBrains AI Assistant、VS Code Copilot Chat、Claude Code全部原生支持MCP。
据MCP官方博客,2026年7月28日发布的最新规范做了重大升级:无状态协议核心、Multi Round-Trip Requests、基于Header的路由、可缓存的列表结果、授权加固。
MCP解决的核心问题就一个:给Agent和工具之间定一个统一的通信标准。
MCP的架构设计
MCP基于JSON-RPC 2.0,定义了三个角色:
- Host:AI应用本身(Claude Desktop、Cursor、自研Agent等),管理多个Client
- Client:Host内负责和单个Server通信的连接器,一个Server对应一个Client
- Server:提供工具、资源、提示模板的服务端
Server可以暴露三种原语:
| 原语 | 说明 | 触发方 | 安全边界 |
|---|---|---|---|
| Tools | 可执行函数(查数据库、调API、发邮件) | 模型决定调用 | 需要权限控制,有副作用 |
| Resources | 只读数据(文件内容、数据库记录、API响应) | 应用决定加载 | 模型不直接触发,应用负责 |
| Prompts | 预定义提示模板(slash command) | 用户显式触发 | 有明确的用户同意 |
这个三原语分离是MCP设计上比较精妙的地方。之前的很多工具调用方案把"模型能执行的操作"和"模型能读取的数据"混在一起,信任边界模糊。MCP明确区分了:Tools有副作用需要guardrails,Resources是只读的由应用控制,Prompts需要用户显式触发。三者的安全模型不一样,协议层面就分开了。
通信流程
MCP的通信流程在2026-07-28规范中有重大变化。旧版需要initialize握手和session,新版改成了无状态:
# 新版:每个请求自描述,不需要握手
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "search", "arguments": {"q": "otters"}},
"_meta": {"io.modelcontextprotocol/clientInfo": {"name": "my-app", "version": "1.0"}}}
方法名和工具名通过Mcp-Method和Mcp-Name HTTP头传递,网关可以直接基于Header做路由、限流和鉴权,不用解析JSON body。
工具发现和调用的JSON-RPC交互:
// Client → Server:发现工具
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
// Server → Client:返回工具列表(带缓存提示)
{"jsonrpc": "2.0", "id": 1, "result": {
"tools": [
{
"name": "query_order",
"description": "查询订单状态",
"inputSchema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}
],
"ttlMs": 300000,
"cacheScope": "server"
}}
// Client → Server:调用工具
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "query_order", "arguments": {"order_id": "ORD-123"}}}
tools/list的结果可以缓存(ttlMs + cacheScope),减少重复拉取。这对工具数量多的场景很重要——不用每次对话都重新拉取完整工具目录。
一个最小的MCP Server
from mcp.server import Server
from mcp.types import Tool, TextContent
server = Server("order-server")
@server.list_tools()
async def list_tools():
return [
Tool(
name="query_order",
description="查询订单状态",
inputSchema={
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单编号"}
},
"required": ["order_id"]
}
)
]
@server.call_tool()
async def call_tool(name, arguments):
if name == "query_order":
order = await db.query_order(arguments["order_id"])
return [TextContent(type="text", text=str(order))]
启动后通过stdio或Streamable HTTP暴露,任何MCP客户端都能直接连接。
传输层设计
MCP定义了两种标准传输,覆盖了本地和远程两种场景:
stdio:Client启动Server作为子进程,通过stdin/stdout交换换行符分隔的JSON消息。没有网络栈、没有认证、没有额外的序列化开销,延迟基本就是JSON解析的成本。适合本地工具——文件系统、Git封装、数据库CLI,任何跟IDE跑在一起的东西。缺点是进程模型——50个开发者连8个Server意味着大约400个并发进程分布在50台机器上,没有集中的认证、审计、限流点。
Streamable HTTP:Server暴露一个HTTP端点(如https://example.com/mcp),同时接受POST和GET。POST发送JSON-RPC消息,Server可以直接返回JSON(快速操作),也可以升级为SSE流(长时间运行的工具需要推送进度)。2026-07-28规范把协议改成无状态后,任何请求可以落到负载均衡器后面的任何实例,不需要粘性会话和共享存储。
MCP比自建方案好在哪
第一,写一次,到处能用。 封装一个GitHub MCP Server,Claude Desktop能用、Cursor能用、VS Code能用、自研Agent也能用。工具层从模型层和平台层彻底解耦——不管背后用Claude、GPT还是DeepSeek,工具定义和调用方式都是一样的。
第二,安全模型内置。 MCP内置OAuth 2.0授权框架。2026-07-28规范加了issuer验证(RFC 9207)、issuer-bound客户端凭证、客户端元数据文档(CIMD)作为首选注册方式,企业管理授权(EMA)作为扩展已经稳定。工具需要什么scope、用户怎么授权、token怎么刷新,都有标准流程。路线图里还在做Agent身份和委托机制——让以云工作负载身份运行的Agent能被Server标准化识别和信任,而不是靠粘贴API Key。
第三,人工确认流程标准化。 2026-07-28引入的Multi Round-Trip Requests(MRTR)解决了"工具执行到一半需要用户输入"的问题。Server可以返回resultType: "input_required",附上需要用户回答的问题,Client拿到用户输入后重试原始调用。不需要长连接,不需要回调URL,无状态协议下也能工作。这在删除数据、发起付款、补充参数等场景非常关键。
第四,可观测性有基础。 方法名和工具名走HTTP Header,网关可以直接做路由、限流、计量和审计日志,不用解析body。标准化的错误码(SEP-2164)让Client能一致地处理"工具不存在""参数错误""权限不足"等情况。
第五,生态已经起来了。 MCP Registry里已有数千个可用Server。文件系统、Git、Postgres、SQLite、Slack、Google Drive、Brave Search、Puppeteer、Sentry都有官方参考实现。SDK覆盖Python、TypeScript、Go、C#、Kotlin、Java、Swift、Rust、Ruby。
MCP也不是银弹
简单场景过度设计。 就两三个工具、一个模型、一个应用,直接function call最简单。引入MCP意味着多一个Server进程、多一层通信、多一份运维负担。
调试链路变长。 function call时代工具调用就在你的代码里,打断点就行。MCP把工具拆成独立进程,通信走JSON-RPC,出了问题要查Client日志、Server日志、传输层日志。不过SDK和工具链在快速改善,MCP Inspector已经能可视化调试Server了。
生态还在快速演进。 2026-07-28是个大版本更新,从有状态改成无状态,SDK升了大版本(Python v2、TypeScript v2拆包),有breaking changes。不过官方给了至少12个月废弃窗口,旧客户端连新Server会自动降级到initialize握手,不会直接炸。
工具选择准确率随数量下降。 连一个有100个工具的Server,模型在选工具时准确率会下降,而且每次都要为完整工具列表付token成本。MCP正在做"渐进式发现"(progressive discovery)——让Server先暴露少量入口工具,根据对话上下文逐步展开更多工具目录。这个特性在路线图上,还没正式落地。目前的缓解手段是按场景拆分多个Server,Client只连当前需要的。
远程Server的运维复杂度。 stdio很简单,但生产环境用Streamable HTTP部署远程Server,需要考虑认证、HTTPS、负载均衡、日志聚合、版本迁移。这些不是MCP特有的问题,但确实意味着从"本地跑个脚本"到"生产级服务"有一道坎。
三种方案怎么选
| 维度 | function call | HTTP动态注册 | MCP |
|---|---|---|---|
| 工具数量 | <5个 | 5-50个 | 任意 |
| 模型兼容 | 绑定单一模型 | 需要适配层 | 模型无关 |
| 跨平台复用 | 不支持 | 不支持 | 原生支持 |
| 安全授权 | 自己实现 | 自己实现 | OAuth 2.0内置 |
| 人工确认 | 自己实现 | 需要回调机制 | MRTR标准支持 |
| 流式/进度 | 不支持 | 需要SSE自建 | 原生SSE |
| 运维成本 | 最低 | 中等 | 中高 |
| 生态复用 | 无 | 公司内部 | 数千个现成Server |
决策建议:
- 工具少、单模型、单应用 → function call,别过度设计
- 工具需要独立部署、多团队维护,但只在自家系统内用 → HTTP动态注册够用
- 工具需要跨平台复用、多模型切换、企业级安全合规 → MCP
- 全新项目、没有历史包袱 → 直接上MCP,别再自建注册中心了
写在最后
从function call到HTTP动态注册到MCP,本质上是一个从紧耦合到松耦合、从私有协议到开放标准的演进过程。
这个过程跟Web开发的演进很像:最早是CGI脚本直接输出HTML,然后是各种Web框架各自搞MVC,最后RESTful API成为共识。工具调用也在走同样的路——先解决"能不能用",再解决"好不好维护",最后统一标准解决"能不能复用"。
MCP不一定是终极答案——协议还在快速迭代,渐进式发现、Agent身份委托、服务端事件推送这些都还在路上。但它是目前这个阶段最好的答案:有开放标准、有主流厂商背书、有活跃生态、有企业级安全模型。
如果在做Agent开发,而且工具数量在增长、需要对接多个模型或平台,现在就该认真看MCP了。不用等它"再成熟一点"——它已经够成熟了。
西安栈上月明软件科技有限公司,专注企业级智能体开发与AI全链路服务。。