一、引言

大语言模型(LLM)在文本生成、代码编写、逻辑推理等领域已经展现出令人惊叹的能力。然而,原生大模型有一个根本性的局限:它们只能处理训练数据范围内的知识,无法实时获取最新信息,无法执行代码,无法操作外部系统

这正是 工具调用(Tool Calling) 技术出现的背景。通过在模型与外部工具之间建立标准化接口,我们可以让大模型:

  • 🌐 实时联网搜索,获取最新信息
  • 🧮 调用计算器,执行精确数学运算
  • 🗄️ 查询数据库,获取业务数据
  • 📧 发送邮件,操作第三方 API
  • 🔧 执行系统命令,完成运维任务

这种能力的本质,是将大模型从"纯文本生成器"升级为"智能体(Agent)"——一个能够感知环境、做出决策并采取行动的自主系统。

本文面向所有软件从业人员,不要求读者有 AI 专业背景。我们将从原理到实践,系统讲解主流大模型平台的工具调用机制,并提供可直接用于生产的代码示例。

二、为什么需要工具调用

2.1 大模型的内在局限

大语言模型的工作原理是基于训练数据中的统计模式生成文本。这种机制带来了几个固有局限:

局限 说明 示例
知识截止 模型的知识停留在训练数据截止的时间点 无法回答训练数据截止之后的事件(除非配备联网搜索工具)
无法执行代码 模型生成的代码只是文本,不会自动运行 让模型"计算 1234 × 5678"可能得到错误答案
无法操作外部系统 模型不能直接查询数据库或调用 API "帮我查一下订单 #12345 的状态"无法直接完成
数学精度有限 基于概率生成的文本不适合精确计算 复杂数学运算容易出错

2.2 工具调用如何突破局限

工具调用的核心思路是:让模型知道有哪些工具可用,让模型决定何时调用哪个工具,让外部系统执行工具并将结果返回给模型

用户提问 → 模型分析意图 → 模型选择工具并生成调用参数 → 外部系统执行工具 → 将结果返回给模型 → 模型结合结果生成最终回答

2.3 典型应用场景

  • 智能客服:查询订单状态、物流信息、退换货政策
  • 数据分析:查询数据库、生成报表、可视化数据
  • 开发辅助:搜索文档、执行测试、部署代码
  • 个人助理:查询天气、设置提醒、发送邮件

三、OpenAI Function Calling 详解

3.1 设计原理

OpenAI 在 2023 年 6 月推出了 Function Calling 功能,并在后续版本中持续增强。其核心设计思想是:

  1. 在 API 请求中声明可用工具:通过 tools 参数传递工具定义的列表
  2. 模型决定是否调用工具:模型分析用户输入后,可以选择调用一个或多个工具
  3. 模型输出结构化调用请求:当模型决定调用工具时,返回一个包含工具名称和参数的结构化对象
  4. 外部系统执行工具:由开发者编写的代码实际执行工具逻辑
  5. 将结果返回给模型:将工具执行结果作为新的消息传递给模型,由其生成最终回答

3.2 工具定义的 JSON Schema 规范

每个工具通过 JSON Schema 格式定义,包含以下关键要素:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "获取指定城市的当前天气信息",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "城市名称,如 '北京'、'Shanghai'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "温度单位,摄氏度或华氏度"
        }
      },
      "required": ["city"],
      "additionalProperties": false
    }
  }
}

关键字段说明:

  • name:工具名称,使用小写字母和下划线,清晰描述功能
  • description:工具功能描述,直接影响模型是否能正确选择该工具
  • parameters:JSON Schema 格式的参数定义
    • properties:各参数的类型和描述
    • required:必填参数列表
    • enum:枚举值约束,帮助模型选择合法值
    • additionalProperties: false:禁止额外参数,提高调用准确性

3.3 完整代码示例

以下是一个使用 OpenAI Python SDK 的完整示例:

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 定义工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# 第一轮:发送用户请求
messages = [
    {"role": "user", "content": "北京今天天气怎么样?"}
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools
)

# 检查模型是否请求调用工具
message = response.choices[0].message

