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

Мы построили агентный воркфлоу по той же схеме. Каждый этап отвечает за свою часть расследования и передаёт результат следующему.

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

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

Поэтому мы изменили подход к тестированию, позаимствовав принцип из поэтапной отладки аппаратуры (hardware bring-up): включить один этап, проверить его с помощью явно заданной контрольной проверки (gate), убедиться, что он стабильно её проходит, и только после этого подключать следующий.

В этой статье мы расскажем, как такой подход помог локализовать сбои до конкретного этапа, контролировать стоимость каждого тестового запуска и обнаружить проблему там, где мы её совсем не ожидали, — в самом механизме оценки.

Почему по итоговому ответу нельзя понять, где сломался воркфлоу

Пайплайн расследования состоял из фиксированной последовательности этапов:

  1. Определить временной интервал инцидента.

  2. Определить наиболее вероятного подозреваемого и оценить его приоритет.

  3. Подтвердить гипотезу независимыми данными из другой плоскости данных (data plane).

  4. Свести доказательства воедино и сформировать ответ.

Каждый этап получает результат предыдущего. Поэтому неудачное решение в начале воркфлоу может повлиять на все последующие этапы.

Это типичная составная (compound) AI-архитектура: сам воркфлоу детерминирован, но внутри каждого узла работает недетерминированная модель.

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

Полные запуски делают и сам цикл разработки довольно дорогим:

  • Каждый этап расходует токены и может порождать несколько вызовов инструментов.

  • Полный запуск занимает минуты, а не секунды.

  • При одном и том же входе следующий запуск может дать уже другой результат.

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

Нам нужен был способ локализовать сбои, не подменяя реальное поведение предыдущих этапов.

Начинаем с одного этапа и постепенно добавляем следующие

Подход строится накопительно. На уровне N мы по-настоящему запускаем этапы с 1-го по N-й и останавливаемся перед этапом N+1.

Накопительный подход. Поэтапная отладка идёт накопительно. Предыдущие этапы на каждом уровне продолжают выполняться вживую, а последующие остаются отключёнными, пока текущий уровень не начнёт стабильно проходить проверку.

Цикл выглядит так:

  1. Обрезать пайплайн после этапа, который сейчас тестируется.

  2. Запустить его и проверить по заранее заданному критерию успешного прохождения.

  3. Если проверка не пройдена, исправить текущий этап.

  4. Повторять запуск, пока этап не начнёт стабильно проходить проверку.

  5. Добавить следующий этап и повторить цикл.

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

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

Что проверяет контрольная проверка

Перед запуском этапа мы определяем, что именно он должен гарантировать. Это условие мы называем контрольной проверкой (gate).

Например:

levels := []struct {

name string

gates []Gate

}{

{"L1: anchor the window", []Gate{hasConcreteWindow}},

{"L2: name the suspect", []Gate{hasConcreteWindow, hasRankedSuspect}},

{"L3: corroborate", []Gate{hasConcreteWindow, hasRankedSuspect, hasSecondPlane}},

// Добавляем новый уровень только после того, как предыдущий начнёт стабильно проходить проверки.

}

Эта таблица задаёт схему поэтапной проверки. Каждая строка добавляет один этап и набор проверок, которые должен пройти накопительный воркфлоу на этом уровне.

Контрольная проверка работает с фактически выполненными действиями агента:

// Gate оценивает фактически выполненные вызовы инструментов, а не необработанный журнал выполнения.

type Gate func(effects []ToolCall) Result

func hasConcreteWindow(effects []ToolCall) Result {

for _, c := range effects {

if c.Name == "query_metrics" && c.Args.Window != "" {

return Pass()

}

}

return Fail("stage produced no concrete time window")

}

Это лишь упрощённый пример, а не набор проверок, который мы используем в продакшене. Главное здесь — интерфейс: контрольная проверка смотрит на то, что агент фактически сделал, то есть на типизированные вызовы инструментов и их аргументы.

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

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

Надёжные решения агента требуют надёжного контекста. Графы знаний дают AI SRE-агентам структурированный контекст, необходимый для рассуждений о взаимосвязях между системами.

Как проваленная проверка сужает область поиска

На уровне N этапы с 1-го по N−1 уже неоднократно прошли свои проверки. Если новый накопительный запуск завершается неудачно, первым делом стоит исследовать последний добавленный этап.

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

Определяйте критерии успеха до запуска

Без контрольной проверки легко оценивать запуск агента по тому, насколько разумно звучит его ответ. Для многоэтапного операционного воркфлоу это слишком субъективный критерий.

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

Почему одного успешного запуска недостаточно

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

