OpenAI Function Calling 指南:把模型接到真实系统里

_

来源:OpenAI 官方文档
原文链接:https://developers.openai.com/api/docs/guides/function-calling

Function calling 也常被称为 tool calling。它的核心价值是:让大模型不只“生成文本”,而是能够在你的应用里调用外部能力,例如查询天气、读取账户信息、执行退款、调用内部服务,或者把结构化参数交给你自己的业务代码处理。

这篇文章基于 OpenAI 官方文档整理,重点讲清以下几件事:

  • 什么是工具、工具调用、工具结果
  • 一次完整的函数调用工作流怎么跑通
  • 如何定义函数 schema,如何让输出更稳定
  • Responses APIChat Completions API 的典型写法
  • 严格模式、并行调用、流式调用要注意什么
  • 什么是自定义工具,以及何时适合用 grammar 约束输入

如果你的应用里工具很多、schema 很大,还可以结合 tool search 按需延迟加载工具。官方文档说明:只有 gpt-5.4 及之后的模型支持 tool_search

一、先建立三个核心概念

在 OpenAI 的术语里,和函数调用相关的对象主要有三类。

1. 工具(Tools)

工具是你告诉模型“它可以使用的能力”。

例如:

  • 获取某个城市今天的天气
  • 根据用户 ID 查询账户详情
  • 为订单执行退款

这些能力本身并不在模型内部执行,而是由你的应用提供。模型只是决定“是否需要调用”,并生成调用该工具所需的参数。

2. 工具调用(Tool Calls)

当模型读完用户输入后,如果判断仅靠当前上下文不足以完成任务,就可能返回一个工具调用请求。

例如用户问:

巴黎今天天气怎么样?

模型可能不会直接编造答案,而是返回一个对 get_weather 工具的调用,并带上参数 location = Paris

3. 工具调用结果(Tool Call Outputs)

工具调用结果是你在应用侧真正执行完逻辑后,再回传给模型的内容。

它可以是:

  • JSON
  • 普通文本
  • 图片内容
  • 文件内容

官方文档强调,工具结果需要和具体的调用关联起来,通常通过 call_idtool_call_id 进行绑定。

二、函数工具和其他工具有什么区别

OpenAI 文档里,“工具”是更大的概念,“函数”只是其中一种。

  • function:基于 JSON Schema 定义输入参数,适合结构化调用
  • custom:输入输出可以是自由文本,更适合代码片段、受 grammar 约束的文本等场景
  • 内建工具:OpenAI 平台自带的一类工具,例如 web searchcode interpreterremote MCP

如果你的目标是“让模型产出稳定的结构化参数,再由业务代码执行”,通常优先使用 function

三、完整工作流:一次函数调用到底怎么走

函数调用本质上是你的应用和模型之间的一段多轮协作。官方文档把它概括成五步:

  1. 向模型发起请求,并附带可调用工具列表
  2. 接收模型返回的工具调用
  3. 在应用侧执行真实逻辑
  4. 把执行结果作为工具输出再发回模型
  5. 获取模型的最终回答,或者继续下一轮工具调用

Function Calling Diagram Steps

这个流程有一个非常重要的工程事实:模型不会替你执行函数,真正执行的是你的代码。

四、一个最小可用示例:星座运势函数

下面是官方文档给出的端到端示例。它定义了一个 get_horoscope 函数,模型先发起调用,你的应用执行后,再把结果喂回模型。

Chat Completions API 示例

from openai import OpenAI
import json

client = OpenAI()

# 1. Define a list of callable tools for the model
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_horoscope",
            "description": "Get today's horoscope for an astrological sign.",
            "parameters": {
                "type": "object",
                "properties": {
                    "sign": {
                        "type": "string",
                        "description": "An astrological sign like Taurus or Aquarius",
                    },
                },
                "required": ["sign"],
                "additionalProperties": False,
            },
            "strict": True,
        },
    },
]

