04 сентября 2026

Telegram-бот, который сам расследует инциденты на LLM-инфре

Всем привет! Инфраструктура запуска моделей по всей компании — это и продакшн-инференс в продуктовых командах, и R&D-эксперименты, и отдельные инстансы vLLM под конкретные задачи. У большого парка хостов есть неприятная особенность: состав постоянно меняется, и единого взгляда на здоровье инфры не получается. Где-то крутится продакшн-сервис с SLA, где-то — временный хост с экспериментальной моделью, где-то — собственный vLLM под отдельный продукт. Через неделю легко забыть, что какой-то хост вообще существует, пока кто-то не пишет: «Возникла проблема, почему /v1/chat/completions отдаёт 503?» Расскажем, как мы собрали бота, который не ограничивается пингом URL: через агента он ходит в логи и метрики и приносит корневую причину. Заодно объясним, почему не стали разворачивать Prometheus + Alertmanager + Grafana на каждой новой инсталляции.

Почему стандартные решения не подошли

Перед тем как писать своё, мы посмотрели на готовые решения:
  • Uptime Kuma. Отличный health-check, но не объясняет ошибки.
  • Prometheus + Alertmanager + Grafana. Корпоративный стандарт, он у нас тоже есть. Но настраивать его под экспериментальные сервисы — отдельная работа. Тут нужны дашборды, алерт-роуты, доступы. Когда таких сервисов много и они создаются и удаляются непрерывно, накладные расходы становятся заметными.
  • Telegram-боты «проверь URL». Все они умеют GET и сравнивать статус-кода с 200, но ни один не понимает, что на хосте крутится vLLM и важна ещё vllm:gpu_cache_usage_perc, а не только HTTP 200.
Главная проблема всех трёх: они отвечают на вопрос «что упало», но не на вопрос «почему». А когда хостов десятки и не у каждого есть выделенный дежурный, понять «почему» — это и есть 80% работы при разборе инцидента. LLM в инфра-боте — мозг агента, который сам ходит в Loki и Victoria Metrics. Модель решает, какой запрос сделать на каждом шаге, агент его исполняет, результат возвращается в контекст — и так до корневой причины. Это исполнительный элемент пайплайна, а не обёртка над алертами.

Подход

Мы свели задачу к четырём шагам:
  1. Пользователь добавляет монитор через /add_monitor в Telegram без YAML и конфигов на хостах.
  2. Бот фоном пингует URL и собирает метрики из выбранного источника: Node Exporter, cAdvisor, NVIDIA DCGM, OpenAI-совместимый эндпоинт, vLLM, Loki, биллинг облачных провайдеров.
  3. Если что-то падает, приходит алерт с человекочитаемым объяснением от LLМ.
  4. Под алертом — кнопка «Расследовать». SGR-агент идёт в логи и метрики и присылает отчёт с корневой причиной.

Архитектура

Общая схема

architechture.png
Архитектура бота
Бот написан на aiogram + FastAPI. Метрики хранит Victoria Metrics — дешевле Prometheus по диску и ставится одним бинарём. Логи — Loki + Promtail на целевых хостах. Для LLМ подойдёт любой OpenAI-совместимый эндпоинт; по умолчанию gpt-4o-mini, но в R&D мы часто переключаемся на свой vLLM. Observability агента — Langfuse.

Источники метрик

Универсального экспортера не бывает, поэтому мы поддержали восемь источников, и у каждого своя роль. Базовую телеметрию хоста — CPU, память, диск, сеть — снимает Node Exporter, метрики по контейнерам даёт cAdvisor. За GPU отвечает NVIDIA DCGM: использование, температура, ECC-ошибки — без него LLМ-инфра слепая. Здоровье самих моделей, поднятых через vLLM, Ollama или собственную обвязку, проверяем через OpenAI-совместимые эндпоинты — простой /v1/models и sanity-check ответа. Для vLLM отдельная история: из десятков time-series в /metrics реально важны три-четыре — vllm:e2e_request_latency_seconds, vllm:num_requests_waiting, vllm:gpu_cache_usage_perc, vllm:request_success_total, остальное — шум для типовых задач. Иногда метрик нет, а ошибки в логах есть — тогда включается отдельный тип монитора, Docker Logs через Loki с LogQL-запросами по уровням ERROR/WARN. И наконец, Cloud Billing шлёт алерты «баланс закончится через N дней по текущему burn rate»: он помогает не столько самому сервису, сколько предупреждает, как скоро его отключат.

Автосетап экспортеров через SSH

Самая раздражающая часть мониторинга — конфигурирование экспортеров на каждом хосте. Нужно зайти, скопировать docker-compose, править порт, проверить firewall — и так десять раз. Мы автоматизировали эту цепочку. Пользователь даёт боту хост и SSH-ключ, бот по SSH разворачивает на целевой машине compose с нужными экспортерами и только потом возвращается в Telegram. Теперь на то, чтобы добавить хост в мониторинг, уходило не больше минуты.

SGR-агент: ядро истории

