← Back to 文字

Pydantic 与配置建模

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 理解 Pydantic 在月份 1 中的角色。
  • 使用 Pydantic 建模聊天消息和请求响应。
  • 让配置读取从“字符串散落各处”升级为“结构化配置对象”。

前置知识

  • 已完成 Python 基础与工程化前两节

1. 为什么 Pydantic 这么重要

Pydantic 是月份 1 的关键桥梁,因为它连接了:

  • Python 对象
  • 配置
  • API 请求
  • API 响应
  • 结构化输出

后面 FastAPI 也会直接建立在 Pydantic 模型之上。

2. 最小模型示例

from pydantic import BaseModel


class ChatMessage(BaseModel):
    role: str
    content: str

使用:

message = ChatMessage(role="user", content="你好")
print(message.role)

3. 为什么它比裸字典更好

  • 字段更明确
  • 自动校验类型
  • 错误信息更清晰
  • 与 FastAPI 兼容

4. 本月统一教学模型

从本节开始,推荐你围绕这些模型练习:

from typing import Literal

from pydantic import BaseModel, Field


ChatRole = Literal["system", "user", "assistant", "tool"]


class ChatMessage(BaseModel):
    role: ChatRole
    content: str


class ChatRequest(BaseModel):
    messages: list[ChatMessage]
    model: str = Field(default="deepseek-chat")
    temperature: float = Field(default=0.2, ge=0, le=2)


class ChatResponse(BaseModel):
    answer: str
    model: str


class ErrorResponse(BaseModel):
    error_code: str
    message: str

说明:

  • 这是后续 CLI、FastAPI 和 LangChain 重构的统一数据基础。
  • 这里的字段不追求一次全对,而是追求统一和稳定。

5. 配置建模

你可以进一步用 Pydantic 管理配置:

from pydantic import BaseModel


class AppSettings(BaseModel):
    deepseek_api_key: str
    deepseek_base_url: str = "https://api.deepseek.com"
    deepseek_model: str = "deepseek-chat"

然后在配置读取后转成对象,减少到处传裸字符串。

6. 你当前阶段要避免的误区

  • 把所有字段都声明成 str
  • 模型越来越多但没有统一命名
  • 请求模型和响应模型混在一起

7. 实操任务

  1. 定义 ChatMessage
  2. 定义 ChatRequest
  3. 定义 ChatResponse
  4. 输入错误类型数据,观察 Pydantic 报错

8. 自测题

  1. 为什么 API 层和核心逻辑层都喜欢 Pydantic?
  2. 为什么要单独定义 ErrorResponse
  3. 为什么请求模型和响应模型不应该混用?

9. 作业与验收

作业:

  • 完成本月统一教学模型的定义,并放到 app/models.py

验收标准:

  • 每个模型都能实例化
  • 非法数据会报清晰错误
  • 你能解释每个模型在哪一层使用

10. 常见错误

  • 把 Pydantic 当成普通字典
  • 字段命名随意变化
  • 错误响应没有结构,导致上层难处理

11. 本章与前文关系

上一章已经把配置收口成了对象,这一章要把这个思路推广到整个月份 1:所有重要输入输出,都应该有清晰结构,而不是到处传裸字典。

这是月份 1 中从“代码能跑”走向“代码可维护”的一个关键台阶。

12. 本章在研发助手项目中的位置

研发助手项目最终至少会包含这些结构化对象:

  • ChatMessage
  • ChatRequest
  • ChatResponse
  • ToolDefinition
  • ToolCallResult
  • ErrorResponse

这些对象既服务于:

  • LLM client
  • Tool Calling
  • FastAPI schema

也服务于你自己理解项目结构。所以本章实际上是在定义后续整个项目的共同语言。

13. 为什么 Pydantic 是月份 1 的关键桥梁

如果只用裸字典:

  • 写起来快,但边界弱

如果只用数据类:

  • 结构清晰,但校验不足

而 Pydantic 刚好把几个关键能力合到了一起:

  • 结构清晰
  • 类型友好
  • 校验明确
  • 错误信息可读
  • 与 FastAPI 兼容

这就是为什么月份 1 会把它视为“桥梁层”,而不是普通库。

14. 裸字典 -> 数据类 -> Pydantic 的升级路径

第一阶段:裸字典

