TL;DR. Пользователи почти никогда не сообщают о багах: напишет хорошо если 1 из 20, остальные молча закроют вкладку и не вернутся. Поэтому мы перестали ждать баг-репортов и посадили на логи автономного ИИ-агента: 20 строк bash, Grafana Loki на бесплатном тарифе и модель за $10 в месяц. Теперь о проблеме в проде мы узнаём максимум через полчаса — из Telegram, с цифрами и причиной. За 84 дня — 113 алертов, лучший превратился в смерженный фикс за 3,5 часа. По дороге агент дважды нам наврал, и половина статьи — про то, как заставить его перепроверять себя. Внутри архитектура, экономика и каркас, который собирается за день.

Зачем это всё

Агент дежурит на проде нашей платформы Creatorry — AI-генерация музыки, фото и видео. Я — Александр из nurokod.dev.

Под капотом — 18 сервисов: Cloudflare Workers, Vercel, пара VPS. Логи льются в Grafana Cloud Loki, около 300 тысяч строк в сутки, из них ~12 500 — error и warn. Команда не большая.

Стандартные варианты меня не устраивали. Пороговые алерты в такой системе воют на каждый штатный ретрай — через неделю их мутят и перестают читать. Читать логи глазами по утрам — значит узнавать о ночном инциденте после пользователей. А пользователи молчал и сообщают крайне редко.

Оставался 3й вариант: пусть логи раз в полчаса читает LLM и решает — будить людей или нет. Именно решает: отличить всплеск реальных ошибок от штатного ретрая воркфлоу порогом в YAML не опишешь, тут нужна голова. Своей головой на эту работу жалко, чужой — не бывает. Модель подошла.

Сразу управление ожиданиями: агент живёт 135 дней, и минимум месяц из них он не работал — молча. Про это тоже расскажу, статья не про идеальный продукт.

Как устроено

Архитектура ИИ Баг Агента
Архитектура ИИ Баг Агента

Рантайм — self-hosted OpenClaw (опенсорсный рантайм для длинноживущих агентов: сессии, память, cron, каналы доставки) на обычном VPS. Никакого LangChain: cron раз в 30 минут дёргает изолированную сессию, промпт ведёт модель по шагам.

Цикл простой:

  1. poll-loki.sh 5 — запрос в Loki за последние 5 минут по {level=~"error|warn"}. Вся «интеграция» — 20 строк bash.

  2. Только известный шум? Цикл заканчивается словом NO_REPLY, в канал не уходит ничего. Так заканчивается ~9 циклов из 10.

  3. Есть кандидат — агент расширяет окно (час, шесть часов): столько же было вчера? Это отсекает хронику.

  4. Фильтр. Алерт уходит ровно в двух случаях: (A) CRITICAL — user-facing 5xx, деньги, потеря данных, массовые падения; (B) рост — частота ≥ 2× к базлайну и минимум 5 событий за 5 минут.

  5. Дедуп: эта сигнатура уже алертилась за 2 часа? Молчим — если не выросла втрое (тогда эскалация «? НАРАСТАЕТ»). Надоело — /mute прямо из канала.

  6. Доставка: явный curl в Telegram-канал. CRITICAL дублируется в личку.

Медиана цикла — 15 секунд. За последние четверо суток: 200 прогонов из 200, 25 алертов, 175 молчаливых циклов.

Главное решение тут не фильтр, а правило выходной двери — дословно из промпта:

Cron работает с --no-deliver. OpenClaw НЕ отправляет финальный текст
агента никуда автоматически. ЕДИНСТВЕННЫЙ способ что-то попадает
в Telegram канал — это ТВОЙ explicit curl на api.telegram.org.
Если ты НЕ делаешь curl — НИЧЕГО не отправляется. Это default.

Почему это правило написано кровью — ниже, в истории про Opus. Сначала — как агент нам врал.

Враньё первое: выдуманный токен

Апрель. Агент находит в логах два реальных бага — детекция отработала идеально. Осталось отправить алерт.

Доставка тогда была описана через внутренний инструмент, которого у cron-сессии в контексте не оказалось. Модель, упёршись в отсутствующий инструмент, не остановилась — она дорисовала реальность: собрала curl к Telegram API сама и подставила токен бота. Выдуманный. Формат правильный, цифры — нет. Настоящий токен лежал в конфиге, агент туда не заглянул.

401 Unauthorized. Два реальных бага так и остались непрочитанными — мы узнали об этом из логов сессии на следующий день.

Вывод из этой истории простой: у автономного агента не должно быть места, где ему выгодно импровизировать. Теперь токен достаётся детерминированно — python-однострочником из конфига прямо в шаге промпта. Модели нечего «вспоминать».

