Skip to content

函数调用

函数调用(Function calling),也常被称为 工具调用(tool calling),是一种强大且灵活的机制,可让 OpenAI 模型与外部系统交互,并访问训练数据之外的信息。本指南将说明如何把模型连接到由你的应用提供的数据与操作能力。我们会展示如何使用基于 JSON Schema 定义的函数工具,以及支持自由文本输入输出的自定义工具。

如果你的应用拥有大量函数或非常大的 schema,可以将函数调用与 tool search 结合使用,把低频工具延迟加载,仅在模型真正需要时再引入。只有 gpt-5.4 及之后的模型支持 tool_search

工作原理

让我们先理解几个与工具调用相关的关键术语。在建立共同术语后,再通过实际示例说明如何使用。

工具:我们赋予模型的功能

函数(function)工具(tool),抽象上是指我们告诉模型“它可以调用的一段功能”。当模型根据提示词生成响应时,它可能会判断自己需要某个工具提供的数据或功能,才能完成提示中的任务。

你可以为模型提供的工具包括:

  • 获取某个地点今天的天气
  • 读取指定用户 ID 的账户信息
  • 为丢失订单发起退款

或者任何你希望模型在响应时“知道”或“做到”的能力。

当我们向模型发起 API 请求时,可以在请求中附带一组工具供模型选择。例如,如果我们希望模型回答世界某地当前天气的问题,就可以为它提供一个 get_weather 工具,并让它接收 location 参数。

工具调用:模型发出的调用请求

函数调用(function call)工具调用(tool call),指的是模型在阅读提示词后,判断自己必须调用某个工具才能完成任务,于是返回的一种特殊响应。

例如,如果模型收到提示词:

what is the weather in Paris?

它可能会返回一个对 get_weather 工具的调用,并把 Paris 作为 location 参数。

工具调用输出:由你的程序返回给模型的结果

函数调用输出(function call output)工具调用输出(tool call output),指的是工具根据模型调用所提供的输入生成的结果。这个结果可以是结构化 JSON,也可以是普通文本,并且通常应通过 call_id 与某次具体工具调用关联起来。

继续上面的天气示例:

  • 模型可以访问一个接收 location 参数的 get_weather 工具
  • 当用户问 “what's the weather in Paris?” 时,模型返回一个包含 location: Paris工具调用
  • 应用执行工具后,会返回 工具调用输出,例如:

然后,我们把工具定义、原始提示、模型返回的工具调用,以及工具输出,一起再次发送给模型,最终得到类似下面的自然语言回答:

text
The weather in Paris today is 25C.

函数与工具的区别

  • 函数 是一种特殊的工具,它通过 JSON Schema 定义。函数定义允许模型把结构化数据传回你的应用,然后由你的代码去读取数据或执行操作。
  • 除函数工具外,还有自定义工具(custom tools),它们支持自由文本输入与输出。
  • 此外,OpenAI 平台还提供了内置工具,可以让模型:

工具调用流程

工具调用本质上是你的应用和模型之间,通过 OpenAI API 进行的一段多轮协作。整体流程通常包含五个高层步骤:

  1. 向模型发起请求,并提供可调用的工具
  2. 接收模型返回的工具调用
  3. 在应用侧执行代码,处理工具调用输入
  4. 将工具输出再次发送给模型
  5. 接收模型的最终响应(或者更多工具调用)

函数工具示例

下面来看一个完整的端到端工具调用流程。假设存在一个 get_horoscope 函数,用于获取某个星座当天的运势。

注意:对于 GPT-5、o4-mini 这类推理模型,如果模型在返回工具调用时同时产生了 reasoning 项,那么这些 reasoning 项也必须和工具调用输出一起传回模型。

定义函数

函数通常是在每次 API 请求的 tools 参数中声明的。
如果结合 tool search,你的应用也可以在交互后续阶段延迟加载函数。无论采用哪种方式,每个可调用函数都遵循相同的 schema 结构。

一个函数定义通常包含以下属性:

字段说明
type固定为 function
name函数名称,例如 get_weather
description函数用途,以及应在何时、如何使用
parameters使用 JSON Schema 定义函数输入参数
strict是否强制启用严格模式

下面是 get_weather 函数的定义示例:

json
{
  "type": "function",
  "name": "get_weather",
  "description": "Retrieves current weather for the given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City and country e.g. Bogotá, Colombia"
      },
      "units": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "Units the temperature will be returned in."
      }
    },
    "required": ["location", "units"],
    "additionalProperties": false
  },
  "strict": true
}

由于 parameters 基于 JSON Schema 定义,你可以利用很多强大能力,例如:

  • 属性类型
  • 枚举值
  • 描述信息
  • 嵌套对象
  • 递归对象

定义命名空间

你可以使用命名空间(namespace)按领域对工具进行分组,例如 crmbillingshipping

命名空间有助于组织相似工具,尤其适合模型需要在多个系统或不同用途的工具之间做选择时。例如:

  • 一个搜索工具用于 CRM
  • 另一个搜索工具用于支持工单系统
json
{
  "type": "namespace",
  "name": "crm",
  "description": "CRM tools for customer lookup and order management.",
  "tools": [
    {
      "type": "function",
      "name": "get_customer_profile",
      "description": "Fetch a customer profile by customer ID.",
      "parameters": {
        "type": "object",
        "properties": {
          "customer_id": { "type": "string" }
        },
        "required": ["customer_id"],
        "additionalProperties": false
      }
    },
    {
      "type": "function",
      "name": "list_open_orders",
      "description": "List open orders for a customer ID.",
      "defer_loading": true,
      "parameters": {
        "type": "object",
        "properties": {
          "customer_id": { "type": "string" }
        },
        "required": ["customer_id"],
        "additionalProperties": false
      }
    }
  ]
}

如果你需要向模型开放一个庞大的工具生态,可以使用 tool_search 来延迟加载部分或全部工具。tool_search 工具允许模型先搜索相关工具,把它们加入上下文,然后再实际使用。只有 gpt-5.4 及之后的模型支持它。详情请阅读 tool search guide