def get_horoscope(sign):
    return f"{sign}: Next Tuesday you will befriend a baby otter."

messages = [
    {"role": "user", "content": "What is my horoscope? I am an Aquarius."}
]

# 2. Prompt the model with tools defined
response = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,
)

messages.append(response.choices[0].message)

for tool_call in response.choices[0].message.tool_calls or []:
    if tool_call.function.name == "get_horoscope":
        # 3. Execute the function logic for get_horoscope
        args = json.loads(tool_call.function.arguments)
        horoscope = get_horoscope(args["sign"])

        # 4. Provide function call results to the model
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps({"horoscope": horoscope}),
            }
        )

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

# 5. The model should be able to give a response!
print(response.choices[0].message.content)
import OpenAI from "openai";

const openai = new OpenAI();

// 1. Define a list of callable tools for the model
const tools = [
  {
    type: "function",
    function: {
      name: "get_horoscope",
      description: "Get today's horoscope for an astrological sign.",
      parameters: {
        type: "object",
        properties: {
          sign: {
            type: "string",
            description: "An astrological sign like Taurus or Aquarius",
          },
        },
        required: ["sign"],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

function getHoroscope(sign) {
  return `${sign}: Next Tuesday you will befriend a baby otter.`;
}

const messages = [
  { role: "user", content: "What is my horoscope? I am an Aquarius." },
];

// 2. Prompt the model with tools defined
let response = await openai.chat.completions.create({
  model: "gpt-4.1",
  messages,
  tools,
});

messages.push(response.choices[0].message);

for (const toolCall of response.choices[0].message.tool_calls ?? []) {
  if (toolCall.function.name === "get_horoscope") {
    // 3. Execute the function logic for get_horoscope
    const args = JSON.parse(toolCall.function.arguments);
    const horoscope = getHoroscope(args.sign);

    // 4. Provide function call results to the model
    messages.push({
      role: "tool",
      tool_call_id: toolCall.id,
      content: JSON.stringify({ horoscope }),
    });
  }
}

response = await openai.chat.completions.create({
  model: "gpt-4.1",
  messages,
  tools,
});

// 5. The model should be able to give a response!
console.log(response.choices[0].message.content);

这个示例说明了函数调用最关键的边界:

  • 模型负责决定“是否调用”和“调用参数是什么”
  • 你的应用负责真正执行函数
  • 结果必须显式回传给模型,它才能继续生成最终回答

五、Responses API 写法

如果你使用的是 Responses API,写法会更接近“把模型输出项和工具结果串起来”。

from openai import OpenAI
import json

client = OpenAI()

# 1. Define a list of callable tools for the model
tools = [
    {
        "type": "function",
        "name": "get_horoscope",
        "description": "Get today's horoscope for an astrological sign.",
        "parameters": {
            "type": "object",
            "properties": {
                "sign": {
                    "type": "string",
                    "description": "An astrological sign like Taurus or Aquarius",
                },
            },
            "required": ["sign"],
        },
    },
]

def get_horoscope(sign):
    return f"{sign}: Next Tuesday you will befriend a baby otter."

# Create a running input list we will add to over time
input_list = [
    {"role": "user", "content": "What is my horoscope? I am an Aquarius."}
]

# 2. Prompt the model with tools defined
response = client.responses.create(
    model="gpt-5",
    tools=tools,
    input=input_list,
)

# Save function call outputs for subsequent requests
input_list += response.output

for item in response.output:
    if item.type == "function_call":
        if item.name == "get_horoscope":
            # 3. Execute the function logic for get_horoscope
            sign = json.loads(item.arguments)["sign"]
            horoscope = get_horoscope(sign)
            
            # 4. Provide function call results to the model
            input_list.append({
                "type": "function_call_output",
                "call_id": item.call_id,
                "output": horoscope,
            })

print("Final input:")
print(input_list)

response = client.responses.create(
    model="gpt-5",
    instructions="Respond only with a horoscope generated by a tool.",
    tools=tools,
    input=input_list,
)

# 5. The model should be able to give a response!
print("Final output:")
print(response.model_dump_json(indent=2))
print("\n" + response.output_text)
import OpenAI from "openai";

const openai = new OpenAI();

// 1. Define a list of callable tools for the model
const tools = [
  {
    type: "function",
    name: "get_horoscope",
    description: "Get today's horoscope for an astrological sign.",
    parameters: {
      type: "object",
      properties: {
        sign: {
          type: "string",
          description: "An astrological sign like Taurus or Aquarius",
        },
      },
      required: ["sign"],
      additionalProperties: false,
    },
    strict: true,
  },
];

function getHoroscope(sign) {
  return `${sign}: Next Tuesday you will befriend a baby otter.`;
}

// Create a running input list we will add to over time
let input = [
  { role: "user", content: "What is my horoscope? I am an Aquarius." },
];

// 2. Prompt the model with tools defined
let response = await openai.responses.create({
  model: "gpt-5",
  tools,
  input,
});

// Preserve model output for the next turn
input.push(...response.output);

for (const item of response.output) {
  if (item.type !== "function_call") continue;

  if (item.name === "get_horoscope") {
    // 3. Execute the function logic for get_horoscope
    const { sign } = JSON.parse(item.arguments);
    const horoscope = getHoroscope(sign);

    // 4. Provide function call results to the model
    input.push({
      type: "function_call_output",
      call_id: item.call_id,
      output: horoscope,
    });
  }
}

console.log("Final input:");
console.log(JSON.stringify(input, null, 2));

response = await openai.responses.create({
  model: "gpt-5",
  instructions: "Respond only with a horoscope generated by a tool.",
  tools,
  input,
});

// 5. The model should be able to give a response!
console.log("Final output:");
console.log(response.output_text);

官方文档特别提醒:对于 GPT-5o4-mini 这类 reasoning 模型,如果返回里包含 reasoning items,并且同时发生了工具调用,那么这些条目也必须一并带回后续请求。

六、如何定义函数:核心是 JSON Schema

函数通常通过请求里的 tools 参数声明。每个函数定义的关键字段如下:

字段 说明
type 固定为 function
name 函数名,例如 get_weather
description 说明该函数在什么情况下用、怎么用
parameters 使用 JSON Schema 定义输入参数
strict 是否开启严格模式

一个典型的天气函数如下:

{
  "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
}

基于 JSON Schema,你可以利用这些能力:

  • 基本类型约束
  • enum
  • 嵌套对象
  • 字段描述
  • 递归对象

如果你有多个同一领域工具,官方也支持用 namespace 进行分组,例如 crmbillingshipping

{
  "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
      }
    }
  ]
}

七、结构化输出与严格模式

OpenAI 在文档里明确建议:尽量始终开启 strict: true

开启严格模式后,函数调用会更可靠地遵循 schema,而不是“尽力而为”。它底层依赖的是 structured outputs

不过严格模式有两个硬性要求:

  1. 每个对象都必须设置 additionalProperties: false
  2. properties 中所有字段都必须出现在 required

如果某个字段是可选的,可以通过把类型写成 ["string", "null"] 这种方式表达“允许为空”。

严格模式示例

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

非严格模式示例

{
    "type": "function",
    "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"],
        }
    }
}

