Function Calling 为什么不够用?深入拆解 MCP 标准协议的设计哲学

作 者:吴佳浩(Alben)
公众号:全栈架构师笔记
系列专栏:《MCP 与 Agent Tools 工程化落地实战》· 第 01 篇
导读
Function Calling 只是单次 HTTP 请求的语法糖,MCP(Model Context Protocol)才是 AI 时代的标准化 USB 总线。
以前每个 Agent 框架都在重复造工具适配器的轮子,写一套代码只能给一个框架用;MCP 第一次把 Tools、Resources 和 Prompts 变成了跨模型、跨框架、跨语言的通用基础设施。
函数调用解决了"模型怎么输出参数",MCP 解决了"智能体如何与世界解耦交互"。
在过去两年中,几乎所有做 Agent 的团队都经历过这样一段痛苦的技术演进: 早期 OpenAI 推出了 Function Calling,大家兴奋地在业务代码里手写一个个 JSON Schema。但随着接入的外部系统越来越多------内部数据库、GitLab、Jira、Kubernetes、飞书文档、本地文件系统------团队会立刻撞上一面坚硬的工程之墙:
| 困境现象 | 具体表现 | 架构根因 |
|---|---|---|
| 1. 碎片化与厂商锁定 | 为 LangChain 写的工具,在 | 缺乏统一的协议标准,每个框架 |
| (Vendor Lock-in) | AutoGen 或 Claude Code 里跑不通 | 都有自己私有的 Tool 抽象基类 |
| 2. 状态与连接管理缺失 | 每次函数调用都是无状态短连接, | 协议层缺乏长会话生命周期维护与 |
| (Stateless Churn) | 无法支持流式推送、事件订阅与鉴权 | 双向通信通道 |
| 3. 资源与上下文混淆 | 文档、日志、表格等静态资源,全被 | 缺乏资源与操作的语义正交解耦, |
| (Semantic Pollution) | 强行包成函数,导致模型意图混乱 | 导致 Prompt 空间极度低效 |
为了解决这种混乱的"工具孤岛"局面,Anthropic 开源了 MCP(Model Context Protocol),并在短短几个月内迅速成为事实上的行业标准。
为什么行业在有了 Function Calling 之后,依然迫切需要 MCP?MCP 底层究竟设计了哪些精妙的机制?
一、从 Function Calling 到 MCP:协议化演进的必然性
要理解 MCP 的价值,我们必须看清工具调用在架构上的四代演进:

- 🔸 第一代(Prompt 裸搓):解析经常失败,极不稳定;
- 🔸 第二代(Function Calling) :模型保证了 JSON 结构的稳定性,但它只是一套 API 序列化协议,完全不涉及工具在哪里运行、如何鉴权、如何跨网络发现;
- 🔸 第三代(框架私有 SDK):形成了严重的框架壁垒,团队在工具维护上浪费了大量无意义的胶水代码;
- 🔸 第四代(MCP 标准协议) :彻底将 Client(宿主应用/Agent) 与 Server(工具与数据提供方) 解耦。无论底层是大模型 A 还是大模型 B,只要支持 MCP,就能即插即用接入全球所有的 MCP Servers。
一句话总结这一章的核心观点:
Function Calling 只是模型接口层面的特性,MCP 则是整个分布式智能体生态的通信协议。
二、MCP 核心三要素:Tools、Resources 与 Prompts 的正交设计
很多初学者把 MCP 简单理解为"远程函数调用(RPC)"。这是极其片面的。MCP 协议的核心精髓,在于它将上下文交互正交拆解为三大支柱:
| 核心要素 | 抽象定位 | 交互模式 | 典型应用场景 |
|---|---|---|---|
| 🛠️ Tools (工具) | 模型可调用的动作 | 动态调用 (Model-Pull) | 执行 SQL、部署容器、 |
| (具备副作用) | 需大模型主动下发参数 | 修改本地文件、调 API | |
| 📄 Resources (资源) | 模型可读取的数据 | 被动装载 (App/User) | 读取日志、查看表结构、 |
| (只读无副作用) | 类似文件或 URI 数据源 | 获取 Git Diff、系统指标 | |
| 💬 Prompts (提示词) | 预定义的交互模板 | 显式触发 (User-Push) | 单元测试生成模板、 |
| (工程化工作流) | 固化的专家级提问范式 | 代码 Review 标准规程 |

