MCP 和 Function Calling 不是一回事
一个常被混用的概念:Function Calling 是模型本身的能力,MCP 是连接模型和工具的协议。这篇讲清楚它们在工具发现、安全边界、可移植性上的真实差别,以及 Spring AI 是怎么统一这两者的。
这两个词经常被人当成同义词用,但它们在一个 AI 应用里处于完全不同的层级。
混用会直接导致架构选错——比如纠结”要不要为了用 MCP 换掉 Function Calling”,其实这两者根本不冲突。
一句话区分
- Function Calling:模型本身具备的能力(what)——模型能在生成回复时判断”我需要调个外部函数”
- MCP:连接模型和工具的协议(how)——定义工具怎么被发现、怎么调用、怎么传参
打个比方:Function Calling 像是编程语言里的函数调用机制,MCP 像是 HTTP 协议。你会问”用 HTTP 还是用函数调用”吗?不会,因为它们解决的是不同层面的问题。
定位对比:
| 维度 | Function Calling | MCP |
|---|---|---|
| 本质 | 模型 API 的特性 | 应用层通信协议 |
| 定义者 | 模型厂商(OpenAI、Anthropic 等) | Anthropic 开源标准 |
| 作用域 | 单个模型与其定义的工具 | 任何 AI 应用与任何工具 |
| 标准化 | 各厂商实现不一致 | 统一标准,模型无关 |
| 工具管理 | 每次请求都要带上工具定义 | 工具独立部署,动态发现 |
四个真正的差别
1. 工具发现机制
Function Calling:每次请求都要把完整的工具定义塞进去。
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
functions=functions, # 每次都得传
function_call="auto"
)
工具少的时候无所谓。当你有五十个工具的时候,每次请求都要带上五十个 schema,token 消耗和延迟都会明显上去。
MCP:工具是独立部署的服务,客户端连上之后自己拉取工具列表。工具定义不占对话的上下文,还能动态增删——加一个新工具,所有连着的客户端立刻能用,不用改任何代码。
2. 安全边界
这是我认为最容易被低估的一条。
Function Calling 的安全完全取决于应用自己怎么实现。模型说”调用 read_file('/etc/passwd')”,你的代码要不要执行、有没有权限控制,全是应用层的责任。
MCP 把安全边界做进了协议:MCP Server 自己声明能访问什么资源,客户端在连接时就能看到并授权。用 MCP 接本地文件系统,权限是 Server 侧控制的,模型没法靠 prompt 绕过。
配合前面那篇讲沙箱的文章看会更清楚——MCP 解决的是”工具该被允许访问什么”,沙箱解决的是”代码执行时怎么隔离”,两者是互补的。
3. 可移植性
Function Calling 最烦的实际问题是各厂商格式不统一。同一个工具,你得维护三份定义:
// OpenAI 的格式
{ "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": {...} } }
// Anthropic Claude 的格式
{ "name": "get_weather", "description": "...", "input_schema": {...} }
// Google Gemini 的格式
{ "function_declarations": [{ "name": "get_weather", "description": "...", "parameters": {...} }] }
想让你的工具同时支持三家模型,就得写三套。MCP 是模型无关的,写一次,任何支持 MCP 的客户端都能用。
4. 资源管理
Function Calling 是无状态的,每次调用独立。
MCP Server 可以持有状态(数据库连接、文件句柄、会话上下文),这意味着工具之间能共享资源、能互相调用形成工作流,而不是每次都从零开始。
Spring AI 怎么统一这两者
如果你用 Java 技术栈,Spring AI 已经把这层差异封装掉了——这也正好说明两者是互补而非互斥的关系:
@Service
public class ToolService {
// 1. 定义工具(同时支持 Function Calling 和 MCP)
@Tool(name = "get_weather", description = "获取天气信息")
public String getWeather(@ToolParam(description = "城市名称") String city) {
return weatherService.getWeather(city);
}
// 2. 配置 MCP 连接
@Bean
public McpClient mcpClient() {
return McpClient.builder()
.serverUrl("http://localhost:8080/mcp")
.build();
}
// 3. 使用统一 API
public String chatWithTools(String query) {
return chatClient.prompt()
.user(query)
.tools("get_weather") // 统一引用,不区分底层是哪种机制
.call()
.content();
}
}
注意最后那个 .tools("get_weather")——你不需要关心这个工具是本地的 Function Calling 还是远程的 MCP Server,框架帮你处理了。
这就是”两者不冲突”的最好证明:在同一个应用里,你可以本地工具走 Function Calling,远程工具走 MCP,用同一套代码调用。
什么时候用哪个
我的判断标准:
用 Function Calling 就够了的情况:
- 工具数量在十个以内
- 工具是应用私有的,不需要给别人用
- 不需要跨会话保持状态
- 只对接一家模型厂商
应该上 MCP 的情况:
- 工具要给别人用(这是 MCP 最核心的价值——你写的工具,别人和别人的 AI 都能直接接)
- 工具数量多,每次全量传定义已经影响成本和延迟
- 需要严格的权限边界(比如访问本地文件、数据库)
- 要对接多家模型
- 工具之间有依赖关系,需要形成工作流
一个具体场景
假设你要让 AI 访问本地文件系统。
纯 Function Calling 的做法:定义 read_file、write_file 两个函数,你的代码里接收路径参数,然后直接读写。风险是——模型完全可能被诱导去读 /etc/passwd 或者你的 SSH 私钥,而你的实现里如果没做路径白名单,就直接裸奔了。
MCP 的做法:起一个 filesystem MCP Server,在 Server 配置里声明只允许访问 ~/Documents/workspace。无论模型怎么被诱导,协议层就把它挡在外面了。
差别不在”能不能实现”——Function Calling 里你也能手写白名单。差别在于MCP 把这件事变成了标准配置,而不是每个开发者各自实现一遍。
小结
- Function Calling 是模型能力,MCP 是连接协议,不在一个层级
- 大方向:应用内部、少量工具 → Function Calling;要对外开放、工具多、要权限控制 → MCP
- Java 栈直接用 Spring AI 的
@Tool+McpClient,两套机制统一调用
前面那篇《写一个 MCP 服务》讲的是怎么把你的工具做成 MCP Server,这篇讲的是它和 Function Calling 的关系,两篇连着看会清楚一些。
关注我