Post

【Python库】Pydantic初体验:数据校验即数据解析

字典的烦恼:为什么我们需要Pydantic? 让我们回到上一节课“工具调用”的例子。我们通过强制 Tool Calls,得到了一个包含用户信息的 JSON 字 符串,例如: 示例1: '{"expression": "123*456"}' ​ ​ 示例2: '{"name": "张三", "age"

先搞技术 阅读 0 点赞 0 评论 0

字典的烦恼:为什么我们需要Pydantic?

让我们回到上一节课“工具调用”的例子。我们通过强制 Tool Calls,得到了一个包含用户信息的 JSON 字 符串,例如:

示例1:
'{"expression": "123*456"}'
​
​
示例2:
'{"name": "张三", "age": 25, "email": "zhangsan@example.com"}'

在代码中我们使用 json.loads() 进行处理,得到了一个字典,但是问题也随之而来:

  • 如何保证age一定是个数字? 如果 LLM “幻觉”了一下,返回了"age": "二十五岁",你的后续代码可 能直接崩溃。

  • 如何检查email格式是否正确? 你需要自己写一堆正则表达式来校验。

  • 代码可读性差 :user_data['name'] 这样的“魔术字符串”会遍布你的代码,既不优雅,IDE 也无法提 供自动补全。

  • 数据结构不清晰 :你的函数签名只能写 def process_user(data: dict),别人根本不知道这个字典里 到底该有什么。

除此之外,我们传递给 LLM 的工具参数说明也是我们手动编写的一段 json,维护也没有这么便捷,如果 工具发生修改,还需要修改硬编码到代码中的参数说明 json 内容。

Pydantic 就是为了解决以上所有问题而生的,Pydantic 的核心思想其实非常简单: 用你熟悉的 Python 类型提示(Type Hints)来定义数据结构,剩下的数据校验、格式转换等全部交给 Pydantic,它会完成

Pydantic初体验:代码即文档,类型即保障

要想使用 Pydantic,首先使用 uv 安装它(目前最新版本为 v2,同时安装了 Pydantic 的邮件校验功 能):

uv add pydantic pydantic[email]

接下来就可以为用户信息定义一个 BaseModel,将 BaseModel 作为:

from pydantic import BaseModel, Field, EmailStr
​
class UserInfo(BaseModel):
    """传递用户的信息进行数据提取&处理,涵盖name、age、email等"""
    name: str = Field(..., description="用户名字")
    age: int = Field(..., gt=0, description="用户年龄,必须是正整数")
    email: EmailStr = Field(..., description="用户的电子邮件")

这段代码理解起来也非常简单:

定义了一个 UserInfo 类,继承自 pydantic.BaseModel。

我们用最自然的 Python 类型提示(str, int)来声明字段。

  • Pydantic的魔法之一 :它提供了 EmailStr 等大量这种开箱即用的“富类型”,它会自动校验字符串是 否符合邮箱格式!

  • Field(..., gt=0):我们不仅定义了age是int,还加了一条规则:它必须大于0 (gt > greater than)。

现在就可以用这个模型来处理 LLM 返回的 JSON 字符串,示例代码如下:

# 假设这是从Tool Calls的arguments中获取的字符串
json_string = '{"name": "张三", "age": 25, "email": "zhangsan@example.com"}'
​
# --- Pydantic的优雅之道 ---
try:
    user = UserInfo.model_validate_json(json_string)  # Pydantic V2 of validation
    # 得到的是一个真正的Python对象,而不是字典!
    print(f"解析成功!用户名: {user.name}")
    print(f"用户年龄: {user.age}")
    print(f"用户邮箱: {user.email}")
    print(user)  # 打印出的对象清晰明了
except Exception as e:
    print(f"数据校验失败: {e}")
​
# --- 让我们试试错误数据 ---
invalid_json_string = '{"name": "李四", "age": -5, "email": "not-an-email"}'
try:
    UserInfo.model_validate_json(invalid_json_string)
except Exception as e:
    print("\n--- 错误数据测试 ---")
    print(f"数据校验失败:\n{e}")

输出内容如下:

解析成功!用户名: 张三
用户年龄: 25
用户邮箱: zhangsan@example.com
name='张三'age=25email='zhangsan@example.com'
------
错误数据测试
数据校验失败:
1 validation error for UserInfo
email
  value is not a valid email address: An email address must have an @-sign.
[type=value_error, input_value='not-an-email', input_type=str]

效果非常赞,UserInfo.model_validate_json() 这一行代码, 同时完成了三件事

  1. 解析 JSON字符串。

  2. 校验 所有字段的类型和我们定义的规则(年龄>0,邮箱格式正确)。

  3. 将数据 实例化 为一个user对象,我们可以通过.来访问属性,享受IDE的自动补全!确保后续程序的稳健性!

这就是 数据校验即数据解析 。Pydantic将原本繁琐、易错的数据处理流程,变成了一行声明式的、极其健壮的代码。

Pydantic V2实用技巧

Pydantic V2带来了性能提升和更多强大的功能,我们来学习几个在Agent开发中特别有用的技巧(更多 技巧我们会在项目开发中边开发边学习),例如有时候,我们需要更复杂的校验逻辑。比如,我们规定 用户名不能是“admin”,可以使用如下代码实现:

from pydantic import BaseModel, field_validator
​
class User(BaseModel):
    name: str
    
    @classmethod
    @field_validator('name')
    def name_must_not_be_admin(cls, value: str) -> str:
        if 'admin' in value.lower():
            raise ValueError("用户名不能包含'admin'")
        return value.title()  # 顺便还能对数据进行清洗,比如首字母大写
​
# test
if __name__ == "__main__":
    try:
        user = User(name="test_admin")  # 这会直接抛出ValueError
    except ValueError as e:
        print(f"验证失败: {e}")
        
    user_ok = User(name="jason")
    print(user_ok.name)  # 输出: Jason

