🐸 青蛙编程网
首页 / AI / LLM / 4. 结构化输出

结构化输出:让模型稳定输出 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 前剥栅栏,失败给兜底。

动手改

  1. 加一个 address 字段,把"西湖区"这类地址信息也抽出来
  2. 去掉 JSON mode,只靠提示词连跑 5 次,统计几次能被 json.loads 成功解析
  3. 把"解析失败给兜底"改成"把模型原话塞回对话,让它重新输出 JSON"

下一章:综合练习,做一个能记住你名字的问答机器人 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。