函数调用
函数调用(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 平台还提供了内置工具,可以让模型:
- 搜索网页
- 执行代码
- 访问 MCP server
- 以及更多能力
工具调用流程
工具调用本质上是你的应用和模型之间,通过 OpenAI API 进行的一段多轮协作。整体流程通常包含五个高层步骤:
- 向模型发起请求,并提供可调用的工具
- 接收模型返回的工具调用
- 在应用侧执行代码,处理工具调用输入
- 将工具输出再次发送给模型
- 接收模型的最终响应(或者更多工具调用)
函数工具示例
下面来看一个完整的端到端工具调用流程。假设存在一个 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)按领域对工具进行分组,例如 crm、billing、shipping。
命名空间有助于组织相似工具,尤其适合模型需要在多个系统或不同用途的工具之间做选择时。例如:
- 一个搜索工具用于 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 来延迟加载部分或全部工具。tool_search 工具允许模型先搜索相关工具,把它们加入上下文,然后再实际使用。只有 gpt-5.4 及之后的模型支持它。详情请阅读 tool search guide。
定义函数的最佳实践
编写清晰且详细的函数名、参数描述和使用说明
- 明确描述函数的用途、每个参数的含义与格式,以及输出代表什么
- 在 system prompt 中说明何时应该使用、何时不应使用某个函数
- 加入示例与边界情况,尤其是针对已知失败模式
(注意:为推理模型加入示例有时可能反而影响表现) - 对于延迟加载工具,把详细说明放在函数描述中,命名空间描述则尽量简洁
应用软件工程最佳实践
- 让函数直观、易懂,符合“最小惊讶原则”
- 使用 enum 与合理对象结构,让无效状态无法表示
(例如toggle_light(on: bool, off: bool)容易出现无效调用) - 用“实习生测试”衡量:一个人只凭你给模型的说明,能否正确使用这个函数?
尽量把负担从模型转移到代码层
- 不要让模型填写你已经知道的参数
例如如果你已经在前一步拿到了order_id,那就不必再让函数暴露order_id参数 - 把总是连续调用的函数尽量合并
- 不要让模型填写你已经知道的参数
让模型初始可见的函数数量尽量少
- 用不同函数数量做评估
- 建议在同一轮对话中,初始暴露的函数少于 20 个
- 对大型或低频工具面,优先使用 tool search 延迟加载
充分利用 OpenAI 提供的资源
- 在 Playground 中生成并迭代函数 schema
- 对于大量函数或复杂任务,考虑通过 fine-tuning 提高函数调用准确率
(可参考 cookbook)
Token 使用量
在底层实现上,函数会以模型训练过的一种语法形式注入到 system message 中。这意味着函数定义本身也会占用上下文窗口,并按输入 token 计费。
如果你遇到 token 限制,建议:
- 减少初始加载的函数数量
- 尽量缩短描述
- 使用 tool search 延迟加载工具
如果你有很多函数定义,也可以通过 fine-tuning 来降低工具定义所需的 token 开销。
处理函数调用
当模型调用函数时,你必须在应用侧执行它,并把结果返回给模型。由于模型响应可能包含 0 个、1 个或多个调用,因此最佳实践是:始终假设会有多个调用。
响应中的 output 数组会包含 type 为 function_call 的条目。每个条目都会包含:
call_id:后续提交函数结果时需要用到name- JSON 编码后的
arguments
如果你正在使用 tool search,在 function_call 之前还可能会看到:
tool_search_calltool_search_output
不过,一旦函数真正加载完成,处理函数调用的方式与普通函数调用完全一致。
结果格式
传入 function_call_output 消息中的结果通常应该是字符串,至于内容格式则由你决定:
- JSON
- 错误码
- 纯文本
- 其他任何你希望模型理解的字符串形式
模型会根据你的返回字符串进行解释。
如果函数返回的是图片或文件,你也可以传入一个图片或文件对象数组,而不是字符串。
如果函数没有返回值(例如 send_email),你只需要返回一个表示成功或失败的字符串,例如:
text
success将结果纳入最终响应
在你把函数结果追加回 input 之后,就可以再次把请求发送给模型,获取最终回答。
其他配置
Tool choice
默认情况下,模型会自行决定是否调用工具,以及调用多少工具。你可以通过 tool_choice 参数强制特定行为。
Auto:(默认)调用 0 个、1 个或多个函数
tool_choice: "auto"Required: 至少调用一个函数
tool_choice: "required"Forced Function: 强制调用某一个特定函数
tool_choice: {"type": "function", "name": "get_weather"}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:
parameters中的每个对象都必须设置additionalProperties: falseproperties中的所有字段都必须被列入required
如果你想表示某个字段是可选的,可以将 null 加入它的 type 选项中。
如果你发送了 strict: true,但 schema 不满足这些要求,请求会被直接拒绝,并返回缺失约束的细节信息。
如果你省略 strict,则不同 API 的默认行为不同:
- Responses API 会自动把 schema 规范化到严格模式(例如补上
additionalProperties: false,并将所有字段设为 required) - Chat Completions API 默认仍是非严格模式
如果你希望在 Responses API 中显式关闭严格模式,并保留非严格的“尽力函数调用”,可以设置:
json
{ "strict": false }所有在 Playground 中生成的 schema,默认都启用了严格模式。
虽然我们推荐启用严格模式,但它也有一些限制:
- JSON Schema 的某些特性并不被支持(详见 supported schemas)
对于 fine-tuned 模型,还需要注意:
- Schema 会在首次请求时经历额外处理(之后会被缓存),如果你的 schema 每次都不同,可能导致更高延迟
- 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 | 正在进行中的函数调用对象,包含 name、arguments 和 id |
接下来你会收到一系列 response.function_call_arguments.delta 事件,这些事件携带 arguments 字段的增量内容。它们包含:
| 字段 | 说明 |
|---|---|
response_id | 当前函数调用所属的 response ID |
item_id | 当前增量所属的函数调用项 ID |
output_index | 函数调用在 response 中的输出索引 |
delta | arguments 字段的增量片段 |
当模型完成函数调用后,会发出一条 response.function_call_arguments.done 事件,其中包含完整函数调用:
| 字段 | 说明 |
|---|---|
response_id | 当前函数调用所属的 response ID |
output_index | 函数调用在 response 中的输出索引 |
item | 完整函数调用对象,包含 name、arguments 和 id |
自定义工具
自定义工具(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 语法:
larkregex
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。
直接提供纯模式字符串
不要把模式包在 // 中。