这里还有一个容易忽略的细节:

  • Responses API 在你未显式设置时,会把 schema 规范化到严格模式语义
  • Chat Completions API 默认仍然是非严格模式

如果你在 Responses API 中想保留非严格、best-effort 的调用行为,需要显式设置 strict: false

八、如何处理一个或多个函数调用

模型一次响应里可能没有工具调用,也可能有一个,或者多个。工程实现上,应该默认按“可能有多个”来处理。

Chat Completions 返回的 tool_calls

[
    {
        "id": "call_12345xyz",
        "type": "function",
        "function": {
            "name": "get_weather",
            "arguments": "{\"location\":\"Paris, France\"}"
        }
    },
    {
        "id": "call_67890abc",
        "type": "function",
        "function": {
            "name": "get_weather",
            "arguments": "{\"location\":\"Bogotá, Colombia\"}"
        }
    },
    {
        "id": "call_99999def",
        "type": "function",
        "function": {
            "name": "send_email",
            "arguments": "{\"to\":\"bob@email.com\",\"body\":\"Hi bob\"}"
        }
    }
]
for tool_call in completion.choices[0].message.tool_calls:
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    result = call_function(name, args)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": str(result)
    })
for (const toolCall of completion.choices[0].message.tool_calls) {
    const name = toolCall.function.name;
    const args = JSON.parse(toolCall.function.arguments);

    const result = callFunction(name, args);
    messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        content: result.toString()
    });
}

Responses API 返回的 function_call

[
    {
        "id": "fc_12345xyz",
        "call_id": "call_12345xyz",
        "type": "function_call",
        "name": "get_weather",
        "arguments": "{\"location\":\"Paris, France\"}"
    },
    {
        "id": "fc_67890abc",
        "call_id": "call_67890abc",
        "type": "function_call",
        "name": "get_weather",
        "arguments": "{\"location\":\"Bogotá, Colombia\"}"
    },
    {
        "id": "fc_99999def",
        "call_id": "call_99999def",
        "type": "function_call",
        "name": "send_email",
        "arguments": "{\"to\":\"bob@email.com\",\"body\":\"Hi bob\"}"
    }
]
for tool_call in response.output:
    if tool_call.type != "function_call":
        continue

    name = tool_call.name
    args = json.loads(tool_call.arguments)

    result = call_function(name, args)
    input_messages.append({
        "type": "function_call_output",
        "call_id": tool_call.call_id,
        "output": str(result)
    })
for (const toolCall of response.output) {
    if (toolCall.type !== "function_call") {
        continue;
    }

    const name = toolCall.name;
    const args = JSON.parse(toolCall.arguments);

    const result = callFunction(name, args);
    input.push({
        type: "function_call_output",
        call_id: toolCall.call_id,
        output: result.toString()
    });
}

一个简单的分发函数可以写成这样:

def call_function(name, args):
    if name == "get_weather":
        return get_weather(**args)
    if name == "send_email":
        return send_email(**args)
const callFunction = async (name, args) => {
    if (name === "get_weather") {
        return getWeather(args.latitude, args.longitude);
    }
    if (name === "send_email") {
        return sendEmail(args.to, args.body);
    }
};

函数返回值通常建议用字符串承载,格式可以由你自己决定,例如:

  • JSON 字符串
  • 普通文本
  • 错误码
  • 成功/失败状态

如果函数返回的是图片或文件,也可以按官方文档要求传入图片对象或文件对象数组。

九、控制模型怎么选工具

默认情况下,模型会自己判断要不要调用工具,以及一次调用几个。你也可以用 tool_choice 主动约束。

  • auto:默认行为,可以调用 0 个、1 个或多个工具
  • required:要求模型必须调用一个或多个工具
  • 指定函数:强制调用某一个函数
  • allowed_tools:只允许调用给定子集
  • none:等价于不让模型使用工具

例如:

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

如果你在做 prompt caching,而又不想频繁改动完整工具列表,allowed_tools 会比较有用。

十、并行函数调用与限制

模型可能会在一次响应里发起多个函数调用。如果你不希望出现这种行为,可以设置:

"parallel_tool_calls": false

这样可以确保一次只会调用 0 个或 1 个工具。

需要注意几点:

  • 使用 built-in tools 时,不支持并行函数调用
  • 文档说明:如果你使用 fine-tuned model,并且模型在一轮里调用了多个函数,那么这些调用的 strict mode 会被禁用
  • 对于 gpt-4.1-nano-2025-04-14 这个快照,官方建议在启用并行调用时要谨慎,因为它有时会对同一个工具重复发起多个调用

