假设你有一位叫 Tom 的海外朋友,他中文流利、热心肠,但一开口就停不下来:“我昨天去超市买了点东西,就那个……零食饮料什么的,一共花了好像一百多?对了还有一个充电宝!” 你听懂了,可要把这段口述报送海关,海关可不接受“好像一百多”。你得拿出一张官方申报表,把“商品名称、数量、单价”一格格地填进去。 大语言模型(LLM)就是那位健谈的 Tom:它肚子里满是知识,输出却像口述一样随性——换个说法、多句废话、少个括号,都会让程序当场崩溃。而 Pydantic 就是那张“官方申报表”:它规定了每个字段叫什么、是什么类型、值必须合法,还能在 Tom 说错时把表格退回去让他重填。这就是“LLM + Pydantic 结构化输出”的核心思想:模型负责“想说”,Pydantic 负责“说对”。 在这篇文章里,我们会从 LLM 为什么“管不住嘴”讲起,逐步学会用 Pydantic 定义模型、写字段描述、处理校验重试、做嵌套与列表输出,最后集成到 OpenAI 等真实 API 中。学会这套方法,你就能让所有模型的回答都像印刷体一样整齐——把 Tom 的口水话,变成一份份严谨的档案。

岗位关联

Pydantic 结构化输出是 AI 应用工程化的“格式保险栓”——它决定你的产品是把模型输出当数据源,还是当废纸篓。掌握它,你就在从 demo 工程师迈向工程化 AI 应用开发者的路上跨过了关键一步。

目录
  1. 可视化演示
  2. 输出失控问题
  3. Pydantic基础
  4. 字段描述引导
  5. 校验与重试
  6. 嵌套结构
  7. 列表批处理
  8. API集成实战
  9. 应用场景落地
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Pydantic模型定义、校验流程与重试、嵌套结构与列表输出。交互切换模型、流程和嵌套视图。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Pydantic模型定义 字段类型:str/int/obj 必填(req)与可选(opt) 红点标记必填字段 class User(BaseModel) Pydantic模型定义字段类型和约束

输出失控问题

Tom 又来了。你问他:“这次回国带了什么?” 他眉飞色舞:“带了好多!有一盒巧克力……不对,是两盒,还有一个黑色的那个叫什么来着——移动电源!哦对,价格?嗯……大概一百多吧?反正不贵。” 他的回答热情洋溢,却完全没有结构:有时把数量说在名称前,有时把价格带个“大概”,甚至中途改口。直接把这句话写进申报单,海关会当场崩溃。

把 Tom 换成大语言模型,崩溃的就是你的代码。LLM 的生成本质是“猜下一个词”:它根据前面所有词的概率分布,逐个采样出后续 token。因此你永远无法保证它输出的 JSON 键名大小写、字段顺序,甚至无法保证它不加多余的解释文字。一段看似合法的 JSON 前后多了一个反引号或逗号,json.loads 就会直接报错——这就是“自由文本不确定”和“JSON 易碎难解析”的根源。

更隐蔽的问题是“键名语义漂移”:上一次模型输出 "customer_name",这一次却输出 "userName",你的业务代码就得同时兼容几十种写法。靠正则表达式去修补,等于给每条河单独架一座桥,永远架不完。正确的做法是引入“硬约束”:在模型生成之前就告诉它“哪些字段必须存在、类型必须是什么”,而不是等它说完再去猜。

正因为 LLM 天生爱自由发挥,我们才需要一张“官方表格”——Pydantic。表格栏位固定、类型明确、校验严格,任何写错的地方都会被标记出来打回。接下来我们就看看这张表格长什么样。

P(w1,…,wn)=∏i=1nP(wi∣w1,…,wi−1)\displaystyle P(w_1,\dots,w_n)=\prod_{i=1}^{n}P(w_i\mid w_1,\dots,w_{i-1})
这个式子告诉我们:LLM 生成每个词都只取决于前文概率,没有人能向它保证“这次一定格式正确”——所以你才需要外力去约束它。
  1. 前缀条件w_1,\dots,w_{i-1} 表示当前已经生成的前文 token 序列,它们是预测下一个词的全部上下文,就像 Tom 前面说过的话决定了他下一句会接什么。
    w1,…,wi−1w_1,\dots,w_{i-1}
  2. 条件概率P(w_i\mid w_1,\dots,w_{i-1}) 表示“在前文已知的条件下,下一个词是某个候选项的可能性”。可能性不是 1,所以模型随时可能换一种说法。
    P(wi∣w1,…,wi−1)P(w_i\mid w_1,\dots,w_{i-1})
  3. 连乘累加\prod_{i=1}^{n} 表示把整句话每个词的生成概率相乘,得到整段序列的出现概率。序列越长,形式越容易漂移,单靠提示词根本无法“锁死”输出格式。
    ∏i=1n\prod_{i=1}^{n}