Универсального числа запусков, после которого систему можно считать надёжной, не существует. Тысячи прогонов платной передовой модели редко бывают практически оправданны, а небольшая выборка не позволяет делать заявления о надёжности на уровне 99-го перцентиля. Поэтому вместо этого мы выбираем порог с учётом риска.

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

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

Ограничивайте объём каждого тестового запуска

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

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

Какие баги помог найти этот подход

Накопительное тестирование выявило несколько проблем, которые легко могли бы остаться незаметными за вполне правдоподобным итоговым ответом:

  • Неверный временной интервал: один из этапов выбрал слишком узкий интервал, поэтому на последующих этапах корреляции не удалось обнаружить реальное начало инцидента.

  • Не тот сигнал на первом месте в рейтинге: этап ранжирования выбрал самый крупный всплеск, а не сигнал, который с наибольшей вероятностью стал причиной инцидента. Корреляцию он принял за причинно-следственную связь.

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

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

Когда баг оказался в самом механизме оценки

Самая полезная ошибка, которую мы обнаружили, оказалась не в агентном воркфлоу, а в коде оценки.

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

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

Механизм сопоставления строк не мог различить два случая:

  • В промпте написано: «Не вызывай X».

  • Агент вызывает X.

В обоих случаях в потоке событий появлялся один и тот же текст. В результате даже корректный запуск не проходил контрольную проверку.

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

Механизм оценки измерял содержимое журнала выполнения, а не поведение системы.

Журнал выполнения содержит и инструкции, и действия. Механизм сопоставления строк может принять упоминание запрещённого действия в промпте за действие, которое действительно выполнил агент.

Оценивайте вызовы инструментов, а не весь журнал выполнения

Мы исправили проблему, переключив все проверки поведения на фактически выполненные вызовы инструментов и их аргументы.

Необработанный журнал выполнения по определению содержит много постороннего с точки зрения такой проверки. В него могут попадать:

  • Системные инструкции и инструкции из ранбука

  • Вывод модели

  • Предупреждения о запрещённых действиях

  • Повторные копии одного и того же события при стриминге

  • Результаты вызовов инструментов и служебная обвязка фреймворка

Для отладки всё это полезно, но как источник истины для проверки поведения системы такой журнал ненадёжен.

Фактически выполненные действия дают гораздо более чёткую границу. Если нужно понять, вызвал ли агент инструмент, указал ли правильный временной интервал или передал ли обязательный параметр, оценщик должен смотреть на типизированный вызов инструмента.

Источник для оценки

Что он отражает

Подходит для проверок поведения?

Необработанный журнал выполнения или поток событий

Инструкции, вывод модели, работу инструментов и шум стриминга

Нет

Сформированный ранбук или промпт

Что было сказано агенту

Нет

Фактически выполненные вызовы инструментов и их аргументы

Что агент действительно сделал

Да

Для агентов это примерно то же самое, что проверять состояние системы, а не делать выводы по логам. Вряд ли инженер стал бы проверять переход приложения из одного состояния в другое, разыскивая нужную строку в выводе println. Но в рантаймах агентов такую ошибку допустить легко, потому что весь процесс выполнения часто выглядит как один большой поток JSON.

Граница на уровне типов Go помогла закрепить исправление. Контрольная проверка, принимающая []ToolCall, физически не может случайно найти совпадение в тексте промпта, потому что промпт вообще не входит в её входные данные.

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

Отладка и оценка надёжности AI-агентов требуют отдельного набора навыков. Проверить свои знания можно с помощью короткого вступительного теста курса «AgentOps: эксплуатация ИИ-агентов».

Что мы сохраним для следующего воркфлоу

  • Тестируйте многоэтапные воркфлоу накопительно: запускайте этапы с 1-го по N-й с реальными результатами, а перед этапом N+1 останавливайтесь.

  • Определяйте контрольную проверку до запуска: каждому этапу нужен явный контракт поведения.

  • Требуйте нескольких успешных прогонов: один удачный запуск ещё не доказывает стабильность недетерминированной системы.

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

  • Ограничивайте объём каждой итерации: не запускайте последующие этапы, пока тестируете более раннюю часть воркфлоу.

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

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

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

Именно такого уровня надёжности мы хотим от SRE-агентов. Дежурный инженер должен иметь возможность посмотреть, что именно сделал агент, проверить данные, на которых основан его вывод, и понимать, стабильность каких частей воркфлоу уже подтверждена.

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

  • 12 октября, 20:00. «Сколько стоит один ответ вашего AI-агента — и почему он думал 40 секунд?» Записаться

  • 27 октября, 18:00. «Langfuse в AgentOps: наблюдаемость и оценка LLM-агентов». Записаться

Полный список открытых уроков по ИИ и ML можно посмотреть в дайджесте.

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