Pydantic对象——从数据校验到Agent工程化

在 FastAPI 和 Agent 中,程序经常要处理来自 HTTP 请求、环境变量和大模型的外部数据。这些数据可能缺少字段、类型错误,或者包含未约定的内容。

查看 CSDN 原文

问题背景

在 FastAPI 和 Agent 中,程序经常要处理来自 HTTP 请求、环境变量和大模型的外部数据。这些数据可能缺少字段、类型错误,或者包含未约定的内容。

如果始终使用普通字典处理,校验逻辑就会分散在各个函数中。Pydantic 的作用,是把数据结构和校验规则集中起来,在数据进入业务逻辑之前完成检查。

本文从 Pydantic 对象的基本概念开始,介绍 BaseModel、Field、ConfigDict 和常用类型约束,再说明它们在 Agent 开发中的使用方式。

一、Pydantic对象

1.1 Pydantic是什么

Pydantic 是一个用于数据校验和序列化的 Python 库。它的核心不是某个单独的函数,而是一套“根据类型声明处理数据”的模型机制。

在 Pydantic 中,模型通常继承自 BaseModel。BaseModel 是 Pydantic 提供的基类,负责读取字段定义、执行运行时校验以及提供序列化方法。

可以把它理解为:

BaseModel 是一个带有类型校验和数据转换能力的 Python 类基类。

因此,平时所说的“Pydantic 对象”,通常就是继承 BaseModel 的模型类所创建出来的实例。

例如:

from pydantic import BaseModel


class User(BaseModel):
	user_id: int
	user_name: str

User 是我们定义的模型类,user_id 和 user_name 是模型字段。创建对象时,Pydantic 会根据字段注解检查输入数据:

user=User(
	user_id=1,
	user_name="Rray",
)

因此,Pydantic 对象和普通 Python 对象的区别在于:它不仅保存数据,还保存了数据的结构和校验规则。

1.2 和字典有什么区别

普通字典没有固定结构:

user_data={
	"user_id":1,
	"user_name":"Rray",
}

后续代码需要自己判断字段是否存在、类型是否正确以及是否允许多余字段。Pydantic 模型则把这些约定写在类定义中。

类型运行时校验序列化能力主要用途
dict无手动处理临时数据
普通类自行实现自行实现复杂行为对象
dataclass默认没有自行处理结构清晰的内部数据
TypedDict主要依赖静态检查无只需要类型提示的字典
BaseModel有内置外部输入和数据边界

这并不意味着所有数据都应该使用 Pydantic。临时的局部数据使用字典更直接;拥有复杂生命周期和行为的对象,也不应该强行变成数据模型。Pydantic 更适合处理跨越模块或系统边界的数据。

1.3 字段类型

模型字段使用 Python 类型注解描述。比如 int 表示整数,str 表示字符串。

如果字段只能取固定值,可以使用 Literal。Literal 是 Python 类型系统中的一种类型约束,表示字段只能等于列出的值:

from typing import Literal

from pydantic import BaseModel


class Message(BaseModel):
	role: Literal["user","assistant","tool"]
	content: str

这里的 role 不是任意字符串,只能是三个值之一。Literal 不是 Pydantic 独有的类,而是 Python 提供的类型工具;Pydantic 会读取它并在运行时执行校验。

如果传入不允许的值,模型创建会失败。对于需要集中管理、复用或附加行为的枚举值,可以使用 Enum。Literal 更适合简单的固定值约束,Enum 更适合具有独立语义的状态集合。

1.4 常用方法

Pydantic v2 中,最常用的方法有以下几个。

model_validate 用于把字典或对象转换为模型对象:

data={
	"user_id":1,
	"user_name":"Rray",
}

user=User.model_validate(data)

model_dump 把模型对象转换成字典,model_dump_json 把模型转换成 JSON 字符串:

user_data=user.model_dump()
user_json=user.model_dump_json()

如果输入本身已经是 JSON 字符串,可以使用 model_validate_json:

user=User.model_validate_json(
	'{"user_id":1,"user_name":"Rray"}'
)

model_json_schema 用于生成 JSON Schema。FastAPI 可以根据模型生成接口文档,Agent 也可以使用参数模型生成工具描述:

tool_schema=User.model_json_schema()

这些方法对应一条常见的数据链:JSON 字符串 → Python 字典 → Pydantic 对象 → Python 字典或 JSON 字符串。解析、校验、业务处理和序列化分别承担不同职责。

1.5 校验和转换

Pydantic 不只是检查输入是否符合类型,还可能根据目标类型进行转换。例如,普通模式下字符串形式的数字可能被转换成整数,嵌套字典也可能被转换成对应的模型对象。

因此,Pydantic 更准确的定位是“带解析能力的数据校验框架”,而不是单纯的静态类型检查器。TypedDict 主要服务于类型提示,Pydantic 则会在程序运行时处理输入数据。

strict=True 可以减少这类隐式转换,但严格模式也应该根据数据来源决定。配置文件、表单数据和模型生成的 JSON,适合的策略可能并不相同。

1.6 嵌套模型

Pydantic 的价值不只在于校验单个字段,也可以递归校验一整棵数据结构:

class ChatRequest(BaseModel):
	messages: list[Message]

创建 ChatRequest 时,列表中的每个元素都会按照 Message 的规则校验。实际的 Agent 请求通常包含消息列表、工具调用列表和工具结果,嵌套模型可以把这些结构表达成稳定的数据契约。

二、Field和ConfigDict

类型注解只能描述基本类型。字段范围、默认值、别名和额外字段等规则,需要通过 Field 和 ConfigDict 补充。

2.1 Field

Field 用于配置单个字段。它既可以定义默认值,也可以增加约束、说明和别名。

from pydantic import BaseModel, Field


class SearchArguments(BaseModel):
	keyword: str=Field(
		min_length=1,
		max_length=100,
		description="搜索关键词",
	)
	page: int=Field(
		default=1,
		ge=1,
	)
	page_size: int=Field(
		default=20,
		ge=1,
		le=100,
	)

这里的约束分别表示:

写法作用
default=1字段未传入时使用默认值
default_factory=list每次创建对象时生成一个新的可变对象
ge=1数值大于等于 1
gt=0数值大于 0
le=100数值小于等于 100
lt=100数值小于 100
min_length=1字符串或集合的最小长度
max_length=100字符串或集合的最大长度
pattern=...字符串必须符合指定正则表达式
description=...为接口文档或工具 Schema 提供说明
alias="..."指定外部数据使用的字段名
validation_alias="..."指定校验输入时使用的别名

约束的价值在于把规则放在数据模型中,而不是让每个业务函数重复判断。例如 page_size 的范围只需要定义一次,FastAPI、工具调用和测试都可以复用。

Field 适合表达通用、局部、声明式的约束。它不应该承担所有业务判断。例如,page_size<=100 可以写在字段上;“开始时间必须早于结束时间”属于多个字段之间的关系,更适合使用模型级校验;“当前用户是否有权限访问资源”则需要业务服务结合数据库或权限系统判断。

可以把校验职责分成三层:

字段类型与范围 → 字段或模型校验 → 权限、数据库和外部服务规则

这样可以避免把复杂业务逻辑全部塞进 Pydantic 模型。

Pydantic v2 提供了 field_validator 和 model_validator。前者适合补充单个字段的特殊校验,后者适合检查多个字段之间的关系。它们仍然属于数据模型校验,不能替代权限判断、数据库查询和外部服务调用。

2.2 默认值

固定默认值可以直接写在字段上:

class AgentConfig(BaseModel):
	model_name: str="mock-model"
	temperature: float=0.7

列表、字典和时间等需要每次重新创建的值,应使用 default_factory:

from pydantic import BaseModel, Field


class TraceInfo(BaseModel):
	tags: list[str]=Field(default_factory=list)

default_factory=list 会在每次创建 TraceInfo 时生成新的列表,避免不同对象意外共享同一份可变数据。

2.3 别名

当外部字段名和 Python 属性名不一致时,可以使用别名:

from pydantic import BaseModel, Field


class User(BaseModel):
	user_name: str=Field(alias="userName")

配置模型中也可以使用 validation_alias 读取环境变量:

from pydantic import Field
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
	model_name: str=Field(
		default="mock-model",
		validation_alias="MODEL_NAME",
	)

程序内部使用 settings.model_name,部署环境使用 MODEL_NAME,两者职责清晰。

输入名称和输出名称不一定相同。validation_alias 主要用于读取外部输入,序列化时可以使用 serialization_alias,并通过 model_dump(by_alias=True) 输出外部协议要求的字段名。这样同一个模型可以连接“外部名称、内部 Python 名称和外部输出名称”三个层次。

2.4 ConfigDict

ConfigDict 用于设置整个模型的行为。

strict=True 可以减少隐式类型转换:

from pydantic import BaseModel, ConfigDict


class ToolArguments(BaseModel):
	model_config=ConfigDict(strict=True)
	user_id: int

在直接传入 Python 对象时,user_id="12" 不会被当作正常整数。严格模式是否适合使用,要根据数据来源判断;配置读取和兼容旧接口有时需要保留一定的转换能力。

extra 用于处理模型没有声明的字段:

class ToolArguments(BaseModel):
	model_config=ConfigDict(extra="forbid")
	user_id: int

它有三种常见取值:

  • ignore:忽略额外字段;
  • forbid:出现额外字段时校验失败;
  • allow:保留额外字段。

工具参数通常适合 forbid,因为错误的字段名应该尽早暴露;配置模型通常可以使用 ignore,避免其他环境变量影响当前程序。

如果字段使用别名,同时希望输入时接受字段名和别名,可以使用:

model_config=ConfigDict(
	validate_by_name=True,
	validate_by_alias=True,
)

validate_by_name 和 validate_by_alias 适用于较新的 Pydantic v2 版本,旧版本项目需要先确认配置项是否可用。

frozen=True 用于禁止模型字段重新赋值,validate_assignment=True 用于在字段被修改时再次执行校验。前者适合不可变上下文,后者适合需要更新的状态对象。

三、类型约束

3.1 Literal和Enum

Literal 直接限制字段的可选值:

class Response(BaseModel):
	status: Literal["success","error"]

如果状态值需要被多个模块复用,或者需要附加方法,可以使用 Enum。两者都能参与 Pydantic 校验,选择标准是是否需要独立的枚举语义。

3.2 Any

Any 表示不对字段内容施加具体类型约束:

from typing import Any


class ProviderPayload(BaseModel):
	payload: Any

不同模型供应商的原始响应结构可能不同,接收层可以暂时使用 Any 保留数据。但 Any 会推迟类型检查,真正进入工具执行和业务核心的数据仍应转换为具体模型。

3.3 Union和可选字段

联合类型表示字段允许多种类型:

class ToolResult(BaseModel):
	result: str | dict[str, object]

str | None 表示字段允许为空。若字段还可以省略,需要同时提供默认值:

class UserProfile(BaseModel):
	display_name: str | None=None

联合类型适合协议本身确实存在多种形态的情况。如果不同类型对应完全不同的处理流程,使用带判别字段的多个模型通常更清晰。

四、Agent中的应用

4.1 请求和响应

FastAPI 可以直接使用 Pydantic 模型描述接口:

from fastapi import FastAPI
from pydantic import BaseModel


class ChatRequest(BaseModel):
	message: str


class ChatResponse(BaseModel):
	answer: str


app=FastAPI()


@app.post("/chat",response_model=ChatResponse)
async def chat(request: ChatRequest)->ChatResponse:
	return ChatResponse(
		answer=f"收到:{request.message}",
	)

请求进入路由时,FastAPI 会校验 ChatRequest;返回值会按照 ChatResponse 序列化。缺少字段或类型不正确时,接口会在入口处返回校验错误。

4.2 工具参数

工具参数来自大模型生成的 JSON,不能直接解包后调用函数。先定义参数模型:

class UserProgressArguments(BaseModel):
	model_config=ConfigDict(
		strict=True,
		extra="forbid",
	)
	user_id: int=Field(ge=1)

执行工具前校验参数:

import json

arguments=json.loads(raw_arguments)
validated=UserProgressArguments.model_validate(arguments)
result=await get_user_progress(**validated.model_dump())

这段代码的关键不在于方法数量,而在于调用顺序:先解析 JSON,再校验字段,最后执行工具。解析失败和校验失败都应该被转换成可处理的工具错误,而不是直接让 Agent 进程崩溃。

4.3 配置

配置模型可以统一管理环境变量、默认值和类型:

from pydantic import Field
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
	model_name: str=Field(
		default="mock-model",
		validation_alias="MODEL_NAME",
	)
	timeout: float=Field(
		default=10.0,
		gt=0,
		validation_alias="MODEL_TIMEOUT",
	)

程序启动时就可以完成配置读取和校验,避免服务运行到第一次请求时才发现配置错误。

4.4 结构化输出

如果希望模型返回固定结构,可以先定义输出模型:

class ArticleOutline(BaseModel):
	title: str=Field(min_length=1)
	sections: list[str]=Field(min_length=1)

模型返回 JSON 后,使用 model_validate_json 校验:

outline=ArticleOutline.model_validate_json(raw_content)

Pydantic 只能检查结果是否符合模型,不能保证模型一定返回合法 JSON 或正确的业务含义。实际流程还需要配合提示词、JSON 输出模式以及失败重试。

工具参数的完整链路可以理解为:

Pydantic模型 → JSON Schema → 大模型生成参数 → model_validate → 执行工具

JSON Schema 是提供给模型的结构说明,model_validate 才是程序真正执行工具前的运行时防线。前者不能替代后者,因为模型仍然可能生成缺少字段、字段名错误或取值越界的参数。

五、工程边界

5.1 数据边界

Pydantic 最适合放在外部数据进入系统的边界上:外部输入 → 解析 → Pydantic 校验 → 内部业务对象 → 业务执行。

“宽松接收”不等于放弃校验,而是先保留供应商差异,等获得足够上下文后再转换为具体模型。工具执行、数据库写入和外部请求发送之前,仍应进行严格校验。

在 Agent 中,可以把校验分成三层:

  • 对象级:字段是否存在、类型是否正确、嵌套结构是否符合模型;
  • 跨对象:工具调用 ID 是否有对应结果、消息顺序是否符合协议;
  • 流程级:是否允许调用工具、是否需要重试、是否超过循环次数。

Pydantic 主要负责第一层,第二层和第三层需要由 Agent 执行器或状态管理逻辑完成。

5.2 错误处理

ValidationError 包含字段位置、错误类型和说明:

from pydantic import ValidationError


try:
	User.model_validate({"user_id":"wrong"})
except ValidationError as error:
	print(error.errors())

API 层可以把它转换为客户端错误;Agent 层可以提取必要信息,让模型重新生成参数;内部任务则应保留完整日志。用户输入错误、模型输出错误和程序内部错误,需要分别处理。

5.3 序列化选项

exclude_none=True 会排除值为 None 的字段,exclude_unset=True 会排除创建对象时没有显式传入的字段。更新接口时,两者的含义不同:前者关注当前值,后者关注调用方是否提供过字段。

因此,字段缺失、字段存在但值为 None、字段存在且有具体值,可能代表三种不同状态。PATCH 请求、工具参数和模型输出都可能依赖这种区别,不能只把“可选字段”理解成一个简单的开关。

5.4 Pydantic的边界

Pydantic 可以保证单个对象的结构正确,但不能独立保证整个 Agent 流程正确。工具调用 ID 是否匹配、消息顺序是否符合协议、工具是否被重复执行、失败后是否重试,都需要由 Agent 执行器和状态管理逻辑负责。

同样,Pydantic 模型也不应该承担数据库事务、权限判断和复杂业务行为。它负责数据契约,业务服务负责行为。

总结

Pydantic 通过 BaseModel、Field 和 ConfigDict 把数据结构与校验规则集中起来,并用 model_validate、model_dump 等方法连接外部数据和内部对象。在 Agent 开发中,它适合处理接口请求、工具参数、配置和结构化输出,但它只负责对象级数据校验,跨对象关系、执行状态和业务规则仍需要由其他模块保证。

本人能力有限,文章如有错误或遗漏之处,欢迎指正。

参考资料

  1. Pydantic 官方文档
  2. Pydantic Fields
  3. FastAPI 官方文档:Request Body