Слогер Создать блог
AI

5 уроков по observability для автономного AI-агента

Как я добавил логирование инструментов и перестал гадать, что на самом деле делает мой агент, пока я не смотрю.

Проблема: доверять агенту — значит не видеть правды

Мой агент работает сессиями по несколько часов без моего контроля. Он читает файлы, редактирует код, запускает команды и возвращает отчёт: «Рефакторинг модуля аутентификации выполнен, все тесты пройдены». Но проблема в том, что этот отчёт написан тем же агентом, который мог ошибиться. Если он молча пропустил шаг, неверно прочитал файл или «починил» не ту функцию, итоговый отчёт всё равно будет уверенным и чистым. У меня не было независимой записи того, что произошло на самом деле — только нарратив самого агента о самом себе.

Однажды это привело к серьёзной ошибке. Агент отчитался об успешном рефакторинге, тесты были зелёными. Но оказалось, что на более раннем шаге редактирование файла молча не применилось, и «проходящие тесты» проверяли старый код, который не изменился. Ничего не упало, ничего не выглядело неправильно. Агент просто сделал меньше, чем заявил. Тогда я понял: тихий частичный сбой хуже громкого. Падение указывает, где искать. Молча пропущенный шаг не даёт никаких зацепок, а итоговый отчёт активно врёт, умалчивая о проблеме.

Решение: логировать на границе, а не внутри агента

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

Что я логирую

Для каждого вызова инструмента я фиксирую:

  • Временная метка
  • Имя инструмента и краткое описание аргументов (без секретов и полного содержимого файлов — только чтобы понять, что произошло)
  • Статус результата: success, error или noop (последняя категория оказалась самой ценной)
  • Длительность
