Structured Output 学习笔记
1. 本章目标
这一章解决的问题是:
如何让 LLM 不只是返回自然语言,而是稳定返回程序可以直接解析和使用的数据结构。
普通 LLM:
User ↓ LLM ↓ 自然语言Structured Output:
User ↓ LLM ↓ JSON / Schema ↓ Parser ↓ Pydantic Validation ↓ Python Object ↓ 业务逻辑2. 为什么需要 Structured Output
例如模型返回:
I recommend AI Application Engineer. You should learn Python, FastAPI and RAG. The match score is about 87%.人可以理解,但程序不好稳定处理。
更希望得到:
{"job":"AI Application Engineer","skills":["Python","FastAPI","RAG"],"match_score":0.87}这样后端可以直接:
result.job result.skills result.match_scoreStructured Output 的本质是:
把 LLM 从“文本生成器”变成软件系统中的数据生成组件。
3. Pydantic Model = 输出数据契约
例如:
frompydanticimportBaseModel,FieldclassJobRecommendation(BaseModel):job:strskills:list[str]match_score:float=Field(ge=0.0,le=1.0)这里规定:
job → str skills → list[str] match_score → float → 0 <= score <= 1它就是一个 Schema / Data Contract。
4. JSON String → Python Dict
模型通常先返回字符串:
answer=""" { "job": "AI Engineer", "skills": ["Python", "RAG"], "match_score": 0.9 } """使用:
json.loads(answer)完成:
JSON String ↓ Python Object例如:
data=json.loads(answer)得到:
{"job":"AI Engineer","skills":["Python","RAG"],"match_score":0.9}5.**dict解包
学习了:
JobRecommendation(**data)如果:
data={"job":"AI Engineer","skills":["Python"],"match_score":0.9}则:
JobRecommendation(**data)相当于:
JobRecommendation(job="AI Engineer",skills=["Python"],match_score=0.9)所以:
*list → 解包 iterable **dict → 解包 keyword arguments6. Structured Output 的两层基础验证
第一层:
json.loads(answer)负责:
JSON Syntax 是否正确。
如果失败:
json.JSONDecodeError第二层:
JobRecommendation(...)或者:
JobRecommendation.model_validate(data)负责:
JSON 的 Schema / 类型 / 约束是否正确。
如果失败:
ValidationError因此:
LLM Answer ↓ json.loads() ↓ JSON Syntax Validation ↓ Pydantic ↓ Schema Validation7. 自定义异常
定义:
classStructuredOutputError(Exception):pass然后把底层异常统一抽象:
defparse_job_recommendation(answer:str)->JobRecommendation:try:returnJobRecommendation(**json.loads(answer))exceptjson.JSONDecodeErrorase:raiseStructuredOutputError(f"LLM returned invalid JSON:{e}")fromeexceptValidationErrorase:raiseStructuredOutputError(f"LLM returned invalid schema:{e}")frome形成:
JSONDecodeError \ → StructuredOutputError ValidationError /这是:
Exception Abstraction
上层业务不需要处理所有底层细节。
8.raise ... from e
学习了:
raiseStructuredOutputError("...")frome作用:
原始异常 ↓ 异常链 ↓ 新的业务异常既能给业务层更清晰的错误,也保留底层 traceback。
9. Parse → Validate → Retry
LLM 输出是不确定的,所以第一次格式错误时,可以给它有限次数的修复机会。
流程:
LLM ↓ Answer ↓ Parse ↓ Validate ↓ 成功? ├── Yes → return └── No ↓ StructuredOutputError ↓ 还有 Retry? ├── No → raise └── Yes ↓ 构造修复 Prompt ↓ 重新调用 LLM典型结构:
forattemptinrange(max_retries+1):如果:
max_retries=2表示:
Initial Call + Retry 1 + Retry 2 = 最多 3 次调用10. Bounded Retry
这一章再次强化了:
AI 系统里的 Retry 必须有限制。
不能:
whileTrue:否则可能导致:
无限 API 调用 无限费用 无限延迟 程序无法终止所以:
max_retries=2属于 bounded retry。
11. Retry Prompt
失败后可以把三类信息重新告诉模型:
1. 上次答案 2. 错误原因 3. 正确 Schema例如:
Your previous response was invalid. Error: match_score must be <= 1 Previous response: ... Return ONLY valid JSON matching this schema: ...这叫:
Self-correction / Repair Prompt
12. JSON Schema
之前给 LLM 的:
{"job":"string","skills":["string"]}只是示例。
真正机器可理解的 Schema 是:
JSON SchemaPydantic 可以自动生成:
JobRecommendation.model_json_schema()返回dict。
然后:
json.dumps(schema,indent=2,ensure_ascii=False)转成 JSON Schema 文本。
这里复习了:
json.loads() String → Python json.dumps() Python → String13. Dynamic Schema
Structured Output 不应该把 Schema 写死。
可以:
model_class.model_json_schema()这样传入不同 Pydantic Model,就自动得到不同 Schema。
例如:
JobRecommendation产生 Job Schema。
ToolDecision产生 Tool Schema。
14. Generic Parser
最开始:
parse_job_recommendation()只能解析一个业务模型。
后来升级成:
parse_structured_output(answer,model_class)可以解析任何 Pydantic Model。
15.type[BaseModel]
学习了 class 和 instance 的区别。
client:LLMClient表示:
client 是 LLMClient instance而:
model_class:type[BaseModel]表示:
model_class 本身是一个 class例如传:
JobRecommendation不是:
JobRecommendation(...)这和 C# 中的:
Type vs object instance可以直接对应。
16. TypeVar
进一步学习:
fromtypingimportTypeVar T=TypeVar("T",bound=BaseModel)意思:
T 可以代表任意 BaseModel 子类。
于是:
defparse_structured_output(answer:str,model_class:type[T])->T:表达:
输入哪个 Pydantic class ↓ 返回对应 class 的 instance例如:
result=parse_structured_output(answer,JobRecommendation)则:
T = JobRecommendation返回:
JobRecommendation instance17. Generic Structured Output
最终目标变成:
awaitget_structured_output(client,prompt,JobRecommendation)或者:
awaitget_structured_output(client,prompt,ToolDecision)同一个函数处理不同 Schema。
核心组成:
Generic Parser + Generic Retry + Dynamic JSON Schema18. build_structured_prompt()
第一次请求就应该给模型 Schema,而不是等模型第一次犯错。
defbuild_structured_prompt(prompt:str,model_class:type[T])->str:schema_text=json.dumps(model_class.model_json_schema(),indent=2,ensure_ascii=False)returnf"""{prompt}Return ONLY valid JSON matching this JSON Schema:{schema_text}Do not include markdown or explanations. """19. build_retry_prompt()
失败时重新构造:
defbuild_retry_prompt(invalid_answer:str,error_message:str,model_class:type[T])->str:内容包括:
Previous Answer + Validation Error + Dynamic JSON Schema + 严格输出要求20. Genericget_structured_output()
最终核心架构:
Prompt ↓ build_structured_prompt() ↓ Dynamic JSON Schema ↓ LLM ↓ Answer ↓ parse_structured_output() ↓ json.loads() ↓ Pydantic Validation ↓ 成功? ├── Yes → return T └── No ↓ build_retry_prompt() ↓ bounded retry ↓ 最终成功 or raise21. Prompt-based vs Native Structured Output
我们还区分了两种方案。
Prompt-based Structured Output
Prompt: “请输出 JSON” ↓ LLM ↓ json.loads() ↓ Pydantic ↓ Retry属于:
Soft Constraint
优点:
兼容性强 几乎任何 LLM 都能使用 适合不同供应商和本地模型Native Schema-constrained Output
部分现代 LLM API 支持:
Pydantic / JSON Schema ↓ API ↓ 生成阶段约束输出格式属于更强的生成约束。
真实项目一般优先:
Native Structured Output + Pydantic Validation + Retry / Fallback但仍然应该掌握手动版本,因为不同模型和推理框架的支持并不完全一致。
22. Schema = Contract
这一章最重要的软件工程思想之一:
Schema 是 LLM 与程序之间的数据契约。
例如:
classJobRecommendation(BaseModel):规定了:
LLM 内部可以变化 模型供应商可以变化 Prompt 可以变化 但是 Backend 对外输出结构保持稳定因此:
GPT Qwen DeepSeek Local Model ↓ 统一 Structured Output Layer ↓ JobRecommendation ↓ Unity / Web / App客户端不需要知道后端使用哪个模型。
23. 本章涉及的 Python 知识
这一章把之前的知识进一步组合起来:
JSON ├── loads() └── dumps() OOP ├── class └── instance Pydantic ├── BaseModel ├── Field ├── ValidationError ├── model_validate() └── model_json_schema() Exception ├── try / except ├── custom exception ├── raise └── raise ... from e Typing ├── type[T] ├── TypeVar └── generic function Async ├── async def └── await Loop ├── range() └── bounded retry24. 本章最终模块
Structured Output │ ├── Pydantic Schema ✅ ├── JSON Parsing ✅ ├── Schema Validation ✅ ├── StructuredOutputError ✅ ├── Exception Chaining ✅ ├── Retry ✅ ├── Bounded Retry ✅ ├── Repair Prompt ✅ ├── JSON Schema ✅ ├── model_json_schema() ✅ ├── Generic Parser ✅ ├── TypeVar ✅ ├── Dynamic Schema ✅ └── Generic Structured Output Pipeline ✅25. Stage 2 当前进度
Stage 2:LLM Application Engineering ├── LLMClient ✅ ├── Messages / Roles ✅ ├── Multi-turn Chat ✅ ├── Context Management ✅ ├── Structured Output ✅ 本章完成 ├── Tool Calling ← 下一章 └── Streaming / Robustness ⏳下一章正式进入:
Tool Calling
核心问题将从:
“如何让 LLM 返回可靠的数据?”升级成:
“如何让 LLM 根据任务选择并调用程序中的工具?”最终会得到:
User ↓ LLM ↓ Tool Decision ↓ Python Function ↓ Tool Result ↓ LLM ↓ Final Answer这也是后面Agent Engineering的直接基础。