← Блог
Технологии4 сентября 2026 · 10 мин чтения

Погодный ИИ-агент на DeepSeek: OpenAI Agents SDK + Pydantic

Простой, но настоящий агент: модель DeepSeek, живой инструмент с данными Gismeteo и строгий контракт ответа на Pydantic. Ниже — полный код, реальный прогон и два грабля, о которых молчат туториалы.

Команда Digital Paragonразработка ИИ-агентов
Иллюстрация к статье «Погодный ИИ-агент на DeepSeek»

«Простой LLM-агент» — это не чат-болталка, а три вещи: модель, которой доверяешь рассуждение, инструменты, которые дают ей факты, и контракт ответа, который проверяется кодом, а не надеждой. В этой статье собираем минимального погодного консультанта на связке DeepSeek + OpenAI Agents SDK + Pydantic и прогоняем его вживую.

Стек и установка

OpenAI Agents SDK — официальный SDK оркестрации агентов: инструменты-функции, трассировка и типизированные контракты из коробки. DeepSeek подключается через него же, потому что API DeepSeek совместим с OpenAI. Версии на момент написания (сентябрь 2026): openai-agents 0.22.0, pydantic 2.13.5 — мы проверили код именно на них.

pip install openai-agents pydantic httpx

Ключ DeepSeek выпускается в кабинете platform.deepseek.com и живёт в переменной окружения. В официальной документации DeepSeek текущие модели — deepseek-v4-flash (быстрая и дешёвая) и deepseek-v4-pro (сильнее в рассуждениях); для агента с простыми инструментами flash более чем достаточен.

Подключаем DeepSeek как модель агента

SDK по умолчанию использует Responses API OpenAI. Для OpenAI-совместимых провайдеров в документации SDK предусмотрен отдельный класс — OpenAIChatCompletionsModel: ему передаётся обычный клиент OpenAI с нужным base_url.

import os
from openai import AsyncOpenAI
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel

client = AsyncOpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com",  # OpenAI-compatible endpoint
)
model = OpenAIChatCompletionsModel(model="deepseek-v4-flash", openai_client=client)

Инструмент: живая погода с Gismeteo

Модель не знает сегодняшнюю температуру — её знает инструмент. Декоратор function_tool превращает асинхронную функцию в инструмент: схема аргументов строится из аннотаций, а docstring становится описанием для модели. Страница города Gismeteo отдаёт текущую температуру в кастомном элементе temperature-value, поэтому парсинг — одна регулярка; первая пара значений — «сейчас» и «ощущается как».

import re
import httpx
from agents import function_tool

CITY_SLUGS = {
    "москва": "weather-moscow-4368",
    "санкт-петербург": "weather-sankt-peterburg-4079",
    "казань": "weather-kazan-4364",
}

@function_tool
async def get_gismeteo_weather(city: str) -> str:
    """Current temperature and feels-like from the city page on Gismeteo."""
    slug = CITY_SLUGS.get(city.strip().lower())
    if slug is None:
        return "City not found. Available: " + ", ".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 "Could not parse weather from the Gismeteo page."
    return f"{city}: now {temps[0]}C, feels like {temps[1]}C (Gismeteo, {url})"

Обратите внимание на возвращаемые строки «город не найден» и «не удалось разобрать»: инструмент обязан отвечать текстом, который модель поймёт, а не падать исключением — так агент сможет сам исправить ошибку, например попросить уточнить город.

Контракт ответа — Pydantic

Ответ агента — не свободный текст, а структура: её удобно показывать в интерфейсе, писать в базу и тестировать.

from pydantic import BaseModel, Field, ValidationError

class WeatherReport(BaseModel):
    city: str = Field(description="City the report is about")
    temperature_c: int = Field(description="Current temperature, C")
    feels_like_c: int = Field(description="Feels-like temperature, C")
    advice: str = Field(description="Practical advice: what to wear and take")

Тут главный сюрприз для тех, кто читал документацию SDK невнимательно. Параметр output_type с Pydantic-моделью на маршруте Chat Completions уходит провайдеру как response_format типа json_schema. Мы проверили вживую: DeepSeek на сентябрь 2026 отвечает ошибкой 400 «This response_format type is unavailable now» — поддерживается только json_object. Поэтому честный рабочий паттерн такой: просим модель вернуть JSON текстом и валидируем его Pydantic-моделью, с одним повтором на случай кривого JSON. Контракт живёт в вашем коде, а не в доброте провайдера — это даже надёжнее.

def parse_report(text: str) -> WeatherReport:
    start, end = text.find("{"), text.rfind("}")
    if start == -1 or end <= start:
        raise ValueError("No JSON in model output: " + text[:200])
    return WeatherReport.model_validate_json(text[start:end + 1])

Сам агент и запуск

import asyncio
from agents import Agent, Runner

SCHEMA_HINT = ('Reply with a single JSON object and nothing else: '
               '{"city": str, "temperature_c": int, '
               '"feels_like_c": int, "advice": str}.')

agent = Agent(
    name="weather-consultant",
    model=model,
    instructions=(
        "You are a weather consultant. Detect the city from the question, "
        "call get_gismeteo_weather and write a short practical report. "
        "Use exactly the temperatures returned by the tool. " + SCHEMA_HINT
    ),
    tools=[get_gismeteo_weather],
)

async def main() -> None:
    result = await Runner.run(agent, "Is it warm in Moscow now? What should I wear?")
    try:
        report = parse_report(result.final_output)
    except (ValidationError, ValueError) as err:
        result = await Runner.run(agent, f"Your previous reply failed validation: {err}. "
                                         f"Return only the JSON per schema.")
        report = parse_report(result.final_output)
    print(f"{report.city}: {report.temperature_c}C, feels like {report.feels_like_c}C")
    print("Advice:", report.advice)

asyncio.run(main())

Реальный прогон этого кода (deepseek-v4-flash, 4 сентября 2026):

Москва: 17C, ощущается 16C
Совет: На улице прохладно, тепло однозначно не будет. Наденьте лёгкую куртку,
джемпер или свитер, а также непромокаемую обувь — вечером может стать свежо.

Агент сам вызвал инструмент по городу из вопроса, подставил фактические температуры и дал человеческий совет. Весь прогон — пара секунд и доли копейки по тарифам flash.

Что улучшить дальше

  • deepseek-v4-pro для сложных сценариев: сравнить «сейчас», «вечером» и «на выходных» с несколькими вызовами инструмента.
  • Кэш погоды на 10–15 минут: один и тот же город не дёргает Gismeteo на каждом запросе.
  • Больше городов и поиск слагов через страницу поиска Gismeteo вместо жёсткого словаря.
  • Телеграм-бот поверх: тот же агент, но с каналом доставки — архитектура не меняется.

Главный вывод: «простой агент» перестаёт быть игрушкой, когда у него есть факты (инструменты) и контракт (Pydantic). Всё остальное — полчаса кода.

Читайте также
Погодный ИИ-агент на DeepSeek и Pydantic AI: тот же агент, другой SDK 4 сен 2026 · 9 мин RAG или дообучение: что выбрать для базы знаний 14 авг 2026 · 9 мин Пилот ИИ-агента за две недели: что реально успеть 28 авг 2026 · 9 мин

Нужен такой агент в вашем процессе?

Обсудить задачу