if message.tool_calls:  # tool_calls 为 None 表示模型直接回答
    # 模型请求调用工具
    tool_call = message.tool_calls[0]
    import json
    arguments = json.loads(tool_call.function.arguments)
    city = arguments["city"]

    # 执行工具(实际项目中这里会调用真实的天气 API)
    weather_result = f"{city}今天晴天,温度25°C"

    # 将工具调用结果返回给模型
    messages.append(message)  # 模型的 tool_call 请求
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": weather_result
    })

    # 第二轮:模型生成最终回答
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages
    )

    print(final_response.choices[0].message.content)

3.4 并行工具调用(Parallel Function Calling)

从 gpt-4-1106-preview 模型版本开始,OpenAI 支持单次响应中调用多个工具。模型可以在一次回复中请求调用 2 个或更多工具,这些工具可以并行执行:

import json
if message.tool_calls:
    results = []
    for tc in message.tool_calls:
        args = json.loads(tc.function.arguments)
        result = execute_tool(tc.function.name, args)
        results.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": result
        })

    messages.append(message)
    messages.extend(results)

3.5 Structured Outputs:强制结构化输出

OpenAI 推出的 Structured Outputs 功能,确保模型的输出严格符合指定的 JSON Schema。在工具调用场景下,通过在工具定义中设置 strict: true 来启用:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    }
                },
                "required": ["city"],
                "additionalProperties": False  # Structured Outputs 要求
            },
            "strict": True  # 启用严格模式
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=messages,
    tools=tools,
    parallel_tool_calls=True  # 允许并行工具调用(默认即为 True)
)

启用 Structured Outputs 的要求:

  • 工具定义的 JSON Schema 必须完整:所有字段都需要有 description
  • 必须设置 additionalProperties: false
  • 必须设置 strict: true
  • 需要使用支持该功能的模型版本(如 gpt-4o-2024-08-06 及之后版本)

Structured Outputs 的优势:

  • 保证输出格式与 JSON Schema 完全一致
  • 减少解析错误和无效调用
  • 提高复杂参数场景的可靠性

四、通义千问 Tool Calling 详解

4.1 接口概览

通义千问(Qwen)系列模型通过 DashScope API 提供工具调用能力。其接口设计与 OpenAI 高度兼容,这使得开发者可以以最小的改动切换模型后端。

4.2 与 OpenAI 的兼容性

通义千问的工具调用接口在以下方面与 OpenAI 兼容:

特性 OpenAI 通义千问
工具定义格式 JSON Schema JSON Schema(相同格式)
单次调用 ✅ 支持 ✅ 支持
并行调用 ✅ 支持 ✅ 支持(部分模型)
流式输出 ✅ 支持 ✅ 支持
结构化输出 Structured Outputs 支持 JSON Schema 约束

4.3 通义千问特有要求

尽管高度兼容,通义千问在使用工具调用时有一些需要注意的细节:

  1. 工具名称:建议使用英文字母和下划线,避免中文
  2. 参数描述:描述越详细、越具体,模型调用越准确
  3. 系统提示:可以通过 system message 指导模型的工具使用策略

4.4 完整代码示例

以下使用 OpenAI 兼容格式的 Python SDK 调用通义千问:

from openai import OpenAI

# 方式一:使用 OpenAI SDK + DashScope 兼容接口
client = OpenAI(
    api_key="your-dashscope-api-key",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

# 方式二:使用 DashScope 原生 SDK
# from dashscope import Generation
# response = Generation.call(
#     model="qwen-plus",
#     messages=messages,
#     tools=tools,
#     result_format="message"
# )

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "根据订单号查询订单详细信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号,如 'ORD20240101001'"
                    }
                },
                "required": ["order_id"]
            }
        }
    }
]

messages = [
    {"role": "user", "content": "帮我查一下订单 ORD20240101001 的状态"}
]

response = client.chat.completions.create(
    model="qwen-plus",
    messages=messages,
    tools=tools
)

message = response.choices[0].message

if message.tool_calls:
    tool_call = message.tool_calls[0]
    import json
    arguments = json.loads(tool_call.function.arguments)
    order_id = arguments["order_id"]

    # 执行查询(实际项目中调用数据库或 API)
    order_info = f"订单 {order_id} 状态:已发货,预计明日送达"

    messages.append(message)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": order_info
    })

    final_response = client.chat.completions.create(
        model="qwen-plus",
        messages=messages
    )

    print(final_response.choices[0].message.content)

五、工具定义规范与最佳实践

工具定义的质量直接决定了模型能否正确选择和使用工具。以下是经过实践验证的最佳实践。

5.1 工具名称命名规范

✅ 推荐:
- get_weather        # 动词_名词,清晰描述功能
- query_order_status # 多段描述,用下划线分隔
- send_email         # 简洁明了

❌ 避免:
- gw                 # 缩写无法传达含义
- 获取天气             # 中文名称可能导致解析问题
- getWeatherDataForUser  # 过于冗长

规范建议:

  • 使用小写英文字母和下划线
  • 采用 动词_名词 或 动词_名词_细节 的格式
  • 长度控制在 3-30 个字符
  • 名称应能独立传达工具的核心功能

5.2 参数描述的清晰度要求

参数描述是模型理解"什么时候传什么值"的关键。好的描述应该包含:

  • 语义说明:这个参数代表什么
  • 格式要求:期望的格式(日期格式、单位等)
  • 示例值:一两个典型示例
{
  "start_date": {
    "type": "string",
    "description": "查询起始日期,格式为 YYYY-MM-DD,例如 '2024-01-01'"
  }
}

5.3 必填与可选参数的设计原则

  • 必填参数(required):工具执行不可或缺的信息
  • 可选参数:有合理默认值或用于细化查询的参数
{
  "parameters": {
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称"
      },
      "days": {
        "type": "integer",
        "description": "预报天数,默认 1 天,最大 7 天"
      }
    },
    "required": ["city"]
  }
}

5.4 枚举值的使用

当参数只能从有限的选项中选择时,使用 enum 约束:

{
  "language": {
    "type": "string",
    "enum": ["zh-CN", "en-US", "ja-JP"],
    "description": "回复语言代码"
  }
}

好处:

  • 防止模型传入无效值
  • 缩小模型的决策空间,提高准确率
  • 便于后端校验

5.5 复杂参数的处理

对于数组和嵌套对象,使用 JSON Schema 的 items 和嵌套 properties

{
  "filters": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "description": "过滤字段名"
        },
        "operator": {
          "type": "string",
          "enum": ["eq", "gt", "lt", "contains"]
        },
        "value": {
          "type": "string",
          "description": "过滤值"
        }
      },
      "required": ["field", "operator", "value"]
    },
    "description": "查询条件过滤列表"
  }
}

5.6 工具描述对调用准确率的影响

根据开发者社区的经验总结,工具描述的质量对调用准确率有显著影响。描述越清晰、越具体,模型越能正确选择工具并生成准确的参数:

描述质量 示例 经验估算准确率
"查天气" 较低,模型可能无法理解工具用途
一般 "获取城市天气" 中等,模型能大致理解但可能选错场景
"获取指定城市的当前天气信息,包括温度、湿度和天气状况" 较高,模型能准确匹配意图

描述编写建议:

  • 说明工具的功能(做什么)
  • 说明工具的适用场景(什么时候用)
  • 说明限制条件(不能做什么)
  • 控制在 1-3 句话

六、多工具路由策略

6.1 单工具 vs 多工具场景

单工具场景:用户意图明确,只需要一个工具即可满足需求。

用户:"北京今天天气怎么样?"
→ 模型选择:get_weather(city="北京")

多工具场景:用户请求涉及多个方面,需要组合多个工具。

用户:"帮我查一下订单 #12345 的状态,如果已发货就查物流信息"
→ 模型先调用:query_order(order_id="12345")
→ 根据结果判断是否需要调用:query_logistics(order_id="12345")

6.2 工具选择的自动路由机制

大模型的工具选择依赖于以下因素:

  1. 工具描述的相关性:模型计算用户意图与各工具描述的语义匹配度
  2. 参数可填充性:模型判断能否从用户输入中提取所需参数
  3. 历史上下文:对话历史中的信息可能补充当前请求

6.3 Agent 循环:思考 → 选工具 → 执行 → 观察

工具调用的核心模式是 Agent 循环(ReAct 模式)

  ┌──────────────────────────┐
  │        思考 (Think)       │
  │    分析用户意图,决定下一步  │
  └──────────┬───────────────┘
             ▼
  ┌──────────────────────────┐
  │      行动 (Act)          │
  │    选择合适的工具并生成参数  │
  └──────────┬───────────────┘
             ▼
  ┌──────────────────────────┐
  │    执行 (Execute)         │
  │     外部系统执行工具逻辑     │
  └──────────┬───────────────┘
             ▼
  ┌──────────────────────────┐
  │      观察 (Observe)       │
  │    将结果反馈给模型         │
  └──────────┬───────────────┘
             ▼
    ┌──── 继续循环 ────┐
    │                 │
    ▼                 ▼
  需要更多工具      已有足够信息
  → 继续循环        → 输出最终回答

