Читать 6 мин

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

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

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

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

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

Ответ об ошибке становится входными данными для планировщика агента

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

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

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

Отказ в соединении до открытия TCP-сеанса обычно относится к локальной ошибке. HTTP 403 означает подтвержденный отказ. Тайм-аут чтения после отправки POST дает неопределенный результат, если только удаленный сервис не позволяет найти операцию по идентификатору. Эти метки должны присутствовать в тестовых случаях и схеме результата инструмента. Не прячьте их в предложении, которое агенту придется интерпретировать.

Компактная структура результата делает контракт проверяемым:

{
  "ok": false,
  "category": "outcome_unknown",
  "operation": "create_deployment",
  "retry": "reconcile_first",
  "correlation_id": "case-ssh-017",
  "message": "Connection closed after the remote command started; remote completion is unknown."
}

Названия не так важны. Важно разделение. retry: "never" для отказа в разрешении и retry: "reconcile_first" для записи, завершившейся тайм-аутом, передают агенту разную информацию, не превращая все сообщение об ошибке в промпт.

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

Стройте матрицу вокруг операций и свидетельств

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

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

СлучайОперацияВнедренное условиеУдаленный эффект известен?Ожидаемая категорияУказание агенту
C01прочитать задачуне удалось выполнить DNS-запросда, запрос не отправленlocal_failureповторить в ограниченном бюджете
C02создать задачутокен истекда, запрос отклоненauthentication_failedостановиться и запросить разрешенное восстановление учетных данных
C03удалить релизотказано в разрешениида, запрос отклоненauthorization_deniedне повторять
C04прочитать сборкуJSON содержит status: 7да, ответ полученmalformed_responseостановиться и сообщить о несоответствии схемы
C05создать развертываниеответ задержан дольше клиентского сроканетoutcome_unknownперед повтором сверить состояние
C06выполнить перезапуск по SSHлокальный помощник завершился после запуска на удаленной стороненетoutcome_unknownпроверить удаленное состояние перед следующей командой
C07обновить записьсервис возвращает 429да, запрос отклоненrate_limitedподождать указанное время и повторить, если это безопасно

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

Для каждой строки нужны четыре проверки:

  1. Проверьте категорию результата инструмента и указание о повторе.
  2. Проверьте следующее действие агента, включая отсутствие самовольного разрушительного повтора.
  3. Проверьте удаленное состояние или документированную причину, по которой оно остается неизвестным.
  4. Проверьте, что в цепочке событий есть идентификатор корреляции и наблюдаемый результат.

Это сложнее, чем проверить ok == false. Зато так выявляются ошибки, которые имеют значение после того, как агент уже выполнил несколько действий.

Истекшие учетные данные и запрещенные действия требуют разного восстановления

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

RFC 9110 определяет 401 как запрос без аутентификации и требует, чтобы сервер отправлял вызов WWW-Authenticate. Код 403 он определяет как отказ выполнить запрос, даже если сервер не сообщает причину. Провайдеры не всегда четко соблюдают это различие, поэтому тестируйте фактический ответ конкретного провайдера. При этом инструмент должен честно распределять наблюдаемые свидетельства по отдельным категориям, когда это возможно.

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

Простая фикстура может описать оба случая, не раскрывая секрет:

cases:
  - id: expired-token
    request:
      method: POST
      path: /v1/releases
    fixture_response:
      status: 401
      headers:
        www-authenticate: Bearer error="invalid_token"
      body: {"error":"token_expired"}
    expect:
      category: authentication_failed
      retry: never
      secret_in_result: false

  - id: denied-release
    request:
      method: POST
      path: /v1/releases
    fixture_response:
      status: 403
      body: {"error":"insufficient_scope"}
    expect:
      category: authorization_denied
      retry: never
      secret_in_result: false

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

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

Некорректные данные требуют контрактного теста, а не теста JSON-парсера

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

Спецификация JSON-RPC 2.0 разделяет ошибки разбора (-32700) и некорректные запросы (-32600). Это полезное разделение: оно отличает нечитаемые байты от читаемого сообщения, нарушающего протокол. Применяйте ту же дисциплину к ответам своего домена: успешный разбор не доказывает соответствие ответа контракту инструмента.

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

  • Замените строковый ID на null, число и объект.
  • Уберите поле, которое следующие вызовы инструментов используют для сверки.
  • Верните успешный статус с телом в форме ошибки.
  • Верните статус ошибки с HTML-телом или усеченным JSON-документом.
  • Продублируйте элемент или измените порядок, если код выбирает первый элемент.

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

Распространенный плохой шаблон выглядит безобидно: response.id || request.id. Он позволяет процессу продолжаться, если провайдер не вернул id, но может привести к обновлению или удалению другого объекта, если идентификаторы запроса и ответа различаются. Проверьте, что отсутствие идентификатора в ответе останавливает операцию. Неудачный процесс дешевле неправильной записи.

