Agent工具调用三年演进:从function call到MCP,三种方案的设计取舍

做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-MethodMcp-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 callHTTP动态注册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全链路服务。。