同事问:MCP 不就是 Function Calling 换了个名字吗
上个月内部分享会,我讲完 MCP 之后有同事问了这个问题。当时我答得不太好,说了些「更标准化」「生态更好」之类的空话。后来我认真想了想,也去读了规范原文,这篇算是个正经的回答。
结论是:不是换名字,是两个层次的东西。Function Calling 是模型的一项能力,MCP 是一个进程间通信协议。这个区别决定了它们解决完全不同的问题。
先把两者的定位摆清楚
| 维度 | Function Calling | MCP |
|---|---|---|
| 本质 | 模型 API 的一个参数 | 开放通信协议(JSON-RPC 2.0) |
| 谁定义 | 各模型厂商(OpenAI、Anthropic、通义…) | Anthropic 发起的开放规范 |
| 解决什么 | 让模型能表达「我想调用某个函数」 | 让工具提供方和消费方解耦 |
| 绑定关系 | 工具定义绑定在某次 API 调用上 | 工具由独立进程提供,可被多方消费 |
| 方向 | 单向(应用 → 模型) | 双向(服务端也能反向请求客户端) |
最直白的说法:Function Calling 是「模型告诉我它想干什么」,MCP 是「我去哪里找到能干这件事的东西」。前者是意图表达,后者是能力供给。
一个具体例子。我们用 Function Calling 接公司的 CMDB:
// 方式一:Function Calling,每个应用都要写一遍
ChatCompletionRequest req = ChatCompletionRequest.builder()
.model("qwen-max")
.messages(messages)
.tools(List.of(
Tool.builder().function(Function.builder()
.name("query_hosts")
.description("按服务名查询主机信息")
.parameters(JsonSchema.builder()
.addProperty("service", Property.of("string", "服务名"))
.required(List.of("service"))
.build())
.build()).build()
))
.build();
这段代码里,工具的定义、调用的执行、结果的格式化,全在同一个进程里。如果有三个应用要用 CMDB,就要写三遍(或者抽成公共库,但那还是代码级复用)。
换成 MCP 之后,CMDB 的接入方实现一次 MCP Server,其他应用只是「连上它」:
// 方式二:MCP,应用侧只需要连接
@Bean
McpSyncClient cmdbClient() {
return McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(30))
.build();
}
cmdbClient.initialize();
// 工具定义由 server 提供,client 直接拉取
var tools = cmdbClient.listTools();
关键差异不是代码行数,是能力的提供者和消费者可以完全独立演进。CMDB 团队加一个新工具,不用通知任何消费方,他们的 client 下次 listTools 就自动看到了。
MCP 的架构:三个角色
规范里定义了 Host、Client、Server 三个角色,这个划分经常被忽略但很重要。
┌──────────────────────────────────────────┐
│ Host(宿主应用) │
│ 例:Claude Desktop、IDE 插件、我们的 Agent │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Client 1│ │Client 2│ │Client 3│ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
└──────┼───────────┼───────────┼──────────┘
│ 1:1 │ 1:1 │ 1:1
┌───┴────┐ ┌───┴────┐ ┌───┴────┐
│Server A│ │Server B│ │Server C│
│ (CMDB) │ │(GitLab)│ │(监控) │
└────────┘ └────────┘ └────────┘
Client 和 Server 是 1:1 的关系,一个 Client 只连一个 Server。Host 负责管理多个 Client。这个设计的好处是隔离——某个 Server 出问题、有安全边界要求、或者需要不同的认证方式,都不会影响其他连接。
我们内部的做法是一个 Host(Agent 运行时)挂了 4 个 Client:CMDB、监控系统、发布平台、知识库。每个 Server 有不同的权限模型和部署位置。
传输层:从 SSE 到 Streamable HTTP
这部分是今年变化最大的地方,也是很多人认知滞后的地方。
早期的 MCP 用 HTTP+SSE 传输,需要两个端点:
GET /sse → 服务端推送事件流(长连接)
POST /message → 客户端发送请求
这个设计有几个明显问题:连接断开后无法恢复、服务端必须维护长连接(无法无状态部署)、不支持断线重连。
现在的规范用 Streamable HTTP,单端点:
POST /mcp → 客户端发 JSON-RPC 请求
服务端可返回:
application/json (普通响应)
text/event-stream (升级为 SSE 流)
GET /mcp → 可选,客户端监听服务端主动推送
DELETE /mcp → 显式终止会话
POST /mcp HTTP/1.1
Host: cmdb.internal
Content-Type: application/json
Accept: application/json, text/event-stream ← 两个都要写
Mcp-Session-Id: 8f3a-4b2c-9d1e ← 可选,服务端分配
{"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"query_hosts","arguments":{"service":"order-service"}}}
这个改动带来两个重要能力:一是服务端可以完全无状态(不分配 Session-Id,每个请求独立处理,随便水平扩展);二是断线可恢复(规范建议用 SSE 事件 ID 支持 Last-Event-ID 重连)。
我们在生产用的是无状态模式。原因很简单:我们的 MCP Server 要部署在 K8s 上,节点随时可能被调度走,无状态是最省心的。代价是没法做服务端主动推送(sampling、进度通知这类需要反向通信的能力用不了),但我们的场景不需要。
顺便说一句,很多老文章和老的 MCP Server 实现还在用 HTTP+SSE 传输。如果你在选库,注意看它支持的是哪个版本。Spring AI 的 MCP 模块两边都支持,通过 mcp-endpoint 配置区分。
能力协商:不是所有 Server 都一样
MCP 有个设计我觉得很聪明——初始化时双向声明能力,双方各自说明自己支持什么。
// Server 声明
{
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": false },
"logging": {},
"completion": {}
}
}
// Client 声明
{
"capabilities": {
"roots": { "listChanged": true }, // 能提供文件系统根目录
"sampling": {}, // 能替 Server 调用 LLM
"elicitation": {} // 能向用户提问(较新的能力)
}
}
roots 和 sampling 是 Function Calling 完全没有的概念,值得单独说。
roots 是客户端告诉服务端「你可以访问这些目录」。比如 IDE 插件把当前项目根目录作为 root 传给文件系统 MCP Server,Server 就知道自己只能在这个范围内操作。这是一种声明式的权限边界。
sampling 更反直觉:服务端可以反过来请求客户端调用大模型。流程是 Server 说「我需要用 LLM 处理一下这段数据」,Client 拿到请求后用自己的模型和额度去调用,把结果返回给 Server。
// Server → Client:请求采样
{"jsonrpc":"2.0","id":42,"method":"sampling/createMessage","params":{
"messages":[{"role":"user","content":{"type":"text",
"text":"把以下日志压缩成一句话:..."}}],
"maxTokens": 200,
"systemPrompt": "你是日志分析助手"}}
// Client → Server:返回结果
{"jsonrpc":"2.0","id":42,"result":{
"role":"assistant",
"content":{"type":"text","text":"订单服务 3 台实例在 14:02 出现连接池耗尽"},
"model":"qwen-plus","stopReason":"endTurn"}}
这个设计解决了两个实际问题:一是 MCP Server 不需要自己持有 API Key(用客户端的额度);二是最终控制权在客户端——用户可以审核、修改、拒绝服务端的采样请求。这比给 Server 一个 API Key 安全得多。
我们内部还没用 sampling,主要因为它要求有状态连接(服务端要记住请求上下文)。但架构上我认可这个设计。
Server 的三个能力:不只是工具
很多人以为 MCP Server 就是暴露工具。实际上规范定义了三类能力,区别很大:
| 能力 | 谁控制 | 语义 | 方法 |
|---|---|---|---|
| Tools | 模型 | 执行动作,可能有副作用 | tools/list、tools/call |
| Resources | 应用/用户 | 读取数据,幂等 | resources/list、resources/read、resources/subscribe |
| Prompts | 用户 | 预置的提示词模板 | prompts/list、prompts/get |
「谁控制」这一列是关键。Tools 由模型主动调用,Resources 和 Prompts 由人或应用主动选择。这个区分在 Function Calling 里完全不存在——Function Calling 只有工具。
Resource 的价值在于它把「知识」和「动作」分开了。我们的 SOP 文档如果做成工具,模型得知道「有个工具能读 SOP」;做成 Resource,可以在客户端界面上展示成可附件的文档,用户点一下就加载进上下文,不需要模型猜。
Prompt 模板则是把人类经验固化。我们写了几个运维场景的标准排查流程模板,新同事直接用,效果和老同事基本一致。这是 Function Calling 完全覆盖不到的场景。
回到那个问题:到底该用哪个
现在可以正经回答了。我的判断标准:
- 工具只被一个应用用、且短期内不会变 → Function Calling 就够了。多一层协议就是多一层复杂度和故障点。我们有个一次性的数据清洗脚本,用的就是 Function Calling,200 行搞定。
- 工具要被多个应用/多个 Agent 复用 → MCP。我们的 CMDB 被三个 Agent 客户端使用,MCP 省掉了三份重复实现。
- 需要把能力开放给外部生态(比如让 Claude Desktop 或 IDE 接入)→ 必须 MCP。因为 Function Calling 没有跨应用的标准,你没法给 Claude Desktop 塞一段 Java 代码。
- 需要资源订阅、提示模板、反向采样 → 只能 MCP。
还有个现实考量:MCP 的调试成本更高。Function Calling 出问题,看一次 API 请求响应就知道;MCP 出问题,要区分是传输层、协议层还是应用层的问题。我们大概有 30% 的 MCP 相关工时花在调试上,Function Calling 大概是 5%。
现在的生态和我们的实践
到这个月,MCP 的生态已经比较成熟了。我们自己跑了 4 个 Server,社区和厂商提供的 Server 也很多(数据库、Git、文件系统、各类 SaaS)。Spring AI 的 MCP 支持已经稳定,我们用了三个月,没遇到协议层面的 bug。
但也有几个地方还不够成熟,提一下:
- 鉴权规范还在演进。早期的 MCP 没有定义认证方式,大家各搞各的(有的用 Header 传 token,有的用环境变量)。现在规范里引入了基于 OAuth 2.1 的授权框架,但实现程度参差不齐。我们内部用的是简单的 Header token + 网关层校验,够用但不标准。
- 工具数量多了之后选择准确率下降。这个我在另一篇里提过,我们的经验是单个 Server 不超过 40 个工具。MCP 协议本身没有提供工具分组或路由的机制,需要自己在架构层解决。
- 无状态模式下的权限校验。有 Session 时可以一次认证多次调用,无状态下每次请求都要重新校验。我们的做法是在网关层做一次认证,把用户身份放进内部 Header,MCP Server 信任这个 Header。
就写到这。如果哪天你也被《MCP 协议详解:AI 应用的 USB-C 接口》里同一个坑绊住,回来翻这篇,能省半小时。