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

_

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

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

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

  • 什么是工具、工具调用、工具结果
  • 一次完整的函数调用工作流怎么跑通
  • 如何定义函数 schema,如何让输出更稳定
  • Responses API 与 Chat 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_id 或 tool_call_id 进行绑定。

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

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

  • function:基于 JSON Schema 定义输入参数,适合结构化调用
  • custom:输入输出可以是自由文本,更适合代码片段、受 grammar 约束的文本等场景
  • 内建工具:OpenAI 平台自带的一类工具,例如 web search、code interpreter、remote 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-5 或 o4-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 进行分组,例如 crm、billing、shipping:

{
  "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 API 与 Chat Completions API 在严格模式默认行为上并不相同。
  5. 使用 reasoning 模型时,带工具调用的 reasoning items 需要在后续请求中保留。
  6. 并行工具调用并不总是适合生产场景,尤其在有副作用的操作上要谨慎。
  7. 自定义 grammar 越复杂,越容易超出支持范围或导致模型输出语义跑偏。

十六、结语

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

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

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

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

进一步阅读:

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

评论区

© 2026 Rex的博客