Агент ответил «готово». В CRM появились две задачи

Обсудить
Агент ответил «готово». В CRM появились две задачи
Реклама. АО «ТаймВэб». erid: 2W5zFHrJvDX

Менеджер просит ИИ-агента поставить задачу: связаться с клиентом завтра. Агент вызывает инструмент, ждёт ответа CRM и получает тайм-аут. Следующая попытка проходит успешно, в чате появляется «Готово». У исполнителя тем временем две одинаковые задачи. Разберём этот условный сценарий: где возникает дубль, зачем хранить намерение отдельно от сетевой попытки и почему после тайм-аута иногда нужно остановиться и проверить результат.

Где именно появилась вторая задача

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

Повторный вызов создаёт задачу 732. Модель могла правильно понять просьбу и заполнить поля. Ошибка возникла в доставке действия и трактовке результата. Аналогичный сбой возможен у фонового обработчика без языковой модели.

В агентной системе повтор может запустить несколько участников: SDK, очередь, восстановившийся процесс или сама модель, повторно выбравшая инструмент. Фраза в промпте «не создавай дубликаты» не связывает эти попытки. RFC 9110 рекомендует автоматически повторять неидемпотентный запрос только при известной безопасной семантике повтора либо возможности установить, что исходное действие не было применено. HTTP Semantics, раздел 9.2.2.

Намерение, попытка и результат

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

У намерения есть постоянный operation_id, владелец, аккаунт интеграции, действие и зафиксированные аргументы. У попытки – отдельный attempt_id, время, транспортная ошибка и доступный идентификатор запроса провайдера. В результате нужны статус и внешний task_id: строка «успешно» без конкретной задачи мало помогает при разборе дубля.

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

Статус unknown означает: запрос мог изменить CRM, но подтверждения пока нет. Это не ошибка с автоматическим повтором. Неизвестность хранится явно и переживает перезапуск так же, как успех.

Состояние операции

Что известно

Следующий шаг

pending

Намерение сохранено, отправка ещё не начата

Захватить операцию для выполнения

in_flight

Попытка начата, результата пока нет

Дождаться её завершения

unknown

Действие могло состояться

Сверить результат или безопасно повторить по контракту API

succeeded

Подтверждена конкретная задача

Вернуть сохранённый результат

rejected

Подтверждён отказ без создания задачи

Сообщить причину; исправление оформить отдельно

Ключ должен обозначать одно поручение

Идемпотентность означает, что повтор одной операции не добавляет предусмотренных ею изменений сверх первого выполнения. Для API создания объектов обычно нужен idempotency key: клиент передаёт постоянный ключ, а сервер распознаёт повтор. В Amazon Builders’ Library объясняется, почему идентификатор от вызывающей стороны лучше попытки угадать намерение по одинаковым параметрам: пользователь может захотеть два одинаковых ресурса. Making retries safe with idempotent APIs.

Ключ создаётся при сохранении намерения. Повтор после обрыва связи использует его же. Новое поручение «поставь ещё одну такую задачу» получает новый operation_id и ключ. Хеш текста «позвонить завтра» не различает эти случаи. Хеш нормализованных аргументов полезен для проверки неизменности запроса, но не определяет число нужных пользователю задач.

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

Проверьте срок хранения ключей, допустимые методы, реакцию на изменившиеся параметры и одновременные запросы. Например, Stripe API v1 сохраняет код и тело первого выполненного запроса, включая ошибки 500; ключи могут удаляться после 24 часов, а несовпадающие параметры вызывают ошибку. Повтор после удаления ключа считается новым запросом. Эти условия нельзя переносить на другую CRM или даже на другую версию API без проверки. Idempotent requests.

Пауза после тайм-аута даёт время сверить журнал с внешней системой. Концептуальная иллюстрация. Изображение сгенерировано с помощью ИИ.

Пауза после тайм-аута даёт время сверить журнал с внешней системой. Концептуальная иллюстрация. Изображение сгенерировано с помощью ИИ

Сначала сохранить, затем отправить

До сетевого вызова фиксируются намерение, ключ, снимок аргументов и основание для выполнения. Одобрение пользователя относится к этому снимку: конкретному клиенту, исполнителю и сроку. Изменённые поля нельзя незаметно отправить под старым одобрением.

В документации AiHummer инструменты, одобрения человеком и идемпотентные побочные эффекты входят в принципы работы платформы. Их границы нужно различать: одобрение разрешает действие, журнал сохраняет историю, а защита от повторного изменения CRM зависит ещё и от контракта её API. Свойства платформы не устанавливают гарантию exactly-once для любого подключённого сервиса. Документация платформы.

