Skip to content

Function Calling 工具调用:原理、定义与并行优化

摘要: 系统阐述了大语言模型 Function Calling 技术的完整工作流程,从 LLM 作为决策者输出结构化调用指令、代码作为执行者的核心原理出发,深入分析了工具定义 JSON Schema 的设计要点与描述质量对调用准确率的影响。对比了单次调用、多轮调用与并行调用的适用场景,并给出并行调用的判断逻辑与性能优势。最后,通过对比 Function Calling 与 Structured Output 的适用边界,为构建具备工具使用能力的 Agent 系统提供技术基础。

1. 原理:模型输出结构化调用指令

核心思想

让 LLM 不再是"只会说话",而是能调用外部函数和 API,真正地"做事"。

1.1 问题起源:LLM 的"手脚束缚"

大语言模型本质上是"纯文本引擎"——你输入文字,它输出文字。它没有能力:

  • 查询实时数据库
  • 发送邮件或 Slack 消息
  • 调用计算器做精确数学运算
  • 获取今天的天气或股价
  • 操作文件系统

Function Calling 就是给 LLM 装上"手和脚"。当用户的问题需要外部操作时,LLM 不直接回答,而是输出一个结构化的"调用指令",由外部程序执行后再把结果还给 LLM。

1.2 工作流程:一个完整的调用周期