Pydantic基础

签证申请表的长相是有讲究的:姓名栏只能填文本、出生年份栏只能填数字、国籍栏只能填文本。Pydantic 里,这张表就定义为 BaseModel 的子类。每个类属性对应一个栏位,属性标注的类型就是栏位的“允许格式”。

这段代码用人类可读的方式声明了一个“用户档案”:name 必须是字符串,age 必须是整数,city 必须是字符串。当你把 Tom 的口述内容交给 Pydantic 时,它会自动“对号入座”:如果传进来的是 {"name": "张三", "age": 28, "city": "上海"},它愉快地收下;如果 age 传成了 "大约三十",它会立刻报告类型不对——这个过程叫“声明式校验”,意思是:你只负责描述规则,执行校验的脏活累活都交给了库。

Pydantic 还提供了双向转换:model_validate() 可以把字典、JSON 字符串变成模型实例(相当于把散乱口述誊进表格);model_dump_json() 则把实例序列化成标准 JSON(相当于把填好的表格复印一份交给系统)。这种“定义即校验”的体验,远胜于手写几十行 if-else 去检查每个字段。

有了这张表,Tom 的口述就能被誊成规范档案了。但表格光有栏位还不够——栏位旁边还要写“填写说明”,告诉 Tom 每栏到底该填什么。这就是下一节的主角:字段描述。

User={ name:str, age:int, city:str }\displaystyle \text{User} = \{\ name:\texttt{str},\ age:\texttt{int},\ city:\texttt{str}\ \}
这行“公式”表示一个数据模型就是一组“字段名 + 类型”的集合:字段名决定系统怎么取值,类型决定 LLM 该填什么格式。
  1. 模型集合\text{User} = \{\cdots\} 把 User 定义成一个由若干字段组成的集合。每个元素都是一个“字段规则”,就像表格上的每一栏。
    User={⋯ }\text{User} = \{\cdots\}
  2. 字段与类型映射name:\texttt{str} 表示字段 name 的值必须是字符串类型。它是硬约束:不是“最好”是文本,而是“必须”是文本。
    name:strname:\texttt{str}
  3. 多字段并列集合中同时列出 age:\texttt{int} 和 city:\texttt{str},声明一张完整表格应有的全部栏目。字段越多,结构化程度越高,LLM 的自由度被收得越紧。
    age:int, city:strage:\texttt{int},\ city:\texttt{str}
python4 行
class User(BaseModel):    name: str    age: int    city: str

字段描述引导

你有没有填过那种语焉不详的表格?只写一个“备注”,你完全不知道对方想让你备注什么。好的表格会在栏位旁边加一行小字:“请填写本人护照上的拼音姓名”。LLM 也一样——光告诉它有 name 字段不够,你还得告诉它 name 到底指什么、格式如何。

Pydantic 的 Field(description=...) 就是这张表格上的“填写说明”。例如定义一个“商品”模型时,name: str = Field(description="商品名称,例如'无线鼠标'") 会让模型清楚地知道:这个字段要放的是人类可读的商品名,而不是型号代码或链接。当我们把 Pydantic 模型转成 JSON Schema 送给 LLM 时,description 会原样出现在 schema 中,相当于把填写说明直接摊开在模型眼前。

除了 description,“填写说明”家族还有 example(示例值)和 default(默认值)。example 给模型一个具体的样板,就像“例如:张三”;default 则规定“如果实在不知道,写这个兜底值”。这些信息都通过 schema 传输,共同构成对模型的“语义约束”——约束的不是语法,而是模型对每个字段含义的理解。

有了清晰的填写说明,Tom 填表时就能少犯一半错误。但万一他还是填错了呢?签证官不会直接把材料扔进碎纸机,而是会标出错误栏位,让申请人修改后重新提交——这就是“校验与重试”。