Переход pending → in_flight и запись попытки выполняются атомарно в БД. Только один обработчик получает право отправки. Проверка «записи ещё нет» с последующей вставкой без ограничения уникальности допускает гонку. После ответа сохраняется внешний идентификатор; сбой этой записи оставляет необходимость восстановления.

Если одновременно требуется изменить локальную бизнес-запись и запланировать отправку, подходит transactional outbox: обе записи фиксируются одной локальной транзакцией, затем отдельный обработчик доставляет команду. Но атомарность БД не распространяется на удалённую CRM. Доставщик может повторить сообщение; это ограничение есть и в описании паттерна AWS. Transactional outbox.

Ниже – Python-подобный псевдокод условного адаптера CRM, а не запущенный пример или API AiHummer. Намерение уже сохранено и разрешено к выполнению. journal выполняет атомарные условные переходы с проверкой версии записи. Адаптер преобразует ошибки с неизвестным исходом в UncertainOutcome.

def execute(op_id):
    op = journal.load(op_id)
    if op.state in ("succeeded", "rejected"): return journal.result(op_id)
    if op.state == "in_flight": return journal.status(op_id)
    if op.state == "unknown": return reconcile(op)
    if op.has_attempts and not crm.retry_contract_valid(op):
        return journal.require_review(op_id)
    attempt = journal.claim_and_commit(op)
    if attempt is None:
        return journal.status(op_id)
    try:
        result = crm.create_task(op.account, op.payload, key=op.key)
    except DefinitiveRejection as error:
        return journal.reject(attempt, error)
    except UncertainOutcome:
        return journal.mark_unknown(attempt)
    return journal.succeed(attempt, result)

def reconcile(op):
    evidence = crm.lookup_operation(op.account, op.key)
    if evidence.confirmed:
        return journal.confirm(op.id, evidence.result)
    if crm.retry_contract_valid(op):
        return journal.requeue_same_operation(op.id)
    return journal.require_review(op.id)

Неизвестный результат нужно сверить

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

lookup_operation обозначает доступный способ получить доказательство: статус запроса у провайдера или поиск объекта по внешней метке операции. Это не обещание, что любая CRM ищет по idempotency key. Если механизма нет, адаптер возвращает отсутствие доказательства. Найденная задача должна относиться к тому же аккаунту и операции; совпадения заголовка недостаточно.

Пустой поиск тоже не всегда доказывает отсутствие задачи. Индекс может обновляться с задержкой, а первый запрос – ещё выполняться. Схема «проверить, что нет, затем создать» оставляет промежуток для гонки. Уникальная внешняя метка предотвращает дубль только тогда, когда сама CRM атомарно обеспечивает её уникальность при создании.

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

Если защита от повторов и надёжная сверка недоступны, автоматическое создание останавливается. Оператор получает operation_id, аккаунт, аргументы, время и известные ответы. Пользователю можно сказать: «CRM не подтвердила создание задачи; проверяю результат». Ручной разбор тоже не устраняет неизвестность сам по себе: решение повторить действие учитывает риск дубля.

Проверьте места, где теряется подтверждение

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

Затем доставьте одну операцию двум обработчикам одновременно и повторите её после перезапуска. Отдельно проверьте новый запрос с прежним текстом, изменение аргументов под старым ключом, переключение CRM-аккаунта и повтор после истечения срока хранения ключа. Для API без идемпотентности имитируйте задержку поискового индекса: пустая выдача не должна автоматически разрешать ещё одно создание.

Добавьте метрики количества и возраста операций unknown, подтверждений через сверку и найденных дублей. Лог связывает operation_id, attempt_id и внешний task_id. Так видно, какие поручения ждут результата, даже если процессы работают и очередь выглядит здоровой.

Ответ агента опирается на состояние операции. succeeded позволяет назвать задачу; rejected – объяснить отказ; unknown – сообщить о проверке. Тогда слово «готово» получает проверяемое основание.

Устойчивость такого сценария начинается с конкретного вопроса к интеграции: «Что мы сделаем, если CRM выполнит запрос, а подтверждение потеряется?» Ответ должен быть записан в контракте адаптера, состояниях журнала и процедуре сверки до того, как агент получит право создавать задачи.

Hello World! Гайды и обзоры для девелоперов разных мастей.

Комментарии

С помощью соцсетей
У меня нет аккаунта Зарегистрироваться
С помощью соцсетей
У меня уже есть аккаунт Войти
Инструкции по восстановлению пароля высланы на Ваш адрес электронной почты.
Пожалуйста, укажите email вашего аккаунта
Ваш баланс 10 ТК
1 ТК = 1 ₽
О том, как заработать и потратить Таймкарму, читайте в этой статье
Чтобы потратить Таймкарму, зарегистрируйтесь на нашем сайте