Towards Data Science

Pydantic + OpenAI: The Cleanest Way to Get Structured Outputs from LLMs

8.5内容质量

TL;DR · AI 摘要

Pydantic与OpenAI结构化输出结合能高效处理LLM输出,提升Python应用可靠性。

核心要点

  • Pydantic通过类型注解自动验证数据结构,减少手动解析JSON的错误
  • OpenAI结构化输出保证返回JSON符合预定义schema
  • 组合使用可将LLM输出直接转换为带类型校验的Python对象

结构提纲

按章节快速跳转。

  1. 介绍LLM结构化输出的三种主流方法及本文重点

  2. 解释Pydantic如何通过类型注解实现数据验证

  3. OpenAI结构化输出

    说明OpenAI如何保证JSON输出符合预定义schema

  4. 阐述Pydantic与OpenAI结合后的优势及实现方式

  5. 展示PersonInfo类与OpenAI API集成的完整流程

  6. 论证该组合在提升LLM应用可靠性方面的具体效果

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Pydantic + OpenAI结构化输出
    • Pydantic作用
      • 类型注解验证
      • 自动数据解析
      • 错误提示机制
    • OpenAI功能
      • 结构化输出保证
      • schema约束解码
      • JSON格式返回
    • 整合优势
      • 减少手动处理
      • 类型安全保证
      • 提升开发效率

金句 / Highlights

值得收藏与分享的关键句。

#Pydantic#OpenAI#LLM#Python#数据验证
打开原文

Pydantic + OpenAI:从LLMs获取结构化输出的最简洁方式 | Towards Data Science

LLM应用

Pydantic + OpenAI:从LLMs获取结构化输出的最简洁方式

如何停止手动解析JSON并开始信任模型的输出

Maria Mouschoutzi

2026年7月14日

10分钟阅读

分享

作者使用ChatGPT Images 2.0创建的图片

在我关于结构化输出的最新文章中,我们讨论了从LLM获取可被机器读取响应的三种主要方法。这些方法分别是JSON模式、函数调用和OpenAI的结构化输出。如果你还没有阅读那篇文章,建议在阅读本文前快速浏览一遍,因为我们将在其基础上直接构建内容。

今天,我们将更进一步,讨论一个会改变结构化输出实际体验的工具——Pydantic。更具体地说,虽然OpenAI的结构化输出功能保证了模型返回符合模式的JSON,但我们在Python端仍需要对这些JSON进行处理。特别是需要解析数据、验证数据类型、访问字段、处理意外值等。而Pydantic正是解决这些问题的关键。

在我看来,Pydantic与OpenAI结构化输出的结合,是目前构建可靠LLM驱动应用最简洁的方案。到本文结束时,你将清楚了解原因。

关于Pydantic

Pydantic是一个基于类型注解的Python数据验证库。这意味着你可以通过定义Python类来描述数据的形状和类型,然后Pydantic会验证传入的数据是否符合该描述。如果不符合,Pydantic会抛出清晰的错误信息,而不是让错误数据在系统中无声传播。

以下是可能的最简单Pydantic模型:

code
from pydantic import BaseModel

class PersonInfo(BaseModel):
    name: str
    age: int
    city: str

通过这种方式,我们现在有了一个强制要求name和city始终为字符串、age始终为整数的模式。如果有人尝试用age="thirty-two"创建PersonInfo实例,Pydantic会立即捕获并明确告知问题所在。

但这不是我们之前用函数调用和结构化输出已经实现的功能吗?是的,但存在一个关键区别。

一方面,使用JSON模式或函数调用时,模型可能返回与我们预期模式匹配或不匹配的响应。即使返回了正确的模式,它仍只是原始JSON字符串,需要我们在Python端手动解析字段、转换类型和验证值。

另一方面,结构化输出通过模型层面的约束解码保证了返回的JSON始终符合我们定义的模式。但即便如此,它仍然是需要我们在Python端自行处理的JSON字符串。

而使用Pydantic时,我们通过Python类定义模式,与OpenAI API的集成意味着我们获得的是真正的Python对象(而非字典),所有字段都会自动完成类型检查和验证。API所需的JSON Schema会从我们的Pydantic模型自动生成,我们无需手动编写。换句话说,Pydantic作为模式定义层位于Python代码与OpenAI API之间,使整个结构化输出体验更简洁、安全且易于维护。

🍨 DataCream 是一份关于人工智能、数据和技术的通讯。如果你对这些主题感兴趣,请在这里订阅!

但让我们实际看看使用 Pydantic 相比仅使用结构化输出能带来什么优势。

在 Pydantic 集成之前,获取结构化输出的实现方式如下:

code
from openai import OpenAI
import json

client = OpenAI(api_key="your_api_key")

# 以原始字典形式定义 schema
tools = [
    {
        "type": "function",
        "function": {
            "name": "extract_person_info",
            "strict": True,
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "city": {"type": "string"}
                },
                "required": ["name", "age", "city"],
                "additionalProperties": False
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "extract_person_info"}},
    messages=[
        {"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
    ]
)

