结构化输出是按照你事先定好的形式给出的回答:一个带命名键的 JSON 对象、一张列固定的表格,或一个每次标题都相同的模板。只要是程序读取回答,你就需要它;即使是人来读,它也有帮助,因为每个回答看起来都一样,便于浏览和比较。
诀窍在于把形式描述得足够精确,让模型没有任何可以自己选择的余地。单单一个“JSON”并不算描述。下面的提示词从一份 bug 报告中提取信息。对比一下只要求“关键信息”和把 schema 写清楚的两种请求。
以下是这份 bug 报告中的关键信息:
- 问题: 导出项目时应用崩溃
- 触发条件: 在超过 50 张照片的项目中点击“导出”
- 开始时间: 最近一次更新之后
- 平台: Android 14
- 应用版本: 3.2.0
- 影响: 高,因为耽误了给客户的交付
较小的项目似乎可以正常导出。
自由文本的回答准确又好读,但每次运行用的标签都不一样,严重程度是一句话而不是一个值,程序也只能去猜每个字段从哪里开始。JSON 的回答则可以直接放进 bug 跟踪系统。注意,这个回复仍然被包在代码块里:聊天应用通常会这样包装 JSON,在代码中解析时,这一点很重要。
如何要求返回 JSON
一个好的 JSON 请求,会把模型原本要替你回答的每个问题都先回答掉:
- 每个键,拼写准确。 把键名按必须出现的样子写在引号里。写明“键必须正好是这些”,这样模型就不会额外添加键。
- 每个值的类型。 字符串、数字、布尔值、字符串数组、嵌套对象。
- 允许的值,适用于取值固定的字段,比如严重程度或类别。没有这个列表,四次运行你可能会得到 "High"、"high"、"severe" 和 "P1"。
- 输入中缺少某个值时怎么办。 如果你不说“没有提到则为 null”,模型往往会用一个看似合理的猜测来填补空缺,而猜出来的应用版本和真实的看起来一模一样。
- 前后什么都不要。 “只返回 JSON,前后不要有任何文字”可以去掉那句友好的开场白。
当结构是嵌套的或不常见时,展示一个完整的示例对象比用文字描述效果更好。这就是把少样本提示用在格式上。让示例中的值和真实输入明显不同,以免模型照抄。
一个可复用的信息提取提示词
下面这个提示词是同样的请求,只是拆成了几个部分。关掉格式部分,模型仍然会返回 JSON,因为约束部分要求了这一点,但它会自己选择键名,比如用 title 而不是 job_title,于是依赖你的键名的代码就会出错。约束部分则防止它把只写了一半的电话号码补全,或根据邮箱域名推测公司名称。把一个真实的邮件签名粘贴到输入框里试试。
{
"name": "string",
"job_title": "string or null",
"company": "string or null",
"email": "string or null",
"phone": "string or null"
}{
"name": "Priya Nair",
"job_title": "Head of Data",
"company": "Northwind Labs",
"email": "priya.nair@northwind.example",
"phone": null
}
表格和固定模板
结构化输出不只是给程序用的。当你自己阅读回答时,markdown 表格或固定模板同样有好处:在看之前,你就已经知道每条信息会在哪里。
| 类型 | 有序 | 可变 | 允许重复 | 典型用途 |
|---|---|---|---|---|
| list | 是 | 是 | 是 | 需要添加、删除或排序的序列 |
| tuple | 是 | 否 | 是 | 一组固定的值,比如坐标 |
| set | 否 | 是 | 否 | 去重和快速判断成员 |
对于较长的文本,模板的用法也一样:按顺序给出各个标题,并说明每个标题下写什么。“用三个加粗的标签回答:原因、修复方法、如何检查”每次都会产生同样的三个标签,一批回答比较起来就很方便。
API 中的 JSON 模式
一些模型 API 提供了强制输出语法正确的 JSON 的设置。在 OpenAI Python SDK 中,这个设置是 response_format。JSON 模式要求你的消息中某处出现“JSON”这个词,所以下面的系统提示词提到了它,并列出了各个键。
import json
from openai import OpenAI
client = OpenAI()
MODEL = "your-model-id" # e.g. from your provider's model list
report_text = "Since yesterday's update the app crashes when I tap Export..."
response = client.chat.completions.create(
model=MODEL,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": (
"Extract the bug report into JSON with the keys "
"summary (string), severity (one of low, medium, high, critical) "
"and steps_to_reproduce (array of strings)."
),
},
{"role": "user", "content": report_text},
],
)
data = json.loads(response.choices[0].message.content)
JSON 模式能确保文本可以被解析(除非回复在 token 上限处被截断),但不能确保它符合你的 schema。键仍然可能缺失,严重程度也仍然可能是 "urgent"。有些提供商还接受完整的 JSON Schema,可以作为输出格式,也可以作为工具(函数)定义的输入 schema,其中一些模式会把回答约束在 schema 之内。具体参数请查阅你所用提供商的文档,因为不同 API 的这些功能各不相同。
在代码中校验结果
像对待程序外部的任何输入一样对待模型返回的 JSON:先解析,再检查。解析能发现语法错误,检查能发现内容不对的有效对象。
ALLOWED_SEVERITIES = {"low", "medium", "high", "critical"}
def problems(data):
if not isinstance(data, dict):
return ["the answer must be a JSON object"]
found = []
if not isinstance(data.get("summary"), str):
found.append("summary must be a string")
if data.get("severity") not in ALLOWED_SEVERITIES:
found.append("severity must be low, medium, high or critical")
steps = data.get("steps_to_reproduce")
if not isinstance(steps, list) or not all(isinstance(s, str) for s in steps):
found.append("steps_to_reproduce must be an array of strings")
return found
检查不通过时,重试一次往往就能解决:把模型自己的输出连同问题列表一起发给它,要求它给出修正后的 JSON。限制重试次数,并记录失败情况,因为一个经常出错的字段说明提示词写得不清楚。在较大的项目中,可以用 Pydantic 这样的校验库或 JSON Schema 校验器来代替手写的函数。
还有两个习惯可以防止悄无声息的错误。把输出 token 上限设得足够大,能容纳你预期的最长回答,因为被截断的对象永远无法解析。当输入很长或来自用户时,用分隔符或 XML 标签把它和你的指令隔开,这样输入里的文字就不太容易被当作指令。如果一个提示词既要推理又要输出 JSON,可以考虑提示词链:让第一步用自由文本思考,第二步再把结果转换成结构化格式。
常见问题
怎样让 ChatGPT 输出 JSON?
说明答案必须是 JSON,列出每个键及其类型,对取值固定的字段给出允许的值。再加一句“只返回 JSON,前后不要有任何文字。”在 API 中,还要用 response_format={"type": "json_object"} 开启 JSON 模式,这要求你的消息中出现 JSON 这个词。
为什么模型会在 JSON 前后加上文字?
聊天模型被训练得善于对话,所以经常以“这是 JSON”之类的句子开头,或者把对象包在 markdown 代码块里。要求它只返回 JSON;在代码中,要么使用 API 的 JSON 模式,要么在解析之前去掉外面的代码块标记。
JSON 模式是什么?
JSON 模式是一项 API 设置,只要回复没有被输出 token 上限截断,它就能让模型生成语法正确的 JSON。但它不能让模型遵循你的 schema:键仍然可能缺失、拼错或类型不对。有些提供商还提供更严格的模式,可以接收完整的 JSON Schema,并把输出约束在其中。
大模型能保证总是返回有效的 JSON 吗?
只靠提示词做不到。即使指令很清楚,也会偶尔失败,而被输出 token 上限截断的回答一定是无效的。用真正的 JSON 解析器解析每一个响应,检查你需要的字段,检查不通过时就重试,或者明确地报错。