甚至可以基于已有字段,计算出新字段:

from pydantic import BaseModel, EmailStr, computed_field
​
class UserWithUsername(BaseModel):
    name: str
    email: EmailStr
    
    @computed_field
    @property
    def username(self) -> str:
        # 从邮箱中提取用户名
        return self.email.split('@')[0]
​
# test
if __name__ == "__main__":
    user = UserWithUsername(name="张三", email="zhangsan_cool@example.com")
    print(user.username)  # 输出: zhangsan_cool
    print(user.model_dump())  # dump出来的json也会包含这个衍生字段

导出模型(model_dump, model_dump_json)以适用于不同的场景(在 API 响应场景使用频率很 高),如下:

user = UserInfo(name="张三", age=25, email="test@test.com")
# 导出为字典
user_dict = user.model_dump()
print(user_dict)  # {'name': '张三', 'age': 25, 'email': 'test@test.com'}
​
# 直接导出为JSON字符串
user_json = user.model_dump_json(indent=2)
print(user_json)

从字典/JSON创建模型 (model_validate、model_validate_json),常用于从接口、数据库中的数据快速 创建 Python 对象:

data_dict = {"name": "李四", "age": 30, "email": "lisi@example.com"}
user = UserInfo.model_validate(data_dict)

除此之外,还可以快速将 BaseModel 的属性生成符合 OpenAI Tool Calls 格式的工具参数声明,也能极 大降低了维护难度(这也是 AI 应用开发框架 LangChain 目前的做法,使用 BaseModel 来声明工具的输 入参数):

from pydantic import BaseModel, Field, EmailStr

class UserInfo(BaseModel):
    """传递用户的信息进行数据提取&处理,涵盖name、age、email等"""
    name: str = Field(..., description="用户名字")
    age: int = Field(..., description="用户年龄,必须是正整数")
    email: EmailStr = Field(..., description="用户的电子邮件")

tools = [
    {
        "type": "function",
        "function": {
            "name": UserInfo.__name__,
            "description": UserInfo.__doc__,
            "parameters": UserInfo.model_json_schema(),
        }
    }
]

Tool Calls+Pydantic结合实现数据提取

现在让我们结合上节课所学习的 Tool Calls、tool_choice参数、Pydantic 来实现让 LLM 将一段文字中的 信息提取成 json 结构化的数据,假设有一段话 “用户名泽辉呀,18岁,联系方式zehuiya@163.com”, 想提取出其中的 usernameageemail ,我们就可以使用 BaseModel 构建一个 虚拟工具 ,然后将 工具信息传递给 LLM,让 LLM 强制调用这个工具,从而实现数据的稳定结构化。

import dotenv
from openai import OpenAI
from pydantic import BaseModel, Field, EmailStr

dotenv.load_dotenv()

class UserInfo(BaseModel):
    """传递用户的信息进行数据提取&处理,涵盖name、age、email等"""
    name: str = Field(..., description="用户名字")
    age: int = Field(..., description="用户年龄,必须是正整数")
    email: EmailStr = Field(..., description="用户的电子邮件")

client = OpenAI()

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "user", "content": "用户名泽辉呀,18岁,联系方式zehuiya@163.com"}
    ],
    tools=[
        {
            "type": "function",
            "function": {
                "name": UserInfo.__name__,
                "description": UserInfo.__doc__,
                "parameters": UserInfo.model_json_schema(),
            }
        }
    ],
    tool_choice={"type": "function", "function": {"name": UserInfo.__name__}}
)

tool_args = response.choices[0].message.tool_calls[0].function.arguments

user_info = UserInfo.model_validate_json(tool_args)
print(user_info)

输出内容

name='泽辉呀'age=18email='zehuiya@163.com'

很简单吧,通过一个巧妙的技巧就可以确保每次提取出来的数据只要不报错就 100% 是正确可以使用 的,哪怕报错我们也可以通过 控制循环 来让 LLM 基于错误多次处理生成,直到拿到稳定的结果,学习 到这里,你已经掌握了创建 调度 AgentMCP 的前置知识了(稳定的让 LLM 分析&拆解任务并格式化 输出)。

Pydantic 核心功能速查

功能

作用

示例

BaseModel

定义数据模型的基础类

class User(BaseModel): ...

Field

字段约束(长度、范围、默认值等)

name: str = Field(min_length=1)

field_validator

单字段自定义校验

@field_validator("name")

model_validator

跨字段校验

@model_validator(mode="after")

TypeAdapter

验证非 BaseModel 类型

TypeAdapter(list[int]).validate_json(...)

ValidationError

捕获校验错误

except ValidationError as e:

序列化/反序列化:

方法

作用

.model_dump()

转 dict

.model_dump_json()

转 JSON 字符串

.model_validate()

从 dict 创建实例

.model_validate_json()

从 JSON 字符串创建实例

.model_json_schema()

生成 JSON Schema

常用字段类型:

类型

说明

str, int, float, bool

基础类型

Optional[str]

可选字段(可为 None)

list[str]

列表

dict[str, int]

字典

EmailStr

邮箱格式校验

datetime

日期时间

Literal["a", "b"]

枚举值

Field 常用参数:

参数

作用

default

默认值

min_length / max_length

字符串长度限制

gt / ge / lt / le

数值大小限制(大于/大于等于/小于/小于等于)

pattern

正则表达式匹配

description

字段描述(用于文档)

示例:

from pydantic import BaseModel, Field, EmailStr
from typing import Optional

class User(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, description="用户名")
    age: int = Field(default=0, ge=0, le=150)
    email: EmailStr
    nickname: Optional[str] = None

... 表示必填字段,没有默认值。

评论