- 🔸 Tools(动作):赋予 Agent 改变世界的能力(有副作用,必须受权限与审批管控);
- 🔸 Resources(数据) :赋予 Agent 观察世界的能力(标准化 URI 寻址,如
git://repo/diff或db://schema/users,只读且幂等); - 🔸 Prompts(模版):固化了人类专家的交互最佳实践,让用户一键激活复杂的多步指令。
一句话总结这一章的核心观点:
Tools 是手,Resources 是眼,Prompts 是任务书。三者解耦,才构成了完整的上下文交互协议。
三、MCP 通信管道:Stdio 与 Streamable HTTP/SSE 的选型权衡
在工程实现上,MCP 支持两种底层传输通道(Transports),它们适用于完全不同的物理场景:
| 对比维度 | Stdio Transport (标准输入输出) | Streamable HTTP / SSE |
|---|---|---|
| 通信机制 | 进程间管道 (stdin / stdout) | 长连接 HTTP + Server-Sent-Evt |
| 部署拓扑 | 本地子进程 (Subprocess) | 跨网络分布式服务 (Microservice |
| 鉴权与隔离 | 依赖操作系统进程级权限 | OAuth2、JWT、mTLS 标准网关 |
| 典型场景 | 桌面客户端、CLI 工具、本地排错 | 企业中台、多 Agent 共享微服务 |
| 优势与代价 | 零网络开销、极速冷启动; | 跨机器共享、弹性伸缩; |
| 无法跨机器共享、调试困难 | 需维护长连接与分布式网关 |

一句话总结这一章的核心观点:
本地极客工具选 Stdio 极速启动,企业级中台必须上 Streamable HTTP/SSE 实现多租户鉴权与共享。
四、生产级代码实战:手写一个标准 MCP Client
以下为基于 Python 3.11+ 与标准库构建的极简、无第三方重型依赖的 MCP Stdio Client 核心实现:
python
"""
mcp_client_runtime.py
生产级轻量 MCP Client 实现
支持:
- Stdio 子进程生命周期管理
- MCP Initialize 能力协商
- Tool Schema 获取
- Tool 调用
"""
import json
import subprocess
import threading
from typing import Any, Dict, List, Optional
class MCPStdioClient:
"""基于 Stdio 的 MCP Client"""
def __init__(self, command: str, args: List[str]):
self.command = command
self.args = args
self.process: Optional[subprocess.Popen] = None
self._request_id = 0
self._lock = threading.Lock()
def start(self) -> None:
"""启动 MCP Server 并完成 Initialize 握手"""
self.process = subprocess.Popen(
[self.command] + self.args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=0,
)
# Initialize
self._send_raw(
{
"jsonrpc": "2.0",
"id": self._next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "FullStackArchitectClient",
"version": "1.0.0",
},
},
}
)
self._read_response()
# Initialized Notification
self._send_raw(
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
}
)
def list_tools(self) -> List[Dict[str, Any]]:
"""获取 MCP Server 暴露的 Tool Schema"""
req_id = self._next_id()
self._send_raw(
{
"jsonrpc": "2.0",
"id": req_id,
"method": "tools/list",
"params": {},
}
)
response = self._read_response()
return response.get("result", {}).get("tools", [])
def call_tool(
self,
tool_name: str,
arguments: Dict[str, Any],
) -> str:
"""调用远程 MCP Tool"""
req_id = self._next_id()
self._send_raw(
{
"jsonrpc": "2.0",
"id": req_id,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments,
},
}
)
response = self._read_response()
content = response.get("result", {}).get("content", [])
return "\n".join(
item.get("text", "")
for item in content
if item.get("type") == "text"
)
def _next_id(self) -> int:
"""生成递增 Request ID"""
with self._lock:
self._request_id += 1
return self._request_id
def _send_raw(self, payload: Dict[str, Any]) -> None:
"""发送 JSON-RPC 请求"""
if not self.process or not self.process.stdin:
raise RuntimeError("MCP process has not been started.")
raw = json.dumps(payload) + "\n"
self.process.stdin.write(raw)
self.process.stdin.flush()
def _read_response(self) -> Dict[str, Any]:
"""读取 JSON-RPC 响应"""
if not self.process or not self.process.stdout:
raise RuntimeError("MCP process has not been started.")
line = self.process.stdout.readline()
if not line:
stderr = ""
if self.process.stderr:
stderr = self.process.stderr.read()
raise RuntimeError(
f"MCP Server closed unexpectedly: {stderr}"
)
return json.loads(line.strip())
def close(self) -> None:
"""关闭 MCP Server"""
if self.process:
self.process.terminate()
self.process.wait()
self.process = None
本篇总结
- 🔸 Function Calling 只是语法糖,MCP 才是 AI 外设生态的标准化 USB 总线;
- 🔸 Tools、Resources、Prompts 三要素正交解耦,构成了完整的上下文交互规范;
- 🔸 Stdio 适合本地,Streamable HTTP/SSE 适合企业级中台;
- 🔸 掌握协议层,才能真正构建可插拔、可演进的工业级 Agent。
在下一篇中,我们将深入实战:《从零手写一个生产级 MCP Server:鉴权、流式传输与状态管理》,带你从零构建一个高可用的企业级数据连接器!
筒子们本篇为《企业级 Agent 实战指南》· 第二章的第 01 篇,后续续会更新完整的agent的开发的全部过程,如果你对Agent开发感兴趣不妨关注一下本合集。