Зачем Pydantic AI-инженеру?

Посмотри на типичный код без Pydantic — агент получает JSON-ответ от LLM и пытается с ним работать:

Без Pydantic — хаос
python
import json

# LLM вернул строку — парсим вручную
raw = '{"action": "search", "query": "asyncio tutorial", "max_results": "5"}'
data = json.loads(raw)

# Каждый доступ — потенциальный KeyError или TypeError
action = data["action"]            # А вдруг ключа нет?
query = data["query"]              # А вдруг None?
max_results = data["max_results"]  # Это строка "5", а не число!

# Забудешь привести тип — баг в рантайме
results = search(query, limit=max_results)  # TypeError: limit должен быть int!

Теперь то же самое с Pydantic:

С Pydantic — надёжно
python
from pydantic import BaseModel

class SearchAction(BaseModel):
    action: str
    query: str
    max_results: int = 5  # Значение по умолчанию

raw = '{"action": "search", "query": "asyncio tutorial", "max_results": "5"}'

# Pydantic автоматически: парсит JSON, валидирует поля, приводит "5" → 5
data = SearchAction.model_validate_json(raw)

print(data.action)       # "search"
print(data.max_results)  # 5 (int, не строка!)
print(type(data.max_results))  # <class 'int'>

# Передаём в функцию — всё типизировано
results = search(data.query, limit=data.max_results)  # Работает!

Это не просто удобство — это надёжность. В production-агентах Pydantic используется повсеместно: для валидации ответов LLM, описания инструментов, конфигурации и состояния агента.

🔒

Валидация

Автоматическая проверка типов и ограничений при создании объекта

🔄

Конвертация

Автоматическое приведение типов: "5"5, "true"True

📋

JSON Schema

Генерация схемы для LLM tool calling — одной строкой

⚙️

Settings

Конфигурация агента из .env файла с валидацией

Установка

Установка пакетов
bash
# Основная библиотека
pip install pydantic

# Для работы с .env файлами (будем использовать в конце гайда)
pip install pydantic-settings python-dotenv

# Проверка установленной версии (нам нужна v2)
python -c "import pydantic; print(pydantic.__version__)"  # 2.x.x
⚠️ Pydantic v1 vs v2

В 2023 году вышел Pydantic v2 — переписан на Rust, работает ~5–50x быстрее. API изменился: dict()model_dump(), parse_raw()model_validate_json(). Все примеры в этом гайде используют v2. Если видишь старый код — вероятно это v1.

BaseModel — основа всего

Любая Pydantic-модель наследует BaseModel. Поля объявляются как атрибуты класса с аннотациями типов:

Базовая модель
python
from pydantic import BaseModel
from datetime import datetime

class AgentMessage(BaseModel):
    role: str               # Обязательное поле
    content: str            # Обязательное поле
    timestamp: datetime     # Pydantic умеет парсить строки в datetime
    tokens_used: int = 0    # Необязательное, значение по умолчанию

# Создание экземпляра — все поля валидируются сразу
msg = AgentMessage(
    role="assistant",
    content="Вот результаты поиска...",
    timestamp="2025-03-22T10:00:00",  # Строка автоматически → datetime
)

print(msg.role)        # "assistant"
print(msg.tokens_used) # 0  (значение по умолчанию)
print(type(msg.timestamp))  # <class 'datetime.datetime'>

# Сериализация в dict
print(msg.model_dump())
# {'role': 'assistant', 'content': 'Вот результаты...', 'timestamp': datetime(...), 'tokens_used': 0}

# Сериализация в JSON-строку
print(msg.model_dump_json())
# {"role":"assistant","content":"Вот результаты...","timestamp":"2025-03-22T10:00:00","tokens_used":0}

Optional поля и значения по умолчанию

Optional и None
python
from pydantic import BaseModel
from typing import Optional

class ToolCall(BaseModel):
    tool_name: str
    arguments: dict
    result: Optional[str] = None     # Может быть None
    error: str | None = None         # Синтаксис Python 3.10+
    retries: int = 0
    metadata: dict = {}              # Мutable default — Pydantic обрабатывает правильно

# Создание без опциональных полей
call = ToolCall(tool_name="web_search", arguments={"query": "pydantic tutorial"})
print(call.result)    # None
print(call.retries)   # 0

# Обновление — модели иммутабельны по умолчанию, используй model_copy
updated = call.model_copy(update={"result": "Нашли 10 результатов", "retries": 1})
print(updated.result)   # "Нашли 10 результатов"
print(call.result)      # None — оригинал не изменился