Function calling в чистом виде выглядит примерно так: модель решает, какую функцию вызвать. Но на практике этого мало. Open-weight модели часто косплеят function calling — формально они знают, как это устроено, но на tool_choice: required могут проигнорировать схему и ответить обычным текстом. Мы поймали это на gpt-oss-120b. В reasoning_content модель прямо пишет «can use the reasoning tool, but not needed» и отдаёт plain text, а агент падает на tool_calls[0] is None. Schema-Guided Reasoning (SGR) всё меняет. Перед каждым доменным tool_call'ом модель обязана заполнить структуру плана. SGR предлагает два шага:
  1. Reasoning-фаза с tool_choice на единственный инструмент reasoning. Модель возвращает pydantic-валидируемый JSON. Если не вернула — поднимается ошибка валидации, шаг повторяется (в проде — с retry).
  2. Action-фаза — вызов нужного домен-tool'а (loki_query, victoria_metrics_query) по next_step из плана.
Структура одного шага:
class ReasoningTool(BaseModel):
    reasoning_steps: list[str]      # как декомпозировал задачу
    current_situation: str          # что знаем сейчас
    plan_status: str                # где в плане
    remaining_steps: list[str]      # что осталось
    enough_data: bool               # хватает ли данных для ответа
    task_completed: bool
    next_step: str                  # что делаем дальше
В логах виден весь ход рассуждения: почему агент пошёл за конкретной метрикой, что от неё ожидал, когда сдался и переключился на логи. Для разбора инцидента это важнее самой корневой причины. Так становится понятна логика расследования, и легко понять, где агент промахнулся. На грабли мы тоже наступили. Loki не принимает запросы без непустого label-selector и на {} |= "ERROR" отвечает 400. На таких запросах агент крутился по 5–7 шагов, в current_situation признавая «query format issue», и подбирал варианты. Лечится это не на стороне модели, а на стороне task'а: бот сам передаёт подсказку с container_name и instance, чтобы первый запрос был корректным. Аналогично с временным окном. «Последний час» агенты считают плохо, и оказалось проще передавать ISO-timestamps в task явно, чем надеяться на арифметику от Current Date.

Веб-дашборд

Telegram хорошо подходит для алертов и быстрой реакции, но смотреть графики метрик и историю проверок в текстовом чате неудобно. К боту прикручен лёгкий FastAPI-дашборд, доступный по /dashboard?token=…. Токен генерируется при первой регистрации пользователя и привязан к telegram_id. Отдельная авторизация не нужна, а ссылка приходит прямо из чата. На странице — список мониторов с цветовой индикацией статуса, графики host- и container-метрик за выбранный период (matplotlib + jinja2, без SPA), последние ERROR/WARN из Loki, история доступности LLМ-моделей для vLLM-эндпоинтов. Этого хватает, чтобы посмотреть на общую картину, не заходя в Grafana.
dashboard.png
Интерфейс веб-дашборда

Бот как инструмент для LLМ пользователя

Параллельно с Telegram-интерфейсом мы подняли MCP-сервер. Через него Claude Desktop, Cursor или собственный агент пользователя вызывают те же методы бота: list_monitors, create_monitor, get_metrics, get_check_results. Приведём кейс из жизни. В Claude Desktop можно попросить создать монитор на http://gpu-host-3:8000/v1/models, тип vLLM, метрики с порта 9080 — модель вызывает MСP-инструмент, бот создаёт монитор, а в Telegram прилетает ссылка на дашборд.
@<Aitip>mcp</Aitip>.tool
async def create_monitor(token: str, url: str, metrics_type: str) -> dict:
    """Create a new monitor for the authenticated user."""
    user = await validate_token(token)
    return await create_monitor_service(user, url, metrics_type)
    
К 2026 году MСP стал базовым контрактом между внутренними сервисами и инструментами инженеров — Claude, Cursor и собственными агентами. Сервис без MСP-фасада возвращает инженера в ручной режим: агент не может дотянуться до данных, и вместо него идёт человек.
mcp-interface.png
MСP-интерфейс

Демо-инцидент: что видит агент шаг за шагом

Системных замеров на размеченном датасете мы пока не делали — это следующий этап, отдельная работа на разметку и вычисление precision по корневой причине. Вместо этого покажем один воспроизводимый сценарий, на котором видно, как агент идёт от алерта до конкретики. Для тестов мы подняли стенд — небольшой FastAPI-сервис с эндпоинтом /error?scenario=…, который эмитит реалистичные ERROR-логи и опционально занимает место на диске или нагружает CPU. Подключаем его к боту обычным /add_monitor, и дальше всё крутится через стандартный алгоритм.

Что забрасываем в стенд

Один curl: scenario=disk, 200 МБ реального файла в /tmp плюс 15 строк disk_write_failed: ENOSPC в stdout сервиса. Promtail с целевой машины перекладывает их в центральный Loki, vmagent шлёт метрики Node Exporter и cAdvisor в Victoria Metrics. С этого момента на бот приходят два независимых трека: HTTP-probe и log-check.

Алерт в Telegram