Враньё второе: 84 ошибки в час из 2,5 реальных

4 мая агент прислал: «84/час exception, 7 за последние 5 минут». Звучит как пожар. Открываем Loki руками: реальная частота — 2,5 в час. В заявленном окне — ноль.

Три дефекта разом, все три — типовые для LLM-агентов:

  1. Self-grading bias. Один промпт и находил ошибку, и «проверял» сам себя. Проверка своей находки своим же контекстом ничего не проверяет.

  2. Экстраполяция. Модель посчитала 7 × 12 = 84/час из пятиминутного окна. Семь, кстати, тоже были посчитаны неверно.

  3. Семантика. В счётчик попали события canceled — клиент закрыл соединение, штатное поведение, не ошибка.

Лечили одним принципом: находке нельзя верить, пока её не пересчитал кто-то, кто её не находил. Как это выглядит:

  • Детектор и верификатор разделены. Верификатор получает только кандидата и конфиг — ни рассуждений детектора, ни его логики. Согласиться со своей же аргументацией невозможно, если ты её не видел.

  • Верификатор обязан считать другим запросом. Детектор искал |= "exception" — верификатор ищет |~ "outcome.*exception". Детектор использовал count_over_time — верификатор считает строки по таймстемпам. На каждый способ детекции прописан обязательный «чужой» способ проверки.

  • Порог железный: ratio = max(заявлено, проверено) / min(...). Больше 2,0 — REJECT. В скилле так и написано: «10 vs 25 = ratio 2.5 → REJECT. Threshold не обсуждается».

  • Отдельный скрипт verify-count.sh пересчитывает без участия модели. Его смоук-тест гоняется прямо на инциденте 4 мая: правильный ответ — ноль.

И моя любимая деталь — таблицы отмазок. В каждом скилле заранее выписаны рационализации, которыми модель будет оправдывать срезание углов:

Отмазка модели

Контраргумент в скилле

«Это явно баг, верификация излишня»

Именно эта мысль вызвала ложный алерт 04.05. Verify обязателен, особенно когда «явно»

«Цифры близки (10 vs 25), это ок»

ratio 2.5 → REJECT. Порог не обсуждается

«Loki недоступен, отправлю как есть»

Нет. inconclusive → retry в следующем цикле

«Очень критично, нет времени на verify»

Verify — это ~10 секунд. False alert портит сигнал

Работает: вот запись из журнала верификации — детектор заявил семь событий, независимый пересчёт нашёл одно, ratio 7,0, алерт умер не родившись.

{
  "service": "front",
  "verdict": "rejected",
  "claimed":  {"count_5min": 7, "window": "12:53-12:58"},
  "verified": {"count_5min": 1, "window": "12:53-12:58"},
  "ratio_5min": 7.0
}

Теперь честно: полный контур «детектор → изолированный верификатор» в получасовом прод-цикле пока не работает — упёрся в ограничение рантайма (изолированным cron-сессиям нельзя спавнить сабагентов). Журнал выше — из ручных прогонов. В проде вместо него статистический фильтр плюс два железных правила в промпте детектора: экстраполяция запрещена, счётчики только сырые. Для канала на трёх человек этого хватает — цена ложного алерта у нас минуты внимания, а не разбуженная on-call смена. Будил бы агент PagerDuty — такой компромисс был бы недопустим.

Про безопасность

Два вопроса, которые вы уже готовите в комментарии.

Prompt injection через логи. Да, логи — недоверенный ввод, и строка лога в теории может содержать инструкцию для модели. Поэтому агент устроен так, чтобы инъекции было нечего у него взять: все доступы — только на чтение (Loki, код на GitHub), к прод-инфраструктуре доступа нет вообще, а единственное действие во внешний мир — сообщение в наш же Telegram-канал. Худшее, что может сделать заражённая строка, — испортить один алерт или заставить агента промолчать цикл. Неприятно, не катастрофа.

Логи уходят в стороннюю LLM. Уходят. Но у нас в логах событийная телеметрия — коды, счётчики, идентификаторы запросов; персональных данных вроде e-mail там нет, это правило платформы, а не случайность. Если в ваших логах живут персданные или секреты — сначала вычистите логи (это стоит сделать независимо от ИИ-агентов), либо смотрите в сторону локальной модели: связка из туториалов «llama.cpp на своей железке» для этой задачи перестаёт быть игрушкой.

История про Opus, или дисциплина важнее ума

Выбор модели для агента обычно обсуждают в терминах «умнее/дешевле». Наша история за четыре месяца: MiniMax → Claude Opus → обратно MiniMax. И вернулись мы не только из-за цены.

