第3章 结构化 Prompt¶
学习目标¶
- 理解结构化输出的必要性及 JSON/XML 两种格式的取舍
- 掌握 OpenAI
response_format参数与 Structured Outputs 机制 - 学会使用 JSON Schema 定义输出契约
- 掌握 Pydantic 模型校验与输出解析的工程实践
- 能够构建端到端的结构化输出流水线
问题导入¶
某团队开发了一个"简历信息提取 Agent",需要从简历文本中抽取姓名、电话、邮箱、技能等信息并存入数据库。最初的 Prompt 是:
请提取以下简历的关键信息:姓名、电话、邮箱、技能。
简历:张三,电话 13800138000,邮箱 zhangsan@xx.com,熟悉 Python 和 Java。
模型返回的是自然语言:"这位候选人叫张三,电话是 13800138000……",无法直接入库。工程师尝试加"请输出 JSON",结果得到的 JSON 各种格式问题:键名不统一、字段缺失、多了多余说明文本、甚至引号不匹配导致 json.loads 报错。
如何让模型稳定输出可被程序直接解析的结构化数据?本章解决这个问题。
核心概念¶
1. 为什么需要结构化输出¶
LLM 默认输出自然语言,而业务系统需要结构化数据。结构化输出的价值:
- 可解析:程序直接
json.loads入库,无需正则提取 - 可校验:通过 Schema 校验字段完整性、类型正确性
- 可集成:与 API、数据库、下游服务无缝对接
- 可测试:断言字段值,自动化测试可行
2. JSON vs XML 输出格式¶
| 维度 | JSON | XML |
|---|---|---|
| 解析难度 | 简单(标准库) | 较复杂(需 lxml 等) |
| Token 效率 | 高(无闭合标签) | 低(标签冗余) |
| 模型友好度 | 高(训练数据多) | 中 |
| 可读性 | 中 | 高(带标签语义) |
| 适用场景 | API、数据库、配置 | 文档、带层级语义 |
生产环境首选 JSON,除非业务系统强制 XML。
3. response_format 参数¶
OpenAI API 提供 response_format 参数控制输出格式:
{"type": "text"}:默认,自由文本{"type": "json_object"}:JSON 模式,保证输出合法 JSON{"type": "json_schema", "json_schema": {...}}:Structured Outputs,按 Schema 输出
4. Structured Outputs(结构化输出)¶
OpenAI 的 Structured Outputs 功能允许通过 JSON Schema 严格约束输出结构:
- 字段名固定:模型必须按 Schema 定义的键名输出
- 类型严格:字符串、数字、布尔、数组、嵌套对象
- 必填可选:
required字段必须出现 - 枚举限制:
enum约束取值范围
这是目前最可靠的结构化输出方案,模型层面保证符合 Schema。
5. Pydantic 模型校验¶
Pydantic 是 Python 生态主流的数据校验库:
- 用类定义数据模型,自动生成 JSON Schema
- 校验字段类型、必填、约束
- 与 FastAPI 深度集成
- 支持
model_validate方法解析字典
6. 输出解析流程¶
完整流程:Prompt 设计 → 模型调用 → JSON 解析 → Schema 校验 → 业务处理 → 错误重试
原理图¶
flowchart TB
A[业务需求] --> B[定义 Pydantic 模型]
B --> C[生成 JSON Schema]
C --> D[调用 LLM<br/>response_format=json_schema]
D --> E[模型输出 JSON]
E --> F[json.loads 解析]
F --> G{解析成功?}
G -->|否| H[重试/报错]
G -->|是| I[Pydantic 校验]
I --> J{校验通过?}
J -->|否| H
J -->|是| K[业务对象]
K --> L[入库/API 返回]
style B fill:#e1f5fe
style D fill:#fff9c4
style I fill:#f3e5f5
style K fill:#c8e6c9
style L fill:#c8e6c9
style H fill:#ffcdd2
从业务需求出发定义模型,生成 Schema 约束 LLM 输出,经过解析与校验后得到可靠业务对象。
示例代码¶
1. 基础 JSON 模式¶
"""
第3章示例:基础 JSON 输出模式
"""
from openai import OpenAI
import json
client = OpenAI()
def extract_info_basic(text: str) -> dict:
"""使用 json_object 模式输出 JSON"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "从文本中提取信息,输出 JSON。"},
{"role": "user", "content": f"提取姓名、电话、邮箱:\n{text}"},
],
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
if __name__ == "__main__":
resume = "张三,电话 13800138000,邮箱 zhangsan@xx.com"
print(extract_info_basic(resume))
# {'name': '张三', 'phone': '13800138000', 'email': 'zhangsan@xx.com'}
2. Structured Outputs + Pydantic 严格校验¶
"""
Structured Outputs:用 Pydantic 定义模型,保证输出严格符合 Schema
"""
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import json
client = OpenAI()
class ResumeInfo(BaseModel):
"""简历信息模型"""
name: str = Field(description="姓名")
phone: str = Field(description="电话号码")
email: str = Field(description="邮箱地址")
skills: list[str] = Field(default_factory=list, description="技能列表")
years_of_experience: int = Field(default=0, description="工作年限")
@field_validator("phone")
@classmethod
def validate_phone(cls, v: str) -> str:
if not v.startswith("1") or len(v) != 11:
raise ValueError("电话号码格式不正确")
return v
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("邮箱格式不正确")
return v
def extract_resume_structured(text: str) -> ResumeInfo:
"""使用 Structured Outputs 提取简历信息"""
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "从简历文本中提取结构化信息。无法确定的字段留空或填默认值。",
},
{"role": "user", "content": text},
],
response_format=ResumeInfo,
)
return response.choices[0].message.parsed
if __name__ == "__main__":
resume_text = """
张三,男,5 年开发经验。
电话:13800138000,邮箱:zhangsan@example.com
技能:Python, Java, Docker, Kubernetes
"""
info = extract_resume_structured(resume_text)
print(info.model_dump_json(indent=2))
# {
# "name": "张三",
# "phone": "13800138000",
# "email": "zhangsan@example.com",
# "skills": ["Python", "Java", "Docker", "Kubernetes"],
# "years_of_experience": 5
# }
3. 带重试机制的解析流水线¶
"""
端到端结构化输出流水线:含解析失败重试
"""
from openai import OpenAI
from pydantic import BaseModel, ValidationError
import json
import logging
client = OpenAI()
logger = logging.getLogger(__name__)
class ProductInfo(BaseModel):
name: str
price: float
category: str
in_stock: bool
def extract_product(text: str, max_retries: int = 3) -> ProductInfo:
"""带重试的产品信息提取"""
last_error = None
for attempt in range(max_retries):
try:
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "从文本提取商品信息。"
+ (f"上次错误:{last_error},请修正。" if last_error else ""),
},
{"role": "user", "content": text},
],
response_format=ProductInfo,
)
return response.choices[0].message.parsed
except ValidationError as e:
logger.warning("第 %d 次校验失败: %s", attempt + 1, e)
last_error = str(e)
except json.JSONDecodeError as e:
logger.warning("第 %d 次 JSON 解析失败: %s", attempt + 1, e)
last_error = str(e)
raise RuntimeError(f"提取失败,已重试 {max_retries} 次")
if __name__ == "__main__":
text = "iPhone 15 Pro,售价 8999 元,分类:手机,目前有货"
product = extract_product(text)
print(product.model_dump_json(indent=2))
4. 手动 Schema 定义(不依赖 Pydantic)¶
"""
直接使用 JSON Schema 字典定义输出结构
"""
from openai import OpenAI
import json
client = OpenAI()
PRODUCT_SCHEMA = {
"type": "json_schema",
"json_schema": {
"name": "product_info",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": "number"},
"category": {"type": "string", "enum": ["手机", "电脑", "配件"]},
"tags": {"type": "array", "items": {"type": "string"}},
},
"required": ["name", "price", "category", "tags"],
"additionalProperties": False,
},
},
}
def extract_with_schema(text: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "提取商品信息。"},
{"role": "user", "content": text},
],
response_format=PRODUCT_SCHEMA,
)
return json.loads(response.choices[0].message.content)
企业案例¶
数据提取 Agent¶
某 HR 科技公司开发简历解析 Agent,每天处理 10 万份简历,需提取 30+ 个字段(基本信息、教育经历、工作经历、项目经验、技能等),存入结构化数据库供搜索匹配。
V1 方案:用正则 + 规则提取。问题:简历格式多样(PDF/Word/纯文本),正则维护成本极高,准确率仅 60%。
V2 方案:LLM + 请输出 JSON。问题:JSON 格式不稳定,json.loads 失败率 15%,需要大量异常处理。
V3 方案:Structured Outputs + Pydantic 多层模型。设计如下:
class EducationItem(BaseModel):
school: str
degree: str
major: str
start_date: str
end_date: str
class WorkItem(BaseModel):
company: str
position: str
start_date: str
end_date: str
description: str
class FullResume(BaseModel):
basic_info: ResumeInfo
education: list[EducationItem]
work_experience: list[WorkItem]
skills: list[str]
self_evaluation: Optional[str]
V3 上线后:JSON 解析成功率 99.8%,字段准确率 92%,处理速度提升 3 倍(无需复杂异常处理)。关键经验:
- 嵌套模型设计:复杂结构用嵌套 Pydantic 模型,逐层校验
strict=True:开启严格模式,模型层面保证符合 Schema- 容错设计:可选字段用
Optional,必填字段给默认值 - 监控告警:记录校验失败案例,持续优化 Prompt
常见问题¶
Q1:json_object 和 json_schema 模式有什么区别?
A:json_object 只保证输出是合法 JSON,但不约束结构(键名、类型可能变);json_schema 严格按 Schema 输出,字段名、类型、必填都受约束。生产环境优先用 json_schema。
Q2:Structured Outputs 支持嵌套对象和数组吗?
A:支持。可以定义多层嵌套的 Pydantic 模型或 JSON Schema,包括 list[SubModel]、Optional[SubModel] 等。注意 additionalProperties: False 必须设置。
Q3:模型输出仍偶尔不符合 Schema 怎么办?
A:①检查 Schema 是否过于复杂(建议单个 Schema 字段不超过 20 个);②开启 strict=True;③加重试机制(本章示例 3);④降低 temperature 到 0;⑤在 Prompt 中提供 Few-shot 示例。
Q4:Pydantic v1 和 v2 在 Schema 生成上有差异吗?
A:有。Pydantic v2 性能更高,API 有变化(如 model_validate 替代 parse_obj,model_dump 替代 dict())。OpenAI SDK 的 response_format 同时支持两者,但推荐用 v2。
Q5:如何处理模型"幻觉"填了不存在的数据?
A:在 Prompt 中强调"无法确定的字段留空或填 null",并在 Pydantic 模型中用 Optional 类型 + field_validator 校验合理性。对关键字段可加二次确认调用。
面试题¶
-
简答题:
response_format的三种取值是什么?各自约束力如何? -
原理题:Structured Outputs 是如何在模型层面保证输出符合 Schema 的?与单纯 Prompt 提示"输出 JSON"有何本质区别?
参考要点:约束解码(constrained decoding)在 token 采样阶段强制符合 Schema,而非依赖 Prompt 提示。
-
设计题:设计一个 Pydantic 模型,用于从新闻中提取"事件"(含时间、地点、人物、事件类型、影响),事件类型限定为枚举。
-
实践题:如何实现"校验失败后自动把错误信息反馈给模型重试"?写出核心逻辑。
-
对比题:JSON 与 XML 在 LLM 结构化输出中各自优劣?为什么生产环境首选 JSON?
练习题¶
-
基础实现:用
json_object模式实现一个"电影信息提取"函数,输入电影介绍文本,输出包含片名、导演、年份、评分的 JSON。 -
Schema 进阶:用 Pydantic 定义一个"订单模型",包含订单号、商品列表(嵌套)、收货地址(嵌套)、总金额,并实现字段校验(金额>0、订单号格式)。
-
容错处理:为本章示例 2 的
extract_resume_structured添加重试机制,当 Pydantic 校验失败时把错误信息反馈给模型重试最多 3 次。 -
性能对比:对比
json_object和json_schema两种模式在同一任务下的成功率与响应延迟,撰写实验报告。
小结¶
结构化 Prompt 是 LLM 走向生产的关键一环,让模型输出从"自然语言"变为"可编程数据"。本章核心要点:
- JSON 是首选格式:解析简单、Token 效率高、模型友好。
- Structured Outputs 最可靠:通过
response_format=json_schema在模型层面保证符合 Schema,远比 Prompt 提示"输出 JSON"可靠。 - Pydantic 是校验利器:定义模型、生成 Schema、校验输出、与 FastAPI 集成,一站式解决。
- 嵌套模型设计复杂结构:用嵌套 Pydantic 模型处理多层业务对象,逐层校验。
- 容错设计不可少:必填校验 + 重试机制 + 监控告警,构建鲁棒流水线。
下一章将学习复杂 Chain 模式,让模型具备多步推理与工具调用能力,应对更复杂的业务场景。