Через 60 секунд после curl-залпа лог-чек-задача находит 15 ERROR в Loki по фильтру {container_name="sgr-test-app"} и присылает в чат алерт с краткой LLМ-выжимкой: «ENOSPC указывает на нехватку места, проверьте df -h, настройте ротацию логов». На этом этапе мы только фиксируем первый сигнал.
alert.png
Алерт в Telegram

Что меняется после нажатия «Исследовать»

Под алертом находится кнопка. Благодаря ней бот отдаёт sgr-agent-core текстовое описание задачи, а также адреса Loki, Victoria Metrics, явный временной диапазон и подсказку, что в запросах надо использовать instance="sgr-test-app" и container_name="sgr-test-app". Дальше агент сам решает, что брать. В нашем прогоне он прошёл по такому маршруту:
  • container_memory_usage_bytes{name="sgr-test-app"} — память в норме, ~44 МБ.
  • rate(container_cpu_usage_seconds_total{name="sgr-test-app"}[5m]) — CPU 0,002, нагрузки нет.
  • {container_name="sgr-test-app"} — нашёл 15 ERROR в нужном окне, увидел один incident_id и один путь /var/lib/app/data/job-...bin.
  • node_filesystem_avail_bytes{instance="sgr-test-app"} — 15,9 ГБ свободно.
И тут самое интересное: метрики говорят, что ничего не сломано, а логи указывают на ENOSPC. Это противоречие агент зафиксировал в reasoning-шаге — «диск свободен, ENOSPC может быть связан с лимитами контейнера или файловой системой /tmp» — и сформировал отчёт. Финальный отчёт, который пришёл в чат:
  • основная проблема: 15 ENOSPC от sgr-test-app в районе 11:59:46;
  • метрики: CPU и память в норме, host disk не страдает;
  • гипотезы: лимит на размер слоя контейнера, временные файлы /tmp, права;
  • немедленные шаги: docker inspect sgr-test-app, очистить /tmp/sgr-test-fill-*;
  • долгосрочно: лимиты, ротация, retry-логика, алерты на ENOSPC.
Алерт даёт односекундную generic-LLМ-реакцию на одну строку. Агент за 30 секунд возвращается с конкретным incident_id, реальными значениями метрик и списком гипотез, которые отличаются от учебника, потому что построены на этих данных.
final-report-1.png
final-report-2.png
Финальный отчёт и рекомендации

Где агент промахнулся

Агент не запросил container_fs_usage_bytes{name="sgr-test-app"}. Там был бы прямая причина: writable-слой контейнера за минуту вырос на 200 МБ. И rate-запрос вокруг 11:59:43 показал бы момент скачка. То есть агент остановился на гипотезах, хотя данных хватило бы и на однозначный ответ. Это направление мы дотачиваем — расширяем системный промпт примерами per-container disk/memory-запросов и подсовываем готовые рецепты в task.

Ограничения и планы

Наша система — хорошее, но не идеальное решение. Вот некоторые проблемы, с которыми мы столкнулись, и наши идеи по их преодолению:
  • SQLite упирается на 50+ хостах. Выбрали ради нулевого порога входа — поднял бота и поехал. Но при 50+ активных мониторах с месячной историей метрик это начинает мешать. В планах у нас переезд на Postgres с миграцией через Alembic.
  • Без настроенных Loki и Victoria Metrics SGR-агент галлюцинирует. Это больше архитектурное ограничение. Если логов нет, агент не признаётся, что их нет, а выдумывает. Решаем проблему двумя способами: жёсткая проверка доступности источника на старте расследования и правило в промпте.
  • Расход токенов. На текущей нагрузке (~50 расследований в месяц по покрытым ботом хостам) — около 600 рублей в месяц на gpt-4o-mini. С разворотом на Claude Sonnet — в 6–8 раз больше, но качество корреляции выше.
  • Промпт-инъекции. В логах лежат строки от пользователей, которые попадают в контекст агента. Закрываем правилом: «Логи помещаем в structured field, не в свободный текст промпта». Но это скорее полумера, чем полноценное решение.
  • Интерфейс вне Telegram. Веб-дашборд закрывает только просмотр; полноценного UI за пределами Telegram пока нет.

Что дальше

Следующий шаг — добавить агенту скиллы. Мы смотрим в сторону OpenClaw, open-source-фреймворка с растущим маркетплейсом скиллов, а ещё хотим подключить Terraform, чтобы он не только находил, что сломалось, но и чинил. И здесь интересно становится даже не с инженерной стороны, а с точки зрения безопасности: что должно произойти, чтобы доверить LLМ terraform apply на проде? К OpenClaw, кстати, у security-сообщества уже есть вопросы из-за широких прав агента. Это та же проблема, только мы хотим решить её для своей инфры. Похоже, ответ начинается со scope-ограничений, dry-run и обязательного human-in-the-loop на write-операциях, а также чего-то вроде NVIDIA NemoClaw как гард-рейла. Но это уже тема отдельной статьи.
Telegram-бот, который сам расследует инциденты на LLM-инфре