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

Тайм-аут, это наблюдение о состоянии клиента, а не вердикт о результате работы. Вызывающая сторона перестала ждать. Больше она ничего не знает. Если агент превращает это наблюдение в «ошибка» и повторяет вызов, который меняет состояние, он может создать второй платеж, второе развертывание, второй тикет в поддержку или дважды запустить удаленную команду на машине, которая уже работает под нагрузкой.
Тайм-ауты инструментов агентов требуют плана восстановления: агенты действуют быстрее людей, которые за ними наблюдают, и склонны считать вывод инструмента абсолютной истиной. Отсутствующий ответ такой истиной не является. Восстановление должно заранее определять, нужно ли повторить запрос, подождать, найти подтверждение или остановиться и передать решение человеку. Постройте этот путь до того, как разрешите агенту выполнять важные вызовы.
После тайм-аута возможны три истории
После тайм-аута клиента запрос обычно соответствует одной из трех историй: сервис его не получил, получил, но еще не закончил работу, или закончил работу, но клиент не получил результат. Сетевой сбой может произойти до открытия соединения, во время передачи тела запроса, пока сервис обрабатывает запрос или во время возврата ответа. Один и тот же тип исключения может охватывать все четыре случая.
От этого различия зависит следующий шаг. Если до установления соединения не удалось выполнить DNS-запрос, повтор может быть разумным. Если сервис принял запрос на удаление ресурса, а ответ исчез, повтор может снова запустить разрушительную операцию. Если сервис поставил асинхронную задачу в очередь, вторая отправка может создать конкурирующую задачу, пока первая еще выполняется.
HTTP не предоставляет магического признака, по которому клиент понимает, какая история произошла. RFC 9110 описывает методы запросов и смысл ответов, включая различие между безопасными и идемпотентными методами. Но стандарт не обещает, что клиент сможет определить выполнение на сервере по потерянному ответу. Это физическое ограничение, а не недостающая настройка SDK.
Команды часто смешивают два разных вопроса:
- Можно ли отправить этот запрос снова, не изменив предполагаемое итоговое состояние?
- Дошла ли исходная попытка до сервиса и повлияла ли на него?
Идемпотентность отвечает на первый вопрос. Сверка, на второй. Системе нужны оба механизма. Идемпотентный PUT можно безопасно повторить, но после тайм-аута все равно неясно, завершилась ли запущенная этим запросом дополнительная работа. Запрос статуса может установить результат, но не защитит от дублей, если сервис примет два неразличимых запроса на создание.
Агенту нужно явно сообщать об этом различии в контракте инструмента. Обычная текстовая ошибка вроде request timed out провоцирует импровизацию. Структурированный результат с полем outcome: unknown подсказывает агенту выйти из ветки повторной попытки и перейти к поиску подтверждений.
Изучите путь запроса до выбора повтора
Хороший план восстановления называет границы, на которых могут появиться подтверждения. Начните с процесса агента, затем пройдите через оболочку инструмента, пул соединений, шлюз или прокси, если они есть, входной слой сервиса, приложение, постоянное хранилище и любой обработчик, который выполняет асинхронную работу. Тайм-аут на одной границе ничего надежного не говорит о следующей.
Представьте вызов для создания развертывания. Агент отправляет запрос через инструмент. Клиент полностью записывает тело запроса, сервер сохраняет запись о развертывании, а затем соединение разрывается до того, как ответ достигает клиента. Инструмент выдает тайм-аут. Агент повторяет вызов с новым запросом. Теперь на сервере есть две записи о развертывании, и каждая выглядит корректной с его точки зрения.
Изменим одну деталь: клиент прекращает ждать во время загрузки тела, а сервер отклоняет неполное тело до запуска кода приложения. Тот же инструмент все равно может вернуть timeout. В этом случае повтор создаст ровно одно развертывание. По одному тайм-ауту вызывающая сторона не может отличить эти ситуации.
Запишите, какие подтверждения может предоставить каждый компонент. Для обычного HTTP-действия это, например:
- Временные отметки клиента, выбранная цель, дайджест тела запроса и сгенерированный вызывающей стороной идентификатор операции.
- Журналы доступа сервиса, показывающие, принял ли входной слой запрос.
- Запись приложения, где идентификатор операции сохранен вместе с зафиксированным результатом.
- Записи обработчика или очереди для действий, продолжающихся после синхронного запроса.
- Конечная точка чтения, возвращающая текущее состояние или статус операции.
Не делайте сетевые журналы единственным источником истины. Журнал балансировщика может показать, что байты дошли, но не докажет фиксацию транзакции в базе данных. Запись в базе может доказать фиксацию, но не подтвердить, что внешний поставщик получил последующий побочный эффект. Авторитетная запись должна соответствовать действию, которое вы пытаетесь подтвердить.
При отправке письма идентификатор принятого сообщения от поставщика, более сильное свидетельство, чем запись приложения «сейчас отправим». Для миграции базы таблица миграций или запись транзакции надежнее кода завершения shell-процесса, который вызывающая сторона так и не получила. Для создания облачного ресурса лучше использовать URL операции или тег ресурса с идентификатором, созданным вызывающей стороной, чем повторять запрос на создание.
Идемпотентность должна относиться к операции, а не к попытке
Схема повторов работает только тогда, когда каждая попытка одного предполагаемого действия несет один и тот же постоянный идентификатор. Создайте его до первого сетевого вызова. Сохраните рядом с описанием действия. Используйте его после перезапуска процесса или инструмента, а также при передаче работы оператору.
Не создавайте новый UUID внутри цикла повторов. На проверке кода такой подход выглядит аккуратно, но полностью разрушает смысл идемпотентности. Сервер воспринимает каждый повтор как новый запрос, и именно так появляются дубли операций.
В зависимости от API значение идемпотентности можно передать в заголовке или поле тела. Детали транспорта менее важны, чем правило на сервере. Сервер должен атомарно связать это значение с операцией и ее результатом. Если одновременно приходят два одинаковых запроса, сервер должен сериализовать их или сделать так, чтобы один увидел результат другого. Кэш, срок действия которого истекает до завершения отложенных повторов, не обеспечивает надежной защиты от дублей.
Практический HTTP-запрос может выглядеть так:
POST /deployments HTTP/1.1
Content-Type: application/json
Idempotency-Key: op_7d5d4d8e4e5a
X-Correlation-ID: run_42_task_9
{"repository":"api","revision":"a1b2c3d4","environment":"staging"}
Сервер должен сохранить значение идемпотентности вместе с отпечатком значимых полей запроса и полученным идентификатором развертывания или операции. Если то же значение приходит с другой ревизией или окружением, запрос нужно отклонить. Возвращать первый результат для другого содержимого опасно: так можно незаметно выполнить не то действие.
Для асинхронной операции возвращайте постоянную ссылку на операцию сразу после принятия работы сервером:
{
"operation_id": "dep_1842",
"state": "accepted",
"status_url": "/operations/dep_1842"
}
После тайм-аута агент запрашивает op_7d5d4d8e4e5a или dep_1842, прежде чем рассматривать еще одну отправку. Если API не поддерживает ни значение идемпотентности, ни поиск по внешней ссылке, считайте запись неоднозначной по замыслу. Для временного тестового ресурса это может быть приемлемо. Для автономного действия, которое создает расходы или меняет состояние production, такой подход не подходит.
Не считайте метод безопасным только потому, что это POST с библиотекой повторов. Названия методов HTTP подсказывают предполагаемую семантику, но не защищают от реализации сервера, которая выполняет работу повторно. Изучите документацию конкретного API и самостоятельно проверьте поведение при дублях.
Дайте агенту явное состояние неизвестного исхода
Инструмент, способный менять внешний мир, не должен возвращать агенту только success или error. Нужен третий результат: unknown. Он предотвращает самое опасное поведение модели, когда неполная история воспринимается как разрешение попробовать немного измененную версию той же команды.
Используйте контракт результата, который фиксирует этап сбоя, но признает, что сам этап может быть неизвестен. Например:
{
"outcome": "unknown",
"operation_id": "op_7d5d4d8e4e5a",
"correlation_id": "run_42_task_9",
"transport_observation": "response deadline exceeded after request write",
"retry_allowed": false,
"reconcile": {
"method": "GET",
"path": "/operations/by-id/op_7d5d4d8e4e5a"
}
}
Поле retry_allowed должно исходить из определения инструмента или действия, а не из догадки агента о значении английских глаголов. Агент не может безопасно решить, что create_release безвреден только потому, что целевое окружение, staging. Развертывание в staging все равно может отправить уведомления, потратить общую квоту или изменить канал релиза.
Задайте агенту ограниченную последовательность восстановления:
- Сохранить в записи сеанса предполагаемое действие, идентификатор операции, цель и наблюдение о тайм-ауте.
- Запросить авторитетный источник статуса, используя тот же идентификатор операции или выданную сервисом ссылку.
- Продолжить только после подтвержденного конечного результата. Повторять запрос можно лишь тогда, когда это разрешено определением действия и источник статуса показывает, что принятая операция отсутствует.
- Остановиться и показать подтверждения, если сервис не может установить результат в пределах срока восстановления операции.
Условие остановки важно. Агент, который опрашивает сервис бесконечно, расходует внимание и может держать задачу активной после того, как ее первоначальный смысл исчез. Агент, который пробует пять вариантов запроса на изменение состояния, может оставить другим полноценный проект по очистке. Для каждой операции задайте отдельный срок восстановления, не совпадающий со сроком ожидания запроса.
Одно лишь одобрение человека не устраняет неизвестный исход. Одобрение отвечает на вопрос «может ли этот вызывающий выполнить действие?», но не на вопрос «удалась ли предыдущая попытка?». Храните подтверждения авторизации и выполнения отдельно и в интерфейсе, и в журналах.
Тайм-ауты чтения требуют другого подхода
Тайм-аут чтения обычно несет меньший риск дублей, но все равно может привести агента к неверному решению. Агент может получить тайм-аут при перечислении ресурсов, затем увидеть неполный или устаревший ответ из кэша и решить, что ресурса нет. После этого он попытается создать объект, который уже существует.
Классифицируйте чтения по решению, для которого они нужны. Безобновительный просмотр панели можно повторить с ограниченной задержкой. Чтение, на основании которого принимается решение о записи, требует четкого правила согласованности. Если сервис предоставляет ETag, номер версии, номер поколения или конечную точку статуса после записи, используйте ее. При eventual consistency сделайте период ожидания и условие ошибки видимыми для агента.
Не задавайте универсальное число повторов. Короткий запрос метаданных может выдержать два быстрых повтора. Отчетный запрос к хранилищу данных может требовать одного длинного срока ожидания без немедленного повторения. Ответ 429 Too Many Requests или явное указание сервиса повторить запрос требует другой обработки, чем тайм-аут сокета. Если считать любую ошибку временной сетевой проблемой, агент превратит частичный сбой в лишнюю нагрузку.
Задержка между повторами помогает защищать сервисы, но не устраняет неоднозначность. Она лишь разносит дублирующие запросы во времени. Для каждой важной записи сочетайте задержку с идемпотентным значением или проверкой статуса.
Используйте условные записи, если API их поддерживает. Запрос If-Match с известным ETag может не дать агенту перезаписать ресурс, изменившийся после чтения. Создание с выбранным клиентом идентификатором ресурса помогает повторам сходиться к одному объекту. Эти механизмы защищают переходы состояния, но не заменяют запись о побочных эффектах за пределами этого ресурса.
SSH скрывает удаленное выполнение за одним оборванным потоком
Восстановление после тайм-аута SSH требует большей осторожности, чем восстановление после тайм-аута HTTP. SSH-сеанс может оборваться после запуска команды на удаленном хосте, во время передачи вывода, после завершения команды или пока дочерний процесс продолжает работать после отключения родителя. Локальное сообщение shell не скажет, какой именно случай произошел.
Опасный вариант, длинная составная команда:
ssh deploy@host 'download-release && migrate-db && restart-service'
Если соединение оборвется после migrate-db, повтор всей команды может запустить миграции дважды или перезапустить сервис, для которого новая версия еще не загрузилась. Терминал свел несколько переходов состояния к одному непрозрачному результату.
Разделяйте удаленную работу на операции с постоянными маркерами, которые можно проверить. Развертывание может записать идентификатор релиза до начала работы, сохранить версии миграций в базе данных и показывать активную ревизию через локальную команду статуса. Инструмент восстановления сначала подключается снова и запрашивает эти маркеры.
Например, агент может использовать удаленную команду статуса, вывод которой предназначен для машин, а не для людей:
ssh deploy@host '/usr/local/bin/release-status --json'
{
"release_id": "rel_202",
"phase": "migrated",
"active_revision": "9f24c1",
"migration_version": "20250308_02"
}
Теперь у восстановления есть основание для решения. Если phase равен migrated, повторно запускать миграции нельзя. Если хост не сообщает release_id, агент может начать операцию только при гарантии удаленной команды, что его отсутствие означает отсутствие предыдущего запуска. Если SSH не удается подключить снова, результат остается неизвестным. Не подменяйте его обнадеживающим повтором, когда действие меняет production-хост.
С блокировками на удаленном хосте нужно обращаться осторожно. Блокировка предотвращает одновременные запуски, но устаревшая блокировка после сбоя хоста может помешать восстановлению. Записывайте в блокировку идентификатор операции и правила истечения срока, а проверку делайте возможной без бездумного удаления. Команда, удаляющая все старые блокировки, тоже меняет состояние и требует собственных подтверждений.
Sallyport может не допускать SSH-учетные данные в процесс агента: соединение выполняет встроенный помощник sp-ssh. Но изоляция учетных данных не делает оборванный сеанс безопасным для повторного запуска. Удаленной команде по-прежнему нужны идентификатор операции, постоянные маркеры и путь для сверки.
Записи аудита помогают расследовать, но не доказывают завершение
Журнал действий должен сохранять достаточно сведений, чтобы восстановить намерение и ход восстановления, не записывая секреты. Фиксируйте сеанс вызывающей стороны, время, идентификатор цели, идентификатор операции, дайджест запроса или шаблон команды, событие авторизации, результат транспорта и итог сверки. Не записывайте токены доступа, закрытые ключи или исходные тела запросов, в которых могут быть учетные данные или персональные данные.
Разделяйте попытку действия и завершенное действие. Строка POST /deployments timeout, это запись о попытке. Последующий запрос, вернувший операцию в состоянии succeeded, это свидетельство завершения. Храните обе записи. Если заменить первую финальным успехом, исчезнет самая полезная часть инцидента: период, когда вызывающая сторона не знала результата.
Защита от подделки важна, когда запуск агента становится предметом спора. Нужно ответить, какой процесс выполнил вызов, что ему было разрешено, получил ли он результат и как команда установила итоговое состояние. Изменяемую таблицу активности легко искать, но это слабое свидетельство, если скомпрометированный процесс может переписать историю.
Sallyport записывает сеансы агентов и отдельные вызовы в зашифрованный журнал аудита с цепочкой хешей, доступный только для записи, а sp audit verify может офлайн проверить цепочку поверх шифротекста. Такая запись показывает действия шлюза и историю вызывающей стороны, а внешний сервис или хост остаются источником истины о том, завершилась ли предполагаемая работа.
Не путайте событие аудита шлюза с транзакцией приложения. Если шлюз записал исходящий запрос, тот мог завершиться ошибкой до фиксации сервисом. Если сервис зафиксировал запрос, шлюз мог так и не увидеть ответ. Расследование работает, когда записи с обеих сторон содержат общий идентификатор операции или корреляции.
Проверяйте неоднозначность заранее, пока это не сделал сбой
План работы с тайм-аутами, который ни разу не сталкивался с потерянным ответом, остается предположением. Проверьте точный сценарий, при котором сервис завершает операцию, а клиент теряет результат. Команды часто пропускают этот случай, потому что обычные тестовые заглушки счастливого пути его не моделируют.
Создайте тестовую конечную точку или прокси-стенд, который принимает запрос, фиксирует постоянную запись, а затем задерживает или отбрасывает ответ. Дважды отправьте один идентификатор операции. Убедитесь, что сервис возвращает одну логическую операцию, агент после тайм-аута запрашивает статус, а журнал аудита сохраняет обе попытки и результат сверки.
Затем проверьте противоположный случай: прервите запрос до того, как сервис его примет. Убедитесь, что восстановление повторяет запрос только после того, как не находит записи об операции. Два теста могут приводить к одному и тому же исключению клиента. Контракт инструмента должен приводить к разным действиям, потому что подтверждения сервиса различаются.
Проверьте также следующие случаи:
- Сервис принимает операцию, но его обработчик остается в ожидании дольше срока восстановления агента.
- Два процесса агента почти одновременно отправляют один идентификатор операции.
- Конечная точка статуса недоступна, хотя основная конечная точка записи работает.
- SSH-команда запускает дочерний процесс, а затем соединение обрывается до финального вывода.
- Человек возобновляет приостановленный сеанс после того, как другой оператор уже выполнил сверку.
Последний случай выявляет проблему, которая часто возникает в реальной эксплуатации: состояние восстановления должно находиться вне разговорной памяти агента. Сохраняйте идентификатор операции и текущую информацию в постоянной записи сеанса. После перезапуска агент должен прочитать эту запись и продолжить сверку, а не придумывать новое действие, потому что ему недоступна прежняя история.
Настройте оповещения для неизвестных исходов, которые превысили срок восстановления. Не оповещайте о каждом первом тайм-ауте, если обычные повторы устраняют проблему безопасного чтения. Оповещение нужно, когда важная запись не имеет подтвержденного конечного состояния, когда одинаковые идентификаторы операций несут разное содержимое или когда удаленные маркеры противоречат ожидаемой последовательности. В таких случаях агенту нужен человек, прежде чем продолжать работу.
План восстановления должен делать отказ обычным решением
Лучшее поведение при тайм-ауте часто заключается в отказе: «Я не могу подтвердить, завершился ли запрос на развертывание, поэтому не буду отправлять его повторно». Это не сбой инструмента. Это правильная реакция на отсутствие подтверждений для необратимого или дорогостоящего действия.
Сделайте такой ответ полезным. Покажите идентификатор операции, целевой объект, последнее подтвержденное состояние, временные отметки и точный запрос статуса или удаленную проверку, которые помогут установить результат. Если авторитетного запроса не существует, скажите об этом прямо и передайте решение человеку, который понимает последствия дубля.
Команды сопротивляются такому подходу, потому что повтор кажется продуктивным, а пауза, медленной. После нескольких двойных записей и незавершенных развертываний компромисс становится очевидным. Минута на сверку дешевле, чем обнаружение того, что две системы считают выполненным одно действие, которое агент должен был выполнить один раз.
Начните с операций, которые перемещают деньги, отправляют внешние сообщения, создают релизы, меняют доступ или удаляют данные. Для каждой из них потребуйте от владельца API три ответа: какой идентификатор связывает повторы с одной операцией, где вызывающая сторона может запросить результат и какое доказательство остается при разрыве соединения. Если на любой вопрос нет ответа, пусть инструмент возвращает unknown и требует осознанного решения человека вместо того, чтобы учить агента угадывать.
Вопросы и ответы
Означает ли тайм-аут API, что запрос завершился ошибкой?
Тайм-аут означает лишь, что вызывающая сторона перестала ждать, не получив пригодного ответа. Сервис мог вообще не получить запрос, все еще обрабатывать его или завершить работу, но потерять ответ по дороге обратно. Считайте результат неизвестным, пока не сверите его с данными на стороне сервиса.
Когда агенту безопасно повторить запрос после тайм-аута?
Автоматически повторяйте запрос только тогда, когда операцию безопасно выполнять повторно или сервис принимает токен идемпотентности, связывающий все повторы с одной логической операцией. Запросы только на чтение обычно безопасны, но даже они могут создавать дополнительную нагрузку во время сбоя. Перед изменением состояния сначала выполните сверку, если API не описывает обработку дублей.
Как понять, завершился ли запрос, на котором произошел тайм-аут?
Сгенерируйте уникальный идентификатор операции до первой попытки, а затем сохраните его вместе с предполагаемым действием и целевым объектом. Запросите результат у сервиса по этому идентификатору либо проверьте конечную точку статуса операции, запись аудита или состояние объекта. Счетчик повторных попыток на клиенте ничего не доказывает: следующую попытку мог выполнить другой процесс агента.
Что такое ключ идемпотентности и зачем он нужен?
Ключ идемпотентности, это неизменное значение, которое обозначает одно предполагаемое изменение состояния, например создание одного счета или отправку одного запроса на развертывание. После тайм-аута клиент снова отправляет то же значение, а сервис возвращает исходный результат, не выполняя действие дважды. Это работает только тогда, когда сервер действительно сохраняет и применяет такую связь.
Можно ли безопасно повторить SSH-команду после разрыва соединения?
Не перезапускайте вслепую SSH-команду, которая меняет состояние удаленной системы. Сначала подключитесь снова и проверьте постоянный маркер, например идентификатор релиза, запись транзакции, версию пакета или файл статуса конкретной команды. Разрыв SSH-соединения почти ничего не говорит о том, завершился ли удаленный процесс.
Какой тайм-аут использовать агенту с ИИ для инструментов API?
Передайте инструменту крайний срок, но сделайте его короче общего бюджета времени агента, чтобы осталось время на сверку и сообщение о результате. Подходящее значение зависит от операции и ее обычной задержки. Один общий тайм-аут для чтения, развертывания и миграций базы данных, плохое инженерное решение.
Нужны ли агентам идентификаторы запросов или идентификаторы корреляции?
Используйте идентификатор операции, который выдает сервер, если он есть, и дополнительно передавайте собственный идентификатор корреляции в запросах и журналах. Идентификатор сервера помогает запросить состояние в конкретном сервисе, а ваш идентификатор связывает план агента, одобрение, повторы и последующее расследование. Не считайте заголовок трассировки гарантией идемпотентности, если API прямо этого не обещает.
Как агенту сообщить пользователю о неизвестном исходе?
Возвращайте структурированный результат с неизвестным исходом, не заявляя об успехе или ошибке. Укажите идентификатор операции, целевой объект, последнее известное состояние транспорта и точное действие для сверки. Если агент не может установить результат, он должен приостановить важный процесс.
Почему автоматические повторы опасны для операций записи?
Нет. Повторы могут привести к двойным списаниям, письмам, тикетам, развертываниям и разрушительным командам, если первая попытка завершилась, а ответ потерялся. Повторы должны выполняться только по правилам конкретной операции, при поддержке идемпотентности и с ограниченной задержкой между попытками.
Что записывать, если вызов инструмента агента завершился тайм-аутом?
Сохраняйте неизменяемую запись о попытке выполнить действие, использованной учетной записи, сеансе вызывающей стороны, временных отметках и итоговом результате сверки. Одни журналы не делают неоднозначное действие безопасным, но помогают понять, что именно пытался сделать агент, и отозвать еще работающий сеанс. Запись с защитой от подделки особенно полезна, если исходный процесс уже завершился.