Agent Failure Modes

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

Суть

Классический APM отвечает на вопрос «сервис работает?». У агента этот вопрос почти всегда имеет ответ «да» — и почти всегда бесполезен. Успешный ответ с кодом 200 может стоить бизнесу полдоллара, вернуть галлюцинацию из-за раздутого контекста и увести клиента не туда. Мониторинг слеп именно к ветвлению графа: он видит транспорт, но не видит смысл.

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

Матрица сбоев

Тип сбоя Как выглядит в APM Что произошло на самом деле
Семантический сбой 200 OK, latency 2 с Модель согласилась выдать закрытые данные — сработала инъекция
Голодание RAG Запрос к базе — 200 OK Ретривер вернул пустой список, ответ сгенерирован на весах модели, то есть выдуман
Бесконечная петля 200 (in progress) Агент повторно вызывает один инструмент, счёт за токены растёт экспоненциально
Отказ безопасности 200 OK Guardrail отсёк выход, клиент получил пустой ответ вместо результата

Чем чинить: возможностью или поведением

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

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

Различать это стоит потому, что по умолчанию чинят вторым способом. Правка текста дешевле, всегда выглядит осмысленной и почти всегда что-то улучшает. Оставленный без рамки автоматический оптимизатор обвязки сваливается в текстовые правки в 91,5% случаев, притом что доля принятых патчей выше как раз у непромптовых: новый инструмент 83%, изменение цикла 71%, изменение инфраструктуры 67% (Harness Optimization).

Важнее соотношение пользы и ущерба. Текстовая правка исправляет сопоставимую долю провалов (58% против 55% у правки кода), но ломает работавшее вдвое чаще — 17% против 8%. Отсюда порядок разбора: сначала спросить, хватает ли агенту возможности, и только потом править формулировки.

Три сигнатуры, на которые ставят алерты

Infinite Loops. Причина — отсутствие условия выхода: агент уходит на двадцать и больше итераций вызова инструмента. Лечится жёстким лимитом max_iterations в оркестраторе плюс алертом на рост числа спанов с span.kind == TOOL внутри одного трейса.

Memory Corruption. В долговременный контекст попадает некорректный JSON, и дальше система работает с испорченным состоянием. Лечится строгими Pydantic-схемами на каждом узле графа, а не проверкой в конце.

Cascade Collapse. Один неверный шаг в цепочке RAG отравляет контекст, и вся последующая генерация идёт от ложной предпосылки. Лечится обязательной проверкой обоснованности (groundedness check) перед сборкой финального ответа.

По внешней оценке, на эти три приходится около 80% сбоев в проде — оставшиеся 20% штучные и редко повторяются.

Почему это отдельная заметка

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

Лестница деградации: что делать, когда упало

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

  1. Повтор запроса — если сбой транзиентный, дальше идти не нужно.
  2. Смена провайдера или канала — тот же класс модели, другой поставщик.
  3. Более дешёвая модель — качество ниже, но ответ есть.
  4. Детерминированный код или кэш — заранее заготовленный ответ, правило, прошлый результат.
  5. Явный отказ с уведомлением — последняя ступень, и она честнее, чем выдуманный ответ.

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

Классы ошибок различаются по тому, что с ними делать, и путать их дорого:

Класс Признак Реакция
Баги 4xx, кроме 429 повторять бессмысленно — чинить код
Лимиты 429 backoff, второй провайдер, поднять tier
Сбои провайдера 5xx ретрай с выдержкой, дальше circuit breaker
Отказ качества 200 OK, но ответ негодный ретраем не лечится — нужен валидатор

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

Контрактное тестирование: репетиция аварии до продакшена

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

Лечится тремя правилами, и все три — про код инструмента, а не про модель:

  1. Перехват на уровне инструмента. Транспортные и системные ошибки ловятся там, где возникли, и выше по стеку не улетают.
  2. Конвертация сбоя в валидный контракт. Таймаут базы или 5xx внешнего сервиса превращается в структурный ответ с флагом ошибки — например, {"status": "error", "reason": "timeout"}. Оркестратор получает данные, с которыми может работать, и продолжает диалог вместо падения.
  3. Мокирование сбоев в тестах. Таймауты HTTP-клиента и граничные значения проигрываются в контрактных тестах до развёртывания. Иначе первая же сетевая просадка в проде становится премьерой.

Третий пункт — тот, который обычно пропускают: тесты пишут на успешный путь, а деградацию проверяют в бою.

«Пусто» — это не успех и не ошибка

Булев результат вызова инструмента теряет самый частый исход. Состояний три:

Статус Что произошло Чем лечится
success данные получены
empty вызов отработал штатно, данных нет пауза и повтор, сужение/расширение запроса, честная пустота вниз
error вызов сломался: сеть, исключение, отказ сервиса немедленный повтор с выдержкой, дальше эскалация

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

