工具调用:Function Calling → MCP → Skills 协议进化史

工具调用是 Agent 能力的边界。本文先给出三种机制的速查对比,再深入原理拆解各自的设计取舍,最后给出生产环境选型原则。


一、概念速查

1.1 为什么需要工具调用

LLM 本身有三大硬伤:知识截止于训练数据、不会精确计算、无法操作外部系统。工具调用就是给 LLM 装上"手脚"——让它可以搜索网页、执行代码、查询数据库、调用 API。

Function Calling
输出结构化参数

原始结果

LLM
只能输出文本

工具调用
搜索 / 代码 / API

Observation
喂回 LLM 决策

1.2 三代技术速查

维度 Function Calling MCP Skills
诞生时间 2023(OpenAI) 2024 年底(Anthropic) 2025(社区演化)
核心定位 LLM 输出结构化参数的机制 工具接入的统一协议 可复用工具单元封装
类比 有线耳机——接口固定 USB-C——统一标准 预装 App——即插即用
工具注册方式 写在 System Prompt MCP Server 动态注册 配置文件声明
与 LLM 的耦合 紧耦合——每个厂商各一套 松耦合——协议统一 松耦合——运行时加载
适用规模 3-5 个简单工具 10-100 个工具 任意规模,侧重复用
2023 OpenAI 发布 Function Calling 首次让 LLM 输出结构化参数 2024 Anthropic 提出 MCP 协议 统一工具接入标准 2025 社区演化出 Skills 机制 工具封装为可复用单元 2026 三者共存协同 FC 做参数提取 MCP 做工具发现 Skills 做能力封装 工具调用进化史

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 架构

初始化

list_tools()

工具列表

call_tool(name, args)

执行结果

MCP Host
(Agent 应用)

MCP Client
(协议客户端)

MCP Server
(工具提供方)

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 把一组相关的工具 + 调用规则 + 触发条件打包成一个可复用单元。

Skill: 科研助手

Paper Search
MCP: arxiv-server

Citation Check
MCP: scholar-server

Summary Gen
MCP: llm-server

依赖声明
pip: transformers
env: API_KEY

触发规则
关键词: 论文/文献/引用

Skills 的核心能力:

能力 说明 类比
工具编排 组合多个 MCP Server 成一个逻辑单元 手机 App 组合多个权限
依赖声明 声明所需的 Python 包、环境变量、外部服务 package.json
触发规则 定义什么用户输入自动激活此 Skill 快捷指令
状态隔离 每个 Skill 有独立的配置和上下文 容器
热加载 Agent 运行时动态安装/卸载 Skill 插件系统

2.4 三者协同:生产环境的标准模式

FC、MCP、Skills 不是替代关系,而是分工不同的三层

Agent 编排层
LangGraph / 百炼

Skills 层
科研助手 / 数据分析 / 客服

MCP 协议层
统一接入标准

Function Calling
参数提取机制

LLM 推理层
Qwen / GPT

一个实际场景的协同流程:

假设用户说"查一下这篇论文的被引量,然后跟去年同期的热门论文做个对比":

做了什么 谁负责
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 选型决策

1-3 个

4+ 个以上

需要几个工具?

只用 Function Calling
简单够用

需要热插拔?

工具间有依赖?

只接 MCP
标准协议接入

Skills + MCP
组合封装

MCP + FC
各自独立


三、架构设计原则

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:

  1. 先用 FC 写死 2-3 个工具,跑通 Agent 循环
  2. 工具超过 5 个时,接入 MCP 统一管理
  3. 出现重复组合时,封装为 Skills
  4. 每个步骤验证"不加这个层会怎样"——能省则省
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