Function Calling 与 Tool Use:让大模型「动」起来

《大模型知识与部署》系列 · No.27 / 35
适合人群:AI 工程师、应用架构师
阅读时间:约 25 分钟


在这里插入图片描述

写在前面

上一篇我们讲了 RAG——让大模型「知道知识」。这一篇讲 Tool Use——让大模型「做事情」

如果 LLM 只是个会聊天的"嘴巴",价值有限。真正的革命发生在它能主动调用工具——查天气、订机票、写代码、跑 SQL、控制设备、操作浏览器……

这就是 Function Calling / Tool Use 的能力。

2024 年下半年开始,几乎所有主流大模型都把 Tool Use 作为核心能力。2025-2026 的杀手应用——Cursor、Claude Code、Devin、Manus……全都建立在这个基础上。

如果你做过相关工作,下面这些问题应该不陌生:

  • OpenAI 的 tools 字段和 Anthropic 的 tool_use 怎么对接?
  • 模型经常调错工具 / 参数错乱,怎么治?
  • 让模型一次调多个工具(parallel calling)怎么实现?
  • MCP(Model Context Protocol)和 Function Calling 是同一个东西吗?
  • 自部署开源模型支持 Function Calling 吗?

读完本文你将能:

  1. 设计标准的 Tool Use 协议
  2. 用 vLLM / Claude / GPT 实现工具调用
  3. 理解 MCP 协议在 2026 年的地位
  4. 用 ReAct 模式做多步推理工具调用
  5. 跑通一个端到端 Tool Use Agent

我们开始。


一、Tool Use 在 LLM 时代的位置

1.1 从"会答"到"会做"

LLM 应用的演进:

2022:ChatGPT  ────────────  会答问题
2023:RAG 应用  ────────────  会查私有知识
2024:Function Calling  ───  会调外部接口
2025:Agent 时代  ─────────  会自主完成任务
2026:MCP / Computer Use  ──  会操作 GUI / 系统

Tool Use 是 Agent 的核心能力——没有它,LLM 只能"说不能动"。

1.2 三类典型 Tool

类型 例子 难度
查询类 查天气、查股价、查文档
执行类 发邮件、下订单、调 API ⭐⭐⭐
计算类 跑 SQL、写代码、做表格 ⭐⭐⭐⭐
GUI 操作类 浏览器自动化、桌面操作 ⭐⭐⭐⭐⭐

1.3 工程师为什么必须懂

如果你做以下任何工作,必须懂 Tool Use:

  • Agent 应用:自动化任务流
  • AI 客服:能查订单、发工单
  • AI Coding:调用 Linter / Test / Build
  • AI 分析师:跑 SQL、画图、生成报告
  • AI 助理:日历、邮件、文档

二、Function Calling 协议详解

2.1 OpenAI 标准(事实主流)

OpenAI 在 2023.06 推出 Function Calling,后来扩展成 Tools,已经成为事实标准。

工具定义
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询指定城市的天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名,如「上海」"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "default": "celsius"
                }
            },
            "required": ["city"]
        }
    }
}]

关键点

  1. description 极其重要——模型只通过 description 理解工具
  2. parameters 用 JSON Schema
  3. required 字段强制约束
调用流程

完整调用是多轮的:

from openai import OpenAI

client = OpenAI()

# 第 1 轮:用户问题 + 工具列表
response = client.chat.completions.create(
    model="gpt-5",
    messages=[
        {"role": "user", "content": "上海明天天气怎么样?"}
    ],
    tools=tools,
    tool_choice="auto",   # auto / none / required / 指定工具
)

# 模型返回:工具调用请求
assistant_msg = response.choices[0].message
# assistant_msg.tool_calls = [{
#     "id": "call_abc123",
#     "function": {
#         "name": "get_weather",
#         "arguments": '{"city": "上海"}'
#     }
# }]

# 第 2 轮:执行工具 + 把结果送回
tool_result = call_real_weather_api("上海")  # 你的真实实现

response = client.chat.completions.create(
    model="gpt-5",
    messages=[
        {"role": "user", "content": "上海明天天气怎么样?"},
        assistant_msg,
        {
            "role": "tool",
            "tool_call_id": "call_abc123",
            "content": json.dumps(tool_result)
        }
    ],
    tools=tools,
)

# 模型基于工具结果生成最终回答
print(response.choices[0].message.content)
# "上海明天晴,最高 28°C,最低 18°C"
Parallel Function Calling

OpenAI / Claude 都支持一次调用多个工具

# 用户问:上海和北京天气怎么样?
# 模型可能返回:
{
    "tool_calls": [
        {"id": "call_1", "function": {"name": "get_weather", "arguments": '{"city": "上海"}'}},
        {"id": "call_2", "function": {"name": "get_weather", "arguments": '{"city": "北京"}'}}
    ]
}

