GPT-5 Function Calling 完全指南:从 JSON Schema 到自由格式工具

本文是一份关于 GPT-5 Function Calling 的实践指南。我们深入探讨了从传统的 JSON Schema 函数到创新的自由格式工具、Lark/CFG 语法约束、工具白名单和 Preamble 等新特性,帮助开发者构建更强大的 AI Agent。

阅读时长: 8 分钟
共 3737字
作者: eimoon.com

Function Calling(函数调用)这个概念并不新鲜,但 GPT-5 对它的实现感觉完全是另一个层次了。它不再仅仅是让模型调用外部函数,而是提供了一整套构建可靠、自主的 Agentic 系统的工具箱。这次更新涵盖了从自由格式工具(不再局限于 JSON)、Lark/CFG 语法约束,到工具白名单和调用前释义(Preamble),这些功能组合在一起,让 GPT-5 成为了一个真正意义上的 Agentic 模型。

在这篇指南里,我会带大家逐一拆解这些新特性,通过具体的代码示例,讲清楚它们是什么、怎么用,以及什么时候该用。

Function Calling 的核心演进

简单来说,Function Calling 就是让大模型能够与你的应用程序、API 或数据库进行交互。模型负责理解用户意图并决定何时调用哪个工具,而你负责执行具体的业务逻辑,并将结果返回给模型,最终生成一个有事实依据的、可靠的回答,甚至完成一系列自动化工作流。

GPT-5 的函数调用主要分为两类:

  1. 函数工具 (Function Tools):基于 JSON Schema,提供结构化的输入和输出,保证了调用的精确性。这是我们比较熟悉的方式。
  2. 自定义工具 (Custom Tools):支持自由格式的文本,比如直接生成 SQL、代码片段或配置文件。这给了我们极大的灵活性。

整个工作流程看起来是这样的:

  1. 你向模型发送一个 prompt,同时声明所有可用的工具。
  2. 模型分析后,决定调用一个或多个工具,并生成所需的参数(结构化或自由文本)。
  3. 你的应用程序接收到调用请求,执行相应的本地函数或 API。
  4. 将执行结果返回给模型。
  5. 模型根据返回结果,生成最终的自然语言回答,或者继续调用其他工具。

Function Calling 工作流程

那么,什么时候该用哪种工具呢?

  • 当你需要严格的输入验证、类型检查和可预测的参数时,函数工具是首选。
  • 当你的后端需要接收原始文本(如 SQL 查询、Shell 脚本)或者你想快速迭代、不想被 Schema 束缚时,**自定义工具(自由格式)**就派上用场了。

函数工具 (JSON Schema):结构化数据的可靠保障

这是最经典的方式。通过定义 JSON Schema,我们能确保模型生成格式正确、类型安全的参数,这对于需要稳定对接后端 API 的场景至关重要。

我们来看一个简单的例子:模型根据用户的请求,选择合适的工具(制作咖啡或提供一个咖啡小知识),然后你的代码来执行它。

1. 环境准备

首先,确保你已经安装了 OpenAI 的 Python SDK,并设置好了环境变量 OPENAI_API_KEY

pip install openai

2. 定义工具

你需要提供一个工具列表,每个工具都包含名称、描述和用于参数的 JSON Schema。

  • make_coffee:需要一个名为 coffee_type 的字符串参数。
  • random_coffee_fact:不需要任何参数。
import os
from openai import OpenAI
import json

client = OpenAI(api_key = os.environ["OPENAI_API_KEY"])

tools = [
    {
        "type": "function",
        "name": "make_coffee",
        "description": "提供一个制作咖啡的简单配方。",
        "parameters": {
            "type": "object",
            "properties": {
                "coffee_type": {
                    "type": "string",
                    "description": "咖啡的种类, 例如 espresso, cappuccino, latte"
                }
            },
            "required": ["coffee_type"],
        },
    },
    {
        "type": "function",
        "name": "random_coffee_fact",
        "description": "返回一个关于咖啡的有趣事实。",
        "parameters": {"type": "object","properties":{}}
    }
]

3. 实现工具逻辑

接着,在你的代码里实现这些函数。

def make_coffee(coffee_type):
    recipes = {
        "espresso": "细研磨, 18g 咖啡粉 → 约 28 秒萃取 36g 浓缩咖啡。",
        "cappuccino": "制作一份浓缩咖啡,打发 150ml 牛奶,倒入并铺上奶泡。",
        "latte": "制作一份浓缩咖啡,打发 250ml 牛奶,倒入以获得丝滑口感。",
    }
    return {"coffee_type": coffee_type, "recipe": recipes.get(coffee_type.lower(), "未知的咖啡类型!")}