# 手动解析 —— 我们得到的是一个普通字典
result = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
print(result["name"])   # "Maria" —— 但如果键缺失了怎么办?KeyError.
print(result["age"])    # 可能是 "32" 而不是 32 —— 类型无法保证

使用 Pydantic 的实现方式如下:

code
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI(api_key="your_api_key")

class PersonInfo(BaseModel):
    name: str
    age: int
    city: str

response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
    ],
    response_format=PersonInfo
)

# 我们得到的是一个完整的 Python 对象,类型安全且经过验证
result = response.choices[0].message.parsed
print(result.name)   # "Maria" —— 始终是字符串
print(result.age)    # 32 —— 始终是整数,从不会是字符串
print(result.city)   # "Athens" —— 始终是字符串

显然,Pydantic 的实现更简洁、更易读且更安全。注意我们是如何将 Pydantic 模型传递给 response_format 的,OpenAI SDK 会自动处理 JSON Schema 的生成,仅需设置 strict: True 并将响应解析为完整的 PersonInfo 对象。通过这种方式,我们实现了类型安全、验证功能以及字段的点符号访问。

此外,注意使用了 .parse() 方法而不是常规的 .create()。这是因为 .parse() 是 OpenAI SDK 专为与 Pydantic 模型配合使用而设计的方法,可以端到端处理完整的结构化输出流程。

使用 Pydantic 构建复杂结构

#### 1. 嵌套模型和列表

Pydantic 真正闪耀的地方在于处理嵌套数据结构。原始 JSON Schema 对嵌套对象的编写非常繁琐,但使用 Pydantic 只需要定义 Python 类:

code
from pydantic import BaseModel
from typing import List

class Address(BaseModel):
    street: str
    city: str
    country: str

class ContactInfo(BaseModel):
    name: str
    email: str
    address: Address
    phone_numbers: List[str]
python
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "user",
            "content": """从以下内容中提取联系信息:
            'Maria Mouschoutzi,
[email protected]
, 
            Ermou 15, Athens, Greece. 
            Phone: +30 210 1234567, +30 697 8901234'"""
        }
    ],
    response_format=ContactInfo
)

contact = response.choices[0].message.parsed
print(contact.name)                 # "Maria Mouschoutzi"
print(contact.address.city)        # "Athens"
print(contact.phone_numbers[0])    # "+30 210 1234567"

Address 模型自然地嵌套在 ContactInfo 内部,Pydantic 会处理嵌套结构的完整验证。我们可以通过清晰的点符号访问字段,例如 contact.address.city 而不是 result["address"]["city"],后者在中间键缺失时可能引发 KeyError。因此,Pydantic 在需要嵌套数据结构的场景中特别实用。

#### 2. 使用 Pydantic 字段进行验证

Pydantic 另一个非常有用的任务是返回数据的验证。这种验证包括检查数据类型,但不仅如此,因为 Pydantic 还允许对返回值的实际内容施加明确的约束。更具体地说,我们可以使用 Field 添加显式的验证约束,为模型提供关于哪些值可接受、哪些不可接受的指令。

例如,假设我们有一个返回产品评论数据的设置:

python
from pydantic import BaseModel, Field
from typing import Optional

class ProductReview(BaseModel):
    product_name: str = Field(description="The name of the product being reviewed")
    rating: int = Field(ge=1, le=5, description="Rating from 1 to 5 stars")
    sentiment: str = Field(description="One of: positive, neutral, negative")
    summary: str = Field(max_length=200, description="A brief summary of the review")
    verified_purchase: Optional[bool] = Field(default=None, description="Whether this is a verified purchase")

response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "user",
            "content": """从以下内容中提取结构化评论:
            'Absolutely love this coffee machine! 
            Makes perfect espresso every time. 
            5 stars, would recommend to anyone.'"""
        }
    ],
    response_format=ProductReview
)

review = response.choices[0].message.parsed
print(review.product_name)   # "coffee machine"
print(review.rating)         # 5
print(review.sentiment)      # "positive"
print(review.summary)        # "Makes perfect espresso every time."

特别是 rating 字段的 ge=1, le=5 告诉模型有效评分范围是 1 到 5。summary 的 max_length=200 确保我们永远不会收到超过 200 个字符的响应。

此外,description 字段作为模型的指令,帮助其准确理解每个字段应包含的内容。这本质上等同于我们在函数调用模式中手动编写的描述字段,指导模型在每个字段中应填入什么内容。

#### 3. 拒绝处理

另一个值得关注的亮点是,通过使用 .parse() 方法,OpenAI SDK 能够优雅地处理模型拒绝的情况。具体来说,当模型拒绝完成某个请求(例如,因为内容违反了模型的政策),parsed 字段将为 None,而 refusal 字段会包含拒绝的原因。这使我们即使在遇到拒绝的情况下,也能获得结构化的结果,至少可以向应用用户说明其请求被拒绝的原因。对于生产环境中运行的 AI 驱动型应用而言,这一点尤为重要,因为我们无法真正预测用户可能会提出什么样的奇怪请求。