每一轮循环中,模型根据观察结果决定:

  • 继续调用其他工具
  • 已有足够信息,生成最终回答
  • 无法完成请求,向用户询问补充信息

6.4 工具组合调用的实践模式

模式一:链式调用(Sequential)

工具 A → 结果 → 工具 B(依赖 A 的结果)→ 结果 → 最终回答

适用于工具之间存在依赖关系的场景。

模式二:并行调用(Parallel)

工具 A ┐
       ├→ 结果合并 → 最终回答
工具 B ┘

适用于工具之间无依赖关系、可以并行执行的场景。

模式三:条件调用(Conditional)

工具 A → 结果判断
         ├─ 条件满足 → 工具 B
         └─ 条件不满足 → 直接回答

适用于基于中间结果决定后续行为的场景。

七、错误处理与重试机制

7.1 工具调用失败的常见类型

失败类型 原因 示例
参数错误 模型传入的参数格式或值不正确 日期格式错误、枚举值不在允许范围内
工具不可用 后端服务宕机或网络故障 API 超时返回
权限不足 工具执行需要但缺少授权 无权查询该订单
业务逻辑错误 参数合法但业务上无法执行 查询不存在的订单号
模型未调用 模型判断不需要调用工具 用户意图理解偏差

7.2 错误信息反馈给模型的方式

当工具执行失败时,错误信息的质量和格式直接影响模型能否自行修复:

# ❌ 差的错误信息(模型无法理解)
# 工具返回的 content 字段值为:"Error 500"

# ✅ 好的错误信息(模型可以自行修复)
{
  "content": "错误:参数 'start_date' 格式无效。期望格式为 YYYY-MM-DD,收到 '2024/01/01'。请重新提供正确的日期格式。"
}

好的错误信息应该包含:

  • 错误类型(参数错误、服务不可用等)
  • 具体问题描述
  • 期望的正确格式
  • 可能的修复建议

7.3 重试策略设计

推荐的重试策略结合了自动重试模型自修复两层机制:

第一层:系统级自动重试

import time
import json
from openai import OpenAI

def call_with_retry(client, model, messages, tools, max_retries=3):
    """系统级重试:处理 API 超时等临时故障"""
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model=model,
                messages=messages,
                tools=tools
            )
            return response
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            # 指数退避:1s, 2s, 4s
            wait_time = 2 ** attempt
            time.sleep(wait_time)

第二层:模型自修复重试

def agent_loop_with_self_healing(messages, tools, max_turns=5):
    """模型自修复:工具执行失败后,将错误信息返回给模型,让其重新生成调用"""
    for turn in range(max_turns):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools
        )

        message = response.choices[0].message

        if not message.tool_calls or len(message.tool_calls) == 0:
            # 模型直接回答(tool_calls 为 None 或空列表),结束循环
            return message.content

        # 执行工具并检查结果
        results = []
        for tc in message.tool_calls:
            try:
                args = json.loads(tc.function.arguments)
                result = execute_tool(tc.function.name, args)
            except ValueError as e:
                # 参数错误:返回描述性错误给模型
                result = f"参数错误:{str(e)}。请修正后重试。"
            except Exception as e:
                result = f"工具执行失败:{str(e)}"

            results.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result
            })

        messages.append(message)
        messages.extend(results)

    # 超过最大轮次仍未完成
    return "抱歉,多次尝试后仍无法完成您的请求。"

7.4 降级策略与兜底方案

当工具调用反复失败时,应有合理的降级方案:

  • 降级为通用回答:告知用户当前无法获取实时数据,提供通用建议
  • 缓存数据兜底:使用最近一次成功查询的缓存数据(标注时效)
  • 转人工处理:超出自动处理能力时,转给人工客服
if turn >= max_turns:
    fallback_message = (
        "抱歉,暂时无法获取实时数据。"
        "您可以稍后重试,或联系人工客服获取帮助。"
    )
    return fallback_message