Function Calling 完整流程示意如下:

  ① 用户输入 + 工具定义           ② LLM 决策:调用哪个函数?
  ┌──────────────────┐           ┌──────────────────────┐
  │ User: "北京今天   │           │ Model 内部推理:       │
  │  天气怎么样?"     │──────────>│ "用户想知道北京天气"   │
  │                  │           │ "我有 get_weather    │
  │ Tools: [         │           │  这个工具..."         │
  │   get_weather,   │           │                      │
  │   get_stock,     │           │ → 输出 JSON:         │
  │   send_email     │           │ {                     │
  │ ]                │           │   "name": "get_weather│
  └──────────────────┘           │   "arguments": {      │
                                 │     "city": "北京"    │
         ┌───────────────────────│   }                   │
         │                        └──────────┬───────────┘
         │  ⑤ LLM 综合生成最终回答           │
         │  ┌──────────────────────┐         │  ③ 你的代码执行函数
         │  │ Model: "北京今天晴, │         │
         │  │  温度 25°C,适合    │         ▼
         │  │  户外活动。"         │  ┌──────────────────────┐
         │  └──────────────────────┘  │ 调用真实天气 API:    │
         │           ▲                │                      │
         │           │                │ get_weather("北京")   │
         │           │                │ → "晴, 25°C, 湿度60%" │
         │           │                └──────────┬───────────┘
         │           │                           │
         │           └───────────────────────────┘
         │              ④ 函数结果返回给 LLM

关键要点LLM 并不真正执行函数,它只输出"我想调用这个函数,参数是这些"。你的代码负责实际执行,然后把结果传回给 LLM。

1.3 类比理解:LLM 是"决策者",代码是"执行者"

类比:餐厅点餐系统

  LLM = 服务员          用户代码 = 厨房
  ──────────────        ─────────────────
  听到顾客说"我要牛排"   收到"牛排 x1"
  写在小票上            实际煎牛排
  不可能真的去做牛排     端出牛排给服务员
  但知道牛排要几分熟     服务员再告诉顾客"您的牛排好了"

  LLM 知道要调用什么函数、传什么参数,但它不(也不能)执行函数。

2. Tool Definition(工具定义 Schema)

2.1 函数定义的结构

告诉 LLM 你有哪些工具可用,你需要以 JSON Schema 的方式描述每个函数:

工具定义的三个要素

  1. 函数名 (name)       — LLM 用来识别调用哪个函数
  2. 函数描述 (description) — LLM 用来判断何时调用这个函数
  3. 参数定义 (parameters)  — LLM 用来填充正确的参数值

  ┌─────────────────────────────────────────────────────────┐
  │ {                                                       │
  │   "name": "get_weather",                                │
  │   "description": "获取指定城市的实时天气信息,            │
  │                   包括温度、湿度、天气状况",               │
  │   "parameters": {                                       │
  │     "type": "object",                                   │
  │     "properties": {                                     │
  │       "city": {                                         │
  │         "type": "string",                               │
  │         "description": "城市名称,使用中文,如'北京'、     │
  │                         '上海'"                          │
  │       },                                                │
  │       "unit": {                                         │
  │         "type": "string",                               │
  │         "enum": ["celsius", "fahrenheit"],              │
  │         "description": "温度单位"                        │
  │       }                                                 │
  │     },                                                  │
  │     "required": ["city"]                                │
  │   }                                                     │
  │ }                                                       │
  └─────────────────────────────────────────────────────────┘

2.2 描述质量直接影响调用准确率

工具的 description 是 LLM 判断"什么时候用这个工具"的唯一依据。写得不好,LLM 就会用错工具或者不用。

差的描述 vs 好的描述

  ❌ 差:
  {
    "name": "search_docs",
    "description": "搜索文档",
    "parameters": {
      "query": {"type": "string", "description": "搜索词"}
    }
  }
  → LLM 不知所措:什么时候用?搜索什么文档?

  ✅ 好:
  {
    "name": "search_internal_docs",
    "description": "在公司内部知识库中搜索技术文档和操作手册。
                   当你需要查找产品规格、API文档、运维流程时使用此工具。
                   注意:此工具仅搜索技术文档,不包含HR政策或财务信息。",
    "parameters": {
      "query": {
        "type": "string",
        "description": "搜索关键词或自然语言查询。中文或英文。建议使用完整的句子而不是单词。"
      },
      "category": {
        "type": "string",
        "enum": ["技术文档", "运维手册", "API参考", "产品规格"],
        "description": "文档分类过滤,默认搜索所有分类"
      }
    }
  }
  → LLM 清楚知道什么场景用、参数怎么填

2.3 工具定义最佳实践

最佳实践说明示例
描述要具体说明工具的用途、适用范围、限制"搜索技术文档(不含HR和财务)"
参数要有约束使用 enum、type 约束参数"enum": ["asc", "desc"]
给出参数示例在 description 中附示例值"如'2024-01-15'"
标记必选/可选使用 required 字段"required": ["query"]
控制工具数量提供太多工具会让 LLM 混淆一次提供 5~20 个工具为佳
命名清晰函数名应自解释get_weather 而不是 f1
区分相似工具在描述中明确区分相似工具的职责"用 search_code 搜索代码,用 search_docs 搜索文档"

2.4 完整示例:一个智能助手的工具集

json
// 为一个智能客服定义的完整工具集
[
  {
    "name": "search_knowledge_base",
    "description": "在知识库中搜索信息。用于回答产品使用、技术支持和常见问题。不包含用户个人数据。",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string", "description": "搜索查询,尽量使用完整句子"},
        "top_k": {"type": "integer", "description": "返回结果数量,默认5", "default": 5}
      },
      "required": ["query"]
    }
  },
  {
    "name": "get_order_status",
    "description": "查询用户的订单状态。需要提供订单号。只能查询当前登录用户自己的订单。",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {"type": "string", "description": "订单号,格式为 ORD-开始,如 ORD-20240001"}
      },
      "required": ["order_id"]
    }
  },
  {
    "name": "create_support_ticket",
    "description": "当用户问题无法自动解决时,创建人工客服工单。需要确认用户同意后再调用。",
    "parameters": {
      "type": "object",
      "properties": {
        "title": {"type": "string", "description": "工单标题,简要概括问题"},
        "description": {"type": "string", "description": "问题详细描述"},
        "priority": {
          "type": "string",
          "enum": ["low", "medium", "high", "urgent"],
          "description": "紧急程度。系统故障使用urgent,一般问题使用medium"
        }
      },
      "required": ["title", "description", "priority"]
    }
  },
  {
    "name": "calculate",
    "description": "执行数学计算。当需要精确的算术运算时使用此工具,而不是让模型心算。支持加减乘除和基本函数。",
    "parameters": {
      "type": "object",
      "properties": {
        "expression": {"type": "string", "description": "数学表达式,如 '2+3*4' 或 'sqrt(16)'"}
      },
      "required": ["expression"]
    }
  }
]

3. 单次调用 vs 多轮调用

3.1 单次调用(Single Tool Call)

LLM 判断只需要调用一个工具就能回答问题。单次调用流程如下:

  [用户] "帮我查一下 ORD-20240001 这个订单的状态"

  [LLM]    分析:这是查订单 → 用 get_order_status
           输出:ToolCall(name="get_order_status", args={order_id: "ORD-20240001"})

  [代码]   执行 get_order_status("ORD-20240001")
           返回:{"status": "已发货", "tracking": "SF123456789", "eta": "2024-03-15"}

  [LLM]    综合结果生成回答:
           "您的订单 ORD-20240001 已发货,快递单号 SF123456789,预计 3月15日送达。"

3.2 多轮调用(Multi-turn Tool Calling)