# 客户端并行执行两个工具,结果一起送回

并行调用大幅减少多步延迟,是 2026 年 Agent 的核心优化

2.2 Anthropic(Claude)的差异

Claude 的协议略有不同:

import anthropic

client = anthropic.Anthropic()

tools = [{
    "name": "get_weather",
    "description": "查询天气",
    "input_schema": {           # 不是 parameters
        "type": "object",
        "properties": {...},
        "required": ["city"]
    }
}]

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "上海天气"}]
)

# response.content 包含混合内容:
# [
#   {"type": "text", "text": "我帮你查询..."},
#   {"type": "tool_use", "id": "...", "name": "get_weather", "input": {"city": "上海"}}
# ]

主要差异:

维度 OpenAI Anthropic
字段名 parameters input_schema
返回格式 tool_calls 数组 content 混合块
强制调用 tool_choice: "required" tool_choice: {"type": "tool", "name": "..."}
并行 默认支持 默认支持
描述 单独字段 单独字段

LiteLLM、LangChain 等工具会自动转换两种格式。

2.3 各家模型的 Tool Use 能力

实测对比(2026.05 主流模型):

模型 单工具准确率 多工具准确率 参数准确率
Claude Opus 4.7 99% 97% 99%
GPT-5 98% 95% 98%
Gemini 2.5 Pro 97% 93% 97%
Qwen3-32B 95% 88% 94%
Llama-3-70B 91% 80% 91%
DeepSeek V3 96% 92% 95%

关键观察

  • 闭源 Top 3 都 > 95%
  • 开源 Qwen / DeepSeek 接近闭源
  • 多工具并行调用是当下分水岭

三、自部署模型的 Tool Use

3.1 vLLM 中启用 Tool Use

vLLM 0.6+ 全面支持 Function Calling:

vllm serve Qwen/Qwen3-32B-Instruct \
    --enable-auto-tool-choice \
    --tool-call-parser hermes \    # 解析器(按模型选)
    --chat-template /path/to/template.jinja \
    --port 8000

关键参数

  • --enable-auto-tool-choice:自动模式(推荐)
  • --tool-call-parser:每个模型有自己的格式

主流模型的 parser 选择:

模型 parser
Qwen3 hermes
Llama 3 llama3_json
Mistral mistral
InternLM internlm

3.2 SGLang 中启用

python -m sglang.launch_server \
    --model-path Qwen/Qwen3-32B-Instruct \
    --tool-call-parser qwen25 \
    --port 30000

SGLang 对结构化输出 / 工具调用支持更原生(第 17 篇讲过)。

3.3 Ollama 中启用

import ollama

response = ollama.chat(
    model="qwen3:32b-instruct",
    messages=[{"role": "user", "content": "上海天气"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询天气",
            "parameters": {...}
        }
    }]
)

Ollama 内置 Tool Use 解析,开箱即用。


四、ReAct 模式:多步推理 + 工具调用

简单的"用户问→工具调用→回答"是基础。真正强大的应用需要多步推理——这就是 ReAct 模式。

4.1 ReAct 的思想

ReAct = Reasoning + Acting

Thought:我需要先查上海天气,再查北京天气,最后对比
Action:调用 get_weather("上海")
Observation:上海 28°C,晴
Thought:现在查北京
Action:调用 get_weather("北京")
Observation:北京 25°C,多云
Thought:可以回答了
Final Answer:上海比北京暖 3°C,且更晴朗

模型在"思考"和"行动"之间循环,直到能给出最终答案。

4.2 实现 ReAct 循环

def react_agent(question, max_iter=10):
    messages = [{"role": "user", "content": question}]
    
    for iteration in range(max_iter):
        # 让 LLM 决定下一步
        response = client.chat.completions.create(
            model="qwen3-32b",
            messages=messages,
            tools=available_tools,
        )
        
        assistant_msg = response.choices[0].message
        messages.append(assistant_msg)
        
        # 如果没有工具调用,说明 LLM 已经给出最终答案
        if not assistant_msg.tool_calls:
            return assistant_msg.content
        
        # 并行执行所有工具调用
        for tool_call in assistant_msg.tool_calls:
            tool_name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            
            try:
                result = TOOLS[tool_name](**args)
            except Exception as e:
                result = f"Error: {e}"
            
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result),
            })
    
    return "达到最大迭代次数"

4.3 工具调用的"思考链"

带 thinking 的模型(如 Claude 4.7、o3、DeepSeek R1)让 ReAct 进入新阶段:

模型内部 thinking(用户不可见):
  分析:用户问的是天气对比
  规划:分两步,先查上海,再查北京
  考虑:可以并行查吗?可以
  决策:发起 parallel tool calls

