ИИ-агент 4 месяца дежурит на наших прод-логах: 113 алертов, 2 галлюцинации и фикс за 3,5 часа
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. Объяснимость. В алерте — счётчики и сработавшее правило, в журнале — исход каждого цикла, включая молчаливые. Тишина без записи в журнал = поломка.
Собирается это за день-два с тестами. Отбивается первым же инцидентом, о котором вы узнали в течение получаса, а не из гневного отзыва через неделю — если пользователь вообще потрудится его написать.
Источник: habr.com
Похожие записи
Оцените материал:
Похожие записи
Я переписал реальный рабочий процесс обработки данных на языке Polar. Pandas не имел ни единого шанса.
12.05.2026
Стартап Einride, занимающийся разработкой беспилотных грузовиков, планирует выйти на биржу через SPAC
12.11.2025
Claude дал неправильную архитектуру. Настоящая ошибка была не в Claude
16.06.2026Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
