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 минут дёргает изолированную сессию, промпт ведёт модель по шагам.
Цикл простой:
poll-loki.sh 5— запрос в Loki за последние 5 минут по{level=~"error|warn"}. Вся «интеграция» — 20 строк bash.Только известный шум? Цикл заканчивается словом
NO_REPLY, в канал не уходит ничего. Так заканчивается ~9 циклов из 10.Есть кандидат — агент расширяет окно (час, шесть часов): столько же было вчера? Это отсекает хронику.
Фильтр. Алерт уходит ровно в двух случаях: (A) CRITICAL — user-facing 5xx, деньги, потеря данных, массовые падения; (B) рост — частота ≥ 2× к базлайну и минимум 5 событий за 5 минут.
Дедуп: эта сигнатура уже алертилась за 2 часа? Молчим — если не выросла втрое (тогда эскалация «? НАРАСТАЕТ»). Надоело —
/muteпрямо из канала.Доставка: явный
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-агентов:
Self-grading bias. Один промпт и находил ошибку, и «проверял» сам себя. Проверка своей находки своим же контекстом ничего не проверяет.
Экстраполяция. Модель посчитала 7 × 12 = 84/час из пятиминутного окна. Семь, кстати, тоже были посчитаны неверно.
Семантика. В счётчик попали события
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». Агент напоминает о нём ровно с той частотой, с какой хроника реально обостряется, — дедуп душит повторы, эскалация пробивает тишину. Журнал за пару месяцев — готовый список «что чинить в первую очередь», отсортированный реальными частотами, а не ощущениями.
Так это выглядит в канале:

Каждый алерт объясняет, почему он вообще случился: счётчики, базлайн, рост, какое правило фильтра сработало, почему не разбужен человек в личке. Решение модели, которое нельзя проверить за минуту, доверия не заслуживает — поэтому у нас проверяется каждое:
{ "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 — одновременно сводка багов и доказательство, что агент жив. Нет дайджеста — значит, чинить надо сторожа.

Приятная деталь на этом скрине — последняя строка: «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. Объяснимость. В алерте — счётчики и сработавшее правило, в журнале — исход каждого цикла, включая молчаливые. Тишина без записи в журнал = поломка.
Собирается это за день-два с тестами. Отбивается первым же инцидентом, о котором вы узнали в течение получаса, а не из гневного отзыва через неделю — если пользователь вообще потрудится его написать.