很多人把 MCP 和 Function Calling 当成竞争关系,其实它们解决的是完全不同层次的问题。这篇文章用一个实际的聊天机器人项目,把两者的关系彻底讲清楚。
先说结论
Function Calling 是 LLM 与工具交互的语言,MCP 是工具注册和调用的标准协议。
两者不是替代关系,而是不同层次的解决方案:
javascript
用户 → Host(Cline/Claude Desktop)→ LLM(决策层)
↓ Function Calling / XML(Host自定义格式)
Host ←────────────────────────────→ MCP Server(工具层)
MCP 协议(JSON-RPC 2.0)
Function Calling 是什么
Function Calling 是 OpenAI 在 2023 年引入的功能,让 LLM 能够以结构化的方式"请求调用"外部函数。
本质上,它是一种约定格式:你告诉 LLM "有这些函数可以用",LLM 在认为需要的时候,输出一个符合格式的 JSON,宿主代码解析后执行对应函数。
工作流程
python
import anthropic
client = anthropic.Anthropic()
# Step 1: 定义工具
tools = [
{
"name": "get_weather",
"description": "获取指定城市的实时天气",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
}
},
"required": ["city"]
}
}
]
# Step 2: 发送请求(带工具定义)
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "北京今天天气怎么样?"}]
)
# Step 3: 检查 LLM 是否请求工具调用
if response.stop_reason == "tool_use":
tool_use = next(b for b in response.content if b.type == "tool_use")
print(f"LLM 请求调用工具:{tool_use.name}")
print(f"参数:{tool_use.input}")
# → 工具名:get_weather
# → 参数:{"city": "北京"}
# Step 4: 执行工具(你自己的代码)
weather_result = "北京今天晴,气温 22°C"
# Step 5: 把结果返回给 LLM
final_response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "北京今天天气怎么样?"},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": weather_result}]
}
]
)
print(final_response.content[0].text)
关键点:工具的定义、调用、结果处理,全部在你的代码里。LLM 只负责"决策"------它说"我要调用 get_weather,参数是 Beijing",但真正的调用是你的代码执行的。
用一个聊天机器人项目说清楚
马克在这期视频里用了一个叫 "MarkChat" 的项目来演示 MCP 和 Function Calling 的关系。
bash
MarkChat 项目结构:
MCP 与 Function Calling 到底什么关系/
└── MarkChat/
├── app.py # 主程序(MCP Host 角色)
├── tools.py # 工具定义(通过 Function Calling 暴露给 LLM)
└── mcp_client.py # MCP 客户端(连接 MCP Server)
场景:MarkChat 想使用一个天气 MCP Server
python
# mcp_client.py:连接 MCP Server,发现工具
class MCPClient:
def __init__(self, server_config: dict):
self.server_config = server_config
self.available_tools = []
async def connect(self):
"""连接 MCP Server 并获取工具列表"""
# 启动 MCP Server 进程
self.process = await asyncio.create_subprocess_exec(
*self.server_config["command"],
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE
)
# 初始化握手
await self._send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {...}})
await self._recv()
# 获取工具列表
await self._send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
result = await self._recv()
# 把 MCP 工具格式转换成 Function Calling 格式
self.available_tools = [
self._mcp_tool_to_fc_format(tool)
for tool in result["result"]["tools"]
]
def _mcp_tool_to_fc_format(self, mcp_tool: dict) -> dict:
"""把 MCP 工具格式转换成 Claude Function Calling 格式"""
return {
"name": f"mcp__{mcp_tool['name']}", # 加前缀区分
"description": mcp_tool["description"],
"input_schema": mcp_tool["inputSchema"]
}
async def call_tool(self, tool_name: str, arguments: dict) -> str:
"""调用 MCP Server 上的工具"""
real_name = tool_name.replace("mcp__", "")
await self._send({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {"name": real_name, "arguments": arguments}
})
result = await self._recv()
return result["result"]["content"][0]["text"]
python
# app.py:主程序,把 MCP 工具通过 Function Calling 暴露给 LLM
class MarkChat:
def __init__(self):
self.mcp_client = MCPClient({
"command": ["uv", "run", "weather.py"]
})
self.claude = anthropic.Anthropic()
async def setup(self):
await self.mcp_client.connect()
# mcp_client.available_tools 现在包含了天气工具,格式是 Function Calling 格式
async def chat(self, user_message: str) -> str:
# 把 MCP 工具列表作为 Function Calling 工具传给 LLM
all_tools = self.mcp_client.available_tools
messages = [{"role": "user", "content": user_message}]
while True:
response = self.claude.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=all_tools,
messages=messages
)
if response.stop_reason == "end_turn":
return response.content[0].text
if response.stop_reason == "tool_use":
# LLM 要调用工具
for block in response.content:
if block.type == "tool_use":
# 通过 MCP 协议实际执行工具
result = await self.mcp_client.call_tool(
block.name,
block.input
)
# 把工具结果返回给 LLM
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": block.id, "content": result}]
})
关系图:两者如何协同工作
scss
┌─────────────────────────────────────────────────────┐
│ MarkChat (Host) │
│ │
│ ┌─────────────┐ Function Calling ┌───────┐ │
│ │ │ ←──────────────────────→ │ │ │
│ │ MCP Client │ (工具定义 + 调用请求) │ LLM │ │
│ │ │ │ │ │
│ └──────┬──────┘ └───────┘ │
│ │ MCP 协议 (JSON-RPC 2.0) │
└─────────┼───────────────────────────────────────────┘
│
↓
┌──────────────┐
│ MCP Server │
│ (weather.py) │
└──────────────┘
Function Calling 发生在 Host(MarkChat)和 LLM 之间:LLM 用 Function Calling 格式表达"我要调用这个工具"。
MCP 协议 发生在 Host(MarkChat)和 MCP Server 之间:Host 用 JSON-RPC 2.0 实际执行工具调用。
为什么不直接用 Function Calling 而要 MCP?
| 维度 | 纯 Function Calling | MCP + Function Calling |
|---|---|---|
| 工具复用 | 每个项目重新实现 | 写一次 MCP Server,所有支持 MCP 的 Host 都能用 |
| 工具发现 | 硬编码在代码里 | 动态发现,新工具自动可用 |
| 跨 Host 兼容 | 不兼容 | 兼容所有 MCP Host |
| 进程隔离 | 工具和 Host 同进程 | 独立进程,崩溃不影响 Host |
| 开发分工 | 工具和 Host 必须同一个团队 | 工具提供方和 Host 开发者可以分离 |
总结
一句话分清两者:
- Function Calling :LLM 说"我想调用 X 工具"的语言格式(在 Host 和 LLM 之间)
- MCP :工具如何被发现、注册和调用的标准化协议(在 Host 和 Server 之间)
实际生产系统里,两者经常同时使用:MCP 负责工具的标准化接入,Function Calling 负责 LLM 与工具的交互接口。
理解了这个关系,你就能看懂为什么 Cursor、Cline、Claude Desktop 都在往 MCP 上迁移------不是为了替代 Function Calling,而是为了让工具生态标准化。