因此,使用 Pydantic 处理模型拒绝的流程如下:

python
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "从上面的工作描述中提取信息。"}],
    response_format=JobPosting
)

message = response.choices[0].message

if message.refusal:
    print(f"模型拒绝: {message.refusal}")
else:
    job = message.parsed
    print(f"提取结果: {job.job_title} 在 {job.company_name}")

实际案例:文档信息提取

让我们通过一个更贴近现实的示例来整合所有内容。假设我们正在构建一个处理工作描述并从中提取结构化信息的流水线。这正是结构化输出大显身手的场景:非结构化输入、定义明确的输出模式,以及需要将结果插入数据库的下游系统。

python
from pydantic import BaseModel, Field
from typing import List, Optional
from openai import OpenAI

client = OpenAI(api_key="your_api_key")

class SalaryRange(BaseModel):
    min_salary: Optional[int] = Field(default=None, description="年薪最低值(单位:美元)")
    max_salary: Optional[int] = Field(default=None, description="年薪最高值(单位:美元)")
    currency: str = Field(default="USD", description="货币代码")

class JobPosting(BaseModel):
    job_title: str = Field(description="职位名称或角色名称")
    company_name: str = Field(description="招聘公司的名称")
    location: str = Field(description="工作地点,例如 '希腊雅典' 或 '远程'")
    employment_type: str = Field(description="工作类型,可选值为:full-time, part-time, contract, freelance")
    required_skills: List[str] = Field(description="所需技术技能列表")
    years_of_experience: Optional[int] = Field(default=None, description="所需的最低工作经验年限")
    salary: Optional[SalaryRange] = Field(default=None, description="如果提及,薪资范围")
    remote_friendly: bool = Field(description="是否允许远程办公")

job_post_text = """
Senior Python Engineer at pialgorithms (Athens, Greece / Remote)

我们正在寻找一位经验丰富的 Python 开发者加入我们的 AI 团队。
您将负责我们的文档管理系统,使用 LLM 和 RAG 管道构建智能搜索和提取功能。

要求:
- 5 年以上 Python 开发经验
- 精通 FastAPI、LangChain 和向量数据库
- 具备 OpenAI API 和 Pydantic 使用经验
- 熟悉 Docker 和云部署(AWS/GCP)

薪资:6万欧元 - 8.5万欧元 每年
全职职位。支持远程办公。
"""
python
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": "你是从职位描述中提取结构化信息的专家。"
        },
        {
            "role": "user",
            "content": f"从这份职位描述中提取所有相关信息:\n\n{job_post_text}"
        }
    ],
    response_format=JobPosting
)

job = response.choices[0].message.parsed

print(f"职位名称: {job.job_title}")
print(f"公司名称: {job.company_name}")
print(f"工作地点: {job.location}")
print(f"远程办公: {job.remote_friendly}")
print(f"所需技能: {', '.join(job.required_skills)}")
print(f"工作经验: {job.years_of_experience} 年")
if job.salary:
    print(f"薪资范围: {job.salary.min_salary} - {job.salary.max_salary} {job.salary.currency}")
code
职位名称: 高级Python工程师
公司名称: pialgorithms
工作地点: 希腊雅典 / 远程
远程办公: True
所需技能: Python, FastAPI, LangChain, 向量数据库, OpenAI API, Pydantic, Docker, AWS, GCP
工作经验: 5 年
薪资范围: 60000 - 85000 EUR

这段代码已经可以投入生产环境使用。嵌套的SalaryRange模型能够优雅地处理可选的薪资信息,当信息缺失时Optional字段会自动默认为None,响应中的每个字段都会自动进行类型定义和验证。提取结果可以直接插入数据库,或作为流水线后续步骤的输入,无需任何额外处理。

我的思考

我认为Pydantic与OpenAI的结合最令人满意之处在于,它完美契合了Python开发者的思维方式。它通过类定义数据结构,使用类型提示,并能及早、明确地捕获类型错误。这些传统上是AI应用中最不可预测的部分,因为我们过去必须手动在Python端完成所有这些工作。Pydantic作为AI模型与Python代码其余部分之间更强大、更可靠的连接方式,显著提升了开发效率。

✨ 感谢阅读!✨

如果你读到这里,可能会对pialgorithms感兴趣:这是我们正在构建的平台,帮助团队在一个安全的地方管理组织知识。

喜欢这篇文章?请关注我的💌 Substack和💼 LinkedIn

所有图片均由作者拍摄,除非另有说明。

作者:Maria Mouschoutzi

数据科学

,

编辑精选

Json

Llm

Pydantic

分享本文

  • 在Facebook上分享
  • 在LinkedIn上分享
  • 在X上分享

Towards Data Science是社区出版物。提交你的见解以触达全球受众,并通过TDS作者支付计划获得收益。

更新为你的实际提交URL

为TDS写作

✦ 结束CTA ✦