Field() — метаданные и ограничения

Field() добавляет ограничения, описания и псевдонимы полей. Описания особенно важны для LLM tool calling — они попадают в JSON Schema:

Field() с ограничениями и описаниями
python
from pydantic import BaseModel, Field
from typing import Literal

class WebSearchTool(BaseModel):
    """Инструмент для поиска информации в интернете."""

    query: str = Field(
        description="Поисковый запрос на русском или английском языке",
        min_length=2,
        max_length=500,
    )
    max_results: int = Field(
        default=5,
        description="Количество результатов поиска (от 1 до 20)",
        ge=1,   # greater or equal — не меньше 1
        le=20,  # less or equal — не больше 20
    )
    search_type: Literal["web", "news", "academic"] = Field(
        default="web",
        description="Тип поиска: web — общий, news — новости, academic — научные статьи",
    )
    language: str = Field(
        default="ru",
        pattern=r"^[a-z]{2}$",  # Строго 2 буквы: "ru", "en", "de"
        description="Язык результатов (ISO 639-1 код)",
    )

# Pydantic проверит все ограничения при создании
tool = WebSearchTool(query="asyncio python tutorial", max_results=10)
print(tool.model_dump())

# Попытка нарушить ограничение → ValidationError
from pydantic import ValidationError
try:
    bad = WebSearchTool(query="ok", max_results=50)  # max_results > 20!
except ValidationError as e:
    print(e)
    # 1 validation error for WebSearchTool
    # max_results
    #   Input should be less than or equal to 20 [type=less_than_equal, ...]
Почему description важен для агентов

Когда ты передаёшь схему инструмента в LLM через tool calling, модель видит именно эти описания. Чем точнее описание — тем правильнее LLM заполняет параметры. description в Field() — это инструкции для модели.

Вложенные модели

Поля Pydantic-модели могут быть другими Pydantic-моделями — Pydantic автоматически валидирует вложенные данные:

Вложенные модели — состояние агента
python
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime

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

class ToolResult(BaseModel):
    tool_name: str
    output: str
    success: bool
    duration_ms: float

class AgentStep(BaseModel):
    step_number: int
    thought: str
    action: Optional[str] = None
    tool_result: Optional[ToolResult] = None  # Вложенная модель
    timestamp: datetime = Field(default_factory=datetime.now)

class AgentState(BaseModel):
    session_id: str
    goal: str
    messages: list[Message] = []        # Список моделей
    steps: list[AgentStep] = []
    is_complete: bool = False
    final_answer: Optional[str] = None

# Создание из вложенных dict — Pydantic разберёт автоматически
state = AgentState(
    session_id="abc-123",
    goal="Найти информацию об asyncio",
    messages=[
        {"role": "user", "content": "Расскажи об asyncio"},  # dict → Message
        {"role": "assistant", "content": "Asyncio — это..."},
    ],
    steps=[
        {
            "step_number": 1,
            "thought": "Нужно поискать информацию",
            "action": "web_search",
            "tool_result": {              # dict → ToolResult
                "tool_name": "web_search",
                "output": "Нашли 5 статей",
                "success": True,
                "duration_ms": 423.5,
            }
        }
    ]
)

print(type(state.messages[0]))           # <class 'Message'>
print(type(state.steps[0].tool_result))  # <class 'ToolResult'>
print(state.steps[0].tool_result.success)  # True

# Сериализация всего состояния в JSON
import json
json_str = state.model_dump_json(indent=2)
print(json_str[:200])

Валидация и обработка ошибок

При любом нарушении Pydantic бросает ValidationError с подробным описанием всех найденных ошибок — не только первой:

ValidationError — все ошибки сразу
python
from pydantic import BaseModel, Field, ValidationError

class LLMConfig(BaseModel):
    model: str
    temperature: float = Field(ge=0.0, le=2.0)
    max_tokens: int = Field(gt=0, le=128000)
    api_key: str = Field(min_length=10)

try:
    config = LLMConfig(
        model="gpt-4o",
        temperature=5.0,     # Ошибка: > 2.0
        max_tokens=-100,     # Ошибка: <= 0
        api_key="short",     # Ошибка: < 10 символов
    )