Field( description=C, example=e0, default=d0 )\displaystyle \text{Field}\left(\, description=\mathcal{C},\ example=e_0,\ default=d_0\,\right)
这条“公式”描述了字段填写说明的三要素:语义描述 C、参考示例 e₀、兜底默认值 d₀,三者叠加给 LLM 传递“这一栏怎么填”的完整指令。
  1. 语义描述description=\mathcal{C} 中 \mathcal{C} 代表一段自然语言约束,如“用户完整真实姓名”。它不改变程序行为,却直接影响模型生成的倾向。
    C\mathcal{C}
  2. 示例值example=e_0 中 e_0 是给模型的一个样例字符串。模型擅长模仿,给它一个漂亮样板,它往往会照着范例的风格输出。
    e0e_0
  3. 默认值default=d_0 中 d_0 表示当输入缺失该字段时采用的备用值。它像表格上的“如无则填无”,避免了字段缺失导致的校验失败。
    d0d_0

校验与重试

Tom 把表格交了上来:“国籍:中国……呃,我写的是 China,行吗?” 签证官摇摇头,把表格退回:“这里要填中文国家名,请重新填写。” 注意,他不是全盘否定一整张表,而是精准指出“第4栏填写错误”。好的校验机制就该如此:不模糊、可重试。

在 Pydantic 中,当数据不符合模型定义时,会抛出 ValidationError 异常。这个异常自带完整的错误报告:哪个字段、什么错误类型(类型不符、字段缺失、值超范围)、错误发生在第几层嵌套。你不必手写解析代码去“猜哪里错了”,异常信息本身就是一张批注好的退回表。

在生产环境中,我们通常会写一个“重试循环”:调用 LLM 生成 JSON → Pydantic 校验 → 如果失败,把 ValidationError 的详细信息拼进新的提示词,告诉模型“你上次错在 age 字段,应该是整数”,然后重新请求一次。由于错误信息足够明确,第二次生成的成功率通常极高。这正是“错误信息回传”的价值:让模型从自己的错误中学习,而不是原地重试一百次。

校验与重试让“填错重填”成为可循环的自动化流程。但如果 Tom 的申报单里还夹着一张“随行家属表”,该怎么办?一张主表叠着一张子表——这就是嵌套结构的用武之地。

valid: Jraw⟶{0,1}\displaystyle \text{valid}:\ \mathcal{J}_{\text{raw}}\longrightarrow\{0,1\}
这个映射表示一次“校验判定”:输入 LLM 产生的原始 JSON,输出二元结果——1 代表校验通过进入业务,0 代表校验失败需要重试。
  1. 原始输入\mathcal{J}_{\text{raw}} 是 LLM 直接吐出的那段未经检查的 JSON 文本。它可能合法,也可能键值错位,一切未知。
    Jraw\mathcal{J}_{\text{raw}}
  2. 校验函数\text{valid} 是 Pydantic 根据模型定义自动生成的总校验流程,内部完成类型检查、必填检查、嵌套检查。
    valid\text{valid}
  3. 二元结果\{0,1\} 表示判定只有两种:通过或失败。失败后,异常携带的错误明细被拼入重试提示词,驱动下一轮生成。
    {0,1}\{0,1\}

嵌套结构

出国旅行时,主申报单上常有一栏问:“是否有随行家属?如有请另填附表。” 附表里有家属的姓名、年龄、护照号。主表加附表,这就是“嵌套结构”最直观的样子:一个模型里嵌着另一个模型。

在 Pydantic 中嵌套毫不费力:直接在字段类型中写另一个 BaseModel 子类即可。比如 class Order(BaseModel): user: User; items: list[Item],user 字段的类型是 User 这个子模型,于是 Order 这张主表里就嵌入了 User 子表。校验时 Pydantic 会递归地检查每一层:先检查主表字段,再深入子表检查 User 的 name/age/city,任何一层出错都会在错误报告中标注完整路径(如 user.age)。

嵌套结构的意义不止于“好看”,它能表达真实世界的层级关系:一张订单包含用户信息和商品列表,一篇新闻包含标题、正文、作者信息。做深度信息抽取时,你只需定义一个树状模型,LLM 的输出就能被逐层“吸”进对应节点,相当于让 Tom 把“我买了啥、我是谁、从哪来”分类填进不同的子表。

嵌套解决了“信息有层次”的问题。可现实生活中,Tom 往往不止带一件商品——他带了五件、十件。把同一种商品写十个不同字段显然很蠢,我们需要“列表”来收纳它们。

Order={ order_id:str, user:User, products:[Item] }\displaystyle \text{Order}=\{\ \text{order\_id}:\texttt{str},\ \text{user}:\text{User},\ \text{products}:[\text{Item}]\ \}
该“公式”展示主表 Order 的三个字段:普通文本字段、一个 User 子模型字段、一个商品 Item 的列表字段——主表里套着子表,子表里套着更细的字段。
  1. 主表字段\text{order\_id}:\texttt{str} 是订单号,一个普通字段,负责描述订单自身的属性。
    order_id:str\text{order\_id}:\texttt{str}
  2. 子模型字段\text{user}:\text{User} 表示 user 字段的类型是另一个完整模型。这就像主表某栏的答案不是一行文字,而是一整张附表。
    user:User\text{user}:\text{User}
  3. 列表叠加嵌套\text{products}:[\text{Item}] 表示该字段是一个 Item 模型组成的数组。这里“列表”和“嵌套”同时出现,数据结构的表达能力一下子翻倍。
    products:[Item]\text{products}:[\text{Item}]

列表批处理

海关申报表背面通常印着一张多栏物品清单:每行填一个商品,想填几个就续几行。“列表”正是为这种重复结构而生的。Pydantic 用 Python 原生的 list 类型表达“多条同类数据”:tags: list[str] 是一个字符串列表,products: list[Product] 是一个商品子模型列表。

当你要让 LLM 从一篇长文章中抽取所有商品报价,或从客服聊天记录里提取所有订单时,列表字段能把一次生成的结果封装成数组,模型会自然输出 [{...}, {...}, {...}] 这样的结构。Pydantic 对列表还会逐元素校验:列表里的每个商品都要过一遍 Product 模型的完整检查——这相当于海关对清单上的每一行商品都逐一核对。

从工程角度看,列表还叠加了对数据的后处理能力:你可以对抽取结果做“批量去重”(两行商品名称相同则合并)、“排序整理”(按价格升序排列)等操作。这些通用操作只需用 Python 内置工具(set、sorted)就能完成,因为 Pydantic 已经把模型转成了干净、可迭代的 Python 对象。

学会了基础模型、描述引导、校验重试、嵌套与列表,你已经能在本地画一张非常复杂的“申报表”了。但 Tom 人在海外,这张表怎么递到他手上?答案是通过大模型的 API:把表格编码成 JSON Schema,塞进请求参数里。

ProductList=[p1, p2, …, pn],pi∈Product\displaystyle \text{ProductList}=[p_1,\ p_2,\ \dots,\ p_n],\qquad p_i\in\text{Product}
该“公式”表示一个商品列表是若干 Product 实例的有序集合,其中每个元素必须满足 Product 模型的全部校验规则——重复的可以合并,顺序可以重排,但个体规则不可豁免。
  1. 列表骨架[p_1,\ p_2,\ \dots,\ p_n] 是长度为 n 的数组形态,n 由模型生成决定,可多可少。
    [p1, p2, …, pn][p_1,\ p_2,\ \dots,\ p_n]
  2. 元素类型约束p_i\in\text{Product} 声明数组每个元素都必须是 Product 类型,这是对“列表里装什么”的硬性规定。
    pi∈Productp_i\in\text{Product}
  3. 有序性列表天然有序,这意味着你可以对 p_i 做排序、截断、切片,为后续“去重排序”等批处理操作提供了结构基础。
    pip_i

API集成实战

想象一下:海关不再给 Tom 发纸质表,而是把一张“电子表格”传到他的手机上,他在 App 里填好再提交。传到手机上的过程,就是 API 集成:你的 Pydantic 模型变成 JSON Schema,随请求一起发给 LLM,LLM 必须在 schema 的框架内作答。

以 OpenAI 为例,只需在调用 chat.completions.create 时加一个参数:response_format={"type": "json_schema", "json_schema": {...}}。其中的 schema 不需要手写——User.model_json_schema() 会用 Pydantic 自动生成一份完整的 JSON Schema(字段、类型、描述、默认值全都有)。这就是“schema 联动”:Pydantic 模型是唯一事实来源,schema 和 API 参数都从它派生,绝无手写不一致的风险。

Anthropic 的 Claude 也支持类似的结构化输出能力,只是参数名和格式略有差异(如 tool_choice 或输出约束相关配置)。无论接哪家,Pydantic 都扮演同一个角色:先把模型变成 schema,再把 API 返回的 JSON 用 model_validate 拉回模型实例。这套“进得去、出得来”的闭环,让你在更换模型厂商时只改几行配置,业务代码纹丝不动。

至此,“表格传至 Tom 手机”已经完全打通。最后一节,我们看看这张表格在各个行业里如何发光发热:从客服对话抽订单,到简历解析、表单填充,再到 Agent 的工具调用。

