工具调用:Function Calling → MCP → Skills 协议进化史
工具调用:Function Calling → MCP → Skills 协议进化史
工具调用是 Agent 能力的边界。本文先给出三种机制的速查对比,再深入原理拆解各自的设计取舍,最后给出生产环境选型原则。
一、概念速查
1.1 为什么需要工具调用
LLM 本身有三大硬伤:知识截止于训练数据、不会精确计算、无法操作外部系统。工具调用就是给 LLM 装上"手脚"——让它可以搜索网页、执行代码、查询数据库、调用 API。
1.2 三代技术速查
| 维度 | Function Calling | MCP | Skills |
|---|---|---|---|
| 诞生时间 | 2023(OpenAI) | 2024 年底(Anthropic) | 2025(社区演化) |
| 核心定位 | LLM 输出结构化参数的机制 | 工具接入的统一协议 | 可复用工具单元封装 |
| 类比 | 有线耳机——接口固定 | USB-C——统一标准 | 预装 App——即插即用 |
| 工具注册方式 | 写在 System Prompt | MCP Server 动态注册 | 配置文件声明 |
| 与 LLM 的耦合 | 紧耦合——每个厂商各一套 | 松耦合——协议统一 | 松耦合——运行时加载 |
| 适用规模 | 3-5 个简单工具 | 10-100 个工具 | 任意规模,侧重复用 |
1.3 快速上手:三种方式对比
Function Calling 方式:
import openai
# 在请求中声明工具
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京天气怎么样?"}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
}]
)
# LLM 自动输出: {"function": "get_weather", "args": {"city": "北京"}}
MCP 方式(Server 端):
from mcp.server import Server, stdio_server
from mcp.types import Tool, TextContent
# 定义一个 MCP Server
server = Server("weather-server")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [Tool(
name="get_weather",
description="查询指定城市的天气",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
)]
@server.call_tool()
async def call_tool(name: str, args: dict) -> list[TextContent]:
if name == "get_weather":
result = await fetch_weather(args["city"])
return [TextContent(type="text", text=result)]
# Server 通过 stdio 协议与 Agent 通信
stdio_server.run(server)
Skills 方式(配置文件):
# skills/weather.skill.yaml
name: weather-search
description: 天气查询工具
tools:
- name: get_weather
description: 查询天气
mcp_server: weather-server
- name: get_forecast
description: 查询天气预报
mcp_server: weather-server
triggers:
- "今天*天气"
- "*气温*"
# Agent 加载 Skill
from skills import load_skill
weather_skill = load_skill("skills/weather.skill.yaml")
agent.register_skill(weather_skill)
# 所有工具声明、MCP Server 连接自动完成
二、底层原理
2.1 Function Calling:一切的开端
核心机制:
Function Calling 不是让 LLM 真的"调用"函数,而是让 LLM 输出一个结构化的函数调用描述,由框架解析后执行。
用户问"北京天气"
│
▼
┌──────────────────────────────┐
│ Step 1: 构造 Prompt │
│ system: 你有以下工具可用 │
│ - get_weather(city): 查天气 │
│ user: 北京天气怎么样? │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Step 2: LLM 输出函数调用 │
│ 模型内部判断"用户想查天气" │
│ → 匹配到 get_weather 工具 │
│ → 输出: {"function": │
│ "get_weather", │
│ "args": {"city": "北京"}} │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Step 3: 框架执行函数 │
│ 解析输出 → 调用 get_weather("北京") │
│ → 返回 "25°C 晴" │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Step 4: 结果喂回 LLM │
│ LLM 拿到结果 → 组织自然语言回答 │
│ → "北京今天 25°C,天气晴朗" │
└──────────────────────────────┘
Function Calling 的三大局限:
| 局限 | 表现 | 根因 |
|---|---|---|
| 工具声明膨胀 | 100 个工具的声明可能超过 Context 窗口 | 所有工具描述写在 System Prompt 里 |
| 厂商锁定 | OpenAI 的 tools 参数不能直接给 Claude 用 | 各厂商格式不同,无统一标准 |
| 静态注册 | 增加/修改工具需要更新 Prompt 并重启 | 工具描述编译到 Prompt 中,无法动态发现 |
这些局限直接催生了 MCP。
2.2 MCP:统一接入协议
MCP 的核心思想: 把工具从"写在 Prompt 里的字符串"变成"独立运行的服务"。
MCP 如何解决 FC 的三个问题:
| FC 的问题 | MCP 的解法 |
|---|---|
| 工具声明膨胀 | Server 按需注册,Agent 只加载当前任务需要的工具 |
| 厂商锁定 | 统一协议,任意 LLM 框架都可接入 |
| 静态注册 | Server 独立部署,新增工具无需重启 Agent |
MCP 的通信流程:
Agent 启动
│
▼
┌──────────────────────────┐
│ 1. 连接 MCP Server │
│ 通过 stdio / SSE │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 2. 拉取工具清单 │
│ list_tools() → │
│ [weather, search, ...] │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 3. 注入工具描述到 Prompt │
│ 只注入当前任务相关的 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 4. LLM 输出函数调用 │
│ → call_tool() │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 5. 返回结果到 LLM │
│ 继续下一轮推理 │
└──────────────────────────┘
截至 2026 年中,MCP 生态已有超过 3000 个 Server,覆盖搜索、数据库、文件系统、代码执行等常见场景。
2.3 Skills:工具的可复用封装
Skills 解决的是 MCP 没有覆盖的问题——工具的组织和复用。
有了 MCP,每个工具都是独立服务了。但一个完整的 Agent 能力单元往往需要多个工具协作。Skills 把一组相关的工具 + 调用规则 + 触发条件打包成一个可复用单元。
Skills 的核心能力:
| 能力 | 说明 | 类比 |
|---|---|---|
| 工具编排 | 组合多个 MCP Server 成一个逻辑单元 | 手机 App 组合多个权限 |
| 依赖声明 | 声明所需的 Python 包、环境变量、外部服务 | package.json |
| 触发规则 | 定义什么用户输入自动激活此 Skill | 快捷指令 |
| 状态隔离 | 每个 Skill 有独立的配置和上下文 | 容器 |
| 热加载 | Agent 运行时动态安装/卸载 Skill | 插件系统 |
2.4 三者协同:生产环境的标准模式
FC、MCP、Skills 不是替代关系,而是分工不同的三层。
一个实际场景的协同流程:
假设用户说"查一下这篇论文的被引量,然后跟去年同期的热门论文做个对比":
| 层 | 做了什么 | 谁负责 |
|---|---|---|
| Skills | 匹配"科研助手"Skill,加载论文搜索 + 引用查询 + 数据分析三个子能力 | Skill 注册表 |
| MCP | 连接 arxiv-server / scholar-server / code-server 三个 MCP Server | MCP Client |
| FC | LLM 输出 search_paper("transformer 2024") → 框架解析参数并执行 |
LLM + 框架 |
| 循环 | 拿结果 → 再输出 compare_citations([...]) → 再执行 → 直到完成 |
Agent 编排 |
2.5 选型决策
三、架构设计原则
3.1 FC 做参数,MCP 做发现,Skills 做封装
这是分层的第一原则。不要试图让一层覆盖所有需求——FC 只负责"LLM 输出结构",MCP 只负责"工具通信",Skills 只负责"组合复用"。
3.2 工具声明压缩
工具描述越长,LLM 选错工具的概率越高。每个工具的描述控制在 1-2 句话,参数控制在 5 个以内。如果工具超过 20 个,用 MCP 分组加载,不要一次性全量注册。
3.3 参数校验前置
LLM 填错参数是工具调用最主要的失败原因。在工具注册表层做参数类型和范围校验,拦截非法参数,不要让非法请求到达后端服务。
# 工具注册表的校验示例
TOOL_VALIDATORS = {
"get_weather": {
"city": lambda v: isinstance(v, str) and len(v) > 0,
"date": lambda v: re.match(r"\d{4}-\d{2}-\d{2}", v)
}
}
def validate_tool_call(name: str, args: dict) -> bool:
if name not in TOOL_VALIDATORS:
return False
for key, validator in TOOL_VALIDATORS[name].items():
if key in args and not validator(args[key]):
return False
return True
3.4 超时和重试
每个工具调用必须设超时。MCP 层的默认超时建议 30s。对于幂等的工具查询,自动重试 1 次;对于非幂等的写入操作,不重试,返回错误让 LLM 决策。
3.5 渐进式接入路径
不要一次性上全量 MCP + Skills:
- 先用 FC 写死 2-3 个工具,跑通 Agent 循环
- 工具超过 5 个时,接入 MCP 统一管理
- 出现重复组合时,封装为 Skills
- 每个步骤验证"不加这个层会怎样"——能省则省
更多推荐


所有评论(0)