复杂问题需要多次调用——可能需要调用不同的工具,也可能同一个工具调用多次。 多轮调用流程如下:
  [用户] "北京和上海今天哪个城市更暖和?"

  轮次 1:
  [LLM]    需要两个城市的天气数据
           输出:ToolCall(name="get_weather", args={city: "北京"})

  [代码]   返回:{"temp": 25, "condition": "晴"}


  轮次 2:
  [LLM]    ToolCall(name="get_weather", args={city: "上海"})

  [代码]   返回:{"temp": 22, "condition": "小雨"}


  轮次 3:
  [LLM]    北京25°C 晴,上海22°C 小雨
           → "北京今天更暖和,25°C且天气晴朗,比上海高3°C。"

注意:多轮调用的每一轮都需要一次完整的 LLM API 请求,这意味着更多的 token 消耗和更高的延迟。这也是后面要讲 "并行调用" 的原因。

3.3 何时用单次、何时用多轮?

决策树:单次 vs 多轮 vs 并行

  用户的问题


  ┌─────────────────────┐
  │ 是否需要调用工具?     │
  └──────┬──────────────┘

    ┌────┴────┐
   否         是
    │         │
    ▼         ▼
  直接回答  ┌─────────────────────┐
           │ 需要几个工具调用?      │
           └──────┬──────────────┘

          ┌───────┼───────────┐
         1个      多个独立      多个有依赖
          │        │            │
          ▼        ▼            ▼
       单次调用   并行调用      多轮调用

4. 并行 Tool Calling

4.1 什么是并行调用?

当 LLM 判断多个工具调用之间互不依赖时,可以在一次响应中同时发起多个调用。

并行调用 vs 串行调用(以查天气为例)

  串行调用(慢):                      并行调用(快):
  ─────────────                       ──────────────
  Tool: get_weather("北京")            Tool: get_weather("北京")  ─┐
    │ 200ms                              │                        │
  Tool: get_weather("上海")            Tool: get_weather("上海")  ─┤ 同时发起
    │ 200ms                              │                        │
  Tool: get_weather("广州")            Tool: get_weather("广州")  ─┘
    │ 200ms                              │
    ▼                                    ▼
  总耗时: ~600ms                      总耗时: ~200ms (最慢的那个)

  节省: 67% 时间

4.2 并行调用的工作方式

并行调用完整流程示意如下:

  [用户] "对比北京、上海、广州今天的空气质量"

  [LLM] 分析:三个城市互相独立,可以并行查询
        输出:
        [
          ToolCall(name="get_air_quality", args={city: "北京"}),
          ToolCall(name="get_air_quality", args={city: "上海"}),
          ToolCall(name="get_air_quality", args={city: "广州"})
        ]


  [代码] 并发执行三个函数调用
        ┌─────────────────┐
        │ Promise.all([   │
        │   getAqi("北京"),│──> {"aqi": 85,  "level": "良"}
        │   getAqi("上海"),│──> {"aqi": 120, "level": "轻度污染"}
        │   getAqi("广州") │──> {"aqi": 55,  "level": "优"}
        │ ])              │
        └─────────────────┘


  [LLM]  综合三个结果:
         "今天空气质量:
          广州最优(AQI 55),北京良好(AQI 85),
          上海轻度污染(AQI 120),建议敏感人群减少户外活动。"

4.3 如何让 LLM 进行并行调用?

在 API 调用层面,这是一个参数设置:

API 调用参数

  # OpenAI / 大多数 API 兼容的方式
  response = client.chat.completions.create(
      model="gpt-4",
      messages=[...],
      tools=[...],
      parallel_tool_calls=True  # ← 关键参数:允许并行
  )
  
  # 如果 parallel_tool_calls=False,
  # LLM 一次只返回一个 Tool Call,需要多轮

何时不适合并行?

  • 后续调用依赖于前一个调用的结果时(如先搜项目负责人的名字,再用名字搜上级)
  • 工具之间有副作用顺序时(如先创建用户再发送欢迎邮件——虽然也可以反过来处理)

4.4 并行调用的最佳实践

并行调用的判断逻辑(LLM 内部推理)

  Q: "帮我查 ORD-001 的订单状态,顺便看看北京天气"

  ├─ get_order_status("ORD-001")   ← 可以并行
  │   └─ 输入来自用户,不依赖其他调用

  └─ get_weather("北京")           ← 可以并行
      └─ 输入来自用户,不依赖其他调用

  → 两个调用完全独立,一次发出!

  Q: "查一下苹果公司最新股价,然后用人民币换算"

  ├─ get_stock_price("AAPL")       ← 第一步
  │   └─ 返回: $198.50

  └─ convert_currency(198.50, "USD", "CNY")  ← 依赖第一步的结果!
      └─ 输入 $198.50 来自第一个调用

  → 不能并行!必须串行。

5. Structured Output / JSON Mode

5.1 为什么需要结构化输出?