SPydantic → model_json_schema()  SJSON Schema → response_format  LLM API\displaystyle \mathcal{S}_{\text{Pydantic}}\ \xrightarrow{\ \text{model\_json\_schema()}\ }\ \mathcal{S}_{\text{JSON Schema}}\ \xrightarrow{\ \text{response\_format}\ }\ \text{LLM API}
这条流程链展示了集成的两个关键跳跃:Pydantic 模型通过 model_json_schema() 生成机器可读的 JSON Schema,再把 schema 通过 response_format 参数传给 LLM——schema 是两边的共同语言。
  1. 源头模型\mathcal{S}_{\text{Pydantic}} 是你定义的 Pydantic 模型,其中包含字段名、类型、描述、嵌套关系等全部语义信息。
    SPydantic\mathcal{S}_{\text{Pydantic}}
  2. 自动转换\xrightarrow{\text{model\_json\_schema()}} 表示 Pydantic 自动完成模型到 JSON Schema 的双向映射,过程无需人工干涉。
    →model_json_schema()\xrightarrow{\text{model\_json\_schema()}}
  3. API 传输\xrightarrow{\text{response\_format}} 表示把 schema 作为参数传入模型 API,让 LLM 在生成时只能输出符合 schema 的 JSON。
    →response_format\xrightarrow{\text{response\_format}}

应用场景落地

签证申请流程走通之后,同一套“表格化”思维可以复制到任何需要“把自由信息变成规范数据”的地方。Tom 的故事发生在海关,但同样的剧本每天都在客服系统、HR 部门、数据处理中心上演。

第一个场景是信息抽取:客服聊天记录里有大量口语化内容——“我要一个无线鼠标,黑色的,79 块,订单号 12345”。通过 Pydantic 定义 OrderInfo 模型,LLM 一次性抽出商品、价格、订单号,客服系统直接入库,省去人工录入。第二个场景是表单填充:用户用自然语言描述需求,系统自动生成一个填好的表单对象(如工单、报销单),用户只需确认即可提交。第三个场景是数据清洗:把散落各处的非结构化文本批量转成统一 schema 的 JSON,model_validate 会顺手纠正类型问题(比如把字符串 "79" 转成数字 79),让下游数据库格式一致。

更前沿的场景是 Agent 工具调用:AI Agent 决定调用哪个工具时,需要返回规范的工具参数(如 search_weather(city="上海", date="2025-01-01"))。用 Pydantic 定义工具参数模型,等于给 Agent 发了一张“工具申报表”,它只能按规定的字段和类型填写,从根上杜绝了参数拼错、字段漏传的问题。

从输出失控到 Pydantic 基础,从描述引导到校验重试,再到嵌套、列表、API 集成与场景落地,我们已经完整复刻了“Tom 填表”的全过程。只要你在项目里沿用这套表格思维,AI 的输出就能从“随机散文”变身为“严谨档案”。

Traw → LLM  Jraw → Pydantic  Mvalid\displaystyle \mathcal{T}_{\text{raw}}\ \xrightarrow{\ \text{LLM}\ }\ \mathcal{J}_{\text{raw}}\ \xrightarrow{\ \text{Pydantic}\ }\ \mathcal{M}_{\text{valid}}
这条管道是全文的总结公式:原始非结构化文本经过 LLM 生成 JSON,再经 Pydantic 校验与转换,最终得到一个可靠有效的结构化模型——从口述到档案的全流程。
  1. 原始文本\mathcal{T}_{\text{raw}} 是未经处理的自然语言(聊天记录、简历、新闻),格式混乱但信息丰富。
    Traw\mathcal{T}_{\text{raw}}
  2. LLM 粗加工\xrightarrow{\text{LLM}} 表示大模型把自由文本转成原始 JSON,这一步解决“语义解析”,但不保证格式完美。
    →LLM\xrightarrow{\text{LLM}}
  3. Pydantic 精加工\xrightarrow{\text{Pydantic}} 表示校验、纠偏、转换一气呵成,最终输出 \mathcal{M}_{\text{valid}}——一个保证合法、可用、稳定的模型实例。
    →Pydantic Mvalid\xrightarrow{\text{Pydantic}}\ \mathcal{M}_{\text{valid}}

AI 应用场景

客服订单信息抽取

阶段:LLM 输出规范化(后处理阶段)