except ValidationError as e:
    print(f"Найдено {e.error_count()} ошибок:")
    print(e)
    # 3 validation errors for LLMConfig
    # temperature
    #   Input should be less than or equal to 2 [type=less_than_equal, ...]
    # max_tokens
    #   Input should be greater than 0 [type=greater_than, ...]
    # api_key
    #   String should have at least 10 characters [type=string_too_short, ...]

    # Программный доступ к ошибкам
    for error in e.errors():
        field = " → ".join(str(loc) for loc in error["loc"])
        print(f"  {field}: {error['msg']}")

@field_validator — кастомная валидация

Когда встроенных ограничений недостаточно — пишем свой валидатор:

Кастомная валидация полей
python
from pydantic import BaseModel, Field, field_validator

ALLOWED_MODELS = {"gpt-4o", "gpt-4o-mini", "claude-sonnet-4-5", "claude-haiku-4-5-20251001"}

class AgentConfig(BaseModel):
    model: str
    system_prompt: str
    max_iterations: int = Field(default=10, ge=1, le=100)

    @field_validator("model")
    @classmethod
    def validate_model(cls, v: str) -> str:
        if v not in ALLOWED_MODELS:
            allowed = ", ".join(sorted(ALLOWED_MODELS))
            raise ValueError(f"Неизвестная модель '{v}'. Доступны: {allowed}")
        return v

    @field_validator("system_prompt")
    @classmethod
    def clean_system_prompt(cls, v: str) -> str:
        """Убираем лишние пробелы и проверяем длину."""
        v = v.strip()
        if len(v) < 10:
            raise ValueError("System prompt слишком короткий (минимум 10 символов)")
        if len(v) > 10000:
            raise ValueError("System prompt слишком длинный (максимум 10000 символов)")
        return v

# Валидная конфигурация
config = AgentConfig(
    model="gpt-4o",
    system_prompt="  Ты — полезный AI-ассистент.  ",  # Пробелы уберутся
)
print(config.system_prompt)  # "Ты — полезный AI-ассистент." (без пробелов)

# Невалидная модель → ValidationError
try:
    bad = AgentConfig(model="gpt-5", system_prompt="Привет")
except Exception as e:
    print(e)  # Value error, Неизвестная модель 'gpt-5'...

@model_validator — валидация всей модели

Для проверки зависимостей между полями используется @model_validator:

@model_validator — валидация зависимостей между полями
python
from pydantic import BaseModel, model_validator
from typing import Optional

class RAGConfig(BaseModel):
    use_rag: bool = False
    vector_db_url: Optional[str] = None
    embedding_model: Optional[str] = None
    top_k: int = 5

    @model_validator(mode="after")
    def check_rag_dependencies(self) -> "RAGConfig":
        """Если RAG включён — нужны обязательные поля."""
        if self.use_rag:
            if not self.vector_db_url:
                raise ValueError("При use_rag=True нужно указать vector_db_url")
            if not self.embedding_model:
                raise ValueError("При use_rag=True нужно указать embedding_model")
        return self

# Работает
config = RAGConfig(use_rag=False)

# Ошибка — RAG включён, но нет URL
try:
    bad = RAGConfig(use_rag=True, embedding_model="text-embedding-3-small")
except Exception as e:
    print(e)  # Value error, При use_rag=True нужно указать vector_db_url

Structured Output от LLM

Это главный паттерн для агентов. LLM должен вернуть структурированные данные — используем Pydantic для валидации ответа:

OpenAI Structured Outputs

OpenAI — structured outputs через Pydantic
python
from openai import AsyncOpenAI
from pydantic import BaseModel, Field
import asyncio

client = AsyncOpenAI()

# Определяем структуру, которую должен вернуть LLM
class TaskPlan(BaseModel):
    """План выполнения задачи агентом."""
    goal_understood: str = Field(description="Как агент понял задачу")
    steps: list[str] = Field(description="Список шагов для выполнения")
    required_tools: list[str] = Field(description="Инструменты, которые понадобятся")
    estimated_complexity: str = Field(
        description="Оценка сложности: low / medium / high"
    )
    clarification_needed: bool = Field(
        description="Нужны ли уточнения от пользователя"
    )

async def plan_task(user_request: str) -> TaskPlan:
    response = await client.beta.chat.completions.parse(
        model="gpt-4o-2024-08-06",  # Только эти модели поддерживают structured outputs
        messages=[
            {"role": "system", "content": "Ты — AI-агент. Составь план выполнения задачи."},
            {"role": "user", "content": user_request},
        ],
        response_format=TaskPlan,  # Передаём Pydantic модель напрямую!
    )
    # OpenAI автоматически вернёт распарсенный объект
    return response.choices[0].message.parsed