Качество анализа у Opus было заметно выше — root-cause-разборы читались как написанные сильным инженером. Но:

Opus многословен, и это сломало доставку. Старая схема отдавала в канал «финальный текст агента» — а Opus любит сначала подумать вслух. Итог зафиксирован в коммите: за 48 часов в канал ушло 16 сообщений, все 16 — мусор («Now I have all data. Let me analyze…», дампы промежуточных выводов) и ноль алертов. Канал на двух человек такой режим убивает за неделю. Так родилось правило выходной двери: в канал попадает только явный curl, ошибки цикла туда не могут попасть физически.

Дорогую модель хочется брать через посредников — и это отдельная ловушка. Официальный API топовых моделей для процесса, жгущего сотни миллионов токенов в месяц, выходит очень дорого. Посредники сильно дешевле — но у нашего под нагрузкой начались 502 и 403, рантайм не умел делать на них failover, и каскад фолбэков сжигал по 4 минуты на цикл впустую: одиннадцать 403 за день — и агент полдня «работает», не работая. Стабильность и происхождение модели — тоже часть архитектуры, и на посреднике вы её не контролируете.

Контекст против таймаута. На Opus цикл с полным контуром скиллов занимал до 270 секунд и раздувал контекст до 200–350K токенов. После упрощения и возврата на MiniMax медиана цикла — 15 секунд.

Сейчас в проде MiniMax-M3 с контекстом, порезанным до 128K, фолбэки — M2.7 и Sonnet. Thinking выключен: для «просей логи по правилам» рассуждения вслух не добавляют качества, зато удваивают счёт.

Экономика: 435 миллионов токенов за $10

За июль агент сделал 1 587 вызовов и прожёг 435 128 408 токенов. Почти полмиллиарда. Живёт на подписке MiniMax за $10 в месяц.

Расход токенов по дням июля
Расход токенов по дням июля

Фокус в том, что 85% объёма — чтение кэша: цикл гоняет один и тот же системный промпт, скиллы и память каждые полчаса, и провайдер отдаёт это из кэша за копейки.

Второе, что видно на графике: вызовов стабильно ~51 в день, а расход гуляет от 5,7 до 25,2 млн. В агентах платишь за контекст, а не за запросы. Мы это уже проходили: в июне файл памяти дедупа распух до 65K токенов и грузился в каждый цикл — после чистки (реже цикл, короче контекст, thinking off) расход упал с 6,8 млн до 0,75 млн в день. По правому краю графика видно, что пора повторять: гигиена контекста у агента — регулярная уборка, как и у людей.

Остальное — почти бесплатно: Grafana Cloud Loki на тарифе Free (наши ~5,4 ГБ логов в месяц влезают с запасом), доля недорогого VPS. Мониторинг 18 сервисов — дешевле чашки кофе в неделю.

Что это дало на деле

Алерты по месяцам
Алерты по месяцам

113 алертов за 84 дня: 69 ERROR, 41 CRITICAL и три ранние записи без метки severity — первые дни журнал писался в другом формате. Провалы мая и июня — простои самого агента, о них ниже. Три кейса, ради которых всё затевалось:

Фикс за 3,5 часа. Утро 1 июня — первый день после большого простоя, и агент открывает смену сразу с CRITICAL: «Maximum number of running container instances exceeded», ~70 ошибок в час, генерация видео у пользователей лежит. Причина нашлась по горячим следам алерта: каждый вызов конвертации создавал новый контейнер (idFromName(crypto.randomUUID())) при лимите в 3 инстанса — пул выжирался двумя параллельными превью. В 10:17 открыт PR (ограниченный warm-пул, лимит выше, idle короче), в 10:31 — смержен. Поиск причины занял минуты, остальные часы ушли на сам фикс. Заметьте: ни один пользователь нам об этом не написал бы — они просто увидели бы вечную загрузку и ушли.

Алерт «не о том», вскрывший дыру. 29 июля — волна 503 на генерации текстов, рост ×7,4. Разбор показал: виноват внешний AI-провайдер, само рассосалось. Можно закрыть и забыть? Но в раскопках выяснилось: у нас есть фолбэк на второго провайдера, утром он работал — а в этой волне даже не попытался включиться. Часть запросов шла через ветку конфига, где роутер собирается без фолбэка. Через 30 минут после алерта в трекере лежал issue с выдержками из логов и гипотезой. Эту дыру не поймал бы ни один порог: каждый отдельный запрос выглядел «просто ошибкой апстрима», а дашборды были зелёными.

Карта техдолга бесплатно. 66% алертов — один и тот же сервис доставки вебхуков с хроническим «worker code hung». Агент напоминает о нём ровно с той частотой, с какой хроника реально обостряется, — дедуп душит повторы, эскалация пробивает тишину. Журнал за пару месяцев — готовый список «что чинить в первую очередь», отсортированный реальными частотами, а не ощущениями.

Так это выглядит в канале:

Лента алертов агента в Telegram
Лента алертов агента в Telegram

Каждый алерт объясняет, почему он вообще случился: счётчики, базлайн, рост, какое правило фильтра сработало, почему не разбужен человек в личке. Решение модели, которое нельзя проверить за минуту, доверия не заслуживает — поэтому у нас проверяется каждое:

{
  "service": "webhook-delivery",
  "severity": "ERROR",
  "count_5min": 7, "count_60min": 16,
  "baseline_per_5min": 1.33,
  "growth_ratio": 5.25,
  "filter_decision": "filter_B_growth_ratio_ge_2_count_5min_ge_5",
  "dm_alex": false,
  "dm_dedup_reason": "ERROR severity, not CRITICAL"
}

Кто сторожит сторожа

Самый смешной урок четырёх месяцев: агент, который следит за продом, сам оказался самой ненадёжной частью системы.

В журнале две больших дыры — 19 дней в мае и 11 в июне. Прод в это время жил как обычно. Умирал агент:

  • cron-планировщик рантайма после обновления молча перестал тикать — хранилище джобов переехало в SQLite, старые джобы не мигрировали;

  • автообновление подняло требование к версии Node, и все gateway-процессы легли до ручного вмешательства;

  • однажды агент сам себя задушил: сжёг недельный лимит API-ключа за четыре дня и замолчал, потому что модель перестала ему отвечать.

Паттерн один и тот же: система умирает молча, а молчание неотличимо от «всё хорошо». В классическом мониторинге это решено десятилетия назад — heartbeat, dead man’s switch. В самодельных ИИ-агентах об этом почему-то вспоминают в последнюю очередь. Мы теперь вспоминаем так: ежедневный дайджест в 08:00 — одновременно сводка багов и доказательство, что агент жив. Нет дайджеста — значит, чинить надо сторожа.

Daily Digest
Daily Digest

Приятная деталь на этом скрине — последняя строка: «Cycle health: 0/1 ok». Дайджест сам докладывает, что реалтайм-мониторингу плохо. Менее приятная: сам дайджест падает в 37% запусков, упираясь в таймаут, — суточная сводка читает на порядок больше данных, чем получасовой цикл. Чиним.

Соберите себе — это день работы

Всё описанное переносится на любой стек и любую модель, фреймворк не нужен. Минимальный каркас:

1. Сбор — тупой скрипт, не инструмент модели. Модель не решает, как ходить в источник данных:

# poll-loki.sh <minutes_back> — вся "интеграция" целиком
START=$(python3 -c "import time; print(int((time.time()-$1*60)*1e9))")
curl -sG "$LOKI_URL/loki/api/v1/query_range" \
  -u "$LOKI_USER:$LOKI_TOKEN" \
  --data-urlencode 'query={level=~"error|warn"}' \
  --data-urlencode "start=$START" \
  --data-urlencode "limit=100"
# limit=100 достаточно: фильтру нужны пороги, а не точный счёт;
# точный пересчёт — работа verify-шага с limit=5000

2. Молчание — состояние по умолчанию. Целевая доля молчаливых циклов 80–90%. Агента, который говорит чаще, перестанут читать.

3. Правило выходной двери. В канал попадает только явное действие агента. Одно это правило отделяет инструмент от спам-бота.

4. Дедуп с эскалацией. Сигнатура {сервис}|{класс}|{первый токен сообщения}, окно 2 часа, повтор только при росте ×3. Память — один JSON, обновляемый атомарно python-однострочником, а не «попроси модель отредактировать файл».

5. Верификация чужими руками. Детектор не отправляет; проверяющий считает другим запросом; ratio > 2 — кандидат умирает. Минимум, если контур не влезает в ваш рантайм: запретить экстраполяцию, требовать сырые счётчики.

6. Таблицы отмазок в промпте. Рационализация → контраргумент, на каждый способ срезать угол. Самый дешёвый способ поднять дисциплину модели из всех, что мы пробовали.

7. Heartbeat. Ежедневный дайджест по расписанию. Нет дайджеста — умер агент, идите чинить сторожа.

8. Объяснимость. В алерте — счётчики и сработавшее правило, в журнале — исход каждого цикла, включая молчаливые. Тишина без записи в журнал = поломка.

Собирается это за день-два с тестами. Отбивается первым же инцидентом, о котором вы узнали в течение получаса, а не из гневного отзыва через неделю — если пользователь вообще потрудится его написать.

Комментарии (0)