message = {"role": "user", "content": "hello"}

问题:

  • 字段名拼错不易发现
  • 类型不明确
  • 没法表达更强约束

第二阶段:数据类

from dataclasses import dataclass


@dataclass
class ChatMessage:
    role: str
    content: str

优点:

  • 结构明确
  • 可读性提升

第三阶段:Pydantic 模型

from pydantic import BaseModel


class ChatMessage(BaseModel):
    role: str
    content: str

新增价值:

  • 输入时就能校验
  • 错误信息更清晰

15. 错误示例 vs 正确示例

错误示例:所有字段都先当字符串

class ChatRequest(BaseModel):
    messages: str
    model: str
    temperature: str

问题:

  • 完全失去结构表达能力
  • 后续每一层都要自己再解析一次

正确示例:按真实语义建模

from pydantic import BaseModel, Field


class ChatRequest(BaseModel):
    messages: list["ChatMessage"]
    model: str = "deepseek-chat"
    temperature: float = Field(default=0.2, ge=0, le=2)

这让模型输入的真实形状被直接写进类型系统和校验规则里。

16. 完整文件级示例:models.py

"""月份 1 统一教学模型。

后续 LLM API、Tool Calling、FastAPI 和综合项目章节,
都会围绕这里的结构做演化。
"""

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, Field


ChatRole = Literal["system", "user", "assistant", "tool"]


class ChatMessage(BaseModel):
    """统一的消息对象。"""

    role: ChatRole
    content: str


class ChatRequest(BaseModel):
    """聊天请求。"""

    messages: list[ChatMessage]
    model: str = Field(default="deepseek-chat")
    temperature: float = Field(default=0.2, ge=0, le=2)


class ChatResponse(BaseModel):
    """聊天响应。"""

    answer: str
    model: str


class ToolDefinition(BaseModel):
    """工具定义。"""

    name: str
    description: str
    parameters: dict[str, Any]


class ToolCallResult(BaseModel):
    """工具执行结果。"""

    tool_name: str
    success: bool
    output: str


class ErrorResponse(BaseModel):
    """统一错误响应。"""

    error_code: str
    message: str

17. 逐段解释这份完整示例

为什么 ChatRoleLiteral

因为角色不是任意字符串。它的取值范围是有限集合。

为什么 temperatureField

因为它不仅有默认值,还有范围约束。这个约束越早表达,后面越不容易在调用链深处才发现问题。

为什么现在就放 ToolDefinitionToolCallResult

因为月份 1 的课程并不是孤立模块。虽然你还没进入 Tool Calling 正文,但统一模型越早建立,后面章节越不容易各写各的。

18. 一个更接近配置建模的增强版示例

你可以把上一章的数据类配置升级成 Pydantic 版本:

from pydantic import BaseModel


class AppSettings(BaseModel):
    deepseek_api_key: str
    deepseek_base_url: str = "https://api.deepseek.com"
    deepseek_model: str = "deepseek-chat"

这和消息模型一起,构成了月份 1 很重要的两类结构:

  • 配置结构
  • 业务数据结构

19. 调试与排错:本章最常见问题

问题一:模型字段与真实数据不匹配

例如你声明 messages: list[ChatMessage],但实际传入的是字符串。这时不应抱怨 Pydantic“太严格”,而应理解这是它在提前帮你发现错误。

问题二:前向引用或顺序问题

如果模型之间互相引用,需要更注意定义顺序。本月尽量保持模型层次简单,避免一开始设计复杂依赖。

问题三:把 Pydantic 模型当成普通 dict 使用

它可以序列化,但它首先是“结构化对象”,不应退化成到处随意索引的字典。

20. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能解释为什么 Pydantic 适合月份 1。
  2. 能设计基础请求与响应模型。
  3. 能说清裸字典、数据类和 Pydantic 的升级关系。
  4. 能为后续 FastAPI 和结构化输出准备统一数据层。

21. 如果你卡在这里,先回看哪几章

22. 从本章过渡到下一章的桥接说明

接下来进入 04-pytest与最小测试.md

你已经开始用更明确的模型表达系统数据,下一步就要学会如何验证这些结构在正常路径和异常路径下都能稳定工作。也就是说,从“表达结构”进一步进入“验证结构”。

Fin