Webrium
Подписаться

Трассировка шагов вместо логов

Как заменить ленту логов на дерево шагов, чтобы видеть не только где упало, но и почему агент решил именно так

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

Лог отвечает не на тот вопрос

Когда агент делает не то, что от него ждали, вопрос звучит не «где упало», а «почему он так решил». Лог отвечает на первый и молчит на втором. В этом различии помещается почти вся мотивация замены.

Лог — это событие: что-то случилось, и в момент случившегося записана строка. Шаг — это запись с началом и концом, с входом и выходом, с местом в дереве причин. Разница глубже формата вывода: у строки лога нет понятия «завершилось». «Вызываю инструмент» и «инструмент вернул» связаны только в голове читающего — система связи между ними не хранит.

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

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

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

Понятий немного, на них держится всё остальное.

Прогон — одна попытка агента выполнить задачу. Шаг — один осмысленный акт внутри прогона: обращение к модели, вызов инструмента, выбор ветки. У шага есть родитель, и в этом главное отличие от кучи логов: дерево связей восстанавливает система в момент выполнения, а не вы вечером по грепу.

Что несёт шаг:

  • идентичность — свой идентификатор, идентификатор прогона, родителя;
  • вход — промпт, аргументы, ссылки на них;
  • выход — ответ модели, результат инструмента;
  • статус — успех, ошибка, отмена;
  • начало и конец, из которых выводится длительность;
  • конфигурацию, повлиявшую на результат: версию промпта, параметры модели, версию инструмента.

О том, что класть в шаг целиком, а что оставлять ссылкой, чтобы трассировка не превратилась в склад, у нас есть отдельный разбор — «Что писать в трассировку, а что нет». Здесь зафиксируем одно: конфигурация — часть записи, а не украшение. Шаг без неё отвечает на «что», но не на «почему».

От привычной трассировки распределённых вызовов это отличается источником границ. Там спаны ложатся на сетевые обращения — границы диктует транспорт. Здесь границы проводит семантика задачи, и это главное проектное решение; к нему вернёмся среди поломок.

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

Тупик: идентификатор прогона в каждой строке

Первое, что пробуют на пути к трассировке, — не трогать архитектуру, а добавить в каждую строку лога идентификатор прогона. Погрепали по нему — собрали всё, что относится к неудачному запуску. Кажется, дальше мелочь.

Не работает, и причина любопытная. Идентификатор собирает строки в кучу, но куча — не дерево. Чтобы получить из кучи шаги, придётся парсить пары «начал/закончил», связывать их по формату строк и надеяться, что новый тип шага не сломает парсер. Так пишут трассировщик внутри лог-пайплайна: ключи на строках, состояние на регулярках, две системы на сопровождении вместо одной. При этом знание о том, где шаг начался и где кончился, существует только у кода, который шаг выполняет, — парсер узнаёт его последним.

Вторая попытка того же класса — «будем писать всё»: поднять детальность логов до максимума. Объём растёт как число шагов, помноженное на средний размер шага, — считайте сами и подставляйте свой бюджет на хранение. А вопрос «почему решил» всё равно требует связки «этот вход → этот выход → это решение». Текст не становится структурой от того, что его стало больше.

Тупик притягивает дешевизной: одно поле, один флаг, код не меняется. Но не хватало не поля — не хватало понятия. Записи, у которой есть завершение и место в дереве, в системе не было, и косметика понятие не добавляет.

Честная оговорка: если вы дошли до того, что в каждой строке пишете идентификатор шага и родителя, вы уже пишете трассировку — только храните её в инструменте, рассчитанном на ленты, а не на деревья. Идея к этому моменту уже принята; развилка в том, где записям жить — в хранилище под греп или в просмотрщике, который сворачивает ветки и сравнивает прогоны.

Почему трассировка ломается именно так

Перенести понятие шага в код — полдела. Поломки у трассировки свои; ниже основные, по механизмам, не по симптомам.

