Обзор PydanticAI: агент с типизированным результатом — это разбор Python-фреймворка, в котором модель возвращает данные объявленного типа. Документация проекта на ai.pydantic.dev описывает его как Python AI SDK: типизированный расширяемый цикл агента, где смена модели выполняется строкой, а механизм резервных моделей позволяет настроить переключение при сбое. Ниже — механизм, учебный контракт, проверка ответов и границы подхода.
Что такое PydanticAI и как устроен цикл агента
Документация проекта на ai.pydantic.dev описывает Pydantic AI как Python AI SDK: типизированный расширяемый цикл агента, в котором каждая модель меняется строкой, а на случай сбоя провайдера можно настроить резервную модель. Один и тот же агент запускается за веб-фронтендом, в терминале, в голосовом вызове, в очереди фоновых задач, в GitHub Actions или как обычный объект, у которого вызывается run(). Генерация изображений и эмбеддинги поставляются в том же пакете.
Класс Agent документация называет контейнером для нескольких вещей. В него входят модель, инструкции, инструменты, зависимости и тип результата. Каждую часть можно настраивать отдельно, а capabilities собирают связанное поведение в переиспользуемые единицы, которые проще компоновать, передавать дальше и загружать из конфигурационных файлов. Один агент способен обслуживать всё приложение или отдельный компонент, при этом несколько агентов взаимодействуют в более сложных сценариях.
Типизация здесь не декорация. В терминах типов агенты обобщены по типу зависимостей и типу вывода: агент, которому нужны зависимости типа Foobar и который выдаёт list[str], имеет тип Agent[Foobar, list[str]]. На практике это означает, что IDE подсказывает, когда тип выбран верно, а статическая проверка типов работает вместе с Pydantic AI. Документация приводит пример агента, который ожидает целочисленную зависимость и выдаёт булев результат: его тип — Agent[int, bool].
Предел механизма простыми словами: фреймворк отвечает за форму данных и порядок вызовов. Он не отвечает за то, правдивы ли факты внутри этой формы. Тип bool гарантирует, что на выходе лежит булево значение, и ничего не говорит о том, верен ли сам ответ по существу задачи.
Запуск агента документация описывает несколькими способами. agent.run_sync() — обычная синхронная функция, возвращающая объект завершённого результата; внутри она вызывает цикл событий. agent.run_stream() — асинхронный контекстный менеджер, возвращающий StreamedRunResult с методами потоковой передачи текста и структурированного вывода как асинхронного итератора; agent.run_stream_sync() даёт синхронный вариант с теми же методами. agent.run_stream_events() выдаёт асинхронный итератор по событиям AgentStreamEvent, завершающийся событием AgentRunResultEvent с финальным результатом запуска.
Тип результата: output_type, схемы и валидация
«Output» в документации — это финальное значение, возвращённое запуском агента. Им может быть простой текст, структурированные данные, изображение или результат функции, вызванной с аргументами от модели. Результат обёрнут в AgentRunResult или StreamedRunResult, чтобы приложение получило и другие данные: расход по запуску и историю сообщений. Обе обёртки обобщены по типу вложенных данных, поэтому информация о типе сохраняется.
Конструктор Agent принимает аргумент output_type, в который входит один или несколько типов либо функций вывода. Поддерживаются простые скалярные типы, списки и словари, включая TypedDict и StructuredDict, датаклассы и модели Pydantic, а также объединения типов — в целом всё, что поддерживается как аннотации типов в модели Pydantic. Можно передать список из нескольких вариантов.
По умолчанию Pydantic AI использует механизм вызова инструментов модели, чтобы получить структурированные данные. Когда указано несколько типов вывода в объединении или списке, каждый элемент регистрируется у модели отдельным инструментом вывода: так схема упрощается, а шансы на корректный ответ растут. Документация отмечает, что это хорошо работает на широком круге моделей. Имена инструментов вывода, нативное структурированное представление модели или передачу схемы в инструкциях задаёт маркер класса режима вывода.
Правило завершения запуска: он заканчивается, когда модель отвечает одним из типов вывода, либо — если тип не указан или среди разрешённых есть str — когда получен обычный текстовый ответ. Запуск может быть отменён при превышении лимитов использования. С DeferredToolRequests или PendingModelResponse в типе вывода запуск способен завершиться досрочно, чтобы приложение утвердило инструменты или дождалось запроса, который провайдер ещё обрабатывает, например пакетного, и продолжило позже.
Если схема типа вывода не является объектом — например, это int или list[int] — тип оборачивается в объект из одного элемента, чтобы схемы всех инструментов, зарегистрированных у модели, оставались объектными. Структурированные выводы, как и инструменты, используют Pydantic для построения JSON-схемы инструмента и для валидации данных, вернувшихся от модели.
| Элемент | Что задаёт | Что проверяет |
|---|---|---|
output_type |
Тип или список типов вывода | Соответствие данных схеме |
| Схема инструмента | JSON-схему для модели | Аргументы до выполнения кода |
AgentRunResult |
Обёртку результата | Сохранение типа данных |
| Правило приложения | Связь полей с задачей | Фактическую правильность |
Отдельная тонкость — статические проверки типов. Pyright и mypy стараются вывести тип вывода агента из указанного output_type, и при передаче функций или нескольких типов в объединении либо списке это удаётся не всегда, хотя Pydantic AI ведёт себя корректно. В такой ситуации проверяльщик типов жалуется даже при верном output_type, и документация советует явно указать обобщённые параметры в конструкторе Agent.
Учебный контракт Ticket: вход, артефакт и эталон
Возьмём учебный контракт, спроектированный для этой статьи. Он описывает заявку в поддержку: category принимает одно из значений delivery, payment, other; order_id — строка или null; needs_human — булево значение. Полный вход: «Заказ A-42 не приехал. Когда будет доставка?» Эталон задачи: {"category":"delivery","order_id":"A-42","needs_human":true}.
Инструмент lookup_order работает только на чтение и возвращает {"status":"in_transit","eta":null}. Это не позволяет придумать срок. Явное учебное правило приложения: если клиент спрашивает срок доставки, а lookup_order возвращает eta=null, итог должен иметь needs_human=true; срок не выводится из статуса in_transit.
import os
from typing import Literal, Optional
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
class Ticket(BaseModel):
category: Literal["delivery", "payment", "other"]
order_id: Optional[str]
needs_human: bool
class Deps(BaseModel):
order_id: str
agent = Agent(
os.environ["PYDANTIC_AI_MODEL"],
deps_type=Deps,
output_type=Ticket,
instructions=(
"Разбери обращение клиента в заявку Ticket. "
"Если клиент спрашивает срок доставки, а lookup_order вернул eta=null, "
"поставь needs_human=true и не выводи срок из статуса."
),
)
@agent.tool
def lookup_order(ctx: RunContext[Deps]) -> dict:
"""Вернуть статус заказа и срок доставки, если он известен."""
return {"status": "in_transit", "eta": None}
if __name__ == "__main__":
result = agent.run_sync(
"Заказ A-42 не приехал. Когда будет доставка?",
deps=Deps(order_id="A-42"),
)
print(result.output.model_dump_json())
Это учебный пример: модель в нём не вызывалась, ответы спроектированы вручную. Имя модели берётся из переменной окружения PYDANTIC_AI_MODEL; задайте её перед запуском учебного кода. Документация проекта описывает @agent.tool так: функция получает RunContext, несущий зависимости; остальная часть сигнатуры и docstring становятся схемой инструмента, аргументы валидируются до выполнения кода, успешный запуск с валидированным выводом возвращает объявленный тип. Ошибки и исчерпание допустимых повторов приложение должно обрабатывать отдельно. Типовая аннотация инструмента помогает статической проверке и не заменяет проверку логики его кода.
Эталон проверки — три спроектированных ответа на один и тот же вход. Первый: {"category":"delivery","order_id":"A-42","needs_human":true} — схема и заданное правило проходят. Второй: {"category":"refund","order_id":"A-42","needs_human":true} — нарушен только enum категории. Третий: {"category":"delivery","order_id":"A-42","needs_human":false} — форма допустима, но нарушено правило ручной передачи при неизвестном ETA. Все ответы содержат только объявленные поля, сопроводительного текста вне Ticket нет.
Структурная корректность и фактическая правильность
Разберём три ответа по двум разным осям. Схема проверяет форму: набор полей, их типы, допустимые значения перечисления. Отдельное правило приложения связывает needs_human с входным вопросом и результатом lookup_order. Это два независимых слоя проверки, и они ловят разные ошибки.
| Ответ | Схема | Правило приложения | Что поймано |
|---|---|---|---|
| Ответ 1 | Проходит | Проходит | Ничего |
| Ответ 2 | Нарушен enum | Проходит | Ошибка формы |
| Ответ 3 | Проходит | Нарушено | Ошибка смысла |
Второй ответ отклоняется на уровне Pydantic: значение refund отсутствует в перечислении, и валидация не пропустит его дальше. Третий ответ проходит валидацию полностью: все поля на месте, типы совпадают, категория допустима. Ошибку видит только правило приложения, которое знает, что при eta=null и вопросе о сроке заявку нужно передать человеку.
Отсюда практический вывод для читателя. Типизированная структура сама не подтверждает фактическую правильность. Модель может вернуть идеально оформленный объект с неверным решением внутри, и валидация Pydantic промолчит. Правила предметной области остаются на стороне приложения: их пишут отдельно и проверяют отдельно.
Документация проекта на ai.pydantic.dev в разделе про тестирование формулирует ту же границу. Модульные тесты проверяют собственный код вокруг модели: инструменты, зависимости, валидацию вывода и обработку сбоев, без запросов к модели и с одинаковым результатом при каждом запуске. Они не говорят, хороши ли ответы модели; для этого предназначены evals. Это разделение стоит держать в голове при проектировании: тест на форму и тест на смысл — разные задачи с разными инструментами.
Инструменты, зависимости и повтор при сбое
Зависимости в Pydantic AI передаются через RunContext. Документация приводит пример: RunContext параметризован типом зависимости int, и при ошибке в типе зависимости возникает ошибка типизации. Тот же механизм несёт зависимости в инструменты и в код вокруг модели.
Повтор при сбое провайдера документация описывает как часть цикла: приложение может настроить резервную модель для переключения при сбое основного провайдера. Смена модели выполняется строкой. Это снижает привязку приложения к одному поставщику, хотя и требует, чтобы резервная модель поддерживала нужный режим структурированного вывода.
Для длительных задач документация описывает Pydantic AI Harness: он даёт агенту память, guardrails, подагентов, планирование, управление контекстом и сохранение состояния, вплоть до готового coding-агента. Отдельные открытые пакеты решают смежные задачи: Pydantic Evals оценивает финальные выводы агента и траектории вызовов инструментов на наборах данных или на живом трафике, Pydantic Graph даёт типизированное управление потоком, когда одного цикла агента мало.
Сохранение, возобновление и ветвление запуска документация описывает без движка: StepPersistence из Pydantic AI Harness снимает снимок каждого завершённого шага в выбранное хранилище, как это делает checkpointer. Движок нужен для восстановления запуска, который упал посреди шага. Для долговечного исполнения документация предлагает TemporalDurability: тот же агент работает внутри workflow Temporal, каждый вызов модели и инструмента становится долговечной активностью, и запуск в фоновой очереди переживает перезапуски, сбои и долгие ожидания. DBOS и Prefect подключаются так же, есть интеграции с Restate, AWS Lambda, Airflow, Absurd, pgtask и Azure Durable Task.
Наблюдаемость строится на OpenTelemetry. Документация проекта на ai.pydantic.dev называет Pydantic Logfire платформой, для наблюдения за приложением, включая вызовы LLM, а Pydantic AI Gateway — одним ключом для всех моделей с мониторингом расходов и контролем бюджета в реальном времени; инструментация при этом обычная OpenTelemetry, поэтому подходит любой уже работающий бэкенд. Статья Langfuse про интеграцию с Pydantic AI описывает похожий путь: ключи Langfuse задаются переменными окружения, клиент инициализируется через get_client(), затем включается инструментация Pydantic AI, которая автоматически захватывает операции и экспортирует спаны OpenTelemetry в Langfuse. Там же отмечено, что при корректной инструментации, но отсутствии наблюдений в Langfuse помогает вызов langfuse.flush() в конце приложения и проверка ключей и базового URL.
Документация OpenInference для PydanticAI описывает библиотеку автоинструментации: трассы совместимы с OpenTelemetry и отправляются в коллектор для просмотра, например в Arize Phoenix или Arize AX. Phoenix по умолчанию слушает http://localhost:6006, и та же страница отмечает, что данные не уходят в интернет и обрабатываются локально на машине.
Подмена модели в тестах и случай, где подход не решает задачу
Документация проекта на ai.pydantic.dev в разделе про модульное тестирование предлагает TestModel и FunctionModel вместо реальной модели, чтобы избежать расхода, задержки и изменчивости настоящих вызовов LLM. TestModel по умолчанию вызывает все инструменты агента, затем возвращает простой текст или структурированный ответ в зависимости от типа вывода. Его «умная» часть пытается сгенерировать валидные структурированные данные для функциональных инструментов и типов вывода по схеме зарегистрированных инструментов. Никакого машинного обучения внутри нет: это обычный процедурный Python, который подбирает данные под JSON-схему инструмента. Результат выглядит некрасиво и нерелевантно, но в большинстве случаев проходит валидацию Pydantic; для более сложной логики документация советует FunctionModel с собственным кодом генерации данных.
Отдельное ограничение: TestModel не умеет эмулировать нативные инструменты, выполняемые провайдером. Если в продакшене агент настроен на такие инструменты через capabilities, в тестах документация советует переопределять их вызовом agent.override(model=TestModel(), native_tools=[]), если тест не проверяет именно передачу нативных инструментов модели. Ещё одна мера безопасности — защита от случайных реальных запросов к LLM во время тестирования, описанная в документации через ALLOW_MODEL_REQUESTS.
Теперь случай, где типизированный результат не решает задачу. Возьмём третий спроектированный ответ: {"category":"delivery","order_id":"A-42","needs_human":false}. Схема проходит, поля на месте, перечисление корректно. Приложение получает аккуратный объект и, если у него нет отдельного правила про eta=null, просто закрывает заявку без передачи человеку. Клиент остаётся без ответа о сроке, хотя формально система отработала без ошибок.
Этот пример показывает границу подхода. Типизация ловит расхождения формы и отсекает часть мусора на входе в бизнес-логику. Она не заменяет правило предметной области и не проверяет, соответствует ли решение реальности заказа. Универсального улучшения здесь нет: для каждого поля, связанного со смыслом задачи, правило пишется отдельно и проверяется отдельно.
| Ситуация | Помогает ли тип результата | Что нужно дополнительно |
|---|---|---|
| Возвращена категория refund вне enum | Валидация отклоняет значение | Обработка ошибки или повтора |
| Возвращена payment вместо delivery | Обе категории допустимы по типу | Сверка с текстом обращения |
| needs_human=false при неизвестном ETA | bool имеет допустимую форму | Явная проверка правила приложения |
| Верный тип order_id, но другой заказ | Строка проходит проверку типа | Сверка идентификатора со входом и правами |
Что проверить перед внедрением
Этот обзор держится в границах Python-фреймворка: класс Agent, модель, тип результата, зависимости, инструменты, валидация, повтор и подмена модели для тестирования. Базовый Pydantic с его моделями и валидацией здесь служит фундаментом и отдельно не разбирается. Инструменты тестирования агентов заслуживают отдельного материала: здесь они нужны только для того, чтобы показать границу между проверкой формы и проверкой смысла.
Перед внедрением стоит пройти по нескольким пунктам. Проверьте, что выбранная модель поддерживает нужный режим структурированного вывода: по умолчанию фреймворк опирается на вызов инструментов, и для объединений типов каждый вариант регистрируется отдельным инструментом. Убедитесь, что статическая проверка типов настроена: pyright и mypy иногда требуют явно указать обобщённые параметры Agent, и без этого проверяльщик будет жаловаться на верный код.
Отдельно решите, где живут правила предметной области. Схема описывает форму, правило связывает поля со смыслом задачи, и эти два слоя стоит держать раздельно, чтобы ошибку было видно на нужном уровне. Для длительных задач заранее выберите механизм сохранения состояния: StepPersistence снимает снимки завершённых шагов, а движок вроде Temporal нужен для восстановления упавшего посреди шага запуска. Для наблюдаемости подойдёт любая OpenTelemetry-совместимая система: документация проекта называет Logfire, статья Langfuse описывает свой путь, документация OpenInference — Phoenix на локальном порту.
Частые вопросы
Что такое PydanticAI простыми словами?
Это Python-фреймворк для агентов, где модель возвращает данные объявленного типа. Документация проекта на ai.pydantic.dev описывает его как Python AI SDK с типизированным расширяемым циклом агента. Агент собирается из модели, инструкций, инструментов, зависимостей и типа результата. Форму ответа проверяет Pydantic, а правила предметной области остаются в приложении.
Почему схема пропустит ошибочное решение о передаче человеку?
Поле needs_human имеет тип bool, поэтому и true, и false допустимы по форме. В учебном обращении клиент спрашивает срок, а lookup_order возвращает eta=null. Условие передачи человеку должно проверяться отдельным правилом: needs_human=true. Третий спроектированный ответ нарушает это правило при корректной структуре.
Инструкция модели уже обеспечивает проверку бизнес-правила?
Инструкция сообщает модели требуемое поведение, но результат всё равно нужно проверить в приложении. В учебном коде lookup_order возвращает фиксированные данные заглушки. Настоящий заказ не запрашивается. Все три ответа спроектированы вручную. Перед рабочим использованием нужно реализовать проверку правила и обработку ошибочного результата.
Как тестировать агента без реальных запросов к модели?
Документация проекта на ai.pydantic.dev советует TestModel и FunctionModel вместо настоящей модели. TestModel вызывает все инструменты и возвращает текст или структурированный ответ по типу вывода, подбирая данные под JSON-схему процедурным кодом. Нативные инструменты провайдера он не эмулирует, и в тестах их переопределяют через agent.override(model=TestModel(), native_tools=[]).
Что даёт Pydantic Graph и когда одного агента мало?
Документация проекта на ai.pydantic.dev описывает Pydantic Graph как типизированное управление потоком для случаев, когда одного цикла агента недостаточно. Это отдельный открытый пакет, как и Pydantic Evals для оценки выводов и траекторий вызовов инструментов. Один агент способен обслуживать всё приложение или компонент, а несколько агентов взаимодействуют в более сложных сценариях.