十一、流式函数调用:边生成参数边展示进度

流式模式适合在前端展示“模型正在调用哪个函数、参数正在怎么被填充”。

Chat Completions 流式示例

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                }
            },
            "required": ["location"],
            "additionalProperties": False
        },
        "strict": True
    }
}]

stream = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "What's the weather like in Paris today?"}],
    tools=tools,
    stream=True
)

for chunk in stream:
    delta = chunk.choices[0].delta
    print(delta.tool_calls)
import { OpenAI } from "openai";

const openai = new OpenAI();

const tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                }
            },
            "required": ["location"],
            "additionalProperties": false
        },
        "strict": true
    }
}];

const stream = await openai.chat.completions.create({
    model: "gpt-4.1",
    messages: [{ role: "user", content: "What's the weather like in Paris today?" }],
    tools,
    stream: true,
    store: true,
});

for await (const chunk of stream) {
    const delta = chunk.choices[0].delta;
    console.log(delta.tool_calls);
}

流式返回的 arguments 会以增量方式到达,例如:

[{"index": 0, "id": "call_DdmO9pD3xa9XTPNJ32zg2hcA", "function": {"arguments": "", "name": "get_weather"}, "type": "function"}]
[{"index": 0, "id": null, "function": {"arguments": "{\"", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "location", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "\":\"", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "Paris", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": ",", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": " France", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "\"}", "name": null}, "type": null}]
null

因此你需要自己把这些 delta 聚合成完整参数对象:

final_tool_calls = {}

for chunk in stream:
    for tool_call in chunk.choices[0].delta.tool_calls or []:
        index = tool_call.index

        if index not in final_tool_calls:
            final_tool_calls[index] = tool_call

        final_tool_calls[index].function.arguments += tool_call.function.arguments
const finalToolCalls = {};

for await (const chunk of stream) {
    const toolCalls = chunk.choices[0].delta.tool_calls || [];
    for (const toolCall of toolCalls) {
        const { index } = toolCall;

        if (!finalToolCalls[index]) {
            finalToolCalls[index] = toolCall;
        }

        finalToolCalls[index].function.arguments += toolCall.function.arguments;
    }
}

聚合完成后会得到类似这样的结果:

{
    "index": 0,
    "id": "call_RzfkBpJgzeR0S242qfvjadNe",
    "function": {
        "name": "get_weather",
        "arguments": "{\"location\":\"Paris, France\"}"
    }
}

Responses API 流式示例

Responses API 的流式模式不是 delta.tool_calls,而是一组事件流。

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "name": "get_weather",
    "description": "Get current temperature for a given location.",
    "parameters": {
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "City and country e.g. Bogotá, Colombia"
            }
        },
        "required": [
            "location"
        ],
        "additionalProperties": False
    }
}]

stream = client.responses.create(
    model="gpt-4.1",
    input=[{"role": "user", "content": "What's the weather like in Paris today?"}],
    tools=tools,
    stream=True
)

for event in stream:
    print(event)
import { OpenAI } from "openai";

const openai = new OpenAI();

const tools = [{
    type: "function",
    name: "get_weather",
    description: "Get current temperature for provided coordinates in celsius.",
    parameters: {
        type: "object",
        properties: {
            latitude: { type: "number" },
            longitude: { type: "number" }
        },
        required: ["latitude", "longitude"],
        additionalProperties: false
    },
    strict: true
}];

const stream = await openai.responses.create({
    model: "gpt-4.1",
    input: [{ role: "user", content: "What's the weather like in Paris today?" }],
    tools,
    stream: true,
    store: true,
});

for await (const event of stream) {
    console.log(event)
}

对应事件可能长这样:

{"type":"response.output_item.added","response_id":"resp_1234xyz","output_index":0,"item":{"type":"function_call","id":"fc_1234xyz","call_id":"call_1234xyz","name":"get_weather","arguments":""}}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"{\""}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"location"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"\":\""}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"Paris"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":","}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":" France"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"\"}"}
{"type":"response.function_call_arguments.done","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"arguments":"{\"location\":\"Paris, France\"}"}
{"type":"response.output_item.done","response_id":"resp_1234xyz","output_index":0,"item":{"type":"function_call","id":"fc_1234xyz","call_id":"call_1234xyz","name":"get_weather","arguments":"{\"location\":\"Paris, France\"}"}}

聚合方式如下:

final_tool_calls = {}

for event in stream:
    if event.type === 'response.output_item.added':
        final_tool_calls[event.output_index] = event.item;
    elif event.type === 'response.function_call_arguments.delta':
        index = event.output_index

        if final_tool_calls[index]:
            final_tool_calls[index].arguments += event.delta
const finalToolCalls = {};

for await (const event of stream) {
    if (event.type === 'response.output_item.added') {
        finalToolCalls[event.output_index] = event.item;
    } else if (event.type === 'response.function_call_arguments.delta') {
        const index = event.output_index;

        if (finalToolCalls[index]) {
            finalToolCalls[index].arguments += event.delta;
        }
    }
}

十二、自定义工具:不一定非要 JSON 参数

除了 function,官方还支持 custom 工具。它和函数工具最大的区别是:模型返回给工具的输入可以是任意字符串,而不是必须符合 JSON Schema 的参数对象。

这适合哪些场景?

  • 你希望模型直接产出一段代码
  • 你希望模型输出符合某种文本语法
  • 你不想把简单文本强行包成 JSON

自定义工具示例

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5",
    input="Use the code_exec tool to print hello world to the console.",
    tools=[
        {
            "type": "custom",
            "name": "code_exec",
            "description": "Executes arbitrary Python code.",
        }
    ]
)
print(response.output)
import OpenAI from "openai";
const client = new OpenAI();

const response = await client.responses.create({
  model: "gpt-5",
  input: "Use the code_exec tool to print hello world to the console.",
  tools: [
    {
      type: "custom",
      name: "code_exec",
      description: "Executes arbitrary Python code.",
    },
  ],
});

console.log(response.output);

返回结果会是一个 custom_tool_call,输入是普通文本:

[
  {
    "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"
  }
]

十三、用 grammar 约束自定义工具输入

对于自定义工具,官方支持通过 grammar 约束模型输出,使其只能生成符合某种上下文无关文法的文本。

目前支持两种语法:

  • lark
  • regex

Lark grammar 示例

from openai import OpenAI

client = OpenAI()

grammar = """
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
"""

response = client.responses.create(
    model="gpt-5",
    input="Use the math_exp tool to add four plus four.",
    tools=[
        {
            "type": "custom",
            "name": "math_exp",
            "description": "Creates valid mathematical expressions",
            "format": {
                "type": "grammar",
                "syntax": "lark",
                "definition": grammar,
            },
        }
    ]
)
print(response.output)
import OpenAI from "openai";
const client = new OpenAI();

const grammar = `
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
`;

const response = await client.responses.create({
  model: "gpt-5",
  input: "Use the math_exp tool to add four plus four.",
  tools: [
    {
      type: "custom",
      name: "math_exp",
      description: "Creates valid mathematical expressions",
      format: {
        type: "grammar",
        syntax: "lark",
        definition: grammar,
      },
    },
  ],
});

console.log(response.output);

结果会被约束为符合该 grammar 的输入,例如:

[
  {
    "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"
  }
]

Regex grammar 示例

from openai import OpenAI

client = OpenAI()

grammar = r"^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$"

response = client.responses.create(
    model="gpt-5",
    input="Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
    tools=[
        {
            "type": "custom",
            "name": "timestamp",
            "description": "Saves a timestamp in date + time in 24-hr format.",
            "format": {
                "type": "grammar",
                "syntax": "regex",
                "definition": grammar,
            },
        }
    ]
)
print(response.output)
import OpenAI from "openai";
const client = new OpenAI();

