Day 14 / 共 68 天 · 阶段2 大模型基础

结构化输出:让模型交出程序能接住的"标准表格"

昨天(Day 13)学会把模型开得又省又快,但它答的还是"一段自由的话",程序拿到只能干瞪眼。今天教你让模型稳定吐出 JSON,并用 pydantic 当"验收员"校验。这是把"聊天玩具"变成"能接进系统的工程"的关键一步,也为下一步(工具调用、Agent)铺路。

📍 你在阶段 2「大模型基础」里的位置
D10 模型是什么 D11 调 API D12 流式输出 D13 参数/成本/延迟 D14 结构化输出 D15 多模型&路由
💡 今天用「填表格 / 报关单」这套世界观兜住全场 把模型的输出想成两种:一种是自由作文(想怎么写怎么写,人看着舒服,机器读着崩溃),一种是标准表格/报关单(每个栏位固定:姓名、金额、类别)。JSON就是那张标准表格,程序照着栏位就能取值;schema(模式)=表格的栏位定义单(哪几栏、什么类型、必填还是选填);pydantic=盖章前的验收员,栏位缺了、类型填错,当场打回。今天的目标就一句话:逼模型别写作文,乖乖填表;填完还得过验收。
L01

为什么必须要结构化输出

🤔 痛点你让模型"提取这段话里的姓名和金额",它回你"这位客户叫张三,消费了大概三百块左右哦~"。这话人能懂,可你后面的代码要拿金额去数据库记账,怎么从这句里可靠地抠出 300
💡 本质(一句话)人读自由文本很轻松,程序读自由文本极痛苦;让模型输出固定格式(JSON),下游代码才能稳定地按字段取值——这就是逼它"填表"而不是"写作文"。
同一件事:自由作文 vs 标准表格 ❌ 自由作文 "这位客户叫张三, 消费了大概三百块左右哦~" 程序:金额到底是多少? "三百""左右""哦" 全是噪声 → 只能写脆弱的正则去猜 ✅ 标准表格 (JSON) {"name": "张三", "amount": 300} 程序:data["amount"] → 直接拿到 300,稳
图注:结构化输出把"读懂人话"的难题,换成了"按字段取值"的简单事。

凡是"模型的输出要交给代码继续处理"的场景,都需要结构化输出:信息抽取、分类打标、给工具传参数(Day 29 工具调用就靠它)、给前端渲染卡片……可以说这是 Agent 工程的地基技能

L02

JSON 是什么(60 秒扫盲)

🤔 痛点老听到 JSON,到底长啥样?和 Python 的字典是一回事吗?
💡 本质(一句话)JSON 就是一种大家都认的"表格文本格式":用 {} 装一组"键:值",几乎所有语言都能读写——它就是程序之间传数据的"普通话"。
{
  "name": "张三",           // 字符串用双引号
  "amount": 300,            // 数字不用引号
  "vip": true,              // 布尔值:true / false
  "tags": ["新客", "投诉"],   // 数组(列表)用方括号
  "note": null              // 空值用 null
}

几条铁律(模型最容易违反的):键必须用双引号字符串用双引号(不是单引号)最后一项后面不能有多余逗号不能写注释(上面的 // 只是讲解用,真 JSON 里不许有)。

👶 一句话记住JSON 长得几乎和 Python 字典一样,Python 里 import json 就能用 json.loads(文本) 把 JSON 文本变成字典,用 json.dumps(字典) 变回文本。
L03

让模型吐 JSON 的几种方式

🤔 痛点怎么才能让模型老老实实只给 JSON,别在前面加"好的,这是您要的结果:",别用 ```json 包起来?
💡 本质(一句话)从"靠嘴求"到"平台硬保证"有三档:①在 prompt 里明确要求 → ②打开 JSON 模式 → ③直接给出字段定义(schema)让平台约束,越往后越可靠。

方式一: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 校验"这套组合,稳妥且到处能用。
L04

用 pydantic 定义字段 + 当验收员

🤔 痛点就算模型给了 JSON,万一它把 amount 写成字符串 "300"、或漏了 name 字段,我的代码后面还是会崩。谁来把关?
💡 本质(一句话)pydantic 让你用一个 Python 类声明"这张表该有哪些栏、什么类型",然后它会像验收员一样自动校验并转换类型——不合格当场报错,绝不放脏数据进系统。

先装: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(用了默认值)
📝 例子:验收员当场打回不合格品 如果模型漏了 name:raw = '{"amount": 300}'
Customer.model_validate_json(raw) 直接抛出校验错误,明确告诉你"缺少字段 name"。
你可以 try/except(回忆 Day 05)捕获,然后让模型重答一次——脏数据一步都进不了系统。
📝 例子:pydantic 直接生成"字段定义单" Customer.model_json_schema() 会输出一份 JSON schema(描述有哪些字段、类型、哪些必填)。这份东西可以直接喂给"方式三"或工具调用——定义一次,校验和约束两头都用,不重复劳动。
L05

schema 与"工具参数"是同一套东西

🤔 痛点老听说"函数调用 / function calling",它和今天讲的结构化输出到底什么关系?要不要先学?
💡 本质(一句话)让模型"调用一个函数",本质就是让它按你给的字段定义单(schema),填出这个函数需要的参数——所以工具调用 = 结构化输出的一个特例。今天学会 schema,Day 29 会一通百通。

比如你有个"查天气"的函数 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)我们会专门把工具调用讲透,那时你会发现"原来就是结构化输出换了个用法"。

L06

常见坑与修复套路

🤔 痛点实际跑起来,模型偶尔还是会在 JSON 前后加废话、字段名写错、类型不对。怎么让整个流程稳?
💡 本质(一句话)没有任何单招能 100% 保证,工程上的答案是"多层兜底":说清要求 + 低温 + 清洗 + 校验 + 校验失败自动重试。
常见坑修复套路
前后加废话 / 用 ```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
👶 一句话记住"要求 + 低温 + 校验 + 重试"是结构化输出的黄金四件套。永远别相信模型第一次就填对表,但要让不合格的表一张都进不了系统。
L07

今日小结 + 动手 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))
明日预告 · Day 15 多模型 & 路由:现在你能把一个模型用得又省又稳、还能拿到结构化结果。但市面上模型一大堆——OpenAI、Claude、国产(DeepSeek/通义/文心)、开源本地跑的(Ollama),贵的强、便宜的快。明天教你按任务难度把活分给不同模型(模型路由):贵活给强模型、便宜活给小模型,用最少的钱办最多的事,收尾整个阶段 2。
← Day 13 参数/成本/延迟 Day 15 · 多模型 & 路由 →