在 Function Calling 中,LLM 输出的是函数调用指令(JSON 格式)。但即使不使用工具,我们也经常需要 LLM 输出结构化的 JSON 数据:

  • 提取文档中的结构化信息(人名、日期、金额)
  • 分类任务(将文本归入预设类别)
  • 数据转换(自然语言 → 结构化数据)

传统方式:在 Prompt 中要求"输出 JSON 格式",但 LLM 可能不遵守,输出带有额外的文字或格式错误。

Structured Output / JSON Mode 是让 LLM"保证"输出合法 JSON 的机制。

5.2 两种实现方式

方式一:JSON Mode(宽松模式)

  Prompt: "提取以下文本中的人名和日期,以JSON格式输出"
  
  LLM 输出(可能有问题):
  好的,以下是提取结果:
  \`\`\`json
  {
    "persons": ["张三", "李四"],
    "date": "2024-03-15"
  }
  \`\`\`
  如果您需要更多信息...
  
  问题:输出中包含了"好的,以下是提取结果"和"如果您需要更多信息..."
       这不是纯 JSON!代码解析会失败。

方式二:Structured Output(严格模式)

  使用 response_format 参数指定 JSON Schema:

  response = client.chat.completions.create(
      model="gpt-4o",
      messages=[...],
      response_format={
          "type": "json_schema",
          "json_schema": {
              "name": "person_extraction",
              "schema": {
                  "type": "object",
                  "properties": {
                      "persons": {
                          "type": "array",
                          "items": {"type": "string"}
                      },
                      "date": {"type": "string"}
                  },
                  "required": ["persons", "date"],
                  "additionalProperties": False
              }
          }
      }
  )
  
  LLM 保证输出:
  {"persons": ["张三", "李四"], "date": "2024-03-15"}
  
  干净、合法的 JSON,不会有任何额外内容。

5.3 Structured Output 的关键特性

  ┌────────────────────────────────────────────────────────────┐
  │  1. 格式保证                                              │
  │     输出一定是合法的 JSON,不会有 Markdown 包装或多余文本 │
  │                                                           │
  │  2. Schema 遵循                                           │
  │     输出一定符合你定义的 JSON Schema(类型、必填字段等)   │
  │                                                           │
  │  3. 拒绝额外字段                                          │
  │     如果设了 additionalProperties: false,不会出现未定义   │
  │     的字段                                                │
  └────────────────────────────────────────────────────────────┘

5.4 Function Calling vs Structured Output

两者都涉及结构化 JSON,但用途不同:

维度Function CallingStructured Output
目的让 LLM 调用外部工具让 LLM 返回结构化数据
后续动作执行函数,结果返回 LLM程序直接使用输出
LLM 行为输出 ToolCall,等待结果直接输出 JSON,结束
典型场景查天气、搜文档、发邮件信息提取、分类、数据转换
JSON 格式固定的 ToolCall 结构自定义 Schema

5.5 实际应用示例

信息提取应用:

  输入文本:
  "我叫张三,是字节跳动的工程师,负责AI平台研发。
   我的手机号是13812345678,邮箱是zhangsan@bytedance.com。
   我于2020年3月入职。"

  Structured Output Schema:
  {
    "name": "employee_info",
    "schema": {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "company": {"type": "string"},
        "role": {"type": "string"},
        "phone": {"type": "string"},
        "email": {"type": "string"},
        "start_date": {"type": "string", "description": "入职日期 YYYY-MM-DD"}
      },
      "required": ["name", "company"]
    }
  }

  LLM 输出:
  {
    "name": "张三",
    "company": "字节跳动",
    "role": "AI平台研发工程师",
    "phone": "13812345678",
    "email": "zhangsan@bytedance.com",
    "start_date": "2020-03-01"
  }

  → 直接存入数据库或传递给下一个系统,无需手动解析!

6. 总结

Function Calling 是让 LLM 从"会说话的模型"变成"能做事的 Agent"的关键技术。

概念核心要点
基本原理LLM 决策调用什么、传什么参数;代码负责执行
工具定义JSON Schema 描述函数名、用途、参数类型和约束
描述质量好的描述是准确调用的基础,比参数定义更重要
单次调用LLM 调用一个工具即完成
多轮调用链式调用,每次基于前次结果决定下一步
并行调用互不依赖的调用同时发出,大幅降低延迟
Structured Output保证输出合法 JSON,用于信息提取等非工具场景

关键要点:

  1. Function Calling 的本质是 LLM 作为"决策者",输出执行指令,代码作为"执行者"。
  2. 工具定义的质量直接决定调用准确性——description 比参数 schema 更重要。
  3. 对于互不依赖的工具调用,始终使用并行模式以降低延迟。
  4. 非工具场景的结构化输出用 Structured Output(JSON Schema)保障格式正确。
  5. Function Calling 是后面要讲的 Agent 的基础能力,是构建智能应用的基础。