定义函数的最佳实践

  1. 编写清晰且详细的函数名、参数描述和使用说明

    • 明确描述函数的用途、每个参数的含义与格式,以及输出代表什么
    • 在 system prompt 中说明何时应该使用、何时不应使用某个函数
    • 加入示例与边界情况,尤其是针对已知失败模式
      (注意:为推理模型加入示例有时可能反而影响表现)
    • 对于延迟加载工具,把详细说明放在函数描述中,命名空间描述则尽量简洁
  2. 应用软件工程最佳实践

    • 让函数直观、易懂,符合“最小惊讶原则”
    • 使用 enum 与合理对象结构,让无效状态无法表示
      (例如 toggle_light(on: bool, off: bool) 容易出现无效调用)
    • 用“实习生测试”衡量:一个人只凭你给模型的说明,能否正确使用这个函数?
  3. 尽量把负担从模型转移到代码层

    • 不要让模型填写你已经知道的参数
      例如如果你已经在前一步拿到了 order_id,那就不必再让函数暴露 order_id 参数
    • 把总是连续调用的函数尽量合并
  4. 让模型初始可见的函数数量尽量少

    • 用不同函数数量做评估
    • 建议在同一轮对话中,初始暴露的函数少于 20 个
    • 对大型或低频工具面,优先使用 tool search 延迟加载
  5. 充分利用 OpenAI 提供的资源

    • Playground 中生成并迭代函数 schema
    • 对于大量函数或复杂任务,考虑通过 fine-tuning 提高函数调用准确率
      (可参考 cookbook

Token 使用量

在底层实现上,函数会以模型训练过的一种语法形式注入到 system message 中。这意味着函数定义本身也会占用上下文窗口,并按输入 token 计费。

如果你遇到 token 限制,建议:

  • 减少初始加载的函数数量
  • 尽量缩短描述
  • 使用 tool search 延迟加载工具

如果你有很多函数定义,也可以通过 fine-tuning 来降低工具定义所需的 token 开销。

处理函数调用

当模型调用函数时,你必须在应用侧执行它,并把结果返回给模型。由于模型响应可能包含 0 个、1 个或多个调用,因此最佳实践是:始终假设会有多个调用。

响应中的 output 数组会包含 typefunction_call 的条目。每个条目都会包含:

  • call_id:后续提交函数结果时需要用到
  • name
  • JSON 编码后的 arguments

如果你正在使用 tool search,在 function_call 之前还可能会看到:

  • tool_search_call
  • tool_search_output

不过,一旦函数真正加载完成,处理函数调用的方式与普通函数调用完全一致。

结果格式

传入 function_call_output 消息中的结果通常应该是字符串,至于内容格式则由你决定:

  • JSON
  • 错误码
  • 纯文本
  • 其他任何你希望模型理解的字符串形式

模型会根据你的返回字符串进行解释。

如果函数返回的是图片或文件,你也可以传入一个图片或文件对象数组,而不是字符串。

如果函数没有返回值(例如 send_email),你只需要返回一个表示成功或失败的字符串,例如:

text
success

将结果纳入最终响应

在你把函数结果追加回 input 之后,就可以再次把请求发送给模型,获取最终回答。

其他配置

Tool choice

默认情况下,模型会自行决定是否调用工具,以及调用多少工具。你可以通过 tool_choice 参数强制特定行为。

  1. Auto:(默认)调用 0 个、1 个或多个函数
    tool_choice: "auto"

  2. Required: 至少调用一个函数
    tool_choice: "required"

  3. Forced Function: 强制调用某一个特定函数
    tool_choice: {"type": "function", "name": "get_weather"}

  4. Allowed tools: 把模型可调用的工具限制为可用工具中的某个子集

何时使用 allowed_tools

如果你希望在不同请求中仅允许模型调用某些工具子集,但又不想修改每次传入的完整工具列表,以便尽可能利用 prompt caching 的缓存收益,那么 allowed_tools 会非常有用。

json
{
  "tool_choice": {
    "type": "allowed_tools",
    "mode": "auto",
    "tools": [
      { "type": "function", "name": "get_weather" },
      { "type": "function", "name": "search_docs" }
    ]
  }
}

你也可以把 tool_choice 设为 "none",以模拟“不传任何函数”的行为。

当你使用 tool search 时,tool_choice 仍然适用于当前轮次已经可调用的工具。这在你加载了工具子集后、希望进一步限制模型只在该子集内选择时尤其有用。

并行函数调用

当你使用内置工具时,不支持并行函数调用。

模型可能在同一轮中选择调用多个函数。你可以将 parallel_tool_calls 设置为 false,从而确保每轮只会调用 0 个或 1 个工具。

注意:

  • 如果你使用的是 fine-tuned 模型,并且模型在同一轮中调用了多个函数,那么这些调用上的 strict mode 会被禁用
  • 对于 gpt-5.4-nano-2025-04-14 这个快照版本,如果开启并行工具调用,它有时会对同一个工具产生多个重复调用,因此建议在该快照下关闭此特性

严格模式(Strict mode)

strict 设置为 true,可以确保函数调用严格遵守函数 schema,而不是仅做“尽力匹配”。我们建议始终开启严格模式。

严格模式底层依赖的是结构化输出能力,因此会额外带来两个Requirements:

  1. parameters 中的每个对象都必须设置 additionalProperties: false
  2. properties 中的所有字段都必须被列入 required

如果你想表示某个字段是可选的,可以将 null 加入它的 type 选项中。

如果你发送了 strict: true,但 schema 不满足这些要求,请求会被直接拒绝,并返回缺失约束的细节信息。

如果你省略 strict,则不同 API 的默认行为不同:

  • Responses API 会自动把 schema 规范化到严格模式(例如补上 additionalProperties: false,并将所有字段设为 required)
  • Chat Completions API 默认仍是非严格模式

如果你希望在 Responses API 中显式关闭严格模式,并保留非严格的“尽力函数调用”,可以设置:

json
{ "strict": false }

所有在 Playground 中生成的 schema,默认都启用了严格模式。

虽然我们推荐启用严格模式,但它也有一些限制:

  1. JSON Schema 的某些特性并不被支持(详见 supported schemas

对于 fine-tuned 模型,还需要注意:

  1. Schema 会在首次请求时经历额外处理(之后会被缓存),如果你的 schema 每次都不同,可能导致更高延迟
  2. Schema 为了性能会被缓存,因此不符合 zero data retention

Streaming

Streaming 可以用来展示工具调用进度,例如:

  • 展示模型当前正在调用哪个函数
  • 显示模型正在逐步填写的参数
  • 实时展示 arguments 的构造过程

流式函数调用与流式普通响应非常类似:你需要把 stream 设置为 true,然后接收不同的 event 对象。

不同的是,这次你不再把增量片段聚合成一个 content 字符串,而是把增量片段聚合成一个编码后的 arguments JSON 对象。

当模型调用一个或多个函数时,每个函数调用都会先触发一条 response.output_item.added 事件,其中包含:

字段说明
response_id当前函数调用所属的 response ID
output_index该函数调用在 response 输出项中的索引
item正在进行中的函数调用对象,包含 nameargumentsid

接下来你会收到一系列 response.function_call_arguments.delta 事件,这些事件携带 arguments 字段的增量内容。它们包含:

字段说明
response_id当前函数调用所属的 response ID
item_id当前增量所属的函数调用项 ID
output_index函数调用在 response 中的输出索引
deltaarguments 字段的增量片段

当模型完成函数调用后,会发出一条 response.function_call_arguments.done 事件,其中包含完整函数调用:

字段说明
response_id当前函数调用所属的 response ID
output_index函数调用在 response 中的输出索引
item完整函数调用对象,包含 nameargumentsid

自定义工具

自定义工具(custom tools)与 JSON Schema 驱动的函数工具在整体使用方式上非常相似。
但不同点在于:你不需要显式告诉模型“工具输入必须符合某个固定参数结构”,模型可以直接把任意字符串作为输入传给你的工具。

这在以下场景非常有用:

  • 不想为简单文本输入额外包一层 JSON
  • 希望对工具输入施加自定义语法约束(后文会讲到)

下面是一个示例:创建一个期望接收 Python 代码文本的自定义工具。

与前面相同,output 数组中会包含模型生成的工具调用;只不过这次,工具输入是纯文本

json
[
  {
    "id": "rs_6890e972fa7c819ca8bc561526b989170694874912ae0ea6",
    "type": "reasoning",
    "content": [],
    "summary": []
  },
  {
    "id": "ctc_6890e975e86c819c9338825b3e1994810694874912ae0ea6",
    "type": "custom_tool_call",
    "status": "completed",
    "call_id": "call_aGiFQkRWSWAIsMQ19fKqxUgb",
    "input": "print(\"hello world\")",
    "name": "code_exec"
  }
]

上下文无关文法(CFG)

上下文无关文法(CFG)是一组规则,用来定义某种文本格式下“哪些文本是合法的”。

对于自定义工具,你可以通过 grammar 参数提供 CFG,用于约束模型生成的工具输入。

目前支持两种 CFG 语法:

  • lark
  • regex

Lark CFG

工具输出应符合你定义的 Lark CFG,例如:

json
[
  {
    "id": "rs_6890ed2b6374819dbbff5353e6664ef103f4db9848be4829",
    "type": "reasoning",
    "content": [],
    "summary": []
  },
  {
    "id": "ctc_6890ed2f32e8819daa62bef772b8c15503f4db9848be4829",
    "type": "custom_tool_call",
    "status": "completed",
    "call_id": "call_pmlLjmvG33KJdyVdC4MVdk5N",
    "input": "4 + 4",
    "name": "math_exp"
  }
]

语法使用的是 Lark 的一个变体,模型采样约束则由 LLGuidance 实现。

以下 Lark 特性当前不支持:

  • lexer regex 中的 lookaround
  • 惰性修饰符(*?+???
  • terminal 优先级
  • templates
  • imports(除内置 %import common 外)
  • %declare

建议使用 Lark IDE 试验你的自定义文法。

保持 grammar 简单

尽量让你的 grammar 保持简单。
如果 grammar 太复杂,OpenAI API 可能直接报错,因此在接入前应先确认它与 API 兼容。

Lark grammar 往往需要反复迭代。简单 grammar 通常最稳定,而复杂 grammar 往往需要你同时调整:

  • grammar 定义
  • prompt
  • tool description

以确保模型不会“脱离分布”。

正确与错误模式

正确示例(单个、边界清晰的 terminal):

text
start: SENTENCE
SENTENCE: /[A-Za-z, ]*(the hero|a dragon|an old man|the princess)[A-Za-z, ]*(fought|saved|found|lost)[A-Za-z, ]*(a treasure|the kingdom|a secret|his way)[A-Za-z, ]*\./

错误示例(将自由文本拆散到多个 rules/terminals 中):

text
start: sentence
sentence: /[A-Za-z, ]+/ subject /[A-Za-z, ]+/ verb /[A-Za-z, ]+/ object /[A-Za-z, ]+/

原因是:小写 rule 不会决定 lexer 如何切分 terminal;真正决定切分行为的只有 terminal 定义本身。
如果你需要“在锚点之间允许自由文本”,应尽量用一个大 regex terminal 一次性表达出来。

terminals 与 rules

Lark 约定:

  • UPPERCASE:terminal(词法层 token)
  • lowercase:rule(语法层 production)

为了保持在受支持子集内并避免意外行为,最好的做法是:

  • grammar 保持简单、显式
  • terminal 与 rule 职责清晰分离

此外,terminal 中使用的 regex 语法遵循的是 Rust regex crate,而不是 Python 的 re 模块。

核心原则与最佳实践

Lexer 先于 parser 运行

terminal 会先由 lexer 匹配(贪婪匹配 / 最长匹配优先),然后 parser 才处理 CFG 规则。因此,你不能指望通过拆分 rules 来“影响” terminal 的切分方式。

如果是在自由文本中提取结构,优先用一个 terminal

如果你要在自然语言中识别某个模式,最好把它写成一个单独 terminal,而不要试图在多个自由文本 terminal 与 rules 之间穿插。

用 rules 组合离散 token

当你组合的是边界明确的 token(数字、关键字、标点)时,rules 很合适;但如果你想限制“两个 token 中间的任意文本”,rules 就不是最佳选择。

terminal 应该简单、边界明确、自包含

尽量使用清晰的字符类与有界量词(如 {0,10}),避免到处使用无界的 *。例如,如果你需要匹配“到句号为止的任意文本”,更推荐:

text
/[^.\n]{0,10}*\./

而不是:

text
/.+\./

用 rules 组合 token,而不要试图操纵 regex 内部结构

一个良好的 rules 示例:

text
start: expr
NUMBER: /[0-9]+/
PLUS: "+"
MINUS: "-"
expr: term (("+"|"-") term)*
term: NUMBER

显式处理空白字符

不要依赖无界 %ignore 指令。过于开放的 ignore 规则可能导致 grammar 过于复杂,或让模型偏离分布。更推荐显式在 grammar 中处理允许出现空白的位置。

故障排查

  • 如果 API 因 grammar 太复杂而拒绝请求,说明你需要进一步简化 rules 与 terminals,并减少无界 %ignore
  • 如果自定义工具被调用时出现意外 token,应先检查 terminal 是否重叠,以及 greedy lexer 是否在错误切分
  • 如果模型“脱离分布”(例如输出过长、重复,虽然语法合法但语义错误):
    • 收紧 grammar
    • 优化 prompt(加入 few-shot 示例)
    • 优化工具描述(解释 grammar 并要求模型遵守)
    • 尝试提高 reasoning effort(例如从 medium 提升到 high)

Regex CFG

你也可以使用 Regex CFG。此时工具输出会符合你提供的正则 grammar,例如:

json
[
  {
    "id": "rs_6894f7a3dd4c81a1823a723a00bfa8710d7962f622d1c260",
    "type": "reasoning",
    "content": [],
    "summary": []
  },
  {
    "id": "ctc_6894f7ad7fb881a1bffa1f377393b1a40d7962f622d1c260",
    "type": "custom_tool_call",
    "status": "completed",
    "call_id": "call_8m4XCnYvEmFlzHgDHbaOCFlK",
    "input": "August 7th 2025 at 10AM",
    "name": "timestamp"
  }
]

与 Lark 语法一样,Regex 同样使用 Rust regex crate,而不是 Python 的 re 模块。

当前不支持的 Regex 特性包括:

  • lookaround
  • 惰性修饰符(*?+???

Regex 的关键实践

模式必须写在一行里

如果你需要匹配换行符,请使用转义序列 \n。不要使用允许多行模式的 verbose / extended mode。

直接提供纯模式字符串

不要把模式包在 // 中。