可见输出:[tool_use × 2 并行]

Thinking 模型的工具使用更智能——会主动规划,避免无效调用。


五、MCP:协议化的工具生态

5.1 MCP 是什么

MCP(Model Context Protocol)是 Anthropic 在 2024.11 推出的开放协议,目标是把 AI 与各种数据源 / 工具的接入标准化

类比理解:

USB-C 之于硬件接口  =  MCP 之于 AI 工具接入

之前每个工具都要自己设计接口给每个模型用——M 个模型 × N 个工具 = M×N 个适配。MCP 把这变成 M+N。

5.2 MCP 架构

┌──────────────────────────────────────┐
│ AI Host(Claude Desktop / Cursor)   │
└──────────────────────────────────────┘
       ↓ MCP Client
┌──────────────────────────────────────┐
│ MCP Server                            │
│ ├─ filesystem 工具                    │
│ ├─ database 工具                      │
│ ├─ web search 工具                    │
│ └─ ... 你想要的任何工具                │
└──────────────────────────────────────┘

5.3 2026 年 MCP 生态

经过 1 年多发展,MCP 已经成为 Agent 时代的事实标准之一:

MCP Server 功能
GitHub 代码、PR、issue 操作
Slack 消息、频道管理
Notion 数据库、文档
PostgreSQL SQL 查询
Filesystem 文件读写
Browser 网页操作
Memory 持久化记忆
你自研 任何业务

主流支持

  • Claude Desktop / Claude Code(原生)
  • Cursor / Windsurf(2025 集成)
  • 各类 Agent 框架(LangChain、CrewAI 等)
  • 国内:Cherry Studio、ChatBox

5.4 写一个 MCP Server

"""
简单的 MCP Server 示例
依赖:pip install mcp
"""
from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server("my-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, arguments: dict) -> list[TextContent]:
    if name == "get_weather":
        city = arguments["city"]
        # 实际逻辑
        result = await fetch_weather(city)
        return [TextContent(type="text", text=json.dumps(result))]

if __name__ == "__main__":
    import asyncio
    from mcp.server.stdio import stdio_server
    
    async def main():
        async with stdio_server() as (read, write):
            await server.run(read, write, ...)
    
    asyncio.run(main())

任何 MCP 兼容的客户端都能用这个 Server。

5.5 MCP vs 传统 Function Calling

维度 Function Calling MCP
协议 每个 API 自己定 标准化
工具描述 写在每次请求 Server 注册
动态加载 容易(即插即用)
跨模型复用 容易
跨应用复用 容易
生态 各家分散 统一

未来趋势:MCP 将逐渐替代各家私有的 Function Calling 协议——但目前两者并存。


六、生产实战:完整 Tool Use Agent

下面是一个生产可用的 Tool Use Agent 完整实现。

6.1 工具定义层

from typing import Callable
from dataclasses import dataclass

@dataclass
class Tool:
    name: str
    description: str
    parameters: dict
    handler: Callable

# 注册工具
TOOLS: dict[str, Tool] = {}

def register_tool(name: str, description: str, parameters: dict):
    def decorator(func: Callable):
        TOOLS[name] = Tool(name, description, parameters, func)
        return func
    return decorator

@register_tool(
    name="search_kb",
    description="在知识库中搜索相关文档",
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "top_k": {"type": "integer", "default": 5}
        },
        "required": ["query"]
    }
)
async def search_kb(query: str, top_k: int = 5):
    results = await rag_engine.search(query, top_k=top_k)
    return [{"text": r.text, "source": r.source} for r in results]

@register_tool(
    name="run_sql",
    description="执行 SQL 查询并返回结果",
    parameters={
        "type": "object",
        "properties": {"sql": {"type": "string"}},
        "required": ["sql"]
    }
)
async def run_sql(sql: str):
    # 加白名单(防 SQL 注入)
    if not is_safe_sql(sql):
        return {"error": "Unsafe SQL"}
    return await db.execute(sql)

6.2 Agent 核心循环

import json
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="http://vllm:8000/v1",
    api_key="dummy"
)

async def agent_loop(question: str, max_iter: int = 15):
    messages = [
        {"role": "system", "content": "你是一个专业助手,可以调用工具完成任务"},
        {"role": "user", "content": question}
    ]
    
    tool_schemas = [
        {"type": "function", "function": {
            "name": t.name,
            "description": t.description,
            "parameters": t.parameters,
        }}
        for t in TOOLS.values()
    ]
    
    for it in range(max_iter):
        response = await client.chat.completions.create(
            model="qwen3-32b",
            messages=messages,
            tools=tool_schemas,
            tool_choice="auto",
        )
        
        msg = response.choices[0].message
        messages.append(msg.model_dump())
        
        if not msg.tool_calls:
            return msg.content
        
        # 并行执行所有工具
        tasks = []
        for tc in msg.tool_calls:
            tool = TOOLS.get(tc.function.name)
            if tool is None:
                tasks.append((tc.id, {"error": "Unknown tool"}))
                continue
            args = json.loads(tc.function.arguments)
            tasks.append((tc.id, tool.handler(**args)))
        
        # 等所有工具
        results = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True)
        
        for (call_id, _), result in zip(tasks, results):
            messages.append({
                "role": "tool",
                "tool_call_id": call_id,
                "content": json.dumps(result, ensure_ascii=False)
            })
    
    return "已达最大迭代次数"