def random_coffee_fact():
    return {"fact": "咖啡是世界上交易量第二大的商品,仅次于石油。"}

4. 发起对话与工具调用

向模型提问,模型会返回一个包含 function_call 的响应。

# 追踪已使用的工具
used_tools = []
messages = [{"role": "user", "content": "我该怎么做一杯拿铁?"}]

response = client.chat.completions.create(
    model="gpt-5-turbo", # 假设模型名称,请使用实际可用模型
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

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

5. 执行工具并返回结果

检查模型的响应,如果包含工具调用,就执行相应的函数,并将结果以特定格式追加到消息历史中。

tool_calls = response_message.tool_calls
if tool_calls:
    available_functions = {
        "make_coffee": make_coffee,
        "random_coffee_fact": random_coffee_fact,
    }
    for tool_call in tool_calls:
        function_name = tool_call.function.name
        used_tools.append(function_name)
        function_to_call = available_functions[function_name]
        function_args = json.loads(tool_call.function.arguments)
        
        if function_name == "make_coffee":
            function_response = function_to_call(coffee_type=function_args.get("coffee_type"))
        else:
            function_response = function_to_call()
            
        messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": json.dumps(function_response),
            }
        )

6. 生成最终答案

再次调用模型,这次它会根据工具返回的结果生成最终的自然语言回答。

final_response = client.chat.completions.create(
    model="gpt-5-turbo",
    messages=messages,
)

print("最终输出:\n", final_response.choices[0].message.content)

print("\n--- 工具使用情况 ---")
for t in tools:
    status = "已使用 ✅" if t["name"] in used_tools else "未使用 ❌"
    print(f"{t['name']}: {status}")

# 预期输出:
# 最终输出:
#  制作拿铁,你需要先制作一份浓缩咖啡,然后打发 250ml 牛奶,并将其倒入以获得丝滑的口感。
# 
# --- 工具使用情况 ---
# make_coffee: 已使用 ✅
# random_coffee_fact: 未使用 ❌

自定义工具 (自由格式):挣脱 JSON 的束缚

这是 GPT-5 的一个重大升级。它允许模型直接输出原始文本,比如一段 SQL、一个 Shell 命令,或者任何自定义格式的字符串,而不需要强制封装在 JSON 里。这在与各种命令行工具、解释器或查询引擎集成时非常有用。

下面的例子里,模型会根据用户提供的食材,生成一个逗号分隔的字符串,然后我们的 Python 函数会用这个字符串来建议一道菜。

import random

# --- 自定义工具定义 ---
tools = [
    {
        "type": "custom", # 注意类型是 custom
        "name": "meal_planner",
        "description": "仅接收一个逗号分隔的食材列表,并建议一道菜。"
    }
]

# --- 本地函数实现 ---
def plan_meal(ingredients: str) -> str:
    ideas = [
        f"快手小炒: 将 {ingredients} 与蒜蓉和酱油一起翻炒。",
        f"一锅烩饭: 将 {ingredients} 放入肉汤中一同煮至松软。",
        f"营养汤: 将 {ingredients} 放入高汤中与香草一起慢炖。",
    ]
    return random.choice(ideas)

# --- 对话开始 ---
messages = [{"role": "user", "content": "我只有鸡肉、米饭和西兰花,有什么晚餐建议吗?"}]

# 1) 首次调用,模型会生成工具所需的自由格式文本
# (此处的 API 调用方式为示意,请参考 OpenAI 官方文档对 custom tool 的具体实现)
# 假设模型返回了一个 custom_tool_call,其 input 字段为 "chicken, rice, broccoli"
ingredients_csv = "chicken, rice, broccoli" 

# 2) 执行本地函数
meal_result = plan_meal(ingredients_csv)

# 3) 将结果返回给模型,并要求它生成菜谱
messages.append({"role": "tool_output", "content": meal_result}) # 示意
messages.append({"role": "system", "content": "请把这个想法变成一个包含 3-4 个步骤的简短菜谱。"})

# 4) 最终调用,生成菜谱
# ... 再次调用 API ...

# 预期结果
print(f"--- 工具调用输入 ---\n{ingredients_csv}")
print(f"--- 工具返回结果 --- \n{meal_result}")
print("\n--- 最终菜谱 ---\n")
print("一锅鸡肉西兰花烩饭\n- 将鸡块用盐和胡椒调味,在锅中用少许油煎至微黄。\n- 加入1杯洗净的米和2杯肉汤。煮沸后盖上锅盖,小火慢煮12分钟。\n- 将西兰花小朵撒在上面,盖上锅盖再煮5-7分钟,直到米饭松软,西兰花变软。\n- 关火后静置5分钟,搅松即可。")

