OpenAI 工具调用与 JSON 输出的进化史:从 Prompt 黑魔法到原生支持

前言

在使用大语言模型构建应用时,两个需求几乎无处不在:

  1. 让模型调用外部工具/函数(比如查数据库、调 API、发邮件)
  2. 让模型输出结构化的 JSON(方便程序解析和处理)

如今在 OpenAI 的 API 中,这两个需求都有了非常成熟的原生支持。但在早期,开发者们可没这么幸运——我们只能靠 Prompt Engineering(提示词工程) 这种”黑魔法”来碰运气。

这篇文章带你回顾一下这段进化史。


第一阶段:纯 Prompt 时代 —— 在黑暗中摸索

在 GPT-3.5 / GPT-4 刚出来的年代,API 根本没有什么”工具调用”或”结构化输出”的概念。模型就是一个黑箱:你给它文字,它返回文字。

工具调用?全靠 Prompt 描述

如果你想做一个能查天气的 AI 助手,你大概会这样写 system prompt:

1
2
3
4
5
6
7
8
你是一个助手。当用户询问天气时,你需要输出一个 JSON 来调用函数。
可用的函数:
- get_weather(location: string, unit: "celsius" | "fahrenheit")

当你想调用函数时,必须严格按以下格式输出:
{"function": "get_weather", "arguments": {"location": "...", "unit": "..."}}

如果你不想调用函数,就正常回复用户。

然后你的代码就得:

  1. 解析模型返回的文本
  2. 用正则或者 JSON.parse 试图提取函数调用
  3. 如果解析失败……重试、换措辞、祈祷 🙏

JSON 输出?更是噩梦

想让模型稳定输出 JSON 就更痛苦了。你能做的只有:

  • 在 prompt 里反复强调”你必须返回合法的 JSON”
  • 给出一两个示例(few-shot)
  • 在 prompt 结尾加上 { 来诱导模型开始输出 JSON
  • 用各种后处理手段修复模型吐出来的”近似 JSON”

即便如此,你还是会经常遇到:

  • 模型在 JSON 前后加了一堆废话:好的,以下是结果:{...}
  • 多了个 trailing comma:{"name": "Tom",}
  • 缺了引号:{name: "Tom"}
  • 凭空编造字段、漏掉 required 字段
  • JSON 嵌套超过一定深度就开始放飞自我

那时候搞 LLM 应用的开发者,代码里最长的部分往往不是业务逻辑,而是解析和容错逻辑


第二阶段:JSON Mode —— 一道曙光

OpenAI 后来推出了 JSON Mode,通过在请求中设置:

1
2
3
{
"response_format": { "type": "json_object" }
}

这让模型保证输出合法的 JSON(不再是”尽力而为”)。

这已经是一个巨大的进步——至少你不用再处理 trailing commas 和缺引号的问题了。但它有一个关键局限:

JSON Mode 只保证合法的 JSON 格式,不保证符合你指定的 Schema。

也就是说,模型输出的确实是合法 JSON,但它依然可能:

  • 漏掉你需要的字段
  • 多出你不知道的字段
  • 字段类型不对(该是 number 的给了 string)
  • 枚举值超出范围

你还是需要在代码里做一层校验。


第三阶段:原生工具调用 + Structured Outputs —— 真正的银弹

这才是真正革命性的变化。OpenAI 在 2024 年 8 月前后推出了 Structured Outputs,同时工具调用也进入了原生 + strict mode 时代。

原生工具调用(Function Calling)

现在的工具调用不再是往 prompt 里塞描述文字了,而是通过 tools 参数结构化的方式声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
tools = [
{
"type": "function",
"name": "get_weather",
"description": "获取指定地点的天气",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和国家,例如:北京, 中国"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location", "unit"],
"additionalProperties": False
}
}
]

response = client.responses.create(
model="gpt-4o",
input=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools
)

模型返回的不再是需要你手动解析的文本,而是结构化的 function_call 对象,带有 call_idname 和准确的 arguments JSON。

而你执行完函数后,也只需返回结构化的 function_call_output,整个流程干净、可靠。

Strict modestrict: true)更是确保了模型传回的参数严格遵循你定义的 Schema,不会多字段、不会少字段、类型也对得上。

详细介绍:OpenAI Tools Guide

结构化输出(Structured Outputs)

对于 JSON 输出的需求,Structured Outputs 是 JSON Mode 的全面进化版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
response = client.responses.create(
model="gpt-4o-2024-08-06",
input=[{"role": "user", "content": "8x + 7 = -23 怎么解?"}],
text={
"format": {
"type": "json_schema",
"name": "math_response",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"explanation": {"type": "string"},
"output": {"type": "string"}
},
"required": ["explanation", "output"],
"additionalProperties": False
}
},
"final_answer": {"type": "string"}
},
"required": ["steps", "final_answer"],
"additionalProperties": False
},
"strict": True
}
}
)

这个方案的核心优势:

JSON Mode Structured Outputs
输出合法 JSON
严格符合 Schema
安全拒绝可被程序检测
支持模型 gpt-3.5-turbo 及更新 gpt-4o-2024-08-06 及更新

有了 Structured Outputs,你不再需要:

  • ❌ 在 prompt 里反复强调输出格式
  • ❌ 写各种 JSON 修复/容错代码
  • ❌ 处理字段缺失或类型错误
  • ❌ 担心枚举值越界

模型会保证输出的 JSON 100% 符合你给的 Schema

详细介绍:OpenAI Structured Outputs Guide


总结

阶段 工具调用 JSON 输出 可靠性
早期(~2023) Prompt 描述 + 文本解析 Prompt 约束 + 后处理修复 😰 经常出幺蛾子
JSON Mode Prompt 描述 + 文本解析 json_object 保证合法 JSON 😐 格式合法,Schema 不保证
现在 原生 tools + strict mode json_schema + Structured Outputs 😎 开箱即用,严格可靠

回顾这段进化史,最大的感受是:大模型应用开发正在从”玄学”走向”工程化”。

以前我们不得不花大量精力在 prompt tuning 和 defensive parsing 上,和模型斗智斗勇;而现在,OpenAI 把这些能力做成了可靠的 API 原语,让我们可以把注意力放回业务逻辑本身。

如果你正在构建 LLM 应用,强烈建议直接上 原生工具调用 + Structured Outputs,别再回到 prompt 手工解析的黑暗时代了。