6.3 流式输出 + 工具调用

生产环境通常要流式输出:

async def agent_loop_stream(question: str):
    messages = [...]
    
    while True:
        stream = await client.chat.completions.create(
            model="qwen3-32b",
            messages=messages,
            tools=tool_schemas,
            stream=True,
        )
        
        # 收集 stream,区分文本 chunk 和 tool_call chunk
        text_buf, tool_calls = "", []
        async for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                yield {"type": "text", "content": delta.content}
                text_buf += delta.content
            if delta.tool_calls:
                # 增量构建 tool_calls
                ...
        
        if not tool_calls:
            return
        
        # 执行工具
        for tc in tool_calls:
            yield {"type": "tool_start", "tool": tc.function.name}
            result = await TOOLS[tc.function.name].handler(...)
            yield {"type": "tool_end", "tool": tc.function.name, "result": result}
            messages.append({"role": "tool", ...})

6.4 错误处理与重试

async def safe_call_tool(tool, args, retries=2):
    for i in range(retries + 1):
        try:
            return await tool.handler(**args)
        except ValidationError as e:
            # 参数错误,立即返回让 LLM 修
            return {"error": f"Invalid args: {e}"}
        except TimeoutError:
            if i == retries:
                return {"error": "Tool timeout"}
            await asyncio.sleep(2 ** i)
        except Exception as e:
            return {"error": str(e)}

让 LLM 看到错误信息可以自我纠错——下一轮它会改用更合适的参数。


七、避坑清单 + 下一篇预告

7.1 6 大常见坑

坑 1:工具 description 模糊

症状:模型频繁选错工具。

对策

  • description 要写"什么时候用"、“返回什么”
  • 加 examples(部分模型支持)
坑 2:参数 JSON Schema 不严格

症状:模型乱传参数。

对策

  • enum 限制可选值
  • pattern 正则约束
  • 强制 required 字段
坑 3:工具太多

症状:30 个工具同时给模型 → 选不对。

对策

  • 工具不要超过 20 个
  • 大量工具用两级路由(先选工具组,再选具体工具)
  • 或用 RAG 动态召回相关工具
坑 4:陷入循环

症状:模型连续 10 次调用同一个工具。

对策

  • max_iter
  • 检测同样工具调用 3 次相同参数 → 终止
  • LLM 提示中加「不要重复调用相同工具」
坑 5:工具结果太长

症状:单个工具返回 50K tokens → 上下文爆炸。

对策

  • 工具自身做摘要 / 分页
  • 引入 “view next page” 子工具
  • 长结果存到 KV store,返回 ID
坑 6:并行工具的依赖关系

症状:A 工具依赖 B 工具的结果,但模型试图并行。

对策

  • 设计工具时让"输入"和"输出"清晰
  • description 里说"这个工具的输入需要…"
  • 用 thinking 模型,规划更稳

7.2 下一篇预告

  • 第 28 篇:Agent 框架对比 - LangChain / LlamaIndex / AutoGen / CrewAI —— Tool Use 是基础,Agent 框架是把它"工程化"。我们会做完整框架对比。
  • 之后是多模态(29 篇)、Prompt 工程(30 篇)。

结语:Tool Use 是 Agent 时代的「关键开关」

读完本文你应该明白:

  • Function Calling 是 OpenAI 定义的事实标准——其他都向它兼容
  • 闭源 Top 3(Claude/GPT/Gemini)多工具调用 > 95%
  • 开源 Qwen / DeepSeek 接近闭源——自部署可用
  • ReAct 模式是多步推理的基础
  • MCP 是 2026 年的协议级革命——统一工具生态
  • 生产 Tool Use 要做好错误处理 + 流式输出

下一篇我们继续:

  • 第 28 篇:Agent 框架对比 —— 把 Tool Use 工程化的下一步。

我们下篇见。


📮 关于「码海寻道」
这里是一个聚焦 AI 工程化、大模型部署、后端架构实战的技术专栏。
写最一线的踩坑经验,做最务实的技术拆解。

如果这篇文章对你有启发,欢迎点赞、转发、关注。我们下篇见。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