解决的问题:传统客服需要人工阅读聊天记录,逐条录入订单信息,不仅慢,而且容易抄错订单号、漏记商品。让 LLM 直接抽取,又常因输出格式不稳定而无法入库。用 Pydantic 定义 OrderInfo,把模型变成“订单申报表”,LLM 抽取结果再经 Pydantic 校验,能稳定得到 {"order_id": "...", "items": [...]} 这样的干净数据。

具体案例:某电商平台的售后机器人每天处理上千条聊天记录。开发团队定义了 OrderInfo(订单号、商品列表、金额、用户备注)和 Product(名称、数量、单价)两个模型,将对话文本交给 GPT-4o,配合 response_format 和 Pydantic 校验,客服系统自动生成可入库工单,人工复核率从 100% 降到 15%。

简历解析与表单填充

阶段:数据清洗与结构化存储

解决的问题:简历格式千奇百怪,PDF、Word、图片扫描件混杂,传统正则规则维护成本极高。用 Pydantic 定义 Resume(姓名、电话、邮箱、教育经历列表、工作经历列表),再让 LLM 按 schema 抽取,就能把每份简历标准化,直接进入人才数据库供搜索排序。

具体案例:一家猎头公司用 Claude + Pydantic 批量解析简历,其中 education: list[Education] 与 experience: list[WorkExperience] 两个嵌套列表字段覆盖了 90% 以上的学历与履历信息。解析失败的简历通过 ValidationError 信息回传重试,最终结构化成功率超过 97%。

AI Agent 工具参数规范化

阶段:Agent 决策与工具调用

解决的问题:AI Agent 需要调用外部工具(查天气、订机票、发邮件),如果参数拼错,整个自动化流程就会中断。用 Pydantic 定义每个工具的参数模型,相当于给 Agent 发放“工具申报表”——模型只能说表上有的字段,值还必须通过类型校验,从源头消灭参数错配。

具体案例:一个旅行规划 Agent 内部定义 search_flight(from_city: str, to_city: str, date: str) 作为工具模型。用户说“帮我看看下周三从上海去北京的高铁”,Agent 返回的参数被 Pydantic 校验为 {"from_city": "上海", "to_city": "北京", "date": "2025-01-15"},调度系统无需任何额外解析即可直接执行。

Python 演示

python48 行
import json from openai import OpenAIfrom pydantic import BaseModel, Field, ValidationError  class Product(BaseModel):    name: str = Field(description="商品名称,例如:无线鼠标")    price: float = Field(description="商品单价,单位:元")    quantity: int = Field(description="购买数量,例如:2")  class OrderInfo(BaseModel):    order_id: str = Field(description="订单编号,形如ORD20250101")    products: list[Product] = Field(description="商品列表")    total: float = Field(description="订单总金额,单位:元")  def extract_order(text: str, client: OpenAI, max_retries: int = 2) -> OrderInfo:    schema = OrderInfo.model_json_schema()    messages = [        {"role": "system",         "content": "你是一个订单信息抽取助手,只输出符合给定JSON Schema的JSON。"},        {"role": "user", "content": text},    ]    for attempt in range(max_retries):        resp = client.chat.completions.create(            model="gpt-4o",            messages=messages,            response_format={                "type": "json_schema",                "json_schema": {"name": "order", "schema": schema},            },        )        raw = json.loads(resp.choices[0].message.content)        try:            return OrderInfo.model_validate(raw)        except ValidationError as exc:            messages.append({"role": "user",                             "content": f"你上次的JSON未通过校验:{exc.errors()},请修正后重新输出。"})    raise RuntimeError("重试多次仍然校验失败")  if __name__ == "__main__":    client = OpenAI()    dialog = "我要一个无线鼠标79块买2个,再来1个机械键盘299元,订单号ORD20250101,总共457元。"    order = extract_order(dialog, client)    print(order.model_dump_json(indent=2))
目的:演示“Pydantic 定义 schema → OpenAI 结构化输出 → 校验失败自动重试”的完整闭环,把客服对话直接变成干净的订单 JSON。
  1. class Product(BaseModel):定义商品子模型,相当于申报单上的“物品栏”模板,每个商品都有名称、单价、数量三个必填格。
  2. name: str = Field(description="商品名称,例如:无线鼠标")用 description 给 LLM 写“填写说明”,模型看到它就知道这栏该填人类可读的商品名。
  3. products: list[Product]用列表类型声明“一个订单可包含多个商品”,模型会生成 JSON 数组,Pydantic 会对每个元素逐一校验。
  4. schema = OrderInfo.model_json_schema()把 Pydantic 模型自动翻译成 JSON Schema,这一步是“表格式样”被机器读懂的关键。
  5. response_format={"type": "json_schema", "json_schema": {"name": "order", "schema": schema}}把 schema 通过 API 传给模型,强迫它在生成阶段就遵守表格约束。
  6. OrderInfo.model_validate(raw)把模型返回的 JSON 字典重新拉回 Pydantic 模型实例,数据格式一旦不合法会立刻抛出 ValidationError。
  7. except ValidationError as exc:捕获校验异常,并把 exc.errors() 中的错误明细拼进提示词回传给模型,驱动它“打回重填”。
  8. messages.append({"role": "user", ...})通过追加一条用户消息把错误信息送回对话,实现无需人工干预的自动重试循环。
  9. print(order.model_dump_json(indent=2))把最终校验通过的模型实例序列化成格式化的 JSON,方便直接入库或展示。