async def main():
    plan = await plan_task("Напиши отчёт о тенденциях в AI за 2025 год")

    print(f"Цель: {plan.goal_understood}")
    print(f"Шаги: {plan.steps}")
    print(f"Инструменты: {plan.required_tools}")
    print(f"Сложность: {plan.estimated_complexity}")
    # Полная типизация — никакого dict["key"]!

asyncio.run(main())

Anthropic / Claude — Pydantic через инструменты

Claude — structured output через tool use
python
import anthropic
from pydantic import BaseModel, Field
import json

client = anthropic.Anthropic()

class SentimentAnalysis(BaseModel):
    """Результат анализа тональности текста."""
    sentiment: str = Field(description="positive / negative / neutral")
    confidence: float = Field(description="Уверенность от 0.0 до 1.0", ge=0, le=1)
    key_phrases: list[str] = Field(description="Ключевые фразы, определившие тональность")
    summary: str = Field(description="Краткое объяснение оценки")

def analyze_sentiment(text: str) -> SentimentAnalysis:
    # Генерируем JSON Schema из Pydantic модели
    schema = SentimentAnalysis.model_json_schema()

    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=500,
        tools=[{
            "name": "save_analysis",
            "description": "Сохрани результат анализа тональности",
            "input_schema": schema,  # Pydantic → JSON Schema
        }],
        tool_choice={"type": "tool", "name": "save_analysis"},  # Принудительный вызов
        messages=[{
            "role": "user",
            "content": f"Проанализируй тональность текста:\n\n{text}"
        }],
    )

    # Извлекаем аргументы вызова инструмента
    tool_use = next(b for b in message.content if b.type == "tool_use")

    # Валидируем через Pydantic
    return SentimentAnalysis.model_validate(tool_use.input)

# Использование
result = analyze_sentiment("Это потрясающий продукт, очень доволен покупкой!")
print(f"Тональность: {result.sentiment}")   # positive
print(f"Уверенность: {result.confidence}")  # 0.95
print(f"Фразы: {result.key_phrases}")       # ["потрясающий продукт", "очень доволен"]

Ручной парсинг JSON от LLM

Иногда LLM возвращает обычный JSON в тексте. Pydantic помогает распарсить его безопасно:

model_validate_json() — парсинг ответа LLM
python
from pydantic import BaseModel, ValidationError
import re

class AgentDecision(BaseModel):
    action: str
    tool: str | None = None
    arguments: dict = {}
    reasoning: str

def parse_llm_response(raw_text: str) -> AgentDecision | None:
    """Извлекаем JSON из ответа LLM и валидируем через Pydantic."""
    # LLM часто оборачивает JSON в ```json ... ``` блоки
    json_match = re.search(r"```json\s*(.*?)\s*```", raw_text, re.DOTALL)
    if json_match:
        json_str = json_match.group(1)
    else:
        # Пробуем весь текст как JSON
        json_str = raw_text.strip()

    try:
        return AgentDecision.model_validate_json(json_str)
    except ValidationError as e:
        print(f"LLM вернул невалидный JSON: {e}")
        return None

# Пример
llm_output = '''
Я решил выполнить поиск.
```json
{
  "action": "use_tool",
  "tool": "web_search",
  "arguments": {"query": "pydantic v2 tutorial"},
  "reasoning": "Нужна актуальная информация о Pydantic v2"
}
```
'''

decision = parse_llm_response(llm_output)
if decision:
    print(f"Действие: {decision.action}")
    print(f"Инструмент: {decision.tool}")
    print(f"Аргументы: {decision.arguments}")

JSON Schema для Tool Calling

Pydantic умеет генерировать JSON Schema из модели — именно этот формат используется для описания инструментов агента:

model_json_schema() — схема для инструментов
python
from pydantic import BaseModel, Field
import json

class CodeExecutionTool(BaseModel):
    """Выполняет Python-код в безопасной sandbox-среде."""

    code: str = Field(
        description="Python-код для выполнения. Должен быть корректным синтаксически."
    )
    timeout_seconds: int = Field(
        default=30,
        description="Максимальное время выполнения в секундах",
        ge=1,
        le=120,
    )
    capture_output: bool = Field(
        default=True,
        description="Захватывать ли stdout/stderr"
    )

# Генерируем схему
schema = CodeExecutionTool.model_json_schema()
print(json.dumps(schema, indent=2, ensure_ascii=False))