八、实战案例:构建一个智能客服 Agent

8.1 场景描述

假设我们要为一个电商平台构建智能客服系统,用户可能会问:

  • "我的订单到哪了?"
  • "这个商品能退款吗?"
  • "发什么快递?"

8.2 工具定义

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order_status",
            "description": "根据订单号或用户信息查询订单状态,返回订单详情包括状态、商品、金额等",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号,如 'ORD20240101001'"
                    },
                    "user_phone": {
                        "type": "string",
                        "description": "用户手机号(当没有订单号时使用)"
                    }
                },
                "required": []
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_logistics",
            "description": "根据订单号查询物流信息,包括快递公司、物流单号和物流轨迹",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号"
                    }
                },
                "required": ["order_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "check_refund_policy",
            "description": "查询商品是否支持退款,基于商品类型和购买时间判断",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_type": {
                        "type": "string",
                        "enum": ["physical", "digital", "service"],
                        "description": "商品类型:实物商品、虚拟商品、服务类"
                    },
                    "days_since_purchase": {
                        "type": "integer",
                        "description": "购买距今天数"
                    }
                },
                "required": ["product_type", "days_since_purchase"]
            }
        }
    }
]

8.3 对话流程演示

第一轮对话:

用户:帮我查一下订单 ORD20240101001

模型输出(tool_calls):
[
  {
    "function": {
      "name": "query_order_status",
      "arguments": {"order_id": "ORD20240101001"}
    }
  }
]

工具执行结果:

{
  "order_id": "ORD20240101001",
  "status": "shipped",
  "product": "iPhone 15 Pro",
  "amount": 7999,
  "ship_date": "2024-01-02"
}

第二轮对话:

模型收到结果后,可能进一步调用物流查询:

模型输出(tool_calls):
[
  {
    "function": {
      "name": "query_logistics",
      "arguments": {"order_id": "ORD20240101001"}
    }
  }
]

最终回答:

您的订单 ORD20240101001(iPhone 15 Pro,¥7999)已于 1月2日 发货,快递为顺丰,单号 SF1234567890。最新物流状态:已到达北京分拨中心,预计明天送达。

8.4 完整代码框架

import json

class CustomerServiceAgent:
    def __init__(self, client, tools):
        self.client = client
        self.tools = tools
        self.messages = []

    def handle(self, user_input):
        self.messages.append({"role": "user", "content": user_input})

        for _ in range(5):  # 最多 5 轮
            response = self.client.chat.completions.create(
                model="gpt-4o",
                messages=self.messages,
                tools=self.tools
            )

            message = response.choices[0].message

            if not message.tool_calls or len(message.tool_calls) == 0:
                return message.content  # 直接回答

            # 执行所有工具调用
            for tc in message.tool_calls:
                result = self.execute_tool(tc)
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": result
                })

            self.messages.append(message)

        return "抱歉,处理您的请求时遇到了困难。"

    def execute_tool(self, tool_call):
        """根据工具名称路由到具体执行函数"""
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)

        if name == "query_order_status":
            return self._query_order(args)
        elif name == "query_logistics":
            return self._query_logistics(args)
        elif name == "check_refund_policy":
            return self._check_refund(args)
        else:
            return f"未知工具:{name}"

九、总结与展望

工具调用(Tool Calling / Function Calling)技术正在快速成熟。从 OpenAI 2023 年 6 月首次推出 Function Calling 到现在,这一能力已经成为大模型落地的核心基础设施

当前趋势

  • 标准化:MCP(Model Context Protocol,由 Anthropic 于 2024 年 11 月开源)等协议正在推动工具调用的标准化,让模型与工具的对接更加统一
  • 多模态扩展:工具调用不再局限于文本,图像生成、语音合成等多模态工具也可以被模型调用
  • Agent 框架成熟:LangChain、LlamaIndex、AutoGen 等框架让构建 Agent 变得更加简单
  • 国产模型跟进:通义千问、文心一言、智谱 GLM 等国产模型均已支持工具调用能力

未来展望

  • 更强大的自动工具发现:模型能够自主学习和注册新工具
  • 更智能的错误恢复:模型遇到工具失败时,能自动尝试替代方案
  • 更安全的权限管控:细粒度的工具权限管理,确保 Agent 行为可控
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