常见误区

误区我明明在 system prompt 里写了“必须输出 JSON,不要任何多余文字”,为什么程序还是一直报 JSONDecodeError?

正解提示词对 LLM 是“软约束”,就像你对 Tom 说“说话简洁点”,他答应后依然可能聊嗨。Pydantic 校验和 API 的 response_format 才是“硬约束”:前者在输出后拦截错误数据,后者在生成时就锁死格式。软硬结合才是可靠方案。

为何常见:大部分时候 LLM 确实会乖乖输出 JSON,偶尔的失败让小概率问题被低估,直到线上爆雷。

误区description 在程序运行时不参与任何逻辑,写不写都一样,纯属给开发者看的注释。

正解description 会通过 model_json_schema() 进入 JSON Schema,再随请求传给 LLM。它相当于表格栏位旁的“填写说明”,直接参与模型对字段语义的理解。描述写得越具体,模型输出的值越精准——比如只写“姓名”和写“用户完整真实姓名,如张三”,效果差别很大。

为何常见:Pydantic 在纯 Python 运行时确实忽略 description,初学者看不到它的直接效果,就误以为是装饰品。

误区校验失败后,我得自己写一堆 if 判断去检查每个字段到底是哪个错,像手工抄录一样累人。

正解ValidationError 自带的 errors() 已经给出结构化错误列表:字段名、错误类型、定位路径一应俱全。你只需要把这个列表拼进提示词,让 LLM 根据错误说明自己修正重试。就像签证官在表上划了红圈,你照圈改就行,不必整张表重填。

为何常见:错误信息冗长且充满术语,新手看一眼就发怵,习惯性选择自己手写解析器而不是利用现成的错误详情。

误区一提到嵌套模型,就担心校验开销大、响应时间翻倍,于是把所有字段全部拍平。

正解Pydantic 的模型校验在本地完成,开销是微秒级的,远小于一次 API 网络的几百毫秒延迟。响应速度主要由模型大小、输入长度和生成 token 数决定。嵌套模型只是“声明结构”,不会增加网络往返,也不会显著增加生成量——担心它拖慢性能属于过度焦虑。

为何常见:把“本地校验耗时”和“网络请求耗时”混为一谈,同时也低估了现代 Python 库的性能。

误区用 Pydantic 和 schema 把输出格式钉死,模型会不会变成只会填表的机器,回答质量变差?

正解结构化输出约束的是“回答的形式”,不是“思考的过程”。模型内部的推理链路完全不受影响,就像签证表格限制了 Tom 的书写格式,却限制不了他组织旅行故事的能力。实际经验中,结构化提示反而让模型更聚焦任务,减少跑题,生成质量往往更高。

为何常见:人们容易把“输出格式”肤浅地等同于“回答上限”,忽略了两者是相互独立的维度。

面试题

Q1. Pydantic的BaseModel核心作用是什么?

单选题 · easy

  • A.定义结构并自动校验← 正确答案
  • B.加速模型迭代训练
  • C.自动生成海量数据
  • D.部署服务节省资源

答案:A。BaseModel 就像一个“表格模板”,它用类型声明定义数据结构,并在数据进入时自动校验。B/C/D 分别涉及训练、数据生成和部署,都不是 Pydantic 的职责。用签证表的比喻来说,BaseModel 就是那张规定了每栏格式的申报单。

Q2. Field中description参数的主要作用是什么?

单选题 · easy

  • A.给字段设置数据密码
  • B.引导LLM按语义生成内容← 正确答案
  • C.对输出文本进行加密
  • D.直接减少API的调用量

答案:B。description 会进入 JSON Schema 传给 LLM,相当于表格栏位旁的“填写说明”,引导模型在生成时理解字段含义并正确填充。它不加密、不设密码,也不能减少调用量。