语法约束 (Lark):强制输出的格式精准无误

这是另一个杀手级功能。GPT-5 支持通过上下文无关文法(CFG)来约束模型的输出格式。你可以用 Lark 这样的语法定义工具,确保模型输出的的格式永远不会出错。这对于需要生成 SQL 查询、数学表达式或其他领域特定语言(DSL)的场景来说,可靠性大大提高了。

下面的例子,我们定义一个只接受标准算术表达式的工具。

# --- 本地求值函数 ---
def eval_expression(expr: str) -> str:
    try:
        # 安全地求值
        result = eval(expr, {"__builtins__": {}}, {})
        return f"{expr} = {result}"
    except Exception as e:
        return f"表达式求值错误: {e}"

# --- 带语法约束的自定义工具 ---
tools = [
    {
        "type": "custom",
        "name": "math_solver",
        "description": "通过输出一个有效的算术表达式来解决数学问题。",
        "format": {
            "type": "grammar",
            "syntax": "lark",
            "definition": r"""
start: expr
?expr: term | expr "+" term -> add | expr "-" term -> sub
?term: factor | term "*" factor -> mul | term "/" factor -> div
?factor: NUMBER | "(" expr ")"
%import common.NUMBER
%ignore " "
"""
        },
    }
]

# --- 用户提问 ---
msgs = [{"role": "user", "content": "计算 (12 + 8) * 3 减去 5 除以 5 的结果是多少?"}]

# 1) 模型首次调用,必须生成符合 Lark 语法的表达式
# ... 调用 API ...
# 假设模型返回的工具输入是:"(12 + 8) * 3 - 5 / 5"
expr = "(12 + 8) * 3 - 5 / 5"
print(f"\n=== 语法约束的表达式 ===\n{expr}")

# 2) 本地执行求值
tool_result = eval_expression(expr)
print(f"\n=== 工具执行结果 ===\n{tool_result}")

# 3) 将结果返回给模型
# ... 再次调用 API,并附上 tool_result ...

# 4) 模型生成最终自然语言答案
print("\n=== 最终输出 ===\n计算结果是 59.0。")

通过这种方式,我们再也不用担心模型输出一些奇奇怪怪的、无法解析的字符串了。

工具白名单:安全可控的保障

当你的应用中定义了大量工具时,allowed_tools 参数就显得尤为重要。它允许你将本次对话中模型可用的工具限制在一个安全的子集内,从而提高可预测性,防止模型调用意料之外的、甚至有风险的工具。

这个功能在构建需要严格控制行为的 Agent 时非常关键。

Preamble:让模型"思考出声"

Preamble(前言或释义)是一个很有意思的功能。你可以通过一个简单的指令,比如“在调用工具前,请解释你为什么要调用它”,让模型在发起工具调用前,先生成一段简短的解释。

这极大地增强了工作流的透明度,方便我们理解模型的“思考过程”,也让调试变得更加容易。

来看个例子:

# --- 系统指令,要求模型输出 Preamble ---
messages = [
    {"role": "system", "content": "在你调用工具之前,用一个以 'Preamble:' 开头的简短句子解释你为什么要调用它。"},
    {"role": "user", "content": "我头疼该吃什么药?"}
]

# ... 完整的工具定义和实现逻辑 ...

# --- 模型的首次响应会包含 Preamble 和工具调用 ---
# 预期模型输出会类似这样:
# Message: "Preamble: 我将调用 medical_advice 工具来为头痛症状提供安全的非处方药建议。"
# Tool Call: medical_advice(symptom="headache")

# 你的应用接下来执行工具调用,并返回结果...
# 最后模型会生成最终的建议。

这个小功能对于提升用户信任和应用的可观察性非常有帮助。

总结

GPT-5 的 Function Calling 功能套件,已经远不止是简单的 API 调用了。它为我们提供了一整套用于构建更健壮、更智能、更可控的 AI 应用的积木。从结构化的 JSON 到灵活的自由文本,再到语法约束和安全控制,这种转变才是其真正强大的地方。

我个人认为,掌握这些工具,是未来开发复杂 AI Agent 的基础。希望这篇指南能帮到你。

关于

关注我获取更多资讯

公众号
📢 公众号
个人号
💬 个人号
使用 Hugo 构建
主题 StackJimmy 设计