import json import time def log_tool_call(tool_name, args_summary, status, duration_ms): entry = { "ts": time.time(), "tool": tool_name, "args": args_summary, "status": status, # "success" | "error" | "noop" "duration_ms": duration_ms, } with open("agent_trace.jsonl", "a") as f: f.write(json.dumps(entry) + "\n")

Каждый запуск дописывается в файл .jsonl — один JSON-объект на строку. Этот формат оказался важен: он append-only, устойчив к сбоям (обрезанная последняя строка не портит предыдущие) и легко анализируется с помощью jq или Python, без базы данных.

Статус noop — настоящее спасение

Ошибка, с которой всё началось — молча пропущенное редактирование — проявилась сразу, как только я добавил статус noop. Инструмент редактирования, который выполняется, но ничего не меняет (потому что целевой текст не найден или файл уже в нужном состоянии) — это не то же самое, что успешное редактирование. Раньше оба случая показывались как «success» в моей голове. После разделения неудачный запуск выглядел бы так:

{"tool": "edit_file", "args": "auth.py: replace validate()", "status": "noop", "duration_ms": 4} {"tool": "run_tests", "args": "test_auth.py", "status": "success", "duration_ms": 812}

Noop, сразу за которым следует success на том, что он должен был исправить — огромный красный флаг, когда вы это видите. Но это было невидимо, когда у меня был только итоговый отчёт агента.

Визуализация сессии

Как только у меня появились структурированные логи, отследить сессию как последовательность стало просто. Например, диаграмма последовательности из пяти строк из сырого лога сразу показала проблему, которую я упускал в течение трёх сеансов отладки, читая прозаический отчёт агента.

Сэмплирование, а не всё подряд

Я не логирую полное содержимое файлов или полный вывод команд — это раздуло бы файл трассировки и вернуло проблему «стены текста, которую никто не читает». Вместо этого я логирую краткую сводку (первые ~100 символов diff, имя команды и код возврата, а не stdout). Если мне нужны полные детали для конкретного шага, я перезапускаю этот шаг вручную. Задача трассировки — сказать мне, где искать, а не быть полным повторением.

Запросы к логу вместо чтения

Когда логи стали структурированными, я перестал читать их сверху вниз и начал отправлять запросы. Однострочник вроде этого отвечает на вопрос «было ли сегодня что-то тихое?» за секунду:

cat agent_trace.jsonl | jq -r 'select(.status == "noop") | "\(.tool) \(.args)"'

После того как я начал запускать его после каждой сессии, выявилась закономерность, которую я раньше полностью упускал: примерно в 1 из 12 сессий был хотя бы один noop на записывающем инструменте. Большую часть времени это было безвредно — файл уже был в нужном состоянии. Но примерно четверть таких noop маскировали реальную проблему — тот же класс ошибок, с которого всё началось. До появления запроса у меня не было способа даже оценить эту пропорцию; я полагался на симптомы, которые замечал спустя дни.

Я также отслеживаю скользящее количество вызовов инструментов на сессию и помечаю всё, что выходит за пределы нормального диапазона. Сессия, которая обычно делает 20–40 вызовов, но внезапно делает 3 и останавливается, так же подозрительна, как и та, которая делает 200 — обычно это означает, что что-то сломалось выше по цепочке.

Честность в накладных расходах

Справедливый вопрос: не добавляет ли всё это логирование накладных расходов и сложности? На практике слой логирования — менее 40 строк кода, а сама запись занимает единицы миллисекунд — это один вызов open().write() на каждый вызов инструмента, без сетевых запросов и внешних сервисов. Стоимость ничтожна по сравнению с минутами (а иногда часами), которые я раньше тратил на восстановление сессии по памяти и скроллу после того, как что-то шло не так.

Выводы

  1. Логируйте на границе, а не внутри рассуждений. Лог, который пишет агент, наследует его слепые зоны. Лог, который генерирует обвязка при каждом реальном вызове инструмента, — это истина, независимая от того, что агент думает о произошедшем.
  2. «Ничего не сделано» заслуживает собственного статуса. Успех/неудача — недостаточно. Инструмент, который выполнился без ошибки, но ничего не изменил, — это отдельный важный случай. Именно там обычно прячутся настоящие баги, и объединение его с «успехом» — причина, по которой я упускал свою ошибку неделями.
  3. Append-only лучше, чем «умное» решение. Я сначала попробовал небольшую SQLite-базу, но она оказалась хрупкой — сбой во время записи мог повредить состояние, а для просмотра нужен был отдельный инструмент. Обычные JSON Lines файлы не требуют ничего, кроме cat и jq, устойчивы к сбоям, и их можно смотреть в реальном времени во время работы.
  4. Суммируйте, не выгружайте. Полный stdout/diff в каждой строке лога делает трассировку нечитаемой и дорогой для хранения. Логируйте достаточно, чтобы знать, где искать, и перезапускайте конкретный шаг для полных деталей, когда они действительно нужны.
  5. Сначала модель данных, потом визуализация. Мне хотелось с первого дня построить красивую панель. Но на самом деле важнее было сначала правильно определить схему лога — категории статусов, что считать одним «событием». Как только это стало надёжным, даже диаграмма последовательности из пяти строк из сырого лога оказалась полезнее той панели, которую я представлял.

Что дальше

Я работаю над тем, чтобы превратить детектор noop в активную проверку: вместо того чтобы я просматривал лог постфактум, обвязка будет помечать noop на любом инструменте редактирования/записи как предупреждение во время выполнения, чтобы тихий noop был обнаружен до того, как агент перейдёт к следующему шагу и выстроит ложный нарратив.

Также я хочу добавить флаги аномалий по длительности — если шаг, который обычно занимает 2 секунды, вдруг выполняется 40 секунд, это стоит проверить, даже если он технически «успешен». И я хочу вести небольшой скользящий базовый уровень (среднее количество вызовов инструментов, средняя длительность на инструмент) для каждого типа задач, чтобы «аномалия» измерялась относительно истории этой задачи, а не единого глобального порога, который сейчас даёт слишком много ложных срабатываний на действительно больших задачах.

Ничто из этого не должно быть сложным. Главный урок: observability не требует панели или вендора — она требует скучного, структурированного, append-only лога фактов, независимого от того, что вы пытаетесь наблюдать.

Заключение

Если вы запускаете любого автономного агента без присмотра, не доверяйте его собственному отчёту как единственной записи о произошедшем — логируйте вызовы инструментов независимо, дайте статус «ничего не сделал» и храните формат настолько простым, чтобы вы могли grep-нуть его в 2 часа ночи.

По материалам: dev.to. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

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

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.