Q3. ValidationError出现时最合适的处理方式是什么?

单选题 · mid

  • A.忽略错误继续运行
  • B.将错误信息回传LLM重试← 正确答案
  • C.清空所有数据重新开始
  • D.修改数据库的表结构

答案:B。ValidationError 自带精确的错误定位(哪个字段、什么类型错误),最高效的做法是把 errors() 拼进提示词让模型修正重试,像签证官划出红圈退回重填。忽略会污染数据,清空重来和改数据库表结构都是杀鸡用牛刀。

Q4. 以下哪些措施能提高LLM输出的结构化稳定性?

多选题 · mid

  • A.使用response_format结构化参数← 正确答案
  • B.在提示词中要求输出JSON← 正确答案
  • C.用Pydantic进行二次校验← 正确答案
  • D.修改模型层数增大参数量

答案:ABC。response_format 是生成期硬约束,提示词是软约束,Pydantic 校验是输出后的兜底检查,三层叠加能显著提升稳定性。修改模型结构既不现实也不解决格式问题——就像换一位更健谈的朋友并不能保证他填表合规。

Q5. model_json_schema() 方法返回的是什么?

单选题 · mid

  • A.JSON Schema 规范文档← 正确答案
  • B.模型训练用数据集合
  • C.模型参数量统计报告
  • D.推理计算时延延迟表

答案:A。该方法把 Pydantic 模型自动翻译成符合 JSON Schema 规范的机器可读结构,用于传给 LLM API 做结构化输出。它不是训练数据、参数统计或性能报告。可以理解为把“表格模板”翻译成海关系统能识别的电子格式。

Q6. 在Pydantic中定义嵌套数据结构最标准的做法是什么?

单选题 · hard

  • A.在模型中嵌入子模型类型← 正确答案
  • B.用字典手动嵌套赋值
  • C.同时共享全局变量组
  • D.直接输出普通字符串组

答案:A。嵌套结构的标准做法是把另一个 BaseModel 子类作为字段类型,例如 class Order(BaseModel): user: User。这样 Pydantic 会自动递归校验每一层。字典手动嵌套没有类型检查,全局变量和字符串则完全丧失结构语义。

Q7. 校验失败后重试逻辑通常包含哪些步骤?

多选题 · mid

  • A.捕获ValidationError异常← 正确答案
  • B.将错误信息反馈给模型← 正确答案
  • C.跳过所有字段不再校验
  • D.重新调用同一段提示词

答案:AB。重试循环的骨架是:捕获异常 → 拼入错误信息 → 重新请求。C 跳过字段会让错误数据流入下游,D 没有把错误反馈给模型等于盲目重试。这就像退回来重填的同时要附上“错在哪”的批注,才能让模型真正纠正。

Q8. 将Pydantic模型实例序列化为JSON字符串的方法是什么?

单选题 · easy

  • A.model_dump_json() 方法← 正确答案
  • B.dict 转字符串方法
  • C.list 提升为元组方法
  • D.for 循环拼接方法

答案:A。Pydantic v2 中 model_dump_json() 是官方序列化方法,输出标准 JSON 字符串。其他选项都不是模型自带的序列化手段。序列化相当于把填好的表格复印一份交给下游系统,必须有标准出口。

Q9. 从多篇新闻中批量抽取人物、时间和地点信息,应选择哪种模型结构?

单选题 · hard

  • A.嵌套模型配合列表字段← 正确答案
  • B.纯文本加正则表达式
  • C.单个字符串变量保存
  • D.无schema的自由提示词

答案:A。批量抽取天然适合“外层模型 + 内层子模型 + 列表字段”的结构,比如 class ArticleSet(BaseModel): articles: list[Article]。正则和纯字符串无法应对语义多样性,自由提示词得不到稳定格式。就像一张总表下面挂一排子表,才能容纳重复的同类信息。

Q10. AI Agent中使用Pydantic结构化输出的价值有哪些?

多选题 · hard

  • A.为工具调用提供规范参数← 正确答案
  • B.让模型返回可验证的结果← 正确答案
  • C.完全替代人类审核环节
  • D.训练时自动冻结所有层

答案:AB。在 Agent 场景中,Pydantic 保证工具参数(如天气查询的城市、日期)格式正确,同时让返回结果可被机器验证,这两点直接提升自动化可靠性。C 夸大了作用——结构化只能保证格式合法,不能保证语义正确,人工抽查仍不可免;D 与结构化输出毫无关系。