结构化输出:让模型稳定输出 JSON
这是什么:结构化输出(structured output) = 让模型吐固定格式的 JSON,而不是一段随手的文字。
为什么用:上一章的程序拿到了一坨通顺的文字——但对程序来说,文字恰恰最难用:想取"姓名"?文字里可没有字段。要让程序接手,就得让模型输出 JSON 这种结构化格式。
如何用:本章先用一段 JSON 看清程序想要的样子,再上"提示词约束 + JSON mode + 解析容错"三招把模型管住。
为什么要 JSON
人读文字舒服,程序读结构舒服。比如从快递信息里抽姓名、城市、电话,程序真正想要的是:
{"name": "王小明", "city": "杭州", "phone": "13800001111"}
拿到它,data["city"] 直接取值,不用任何"阅读理解"。
三招管教模型
第一招:提示词约束。 白纸黑字写清"只输出 JSON、字段是什么、别的都不要"——简单有效,但模型偶尔会多嘴两句。
第二招:JSON mode。 主流兼容服务都支持加参数 response_format={"type": "json_object"},由服务端保证输出是合法 JSON(注意:提示词里必须出现"JSON"字样)。
第三招:解析加容错。 json.loads 前先剥掉可能的 ```json 代码栅栏,解析失败给兜底值,程序绝不崩。
完整代码
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
)
prompt = """从文本中抽取收件人姓名、城市、电话,只输出 JSON,
字段为 name、city、phone,不要输出任何其他内容。
文本:麻烦送到杭州市西湖区,收件人王小明,电话 13800001111。"""
resp = client.chat.completions.create(
model="gpt-4o-mini", # 换成你所选服务的模型名
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}, # JSON mode
)
raw = resp.choices[0].message.content.strip()
if raw.startswith("```"): # 容错:剥掉代码栅栏
raw = raw.strip("`").removeprefix("json").strip()
try:
data = json.loads(raw) # JSON 字符串 → 字典
except json.JSONDecodeError:
data = {"name": "", "city": "", "phone": ""} # 解析失败的兜底
print(data["name"], data["city"])
你需要自备 API key 才能实际运行,你会看到(示意):
王小明 杭州
| 手段 | 干什么 |
|---|---|
| 提示词约束 | 说清字段,要求"只输出 JSON" |
response_format |
服务端保证输出是合法 JSON |
json.loads |
把 JSON 字符串变成 Python 字典 |
try/except |
解析失败给兜底,程序不崩 |
记忆口诀:先说清字段,再开 JSON 模式;loads 前剥栅栏,失败给兜底。
更复杂的例子:抽取订单信息
有时候需要抽取多个字段,甚至嵌套结构:
prompt = """从下面的订单文本中抽取信息,只输出 JSON,字段如下:
- order_id: 订单编号(字符串)
- customer: 客户姓名(字符串)
- items: 商品列表,每个商品有 name(名称)、qty(数量)、price(单价)
- total: 订单总金额(数字)
文本:订单ORD-20240520-001,客户张三,购买了笔记本电脑 1 台(单价5500元),鼠标 2 个(单价89元),共计5678元。"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
raw = resp.choices[0].message.content.strip()
if raw.startswith("```"):
raw = raw.strip("`").removeprefix("json").strip()
try:
data = json.loads(raw)
except json.JSONDecodeError:
data = {"order_id": "", "customer": "", "items": [], "total": 0}
print(data["customer"], data["total"])
期望输出(示意):
{"order_id": "ORD-20240520-001", "customer": "张三", "items": [{"name": "笔记本电脑", "qty": 1, "price": 5500}, {"name": "鼠标", "qty": 2, "price": 89}], "total": 5678}
❓ 常见问题
Q1:提示词里说了"只输出 JSON",模型还是有时候输出多余文字怎么办?
开启 response_format={"type": "json_object"},服务端会强制只输出合法 JSON;同时保留 try/except 容错,万一解析失败有兜底。
Q2:json.loads 报 JSONDecodeError 常见原因?
- 模型输出了
```json ... ```代码栅栏,没剥掉 - 模型输出了中文逗号、中文引号
- 模型多输出了一句"这里是结果:"
修正:在 json.loads 前加剥栅栏代码,见上面的完整示例。
Q3:字段名模型拼错了(如 Name 而不是 name)?
提示词里明确列出字段名,并强调"字段名必须完全一致"。必要的话,在代码里做字段名映射容错。
⚠️ 新手翻车现场
翻车一:直接 json.loads 模型原文,不剥代码栅栏
# 错误:模型输出了 ```json {...} ```,json.loads 直接报错
data = json.loads(resp.choices[0].message.content)
问题:json.loads 只认纯 JSON 字符串,认不得 Markdown 代码栅栏。
修正:先判断是否以 ` ```` 开头,剥掉后再解析,见完整代码。
翻车二:忘记设置 response_format,只靠提示词
# 模型可能偶尔输出合法 JSON,但不保证每次都对
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
# 缺少 response_format={"type": "json_object"}
)
问题:没有 JSON mode 时,模型只是"尽量"输出 JSON,稍有偏差就解析失败。
修正:加上 response_format={"type": "json_object"},服务端兜底保证格式。
🎯 什么时候用结构化输出
- 需要把模型回答接到后续程序(如入库、发邮件、调另一个 API)
- 抽取信息(发票识别、简历解析、订单提取)
- 需要稳定、可机器读取的格式,不想写复杂的正则去"阅读理解"模型输出
📝 本章小结
- 程序不喜欢读自然语言,喜欢读 JSON
- 三招:提示词约束字段、
response_format保证合法 JSON、try/except容错兜底 json.loads前记得剥掉```json代码栅栏- 记忆口诀:先说清字段,再开 JSON 模式;loads 前剥栅栏,失败给兜底。
动手改
- 加一个
address字段,把"西湖区"这类地址信息也抽出来 - 去掉 JSON mode,只靠提示词连跑 5 次,统计几次能被
json.loads成功解析 - 把"解析失败给兜底"改成"把模型原话塞回对话,让它重新输出 JSON"
下一章:综合练习,做一个能记住你名字的问答机器人 🐸