const grammar = "^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$";

const response = await client.responses.create({
  model: "gpt-5",
  input: "Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
  tools: [
    {
      type: "custom",
      name: "timestamp",
      description: "Saves a timestamp in date + time in 24-hr format.",
      format: {
        type: "grammar",
        syntax: "regex",
        definition: grammar,
      },
    },
  ],
});

console.log(response.output);

输出会类似:

[
  {
    "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"
  }
]

文档还特别说明:

  • grammar 使用的是 Rust regex crate syntax,不是 Python re
  • grammar 过于复杂时,API 可能直接报错
  • Lark 并不是完整支持,部分特性不可用

十四、最佳实践:把模型当成“参数规划器”,不是业务执行器

官方文档里关于函数定义和调用,最值得落实到工程里的建议主要有这些。

1. 函数名、参数名、描述都要写清楚

  • 函数用途要明确
  • 参数格式要明确
  • 返回结果代表什么要明确
  • 系统提示词里要说明“什么时候用、什么时候不要用”

如果你发现模型总是在某个边界条件下调用错误,优先补充描述、示例和约束,而不是立刻怀疑模型本身。

2. 用 schema 消灭无效状态

例如不要设计这种接口:

toggle_light(on: bool, off: bool)

因为它天然允许无效组合。更好的方式是让 schema 本身表达真实业务约束,比如用枚举或单一明确参数。

3. 已知参数不要再让模型猜

如果某个 order_id 已经由你的业务流程确定,就不要再把它暴露给模型去填。你的代码直接注入就行。

这是函数调用里一个非常重要的原则:能由程序确定的,就不要交给模型推断。

4. 总是顺序调用的能力可以合并

如果 query_location() 后面一定会接 mark_location(),那就考虑合成一个更高层函数,减少模型决策负担。

5. 初始可见工具数量尽量少

官方建议是一个 soft suggestion:单轮开始时尽量少于 20 个工具。工具越多,模型选错的概率越高,token 消耗也越大。

如果工具很多,优先考虑 tool search 延迟加载。

6. 关注 token 成本

函数定义本质上会被注入系统消息,因此:

  • 会占用上下文窗口
  • 会计入输入 token
  • schema 越长,成本越高

如果你碰到 token 限制或成本过高,官方建议:

  • 减少初始加载的工具数量
  • 缩短描述
  • 使用 tool_search
  • 在某些场景下考虑 fine-tuning 提升函数调用准确率

十五、几个容易踩坑的点

最后把文档里最容易被忽略、但在工程里非常关键的点集中列一下。

  1. 模型不会真的执行函数,执行方始终是你的应用。
  2. 一次响应可能包含多个工具调用,不要只按单调用写逻辑。
  3. 严格模式下,schema 不满足约束会直接报错,不是“尽力兼容”。
  4. Responses APIChat Completions API 在严格模式默认行为上并不相同。
  5. 使用 reasoning 模型时,带工具调用的 reasoning items 需要在后续请求中保留。
  6. 并行工具调用并不总是适合生产场景,尤其在有副作用的操作上要谨慎。
  7. 自定义 grammar 越复杂,越容易超出支持范围或导致模型输出语义跑偏。

十六、结语

如果只用一句话总结 Function Calling,那就是:

让模型负责理解意图和组织参数,让应用负责真实执行和状态控制。

这也是把大模型接入真实业务系统时最稳妥的边界划分。

当你把 schema、工具粒度、错误处理、流式聚合和权限边界都设计好以后,函数调用就不只是“让模型会调用工具”,而是可以逐步演进成一套可靠的 AI Agent 执行基础设施。

进一步阅读:

Function Calling 与 MCP 协议|深究 MCP 协议的设计 2026-04-27
多 Agent 系统何时值得做:先从单 Agent 开始 2026-04-27

评论区