字典的烦恼:为什么我们需要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() 这一行代码, 同时完成了三件事 :
解析 JSON字符串。
校验 所有字段的类型和我们定义的规则(年龄>0,邮箱格式正确)。
将数据 实例化 为一个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”, 想提取出其中的 username 、 age 和 email ,我们就可以使用 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 基于错误多次处理生成,直到拿到稳定的结果,学习 到这里,你已经掌握了创建 调度 Agent 、 MCP 的前置知识了(稳定的让 LLM 分析&拆解任务并格式化 输出)。
Pydantic 核心功能速查
序列化/反序列化:
常用字段类型:
Field 常用参数:
示例:
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
... 表示必填字段,没有默认值。
评论