Клиентам инструментов MCP нужна такая же осторожность. Формат результата инструмента Model Context Protocol поддерживает сигнал isError для ошибки на уровне инструмента. Используйте его, когда сам инструмент не может выполнить обещанную работу, но содержание результата должно быть достаточно конкретным, чтобы агент выбрал безопасную ветку. Не маскируйте некорректный ответ вышестоящего сервиса под обычный текст, начинающийся с «Error:». Многие клиенты воспримут это как успешное выполнение инструмента и оставят агенту необходимость додумывать остальное.

Тайм-ауты становятся неоднозначными после начала записи

Проверять цепочку доказательств
Проверяйте зашифрованную хеш-цепочку офлайн с помощью sp audit verify, не открывая хранилище.

Тайм-аут сообщает, что истек ваш срок ожидания. Он не определяет состояние удаленной операции. Это очевидно в теории, но в цикле повторов потерянный ответ легко превращается в дублированный счет, два развертывания или второй перезапуск.

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

case=C05 request_id=case-http-005 received=true
case=C05 request_id=case-http-005 mutation_committed=true
case=C05 response_write=delayed
client case=C05 deadline_exceeded=true

Проверять нужно не то, что «клиент получил тайм-аут». Проверяйте, что клиент возвращает outcome_unknown, не отправляет второй POST и перед продолжением использует проверку статуса или механизм идемпотентности.

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

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

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

Прерванные удаленные команды должны сохранять неопределенность

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

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

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

# remote command used only in an isolated test environment
id="case-ssh-017"
printf '%s start\n' "$id" >> /tmp/agent-tool-test.log
sleep 20
printf '%s complete\n' "$id" >> /tmp/agent-tool-test.log

Запустите команду через тот же SSH-путь, который использует инструмент, дождитесь появления маркера запуска и завершите локальный помощник. После истечения удаленного ожидания проверьте журнал. Выполните тест дважды: один раз, когда удаленный процесс завершится, и один раз, когда удаленная сторона убьет его после маркера запуска. Локальное прерывание будет одинаковым, но восстановление должно отличаться.

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

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

Отказ человека это обычный результат, а не сломанный тест

Видеть каждую попытку действия
Журнал Activity записывает отдельные HTTP- и SSH-вызовы в том же зашифрованном аудиторском журнале.

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

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

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

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

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

Логи должны объяснять произошедшее, не раскрывая доступ

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

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

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

Такой формат подходит для локального тестового стенда:

{"time":"2025-04-12T10:18:03Z","case":"C05","id":"case-http-005","event":"dispatch_started"}
{"time":"2025-04-12T10:18:03Z","case":"C05","id":"case-http-005","event":"remote_committed"}
{"time":"2025-04-12T10:18:08Z","case":"C05","id":"case-http-005","event":"client_timeout"}
{"time":"2025-04-12T10:18:08Z","case":"C05","id":"case-http-005","event":"result","category":"outcome_unknown"}

После этого проверка может сопоставить записи клиента и фикстуры по id. Если фикстура сообщает remote_committed, а инструмент возвращает confirmed_failure, тест должен завершиться ошибкой. Такое расхождение выявляет небезопасное утверждение, даже если все ветки кода вернули аккуратный объект ошибки.

Для защищенной от незаметного изменения цепочки проверяйте и саму верификацию. Sallyport формирует записи сессий и действий из зашифрованного аудиторского журнала, связанного цепочкой хешей, а sp audit verify проверяет цепочку офлайн без ключа хранилища. Тест на ошибку должен добавить известную последовательность событий, проверить ее, изменить копию записи и убедиться, что проверка измененной копии завершается ошибкой.

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

Тестируйте восстановление агента, а не только адаптер

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

Давайте каждому запуску узкую задачу и четкую границу. Например: создать одну запись, прочитать ее, затем добавить заметку. Задержите ответ на создание после фиксации записи. Правильное поведение агента состоит в том, чтобы сначала проверить запись по идентификатору корреляции или токену идемпотентности, а не отправлять второй запрос на создание. Тест должен завершаться ошибкой, если агент создает еще одну запись, даже если в итоге задача выполнена.

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

create -> outcome_unknown -> lookup_by_request_id -> found -> attach_note
create -> outcome_unknown -> create

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

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

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

Вопросы и ответы

Что должна включать матрица тестирования сбоев инструментов AI-агента?

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

Как тестировать истекшие API-учетные данные агента?

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

Чем отличаются истекшие учетные данные от запрещенного действия?

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

Как тестировать некорректные ответы API от инструмента?

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

Безопасно ли повторять вызов инструмента агента после тайм-аута?

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

Как смоделировать прерванную SSH-команду?

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

Как тестировать отказ пользователя от действия агента?

Тестируйте отказ в подтверждении как отдельную ветку, когда человек отклоняет запрос на границе авторизации. Агент должен получить понятный результат отказа, завершить текущую ветку и не повторять запрос для того же действия. Проверки одного появления диалогового окна недостаточно.

Какие логи нужны для отладки сбоев инструментов?

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

Должны ли инструменты агента возвращать одну общую ошибку для всех сбоев?

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

Как часто запускать тесты сбоев инструментов агента?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov