Читать 6 мин

Журналирование повторов AI-агента, которое выявляет дублирующиеся эффекты

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

Журналирование повторов AI-агента, которое выявляет дублирующиеся эффекты

Повтор агента не является второй копией действия. Это вторая попытка выполнить одно заявленное действие, и журнал аудита должен сохранять это различие. Если в записях виден только ряд похожих HTTP-вызовов, восстановление и повтор выглядят одинаково.

Особенно важно это для действий, которые меняют внешний мир: создания задачи, отзыва доступа, публикации платежа, отправки деплоя или выполнения команды по SSH. Агент может получить тайм-аут уже после того, как система назначения выполнила действие. Он также может повторить запрос после локальной ошибки, когда ни один байт ещё не покинул компьютер. Эти случаи требуют разных решений, но слишком многие системы записывают оба как request failed, retrying.

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

Повтор относится к операции, а не к строке журнала

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

Допустим, агент хочет отключить учётную запись. Он создаёт операцию op_7f2c. Первый запрос, att_01, достигает API идентификации, но соединение закрывается до получения ответа. Второй запрос, att_02, может быть обоснованной попыткой восстановления. Обе записи должны указывать на op_7f2c, а att_02 должен напрямую ссылаться на att_01 как на попытку, которая его вызвала.

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

Используйте идентификатор операции, когда агент формирует обязательство вроде «отключить учётную запись A», «создать один инцидент для оповещения B» или «один раз выполнить эту миграцию на узле C». Сохраняйте намерение в структурированных полях. Обычное описание на естественном языке удобно людям, но не может быть единственным идентификатором: формулировки меняются между запусками агента.

Чистая иерархия выглядит так:

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

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

Неизвестный результат, а не сообщение об ошибке

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

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

Для попытки разделяйте локальное наблюдение и установленное состояние операции. Попытка может иметь состояние not_sent, sent_no_response, response_received или execution_error. Операция может быть open, succeeded, failed, unknown или cancelled. Названия могут различаться, но разделение должно сохраняться.

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

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

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

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

Семантика HTTP не делает бизнес-действие безопасным для повтора

Методы HTTP описывают протокольную семантику, а не гарантии вашего бизнеса. RFC 9110 называет метод идемпотентным, если предполагаемый эффект нескольких одинаковых запросов совпадает с эффектом одного запроса. В документе PUT, DELETE и безопасные методы считаются идемпотентными, а POST по умолчанию таковым не является.

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

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

Endpoint POST часто поддерживает токен идемпотентности. Отправляйте стабильный токен, полученный из ID операции, а не из ID попытки. Если у att_01 и att_02 разные токены, защита от повторного эффекта перестаёт работать.

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

{
  "operation_id": "op_7f2c9c",
  "attempt_id": "att_01",
  "idempotency_key": "op_7f2c9c",
  "intent": {
    "kind": "disable_account",
    "subject_ref": "user:1842"
  },
  "destination": {
    "method": "POST",
    "route_template": "/v1/accounts/{id}/disable",
    "authority_ref": "vault:identity-prod"
  },
  "request_fingerprint": "sha256:...",
  "dispatch_state": "sent_no_response"
}

Не записывайте заголовок авторизации, cookie сессии, закрытый материал SSH или тело с секретами. Сохраняйте ссылку на учётные данные и отпечаток канонического представления запроса. Отпечаток помогает сравнивать попытки и не превращает журнал аудита в ещё одно хранилище секретов.

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

Повтор SSH может повторить несколько команд

SSH усложняет учёт повторов: одно соединение может выполнять shell-синтаксис, конвейеры, перенаправления и команды, которые завершаются частично. Ошибка SSH-сессии не показывает, какие части удалённой команды успели выполниться.

Рассмотрим команду:

create-user deployer \u0026\u0026 install-key deployer /tmp/new.pub \u0026\u0026 restart-service api

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

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

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

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

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

Для каждой последующей попытки нужны причина и родитель

Связывать повторы с запусками агента
Журнал Sessions показывает процесс агента, который выполнил действие, и позволяет мгновенно отозвать его права.

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

Используйте ограниченный набор причин, а подробности прикрепляйте отдельно. Полезны connection_not_established, rate_limited, destination_5xx, response_lost_after_dispatch, credential_refreshed и operator_requested. Не позволяйте агенту придумывать успокаивающее текстовое объяснение, которое нельзя сгруппировать или проверить.

Для каждого повтора сохраняйте такие связи:

{
  "operation_id": "op_7f2c9c",
  "attempt_id": "att_02",
  "retry_of_attempt_id": "att_01",
  "retry_reason": "response_lost_after_dispatch",
  "retry_decision": "destination_idempotency_confirmed",
  "attempt_budget_remaining": 1,
  "request_fingerprint": "sha256:...",
  "prior_request_fingerprint": "sha256:..."
}

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

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

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

Токены идемпотентности и идентификаторы аудита решают разные задачи

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

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

У ID операции более широкая роль. Он связывает сессию агента, доказательство одобрения, создание запроса, транспортные попытки, удалённый ответ и последующую сверку. Он должен оставаться действительным, даже если API поставщика не поддерживает идемпотентность, действие выполняется по SSH или окончательное восстановление вручную выполняет оператор.

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

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

Сверка закрывает неопределённость, не переписывая историю

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

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

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

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

Если запрос находит две подходящие записи, не объявляйте операцию успешной и не переходите дальше. Закройте её как duplicate_effect_confirmed, сохраните оба удалённых идентификатора и создайте отдельную операцию исправления. Операция исправления не должна использовать исходный ID, поскольку её намеренный эффект другой.

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

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

Делегирование агентом требует одного владельца операции

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

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

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

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

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

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

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

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

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

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

Изменение тела запроса во время восстановления оформляйте как явное ответвление. Исходная операция остаётся открытой или получает установленный результат. Изменённое действие получает новый ID операции и ссылку вроде supersedes_operation_id. Такая запись говорит правду: агент не просто повторил запрос, а изменил намерение.

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

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

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

Всегда ли повторы AI-агента представляют угрозу безопасности?

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

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

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

Означает ли тайм-аут, что запрос API завершился ошибкой?

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

Устраняют ли ключи идемпотентности необходимость в журналах повторов?

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

Как записывать неизвестный результат запроса?

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

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

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

Достаточно ли кода состояния HTTP для аудита AI-агента?

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

Как работают повторы, когда агент делегирует работу субагентам?

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

Можно ли редактировать запись о повторе после выяснения результата?

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

Какие доказательства нужны после дублирования эффектов API?

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

Sallyport

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

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