Погодный ИИ-агент на DeepSeek и Pydantic AI: тот же агент, другой SDK
Та же задача, что в прошлой статье, но на фреймворке команды Pydantic: нативный провайдер DeepSeek из коробки и контракт ответа, который фреймворк валидирует сам. Сравниваем с OpenAI Agents SDK на живом прогоне.
В прошлой статье мы собрали погодного консультанта на OpenAI Agents SDK и наткнулись на грабли: DeepSeek не принимает response_format типа json_schema, поэтому контракт ответа пришлось валидировать вручную. Сегодня собираем ровно того же агента — тот же инструмент с Gismeteo, тот же контракт — но на фреймворке команды Pydantic, и посмотрим, сколько кода исчезнет.
Стек и установка
Pydantic AI — агентный фреймворк от команды, которая делает сам Pydantic: типизация и валидация здесь первоклассные граждане. Версия на момент написания (сентябрь 2026): 2.32.0 — код в статье проверен именно на ней.
pip install pydantic-ai
Ключ, как и раньше, выпускается в кабинете platform.deepseek.com и кладётся в переменную окружения DEEPSEEK_API_KEY — провайдер подхватит её сам.
Модель и контракт: одна строка провайдера
Никакого клиента AsyncOpenAI с base_url: у Pydantic AI есть нативный провайдер для DeepSeek, модель задаётся строкой «провайдер:модель». Контракт — та же Pydantic-модель, что в прошлой статье, передаётся параметром output_type. Главное отличие: JSON из ответа модели фреймворк парсит и валидирует сам, при ошибке — повторяет попытку, ручной разбор текста не нужен.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class WeatherReport(BaseModel):
city: str = Field(description="Город, для которого составлен отчёт")
temperature_c: int = Field(description="Текущая температура, °C")
feels_like_c: int = Field(description="Ощущаемая температура, °C")
advice: str = Field(description="Практичная рекомендация: что надеть и взять с собой")
agent = Agent(
"deepseek:deepseek-v4-flash", # нативный провайдер: ключ берётся из DEEPSEEK_API_KEY
output_type=WeatherReport, # контракт ответа, фреймворк валидирует его сам
system_prompt=(
"Ты — консультант по погоде. По вопросу пользователя определи город, "
"вызови get_gismeteo_weather и составь короткий практичный отчёт. "
"Температуры — ровно те, что вернул инструмент, без выдумок."
),
)
Грабли версии: фреймворк быстро едет
Честно о первых минутах переноса: в актуальной версии 2.32 параметр называется output_type, а не result_type, как в старых туториалах, а ключ не передаётся в конструктор агента — он берётся из окружения либо через явный провайдер. Оба раза мы получили TypeError и поправили код; если будете повторять по старым примерам — не удивляйтесь. Плюс фреймворка в том, что ошибки громкие и мгновенные: контракт не съезжает в тихий мусор.
Инструмент: та же живая погода с Gismeteo
Инструмент регистрируется декоратором @agent.tool; первым аргументом идёт контекст запуска, дальше — обычные аннотированные параметры. Логика парсинга — ровно та же, что в прошлой статье.
import re
import httpx
from pydantic_ai import RunContext
CITY_SLUGS = {
"москва": "weather-moscow-4368",
"санкт-петербург": "weather-sankt-peterburg-4079",
"казань": "weather-kazan-4364",
}
@agent.tool
async def get_gismeteo_weather(ctx: RunContext[None], city: str) -> str:
"""Текущая температура и «ощущается как» со страницы города на Gismeteo."""
slug = CITY_SLUGS.get(city.strip().lower())
if slug is None:
return "Город не найден. Доступны: " + ", ".join(sorted(CITY_SLUGS))
url = f"https://www.gismeteo.ru/{slug}/"
async with httpx.AsyncClient(timeout=15, headers={"User-Agent": "Mozilla/5.0"}) as http:
page = (await http.get(url)).text
temps = re.findall(r'<temperature-value[^>]*value="(-?\d+)"[^>]*from-unit="c"', page)
if len(temps) < 2:
return "Не удалось разобрать погоду на странице Gismeteo."
return f"{city}: сейчас {temps[0]}°C, ощущается как {temps[1]}°C (данные Gismeteo, {url})"
Запуск
import asyncio
async def main() -> None:
result = await agent.run("Сейчас в Москве тепло? Что надеть на прогулку?")
report = result.output # уже провалидированная Pydantic-модель
print(f"{report.city}: {report.temperature_c}°C, ощущается {report.feels_like_c}°C")
print("Совет:", report.advice)
asyncio.run(main())
Реальный прогон (deepseek-v4-flash, 4 сентября 2026):
Москва: 17°C, ощущается 16°C Совет: На улице прохладно, но не холодно — одевайтесь в несколько слоёв. Подойдут лёгкая куртка или джинсовка, свитер или лонгслив, удобные кроссовки. Возьмите с собой ветровку или лёгкий шарф на случай ветра.
Тот же город, те же температуры с Gismeteo, тот же по смыслу совет — но в коде нет ни строчки ручного парсинга JSON. Сравните с прошлой статьёй: там контракт держался на нашей функции разбора с повтором, здесь он — ответственность фреймворка.
Какой SDK выбрать
- OpenAI Agents SDK берите, если вы в экосистеме OpenAI и вам нужны трассировка, эскалации-хендоффы между агентами. На DeepSeek учтите грабли из прошлой статьи: структурированный вывод через output_type там не работает, контракт придётся закрывать вручную.
- Pydantic AI берите, если хотите меньше кода и валидацию вывода из коробки: нативный провайдер «провайдер:модель», контракт в одну строку, ретраи при ошибках валидации. Цена — молодой фреймворк с быстрыми переименованиями.
Обе реализации мы прогнали вживую на одной задаче: цифры погоды совпали, контракт выполнен в обоих случаях. Выбор — вопрос вкуса команды, а не возможностей.
Что улучшить дальше
- retries — параметр конструктора агента: сколько раз повторять попытку при ошибке валидации ответа.
- deps_type — типизированные зависимости (конфиг, клиенты), которые инструменты получают через тот же контекст.
- Стриминг и Logfire — потоковый ответ и наблюдаемость от той же команды разработчиков.
- Кэш погоды и больше городов — точно так же, как в прошлой статье: архитектура это позволяет без изменений.
Итог пары статей: простой агент — это модель, факты и контракт. Фреймворк можно взять любой из двух; главное, чтобы контракт проверялся кодом, а данные приходили из инструмента.