У empty своё лекарство — время, а не повтор. Динамический источник (страница за проверкой браузера, отчёт, который ещё считается, индекс после переиндексации) отвечает пустотой, пока не готов. Значит, повтор нужен не немедленный, а с растущей паузой:

RETRY_WAIT = [15, 30]                       # пауза перед 2-й и 3-й попыткой
for attempt in range(1, MAX_ATTEMPTS + 1):
    s = scrape_one(p)
    if s.scrape_status == "success":
        break
    if attempt < MAX_ATTEMPTS:
        time.sleep(RETRY_WAIT[attempt - 1])  # источнику нужно время, а не настойчивость

В замерах учебного пайплайна это выглядело так: первая попытка — пусто за 62 секунды, пауза 15 секунд, вторая — снова пусто за 97 секунд, пауза 30 секунд, третья — успех. Мгновенный ретрай здесь не дал бы ничего, кроме утроенного счёта.

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

Порог по числу спанов ставится так же, как любой другой агентный порог: не абсолютным числом, а множителем к p95 этой операции за прошлую неделю — иначе легитимная длинная задача и зациклившийся агент попадают под один лимит (Agent Observability, Agent CostControl). Раньше порога срабатывает детектор отсутствия прогресса: повтор одного вызова с теми же аргументами информативнее, чем общее число шагов (ReAct).

Хуже пустоты: результата не пришло вовсе

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

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

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

Типичный источник — гейт подтверждения, поставленный сигналом вместо ворот. Механизм, который помечает вызов как «требует подтверждения» и пропускает исполнение, рассчитан на то, что обвязка подхватит запрос и доведёт его до человека. Если этой половины нет, вызов просто исчезает.

Отсюда правило, не зависящее от инструментария: любой вызов инструмента обязан вернуть строку — в том числе отказ. «Заблокировано: команда требует подтверждения» — валидный результат, который модель передаст пользователю честно. Отсутствие результата валидным исходом не является (Tool Calling, Human in the Loop).

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

«Нет действия» — не один режим отказа

Модель вернула текст, но parser не нашёл допустимого tool call. На уровне цикла симптом один — выполнять нечего, — однако как минимум две причины требуют разной диагностики:

Причина Provider signal Что чинить
генерация оборвалась finish_reason=length или незавершённый tool-call fragment освободить output budget, сократить ответ или безопасно повторить
генерация завершилась штатно, но действие неверно stop, неизвестное имя инструмента, невалидные аргументы вернуть модели точную ошибку контракта; проверить schema и adapter

Одинаковое сообщение «неверный формат, попробуй ещё раз» скрывает первый случай и может повторно обрезать ответ тем же лимитом. Поэтому parser протаскивает termination reason до оркестратора, а trace сохраняет сырой provider response и уже начисленную стоимость (Agent Audit Log). Метрика также разделяется: truncated_action говорит о бюджете, malformed_action — о соответствии протоколу.

Ответ может испортиться после того, как он получен

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

Два источника, и они разной природы:

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

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

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

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

Закон Люссера: почему 99% на узел — это мало

Надёжность цепочки равна произведению надёжностей узлов, а не минимуму из них. Путь одного запроса проходит шесть звеньев — пользовательский ввод и файлы, ваш бэкенд, сеть, шлюз, провайдер модели, инфраструктура GPU. При честных 99% на каждом:

0.99⁶ ≈ 94%

Шесть процентов запросов ломаются при том, что ни один компонент не считается проблемным. Хуже того, каждый шаг агента проходит цепочку заново и умножает ещё раз: у агента из десяти шагов от исходных 94% почти ничего не остаётся.

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

Отраслевая статистика отказов

Исследование LangChain 2025–2026 годов, опрошено более 2400 команд:

  • 88% организаций сообщили об инциденте с ИИ-агентом за последний год;
  • 57% уже держат агентов в продакшене;
  • 40% видели каскадный отказ, когда сбой одного агента расходился по нескольким смежным системам;
  • 32% называют качество барьером номер один — выше стоимости и выше безопасности.

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

Связано с

  • Tool Calling — правило «отказ тоже возвращается строкой», которым лечится исчезнувший вызов
  • Agent Observability — трейсинг как способ вообще увидеть эти сбои
  • OpenInference — типы спанов, по которым распознаются сигнатуры
  • Agent Evals — оценка качества ловит семантические сбои, которых нет в метриках
  • Guardrails — источник четвёртого режима: защита сработала, а пользователь остался без ответа
  • Agent CostControl — бесконечные петли бьют в первую очередь по бюджету
  • Harness Optimization — что делать с разобранным провалом: возможностью или поведением, и как проверить, что починка не сломала соседнее
  • Unknown Not A Value — тот же разбор со стороны читателя результата: пустой вывод не факт завершения, а неизвестное не ноль
  • Instruction Compliance Measurement — как узнать, что правило обвязки не исполняется, до того как это проявится отказом