Надёжность AI-агентов
Идемпотентность AI-агента при работе с 1С, CRM и платёжными системами
Тайм-аут не означает, что операция не выполнилась. Если агент без проверки повторит вызов, в 1С появится второй документ, в CRM — ещё одна задача, а в платёжной системе — повторная операция. Ниже — схема, которая делает такие повторы контролируемыми.
Почему обычного retry недостаточно
Идемпотентность — свойство операции давать один и тот же значимый результат при повторном выполнении с теми же параметрами. Для AI-агента это означает: один бизнес-намер должен создать не более одного бизнес-объекта или денежного поручения, сколько бы раз оркестратор ни повторил вызов.
Рассмотрим типичный сценарий. Агент отправляет команду создания счёта в 1С. Система сохраняет документ, но ответ теряется из-за сетевого тайм-аута. Агент видит ошибку транспорта и повторяет запрос. Если принимающая сторона не узнаёт исходную операцию, она создаёт второй счёт.
Проблема возникает на границе двух истин:
- для агента результат неизвестен;
- для бизнес-системы изменение уже могло быть зафиксировано.
Поэтому безопасная стратегия — не «повторять при ошибке», а «повторно предъявлять тот же идентификатор намерения и получать ранее зафиксированный результат».
Архитектура: ключ, журнал и адаптер
Минимальная надёжная конструкция состоит из трёх элементов:
- Ключ идемпотентности связывает все попытки с одним бизнес-намерением.
- Журнал операций хранит состояние, отпечаток запроса и итоговый ответ.
- Адаптер бизнес-системы передаёт ключ во внешнюю систему или использует его как уникальный внешний идентификатор.
AI-агент
│ operation_key + payload
▼
Шлюз инструментов
│
├─ журнал: новая операция → выполнить
├─ журнал: завершена → вернуть сохранённый результат
├─ журнал: выполняется → сообщить «результат уточняется»
└─ тот же ключ, другой payload → отклонить
│
▼
Адаптер 1С / CRM / платёжного провайдера
Журнал не заменяет защиту на стороне целевой системы. Между внешним эффектом и записью локального результата остаётся окно сбоя. Лучший вариант — передавать ключ дальше и обеспечивать уникальность непосредственно там, где создаётся документ, задача или платёж.
Шаг 1. Определите границу бизнес-операции
Ключ должен представлять намерение пользователя, а не отдельную сетевую попытку. Не генерируйте новый ключ внутри каждого вызова инструмента: при retry он станет бесполезным.
Примеры границ:
| Система | Бизнес-операция | Что не должно дублироваться |
|---|---|---|
| 1С | Создать счёт по подтверждённому заказу | Документ одного типа для заказа и версии команды |
| CRM | Поставить менеджеру задачу перезвонить | Задача для сделки и конкретного события |
| Платёжная система | Создать поручение на списание | Денежная операция по одному подтверждению |
Один диалог агента может содержать несколько намерений. Например, пользователь вправе дважды оплатить разные заказы на одинаковую сумму. Поэтому сумма, телефон или идентификатор клиента сами по себе не подходят на роль ключа.
Шаг 2. Сформируйте стабильный ключ
Практический формат ключа:
<контур>:<операция>:<идентификатор-намерения>:<версия>
Примеры ниже иллюстративны:
prod:invoice.create:order-48291:v1
prod:crm.callback:create:event-90173:v1
prod:payment.charge:approval-6f3c2a:v1
Ключ должен быть:
- стабильным для всех повторов одной операции;
- уникальным между разными намерениями;
- не содержащим паспортные данные, токены, номера карт и другие секреты;
- достаточно коротким для ограничений API и индексов;
- неизменяемым после первого принятого запроса.
Если агент сам инициирует операцию, оркестратор создаёт случайный идентификатор один раз и сохраняет его в состоянии задания. Если команда приходит из очереди или webhook, предпочтительно использовать стабильный идентификатор входного события, добавив область операции.
Шаг 3. Канонизируйте запрос и вычислите отпечаток
Один ключ нельзя разрешать для разных параметров. Иначе повтор с изменённой суммой может получить результат старой операции. Для проверки сохраните криптографический отпечаток канонической формы запроса.
{
"operation": "payment.charge",
"account_id": "merchant-account-17",
"order_id": "order-48291",
"amount_minor": 125000,
"currency": "RUB"
}
Перед хешированием задайте детерминированные правила:
- фиксированный набор значимых полей;
- лексикографическая сортировка ключей JSON;
- денежные значения в минимальных единицах целым числом;
- валюта в едином регистре;
- даты в одном формате и часовом поясе;
- исключение транспортных полей: номера попытки, времени отправки и трассировки.
Безопасная локальная команда для проверки SHA-256 на демонстрационном значении:
printf '%s' '{"account_id":"merchant-account-17","amount_minor":125000,"currency":"RUB","operation":"payment.charge","order_id":"order-48291"}' | sha256sum
Команда не обращается к сети и не содержит реальных реквизитов. В рабочей реализации сериализацию выполняет библиотека приложения, а не ручная сборка строки.
Шаг 4. Создайте журнал операций
Ниже приведён пример схемы PostgreSQL. Имена и сроки хранения следует адаптировать к вашей инфраструктуре и требованиям к данным.
CREATE TABLE agent_operation_log (
operation_key text PRIMARY KEY,
operation_type text NOT NULL,
request_hash text NOT NULL,
status text NOT NULL
CHECK (status IN (
'started',
'succeeded',
'failed_retryable',
'failed_final',
'unknown'
)),
target_system text NOT NULL,
target_object_id text,
response_json jsonb,
error_code text,
attempt_count integer NOT NULL DEFAULT 1,
lease_until timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz
);
CREATE INDEX agent_operation_log_status_idx
ON agent_operation_log (status, lease_until);
Не записывайте в журнал платёжные реквизиты, токены авторизации и полный ответ внешней системы без необходимости. Сохраняйте минимальный нормализованный результат: идентификатор объекта, бизнес-статус, безопасный код ошибки и данные, необходимые для воспроизведения ответа агенту.
Переходы состояний должны быть явными:
started ───────────────► succeeded
│
├───────────────────► failed_final
│
├───────────────────► failed_retryable ─► started
│
└─ тайм-аут после отправки ─► unknown ─► сверка ─► succeeded
└──► failed_retryable
Состояние unknown принципиально отличается от ошибки. Оно означает: внешний эффект мог произойти, поэтому слепой повтор запрещён до сверки.
Шаг 5. Захватывайте операцию атомарно
На входе шлюз пытается вставить запись. Уникальный первичный ключ разрешает только одному исполнителю стать владельцем новой операции.
INSERT INTO agent_operation_log (
operation_key,
operation_type,
request_hash,
status,
target_system,
lease_until
)
VALUES (
:operation_key,
:operation_type,
:request_hash,
'started',
:target_system,
now() + interval '30 seconds'
)
ON CONFLICT (operation_key) DO NOTHING
RETURNING operation_key;
Дальнейшее решение принимается по существующей записи под блокировкой:
SELECT operation_key,
request_hash,
status,
response_json,
target_object_id,
lease_until
FROM agent_operation_log
WHERE operation_key = :operation_key
FOR UPDATE;
- Если хеш отличается — вернуть конфликт и ничего не выполнять.
- Если статус
succeeded— вернуть сохранённый результат. - Если
failed_final— вернуть сохранённую окончательную ошибку. - Если
startedи lease активна — не запускать второго исполнителя. - Если результат
unknown— сначала запросить статус по внешнему ключу. - Если ошибка допускает повтор — захватить новую lease условным обновлением.
Транзакцию базы данных не следует держать открытой во время сетевого вызова. Она нужна для короткого захвата состояния; затем запрос выполняется вне транзакции, а результат фиксируется отдельным атомарным обновлением.
Шаг 6. Передайте защиту в целевую систему
1С
Передавайте ключ в отдельный реквизит интеграции или регистр сведений и установите логическую уникальность пары «тип операции + внешний ключ». Обработчик сначала ищет ранее созданный объект, проверяет значимые параметры и только затем создаёт новый.
POST /integration/invoices
Idempotency-Key: prod:invoice.create:order-48291:v1
Content-Type: application/json
{
"order_id": "order-48291",
"amount_minor": 125000,
"currency": "RUB"
}
HTTP-заголовок здесь — пример контракта интеграционного шлюза, а не утверждение о стандартном интерфейсе любой конфигурации 1С.
CRM
Если CRM поддерживает внешний идентификатор, записывайте ключ туда и выполняйте upsert. Если такого поля нет, создайте интеграционную таблицу соответствий и перед созданием задачи ищите запись по ключу. Поиск по заголовку задачи или имени клиента ненадёжен: эти поля изменяемы и не уникальны.
Платёжная система
Используйте штатный ключ идемпотентности провайдера, если он предусмотрен контрактом API. Один и тот же ключ должен сопровождать все транспортные повторы. После неопределённого ответа сначала запросите операцию по ключу или вашему внешнему идентификатору. Не создавайте новый платёж до установления статуса предыдущего.
Авторизацию пользователя на денежное действие храните отдельно от технической попытки. Retry уже подтверждённого поручения не должен заново спрашивать согласие, но изменение суммы, получателя или валюты образует новое намерение и требует новой проверки по правилам продукта.
Шаг 7. Настройте безопасные повторы
Повтор допустим только для классифицированных транспортных и временных ошибок. Используйте ограниченное число попыток, экспоненциальную задержку и случайный разброс. Не повторяйте автоматически ошибки валидации, запрета доступа, недостатка средств или конфликт параметров.
retry:
max_attempts: 4
initial_delay_ms: 500
multiplier: 2
max_delay_ms: 5000
jitter: true
rules:
retry:
- connection_reset_before_response
- http_429
- http_502
- http_503
reconcile_before_retry:
- timeout_after_request_sent
- connection_lost_while_waiting_response
never_retry:
- validation_error
- authorization_denied
- idempotency_key_payload_mismatch
Это пример конфигурации, а не универсальный список кодов. Реальные правила должны учитывать документацию и семантику конкретного API.
Проверка результата
Проверяйте систему в тестовом контуре на синтетических данных. Цель проверки — не только одинаковый HTTP-ответ, но и единственный бизнес-эффект.
- Отправьте запрос с ключом
test:invoice.create:order-demo-1:v1. - Повторите тот же запрос с тем же ключом и тем же телом.
- Убедитесь, что возвращён тот же
target_object_id. - Проверьте в целевой системе, что объект создан один раз.
- Повторите ключ с изменённой суммой и ожидайте конфликт без нового объекта.
- Сымитируйте потерю ответа после отправки и проверьте переход в
unknown. - Запустите сверку и убедитесь, что она находит существующий объект по внешнему ключу.
- Одновременно отправьте два одинаковых запроса и проверьте, что внешний вызов выполняет один владелец lease.
Диагностический запрос к журналу:
SELECT operation_key,
status,
attempt_count,
target_object_id,
created_at,
completed_at
FROM agent_operation_log
WHERE operation_key = 'test:invoice.create:order-demo-1:v1';
Критерии успешной реализации:
- один ключ и один payload дают один бизнес-результат;
- один ключ с другим payload отклоняется;
- параллельные попытки не создают параллельные эффекты;
- неопределённый исход проходит сверку до повтора;
- операцию можно проследить от вызова агента до объекта целевой системы.
Типовые ошибки
- Новый UUID при каждом retry
- Система воспринимает повтор как новую операцию. Генерируйте идентификатор на уровне бизнес-намерения и сохраняйте его до завершения.
- Ключ равен идентификатору клиента
- Все последующие законные операции клиента конфликтуют. Добавляйте тип операции и отдельный идентификатор намерения.
- Повтор после любого тайм-аута
- Запрос мог завершиться на стороне сервера. Сначала переводите результат в
unknownи выполняйте сверку. - Уникальность только в памяти процесса
- После рестарта или при нескольких экземплярах защита исчезает. Используйте общий долговечный журнал с уникальным ограничением.
- Сравнение только ключа
- Изменённые параметры незаметно получают старый результат. Всегда сопоставляйте ключ с отпечатком значимого payload.
- Сохранение «успеха» до внешнего вызова
- Агент получает ложноположительный результат. До подтверждения храните
started, а после неясного исхода —unknown. - Бесконечная lease
- Сбой исполнителя навсегда блокирует операцию. Lease должна иметь срок, но её истечение разрешает сначала захват и сверку, а не безусловное повторение.
- Логи с чувствительными данными
- Идемпотентность не требует сохранять секреты или полные платёжные реквизиты. Храните минимальный набор и применяйте маскирование.
Ограничения подхода
Идемпотентность не превращает распределённую операцию в единую транзакцию. Если процесс создаёт счёт в 1С, задачу в CRM и платёж отдельно, каждому эффекту нужен собственный ключ и состояние. Для составного процесса полезна сага: последовательность шагов с явными статусами и, где возможно, компенсирующими действиями.
Компенсация не всегда равна отмене. Проведённый документ, отправленное уведомление или завершённый платёж могут требовать отдельной юридически и бухгалтерски значимой операции. Агент не должен «удалять следы» ради восстановления технической симметрии.
Срок хранения ключей тоже влияет на гарантии. После удаления записи старый повтор снова станет новым запросом. Период хранения выбирают с учётом максимального окна повторной доставки, правил платёжного провайдера, аудита и политики обработки данных.
Наконец, ключ защищает только тот контракт, где его действительно проверяют. Если промежуточный коннектор игнорирует заголовок или 1С не обеспечивает уникальность внешнего идентификатора, локальный журнал уменьшит риск, но не устранит окно двойной записи.
Итоговая памятка
- Выделите одно бизнес-намерение и создайте ключ один раз.
- Свяжите ключ с хешем канонического запроса.
- Атомарно зарегистрируйте операцию в общем журнале.
- Передайте ключ до системы, создающей бизнес-эффект.
- Сохраняйте нормализованный результат для повторной выдачи.
- Отличайте временную ошибку от неопределённого исхода.
- Перед повтором неопределённой операции выполняйте сверку.
- Проверяйте единственность эффекта при последовательных и параллельных вызовах.
Другие практические материалы доступны в разделе руководств, а определения терминов — в глоссарии Agent Lab Journal.