# Вывод:
# {
#   "description": "Выполняет Python-код в безопасной sandbox-среде.",
#   "properties": {
#     "code": {
#       "description": "Python-код для выполнения...",
#       "title": "Code",
#       "type": "string"
#     },
#     "timeout_seconds": {
#       "default": 30,
#       "description": "Максимальное время выполнения...",
#       "maximum": 120,
#       "minimum": 1,
#       "title": "Timeout Seconds",
#       "type": "integer"
#     },
#     ...
#   },
#   ...
# }

# Использование в OpenAI tool calling
openai_tool = {
    "type": "function",
    "function": {
        "name": "execute_code",
        "description": CodeExecutionTool.__doc__,
        "parameters": schema,
    }
}

# Или сразу передаём в Anthropic
anthropic_tool = {
    "name": "execute_code",
    "description": CodeExecutionTool.__doc__,
    "input_schema": schema,
}

pydantic-settings — конфигурация агента

Агент читает API-ключи и настройки из переменных окружения. pydantic-settings делает это безопасно и с валидацией:

.env файл проекта
bash
# .env — не коммить в Git!
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
TAVILY_API_KEY=tvly-...

AGENT_MODEL=gpt-4o
AGENT_MAX_ITERATIONS=20
AGENT_TEMPERATURE=0.7
AGENT_DEBUG=false

VECTOR_DB_URL=http://localhost:6333
VECTOR_DB_COLLECTION=agent_memory
AgentSettings — типизированный конфиг из .env
python
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache

class AgentSettings(BaseSettings):
    # Читает из переменных окружения или .env файла
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,  # OPENAI_API_KEY = openai_api_key
    )

    # API ключи — SecretStr чтобы не светить в логах
    openai_api_key: SecretStr = Field(description="OpenAI API key")
    anthropic_api_key: SecretStr | None = None
    tavily_api_key: SecretStr | None = None

    # Настройки агента
    agent_model: str = "gpt-4o"
    agent_max_iterations: int = Field(default=10, ge=1, le=100)
    agent_temperature: float = Field(default=0.7, ge=0.0, le=2.0)
    agent_debug: bool = False

    # Vector DB
    vector_db_url: str = "http://localhost:6333"
    vector_db_collection: str = "agent_memory"

    def get_openai_key(self) -> str:
        """Возвращает ключ как строку (SecretStr скрывает его в repr)."""
        return self.openai_api_key.get_secret_value()

# Синглтон — создаём один раз, переиспользуем
@lru_cache(maxsize=1)
def get_settings() -> AgentSettings:
    return AgentSettings()

# Использование в коде
settings = get_settings()
print(settings.agent_model)         # "gpt-4o"
print(settings.openai_api_key)      # ***** (скрыт в выводе!)
print(settings.get_openai_key())    # sk-proj-... (реальное значение)
print(settings.agent_debug)         # False (str "false" → bool автоматически)
SecretStr — защита API ключей

Используй SecretStr для всех секретных значений. При логировании, print() или сериализации в JSON они автоматически маскируются как *****. Это предотвращает случайную утечку ключей в логи.

Паттерны для AI-агентов

Собираем всё вместе — типичные Pydantic-паттерны в реальном агенте:

Полный паттерн: типизированный агент
python
from pydantic import BaseModel, Field
from typing import Literal, Optional
from datetime import datetime
import asyncio
from openai import AsyncOpenAI

# ---- 1. Определяем инструменты через Pydantic ----

class WebSearchInput(BaseModel):
    """Поиск информации в интернете."""
    query: str = Field(description="Поисковый запрос")
    num_results: int = Field(default=5, ge=1, le=10)

class CalculatorInput(BaseModel):
    """Вычисляет математическое выражение."""
    expression: str = Field(description="Математическое выражение, например '2 + 2'")

# ---- 2. Типизируем ответ агента ----

class AgentAction(BaseModel):
    """Решение агента: что делать дальше."""
    thinking: str = Field(description="Внутренние рассуждения агента")
    action_type: Literal["use_tool", "final_answer"] = Field(
        description="Тип действия"
    )
    tool_name: Optional[str] = Field(
        default=None,
        description="Имя инструмента (если action_type == 'use_tool')"
    )
    tool_input: Optional[dict] = Field(
        default=None,
        description="Аргументы инструмента"
    )
    final_answer: Optional[str] = Field(
        default=None,
        description="Финальный ответ (если action_type == 'final_answer')"
    )

# ---- 3. Типизируем состояние агента ----

class Step(BaseModel):
    number: int
    action: AgentAction
    result: Optional[str] = None
    timestamp: datetime = Field(default_factory=datetime.now)

class AgentRun(BaseModel):
    session_id: str
    user_query: str
    steps: list[Step] = []
    final_answer: Optional[str] = None
    status: Literal["running", "complete", "failed"] = "running"

# ---- 4. Агент ----

client = AsyncOpenAI()

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": WebSearchInput.__doc__,
            "parameters": WebSearchInput.model_json_schema(),  # Схема из Pydantic!
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": CalculatorInput.__doc__,
            "parameters": CalculatorInput.model_json_schema(),
        }
    }
]

async def run_agent(query: str) -> AgentRun:
    run = AgentRun(session_id="demo-001", user_query=query)

    messages = [
        {"role": "system", "content": "Ты — полезный агент с инструментами."},
        {"role": "user", "content": query},
    ]

    for step_num in range(1, 11):  # Максимум 10 шагов
        response = await client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=TOOLS,
        )

        choice = response.choices[0]

        if choice.finish_reason == "stop":
            # Агент дал финальный ответ
            run.final_answer = choice.message.content
            run.status = "complete"
            break

        if choice.finish_reason == "tool_calls":
            # Агент вызвал инструмент
            tool_call = choice.message.tool_calls[0]
            tool_name = tool_call.function.name

            # Валидируем аргументы через Pydantic
            if tool_name == "web_search":
                args = WebSearchInput.model_validate_json(tool_call.function.arguments)
                result = f"Результаты поиска по '{args.query}': ..."  # Реальный поиск
            elif tool_name == "calculator":
                args = CalculatorInput.model_validate_json(tool_call.function.arguments)
                result = str(eval(args.expression))  # В prod — безопасный eval!

            # Добавляем в историю
            messages.append(choice.message)
            messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})

    return run  # Типизированный результат!

async def main():
    run = await run_agent("Сколько будет 123 * 456?")
    print(f"Статус: {run.status}")
    print(f"Ответ: {run.final_answer}")

asyncio.run(main())

Шпаргалка

Pydantic cheatsheet для AI-инженера
python
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic_settings import BaseSettings
from typing import Optional, Literal
from datetime import datetime

# 1. Базовая модель
class MyModel(BaseModel):
    name: str
    count: int = 0
    tags: list[str] = []

# 2. Field с ограничениями
class Strict(BaseModel):
    value: float = Field(ge=0, le=1)
    label: str = Field(min_length=1, max_length=100, description="Для LLM tool calling")

# 3. Создание экземпляра
obj = MyModel(name="test", count="5")  # "5" → 5 автоматически

# 4. Сериализация
obj.model_dump()            # → dict
obj.model_dump_json()       # → JSON строка
obj.model_dump(exclude={"name"})  # Исключить поля

# 5. Десериализация
MyModel.model_validate({"name": "x", "count": 1})      # из dict
MyModel.model_validate_json('{"name": "x", "count": 1}')  # из JSON строки

# 6. Схема для LLM
MyModel.model_json_schema()  # → dict с JSON Schema

# 7. Копирование с изменениями
new_obj = obj.model_copy(update={"count": 42})

# 8. @field_validator
class WithValidation(BaseModel):
    email: str

    @field_validator("email")
    @classmethod
    def check_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Невалидный email")
        return v.lower()

# 9. @model_validator
class CrossField(BaseModel):
    start: int
    end: int

    @model_validator(mode="after")
    def check_order(self) -> "CrossField":
        if self.end <= self.start:
            raise ValueError("end должен быть больше start")
        return self

# 10. Settings из .env
class Settings(BaseSettings):
    api_key: str
    debug: bool = False

    model_config = {"env_file": ".env"}

settings = Settings()  # Читает OPENAI_API_KEY из окружения

Практическое задание

Задание: типизированный инструментарий агента

  1. Создай Pydantic-модель EmailTool для инструмента отправки писем: получатель, тема, тело письма, список CC (необязательный). Добавь валидацию email через @field_validator
  2. Создай модель AgentResponse с полями: action (Literal с 3+ вариантами), confidence (float 0–1), explanation, next_tool (Optional)
  3. Напиши функцию parse_response(json_str: str) -> AgentResponse | None, которая парсит JSON от LLM с обработкой ValidationError
  4. Создай AppSettings(BaseSettings) с API-ключами и настройками. Убедись, что ключи хранятся как SecretStr
  5. Вызови AgentResponse.model_json_schema() и посмотри, какую схему получишь — именно её нужно передавать в LLM

Что дальше