Технологии

Отладка агента: как читать его трассировку

Ошибка агента почти всегда видна на первом неверном наблюдении, а не в финальном ответе. Разбираем реальную трассировку с точкой сбоя и даём шесть вопросов к логу.

«Агент сделал не то» — с этой формулировкой приходят чаще всего, и почти всегда следом идёт попытка починить финальный промпт. Промпт переписывается, поведение меняется случайным образом, через день всё повторяется.

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

Трассировка читается как улика: не «что он ответил», а «где он впервые увидел неправду».

#Быстрая проверка

Три вопроса, отсекающие большинство случаев. Задайте их до того, как открывать лог целиком.

  1. Вызовы вообще были? Если в логе нет ни одного вызова инструмента, а вы ждали действия — проблема не в цепочке, а в том, что модель не вызвала инструмент. Это другой класс, разобранный в материале про пределы агентов на маленькой локальной модели.
  2. Первый вызов вернул то, что вы ожидали? Откройте результат первого вызова и прочитайте его глазами. В большинстве разобранных случаев ответ здесь «нет», и дальше можно не смотреть.
  3. Сколько было шагов? Если цепочка упёрлась в предел — вы отлаживаете не ошибку, а исчерпание бюджета.

#Что должно быть в трассировке

Лог, по которому нельзя ответить на шесть вопросов из следующего раздела, бесполезен. Минимальный состав записи на каждый вызов:

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

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

Про то, почему логи стоит писать структурированно, а не строкой, — в материале про структурированные логи.

#Пример трассировки с точкой сбоя

Задача: «посчитай, сколько я потратил на подписки в прошлом месяце, данные в папке finance».

шаг 1  ВЫЗОВ       list_dir({"path": "finance"})
       СТАТУС      ok, 40 мс
       РЕЗУЛЬТАТ   FILE subs-2026-07.csv 2100b
                   FILE subs-2026-08.csv 1980b
                   FILE archive.zip 44100b

шаг 2  ВЫЗОВ       read_file({"path": "subs-2026-08.csv"})
       СТАТУС      ok, 12 мс
       РЕЗУЛЬТАТ   name,amount,date
                   Хостинг,890,2026-08-03
                   Музыка,299,2026-08-07
                   … 14 строк …
                                          ◀── ТОЧКА СБОЯ
                   здесь август, а просили июль

шаг 3  ВЫЗОВ       read_file({"path": "subs-2026-08.csv"})
       СТАТУС      ok, 11 мс
       РЕЗУЛЬТАТ   (то же самое)
                                          ◀── повтор, дедупликации нет

шаг 4  ОТВЕТ       «За прошлый месяц вы потратили 4188 ₽
                    на 16 подписок»

Что видно из лога и не видно из ответа.

Первое. Ошибка на шаге 2, а не на шаге 4. Модель прочитала августовский файл вместо июльского. Число в ответе получено правильным суммированием неправильных данных — и выглядит совершенно достоверно.

Второе. Причина ошибки — не «модель глупая». Ей никто не сказал, какой сейчас месяц. «Прошлый месяц» невычислим без текущей даты, а в контексте её не было. Чинить надо не промпт, а подстановку даты.

Третье. Шаг 3 — повтор шага 2. Он не изменил результат, но стоил вызова модели и места в контексте. Это отдельный дефект: нет дедупликации одинаковых вызовов.

Четвёртое. Все статусы — ok. Отсутствие ошибок в логе не означает отсутствия ошибок в работе. Самые дорогие сбои проходят со статусом «успех».

#Шесть вопросов к логу

Чек-лист, который проходится по порядку. Первый же ответ «нет» указывает, где чинить.

1. Был ли вызван инструмент вообще?
   нет → проблема в описании инструмента или в промпте

2. Первый вызов получил ожидаемые аргументы?
   нет → модели не хватило данных на входе (дата, путь, имя)

3. Первый результат — то, что вы бы получили сами?
   нет → проблема в инструменте или в данных, не в модели

4. Есть ли повторяющиеся вызовы с одинаковыми аргументами?
   да  → нет дедупликации; смотрите, почему модель не поняла результат

5. Все ли статусы «успех» при неверном итоге?
   да  → инструмент возвращает правдоподобную неправду