Границы по модулям, а не по решениям. Ставить вызовы трассировщика удобно по границам функций и модулей. Информативно — там, где агент принимает решения. Когда границы проходят не по решениям, симптом узнаваемый: вы открываете трассу с вопросом «почему он дважды позвал инструмент», а ответ не лежит ни в одном шаге — он между шагами. Правило: шаг — это решение, а не функция. У той же поломки вторая сторона — размер: слишком крупные шаги дают трассу без детализации, слишком мелкие — дерево, которое никто не осилит. Размер подбирается по вопросам, которые вы задаёте прогонам, а не по удобству расстановки вызовов.

Повторы. Ретрай — один логический шаг и несколько физических попыток. Каждая попытка отдельным шагом — и трасса превращается в шум из близнецов. Схлопнуть попытки в одну — потерять разницу между ними, а разница часто и есть весь материал разбора: что изменилось между попытками — аргументы? ответ модели? Рабочее направление: попытки становятся детьми логического шага, а разница между ними — на видном месте.

Недетерминированность. Один и тот же вход даёт разные деревья. Главный приём отладки — «сравнить рабочий прогон со сломанным» — ломается наивно: если шаги идентифицированы порядковым номером, сравнение прогонов с разным числом узлов даёт мусор. Сравнивать нужно по смыслу — имя шага, состав аргументов, — а не по позиции. Формат, в котором шаги различаются только позицией, убивает главное применение трассировки.

Крах в середине шага. Незавершённый шаг не закрывается. Если трассировщик пишет шаг пачкой по завершению — а именно так пишет наивная реализация «собрали и сбросили» — самый интересный для разбора прогон, упавший, остаётся с пустой трассой. Поэтому шаг открывается в момент старта. Заодно нужен третий статус, «отменён»: параллельная ветка, снятая из-за ошибки соседней, — не ошибка и не успех, и при двух статусах она портит картину.

Выборка. Хранить всё нельзя: бюджет хранения конечен, а прогоны идут потоком. Отбирать по ошибкам — теряете базу для вопроса «это норма или нет», потому что норму меряют по здоровым прогонам. Отбирать случайно — редкие сбои проскакивают мимо. Универсального ответа нет; хуже любой политики — её отсутствие, когда хранилище решает за вас настройками по умолчанию.

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

Позиция редакции

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

Аргумент короткий. Вопросы сместились с «где» на «почему», а «почему» по определению требует связки входа, выхода и решения — то есть шага. К тому же запись работает дважды: прогоны, сохранённые как деревья шагов, переигрываются против нового промпта или новой конфигурации до релиза — из них же собирается оценка качества агента, тема нашего отдельного материала. Неструктурированные записи оценке дать нечего.

Условие, при котором мы неправы: если выполнение линейно и детерминированно — один и тот же вход всегда проходит один и тот же путь и даёт один и тот же результат, — трасса вырождается в линию, и хвост лога прилично её приближает. Тогда инструментировать шаги — получить сопровождение без отдачи; оставайтесь с логами. Наш совет оправдан ровно тогда, когда в выполнении есть ветвление, повторы или недетерминированность; если ничего этого нет, следовать ему вредно.

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

И последнее. Начинать мы советуем не с кода, а с вопросов: выпишите, что вы спрашиваете у сломанного прогона. Правильного размера шага не существует — он выводится из ваших вопросов, и только из них. Сначала вопросы, потом границы в коде.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Четыре показателя-поля трейса: ID запроса, шаг вызова и код ошибки в норме, а тело запроса уходит за шкалу — его место в архиве, а не в трейсетело запроса
Предыстория

Что писать в трассировку, а что нет

База перед этим текстом: какие поля писать в спанах, а какие выносить в архив, чтобы трейс сразу показывал, где сломался запрос.

Сетка прогонов агента: успешными заполнена лишь часть ячеек, рядом счётчик типов отказовтипы отказов
Практика

Оценка качества агента до релиза

Трассировка шагов покажет, что делал агент, но не ответит, готов ли он к релизу. Практика: постусловия вместо текстов и порог по классам отказов, а не по среднему.

Поле, разделённое швом: элементы OpenTelemetry пересекают границу между тем, что уже стало стандартом, и тем, что остаётся удобным клеемСтало стандартомУдобный клей
Углубление

OpenTelemetry: что стало стандартом, а что ещё нет

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

Фильтрация логов: из множества событий выделяется критичный сигнал для анализа сбоевВсе событияКритичный сигнал
Углубление

Уровни логирования: что писать, а что выключить

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

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть