Назад в блог

Как я мониторю 13 автономных агентов: что логировать, что алертить, что игнорировать

У меня работают 13 агентов Claude без моего участия. На прошлой неделе двое из них упали. Я узнал об этом через 2 минуты — не потому что поставил сложный мониторинг, а потому что с первого дня вписал в каждую задачу пять строчек JSONL.

Вот как это устроено и почему я не стал строить дашборд.

Автономный — не значит невидимый

Первые три недели система работала без мониторинга. Агенты запускались по cron, делали что-то, заканчивали. Я проверял вручную: зашёл в папку, увидел новые файлы — значит, сработало.

Проблема появилась тихо. Один агент выполнялся без ошибок и писал файлы — но с другим содержимым, чем ожидалось. Я заметил это через 9 дней и только потому, что случайно открыл старый прогон рядом. Технически — никакого сбоя. Фактически — потерянные циклы.

Это типичный паттерн автономных систем: молчаливая деградация хуже явной ошибки, потому что она не ломает процесс — она смещает его. Процесс продолжается, output приходит, но не тот.

После этого случая я принял два решения. Первое — ввести три статуса вместо двух. Второе — записывать каждый прогон в JSONL ещё до основной логики, с первой строки.

Структура лога: одна строка на событие

Каждый агент пишет в свой файл 06 - Операции/cron/logs/T{N}.jsonl. Структура строки выглядит так:

{"ts":"2024-06-14T03:10:22Z","task":"T14","status":"ok","slot_date":"2024-06-01","pillar":3,"slug":"ai-agent-roi-before-adoption","score":8.3,"duration_s":412}

Семь полей. ts — UTC ISO8601. task — идентификатор задачи. status — один из трёх. Остальные поля специфичны для задачи. Но эти три — обязательный минимум везде.

Почему JSONL, а не реляционная база? Потому что grep, jq и Python работают с JSONL без setup. Для 13 задач, каждая из которых пишет 1–5 строк в день, это 5–10 КБ в день. Через год — 3 МБ. Не проблема ни по storage, ни по разбору постфактум.

Три статуса, которые меняют всё

Не success/failure. Такая бинарность скрывает всё интересное.

Первый статус — ok: задача выполнена в полном объёме, вывод соответствует ожиданиям. Второй — error: агент завершился без полезного вывода, нужно вмешательство. Между ними есть третий, который и оказался самым важным.

degraded — задача выполнена частично, но вывод есть и он полезен. Пример: агент T14 собирает topic-кандидатов из внешних источников. Если все WebFetch упали из-за сетевых ошибок, агент не возвращает error — он достаёт кандидата из локальной Topic Library и пишет degraded. Процесс продолжается с меньшими данными.

Это разграничение пришло из практики: за первый месяц из 13 задач семь хотя бы раз вернули degraded. Ни одна не требовала ручного вмешательства. При бинарном success/failure я бы получил поток «неполных успехов» и привык бы их игнорировать. Именно так ломается alerting-культура.

Есть и отдельные статусы ранних выходов — inbox_full, no_pending_slots. Это не error и не degraded. Это нормальное завершение с конкретной причиной. Разделять такие состояния важно, иначе получаешь алерты там, где их не должно быть.

О том, как решать, что доверять выводу агента, я писал отдельно — см. AI агент в продакшне: 3 паттерна доверия. Здесь другая задача: знать, что агент вообще сделал что-то полезное.

Паттерн claude_start: отличить «не запустился» от «упал внутри»

Первая строка в логе — до любой бизнес-логики:

{"ts":"2024-06-14T03:10:01Z","task":"T14","status":"started"}

Зачем? Потому что есть два вида падений. Первый — агент запустился, но упал на середине. Второй — агент не запустился вообще: cron не сработал, Python не нашёл зависимость, lockfile остался с прошлого прогона.

Без started-записи вы видите только отсутствие ok и не можете понять, было ли падение внутри или снаружи. С ней — сразу ясно: если нет started за ожидаемое время, проблема в запуске. Если started есть, а ok/degraded нет — проблема внутри логики.

Эта строка стоит ровно пять секунд на написание. Возвращает кратно больше при разборе сбоя постфактум.

Алерты без Datadog

Никаких дашбордов. Один shell-скрипт в cron, раз в час:

for task in T14 T15 T16; do
  recent=$(tail -20 "cron/logs/${task}.jsonl" | grep '"status":"error"' | wc -l)
  if [ "$recent" -gt 0 ]; then
    ./send_tg_notification.sh "⚠️ ${task}: error в последних 20 прогонах"
  fi
done

Уведомление идёт в Telegram. Не на почту — её я не читаю сразу. TG-сообщение приходит на телефон, я вижу его в течение нескольких минут.

Это весь мониторинг. Без collector-agent, без time-series database, без Grafana. Настройка заняла 20 минут. Поддержка — нулевая.

Что я намеренно не мониторю

Первые несколько недель хотелось добавить метрики качества: длина вывода, соответствие шаблону, «Score» полезности. Остановился, потому что это не метрики надёжности — это метрики качества контента. Качество — это review, не мониторинг.

Время выполнения тоже не мониторю. Агент может занять 3 минуты или 25 — зависит от внешних сервисов и объёма контекста. Пока завершается без error, время не критично. Threshold по времени → ложные срабатывания при медленном API → привыкание игнорировать алерты → смерть alerting-культуры.

Один раз пройти этот цикл мысленно лучше, чем проходить его на production через полгода.

Три месяца спустя

degraded оказался самым полезным статусом. И самым неожиданным: я ожидал, что большинство падений будут error. По факту error — редкость. Большинство сбоев — это внешние нестабильности: WebFetch, rate limits, сетевые таймауты. Агенты справляются сами, если в промпте прописан fallback.

Это изменило подход к написанию промптов. Раньше я писал «если это не работает — останови». Теперь — «если это не работает — сделай вот так и пометь как degraded». Разница маленькая в промпте, большая в операционном поведении.

За три месяца накопилось ~12 МБ JSONL-логов по всем задачам. Статус error — около 2%. Всё остальное — ok, degraded и нормальные ранние выходы. Это хорошая картина, и она была бы невидима без структурированного лога.

Если запускаете первого автономного агента — начните именно с этого: три статуса, одна строка на событие, TG-алерт только на error. Ничего лишнего. Подробнее о том, как писать спецификацию до первого запуска агента, — в этой статье.