6. Цепочка закончилась сама или упёрлась в предел?
   предел → это не ошибка рассуждения, это бюджет

Вопрос 3 — центральный. Формулируется он именно так: «то, что вы бы получили сами». Не «выглядит ли результат правдоподобно» — правдоподобно выглядит почти всё.

#Типовые находки

Инструмент вернул ошибку в человекочитаемом виде, и модель приняла её за данные. Строка «[error] файл не найден» — это текст, и модель может пересказать её как содержимое. Признак — в ответе появляются формулировки из ваших сообщений об ошибках.

Аргументы пришли с лишним префиксом. Модель добавила к пути имя рабочей папки, потому что увидела его в выводе списка каталога. Лечится тем, что список выводит пути ровно в том виде, в котором их надо передавать обратно, и говорит об этом прямо.

Результат обрезан на середине. Инструмент вернул больше, чем помещается в предел, и модель увидела половину таблицы. В логе это видно по длине результата, упирающейся в круглое число.

Вызов есть в логе, но эффекта нет. Значит, вызов был отклонён политикой прав. Отказ обязан попадать в лог как отдельная запись, иначе он неотличим от того, что инструмент не вызывался.

Между шагами прошло много времени. Инструмент ходил в сеть и упирался в таймаут. Длительность в записи существует именно для этого.

#Что делать с находкой

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

  1. Подставить недостающие данные. Текущая дата, рабочий каталог, часовой пояс — всё, что модель не может знать, должно приходить в контекст, а не угадываться.
  2. Переписать описание инструмента. Пример значения параметра, явное указание, что вернётся при ошибке, непохожее имя. Правила формулировок — в руководстве по написанию своего MCP-сервера.
  3. Сузить инструмент. Заменить универсальный на конкретный: не «прочитать файл», а «прочитать выписку за месяц».
  4. Добавить дедупликацию и пределы. Одинаковые вызовы, предел результата, предел числа шагов.
  5. И только потом трогать промпт. Он последний в списке, потому что его правки хуже всего воспроизводятся.

Механика самого цикла, который вы читаете в логе, — в разборе паттерна ReAct.

#Если ничего не помогло

Три вещи, которые стоит проверить, когда лог выглядит нормально, а результат нет.

Лог неполный. Проверьте, попадают ли в него отклонённые вызовы, обрезанные результаты и вызовы, упавшие с исключением. Дыра в логе выглядит как отсутствие проблемы.

Вы читаете лог не того хода. При параллельных разговорах записи перемешиваются. Если в записи нет идентификатора разговора и сообщения, добавьте его — это дешевле, чем гадать.

Проблема не в агенте. Часть жалоб на агента — это задачи, где агент не нужен: последовательность известна заранее и должна быть кодом. Признак — вы каждый раз ожидаете от него одной и той же цепочки действий.

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

#Частые вопросы

#С чего начинать разбор, если агент выдал неверный ответ?

С первого вызова инструмента, а не с финального текста. Прочитайте его результат глазами и сравните с тем, что получили бы сами. В большинстве случаев расхождение обнаруживается здесь, и дальше по логу идти не нужно.

#Что обязательно писать в лог вызова?

Имя инструмента, аргументы целиком, статус, начало результата, длительность и идентификаторы разговора и сообщения. Аргументы «целиком» — принципиально: запись «вызван read_file» не позволяет понять, какой именно файл был прочитан, а это и есть половина всех ошибок.

#Почему все статусы «успех», а результат неверный?

Потому что инструмент отработал корректно и вернул не те данные: не тот файл, устаревшую запись, пустой список. Статус описывает исполнение инструмента, а не соответствие результата задаче. Именно поэтому пятый вопрос чек-листа существует отдельно.

#Нужно ли логировать содержимое результатов?

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

#Как отличить ошибку модели от ошибки инструмента?

По третьему вопросу: если бы вы выполнили тот же вызов с теми же аргументами руками и получили тот же результат — инструмент отработал правильно, и дело в том, какие аргументы выбрала модель. Если результат отличается от ожидаемого — дело в инструменте или в данных.

#Сколько прогонов нужно, чтобы найти закономерность?

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

#Вывод

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

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

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт