结构化输出:让模型交出程序能接住的"标准表格"
昨天(Day 13)学会把模型开得又省又快,但它答的还是"一段自由的话",程序拿到只能干瞪眼。今天教你让模型稳定吐出 JSON,并用 pydantic 当"验收员"校验。这是把"聊天玩具"变成"能接进系统的工程"的关键一步,也为下一步(工具调用、Agent)铺路。
为什么必须要结构化输出
300?凡是"模型的输出要交给代码继续处理"的场景,都需要结构化输出:信息抽取、分类打标、给工具传参数(Day 29 工具调用就靠它)、给前端渲染卡片……可以说这是 Agent 工程的地基技能。
JSON 是什么(60 秒扫盲)
{} 装一组"键:值",几乎所有语言都能读写——它就是程序之间传数据的"普通话"。{
"name": "张三", // 字符串用双引号
"amount": 300, // 数字不用引号
"vip": true, // 布尔值:true / false
"tags": ["新客", "投诉"], // 数组(列表)用方括号
"note": null // 空值用 null
}
几条铁律(模型最容易违反的):键必须用双引号;字符串用双引号(不是单引号);最后一项后面不能有多余逗号;不能写注释(上面的 // 只是讲解用,真 JSON 里不许有)。
import json 就能用 json.loads(文本) 把 JSON 文本变成字典,用 json.dumps(字典) 变回文本。让模型吐 JSON 的几种方式
方式一:prompt 明确要求(哪都能用,但不保证)
# 在指令里把话说死:只要 JSON、给出字段
prompt = """从下面文本抽取信息,只输出 JSON,不要任何多余文字。
字段:name(字符串), amount(数字)。
文本:客户张三今天消费了 300 元。"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0, # 抽取类任务用低温,稳定(回忆 Day 13)
)
print(resp.choices[0].message.content) # 期望:"{\"name\":\"张三\",\"amount\":300}"
方式二:JSON 模式(平台保证输出是合法 JSON)。很多平台提供 response_format={"type": "json_object"} 这类开关,打开后平台会保证返回的是能被解析的 JSON(但字段对不对还得你自己校验,见 L04)。用它时prompt 里仍要说清要哪些字段。
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}, # 开启 JSON 模式
temperature=0,
)
import json
data = json.loads(resp.choices[0].message.content) # 放心解析成字典
print(data["amount"]) # 300
方式三:给出字段定义(schema)。更强的平台支持你直接传一份"字段定义单",让平台按定义约束输出(有的叫 structured outputs / JSON schema)。这和 Day 29「工具调用」用的是同一套机制——见 L05。
用 pydantic 定义字段 + 当验收员
"300"、或漏了 name 字段,我的代码后面还是会崩。谁来把关?先装:pip install pydantic。然后定义"表格":
from pydantic import BaseModel
# 这就是"栏位定义单":一张表该长什么样
class Customer(BaseModel):
name: str # 姓名,必须是字符串
amount: int # 金额,必须是整数
vip: bool = False # 有默认值 => 选填
# 模型吐回来的 JSON 文本(假设已拿到)
raw = '{"name": "张三", "amount": "300"}' # 注意 amount 是字符串"300"
# 验收 + 自动转换类型
c = Customer.model_validate_json(raw) # pydantic 会把 "300" 转成 int 300
print(c.name, c.amount, type(c.amount)) # 张三 300
print(c.vip) # False(用了默认值)
raw = '{"amount": 300}'→
Customer.model_validate_json(raw) 直接抛出校验错误,明确告诉你"缺少字段 name"。你可以
try/except(回忆 Day 05)捕获,然后让模型重答一次——脏数据一步都进不了系统。
Customer.model_json_schema() 会输出一份 JSON schema(描述有哪些字段、类型、哪些必填)。这份东西可以直接喂给"方式三"或工具调用——定义一次,校验和约束两头都用,不重复劳动。
schema 与"工具参数"是同一套东西
比如你有个"查天气"的函数 get_weather(city: str, date: str)。你把它的参数写成一份 schema 交给模型,模型看懂用户说"明天北京冷不冷",就会填出:
{ "city": "北京", "date": "2026-07-13" }
你的代码拿到这份"填好的表",就能真的去调 get_weather("北京", "2026-07-13")。看到没——模型没"运行"任何东西,它只是按栏位填了张表,真正执行的还是你的代码。
👶 小白:那我现在需要把 function calling 学透吗?
👨🏫 老师:不用,今天你只要建立这个认知——"工具调用 = 让模型填一张参数表"。你今天练熟的 pydantic + JSON 校验,正是它的地基。到阶段 5(Day 29~32)我们会专门把工具调用讲透,那时你会发现"原来就是结构化输出换了个用法"。
常见坑与修复套路
| 常见坑 | 修复套路 |
|---|---|
| 前后加废话 / 用 ```json 包起来 | prompt 强调"只输出 JSON";解析前把 ```json 这类标记剥掉 |
| 输出不是合法 JSON | 优先用"JSON 模式";否则 try/except 捕获解析失败 |
| 字段缺失 / 类型错 | 用 pydantic 校验,捕获错误 |
| 答歪了 | 把错误信息回喂给模型,让它重答(最多重试 N 次) |
import json
from pydantic import BaseModel, ValidationError
class Customer(BaseModel):
name: str
amount: int
def extract(text, max_retry=2):
prompt = f"抽取信息,只输出 JSON,字段 name/amount。文本:{text}"
for i in range(max_retry + 1): # 给几次机会
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0, # 稳定,减少重试
)
raw = resp.choices[0].message.content
try:
return Customer.model_validate_json(raw) # 解析+校验一步到位
except (ValidationError, json.JSONDecodeError) as e:
prompt += f"\n上次输出不合格:{e},请修正后重新只输出 JSON。" # 把错误喂回去
raise RuntimeError("多次尝试仍无法得到合格 JSON") # 兜底:老实报错
print(extract("客户李四消费了 520 元")) # name='李四' amount=520
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 为什么要结构化输出?(程序读自由文本太痛苦,JSON 能按字段取值)
- JSON 的几条铁律?(双引号、无尾逗号、无注释)
- 让模型吐 JSON 有哪三档方式?(prompt 要求 / JSON 模式 / schema 约束)
- pydantic 起什么作用?(声明字段 + 校验 + 自动转类型)
- 工具调用和结构化输出什么关系?(工具调用=让模型填一张参数表)
- 让流程稳的四件套是什么?(要求+低温+校验+重试)
✋ 动手(约 10 分钟)
目标:让模型把一句话变成合格的、能被程序取值的 JSON。存成 day14.py(先 pip install pydantic openai)。
from openai import OpenAI
from pydantic import BaseModel
import json
client = OpenAI()
class Order(BaseModel): # 定义你要的"表格"
product: str # 商品名
quantity: int # 数量
urgent: bool = False # 是否加急(选填)
text = "帮我下单 3 台加湿器,要加急"
prompt = f"""从文本抽取订单信息,只输出 JSON,字段:
product(字符串) / quantity(整数) / urgent(布尔)。
文本:{text}"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}, # 开 JSON 模式
temperature=0,
)
raw = resp.choices[0].message.content
order = Order.model_validate_json(raw) # 校验 + 转类型
print("原始JSON:", raw)
print("拿到字段:", order.product, order.quantity, order.urgent)
# 进阶:把定义单打印出来看看
print(json.dumps(Order.model_json_schema(), ensure_ascii=False, indent=2))