ИИ-агент не получил ответа от API. Он записал или нет?
Мы делаем Сам Решу — ИИ-агента, который работает в кабинетах маркетплейсов, складских программах, банках и CRM. За последний год мы поняли: самые дорогие ошибки агента случаются не в диалоге и не в «галлюцинациях». Они случаются в момент, когда агент что-то записывает во внешнюю систему, а ответ не приходит.
Разберу, почему это отдельный класс ошибок, как мы его закрываем и какие вопросы стоит задать любому поставщику агентов, прежде чем пускать его к деньгам и складу.
Три исхода вместо двух
Когда агент читает — остатки, заказы, выписку, — сбой безвреден. Не получилось, прочитал ещё раз.
Когда агент пишет, у запроса три исхода:
- прошёл — сервис ответил и вернул результат;
- отклонён — сервис ответил отказом: нет прав, неверные данные;
- неизвестно — запрос ушёл, ответ не пришёл: таймаут чтения, обрыв соединения, 5xx на стороне сервиса.
Большинство автоматизаций знают только первые два. Для них «нет ответа» — это ошибка, и дальше сценарий либо падает, либо повторяет шаг. Но запрос без ответа мог дойти до сервиса и выполниться. Сервис создал заказ, а ответ потерялся по дороге обратно.
В RFC 9110 это сформулировано прямо: неидемпотентный запрос нельзя повторять автоматически, если нет способа узнать, что первый не применился.
Что делает агент, у которого нет этого различения
Возьмём заказ поставщику на 40 позиций. Запрос ушёл, 30 секунд тишины. Дальше три варианта, и все плохие:
- повторить — если первый прошёл, у вас два заказа, поставщик получает двойную заявку;
- сказать «не получилось» — заказ создан, но вы о нём не знаете, и следующая попытка сделает тот же дубль вашими руками;
- сказать «готово» — агент не видел ответа, но отчитался. Звучит убедительно, но это неправда.
У LLM-агента есть и своя специфика: модель склонна закрывать неопределённость уверенным ответом. Если не дать ей явного состояния «исход неизвестен», она выберет одно из двух привычных.
Одно правило на все интеграции
У нас больше ста интеграций, и решать вопрос повтора в каждом сценарии по-своему — верный способ получить сто разных ответов. Поэтому решение принимает один слой, через который проходят все вызовы агента. Правило такое:
- Чтение повторять можно. Оно ничего не меняет.
- Идемпотентную запись повторять можно. PUT и DELETE приводят систему в то же состояние: поставить цену 990 ₽ дважды — всё равно 990 ₽.
- Создание можно повторять, только если сервис сам обещает дедупликацию. Это обещание записано в описании интеграции явно, для конкретных эндпоинтов. Заголовок, который кто-то придумал сам, слой не принимает за обещание: поддержку ключа подтверждает сервис, а не вызывающий код.
- Всё остальное слой не повторяет. Агент получает явное состояние: «исход неизвестен, проверь, прежде чем повторять запись».
Отдельно различаем фазы сбоя. Таймаут на установке соединения значит, что запрос никуда не ушёл, и повтор безопасен. Таймаут на чтении ответа значит, что запрос ушёл и мог выполниться. Это разные коды, и агент получает разные инструкции.
Поверх этого есть собственная страховка: каждый пишущий вызов из песочницы агента получает ключ, и шлюз пять минут помнит результат. Если тот же вызов с тем же ключом придёт снова, в сервис он второй раз не уйдёт — вернётся сохранённый ответ.
Кто ставит ключ: программа, а не модель
Самая неочевидная ошибка — поручить ключ идемпотентности модели. Попросите её «сгенерировать уникальный id для запроса» — на повторе она сгенерирует новый, и защита исчезнет ровно тогда, когда она нужна.
Поэтому ключ у нас всегда ставит код: один ключ на одну логическую операцию, и его несут все попытки этой операции. В ЮKassa это заголовок Idempotence-Key: сеть оборвалась, запрос ушёл снова, ЮKassa видит знакомый ключ и возвращает тот же платёж, а не проводит второй. У Stripe механизм устроен так же и рекомендация та же: повторять с тем же ключом и теми же параметрами.
МойСклад: поиск по syncId вместо повтора вслепую
У МойСклад есть поле syncId. Если прислать повторный POST с уже занятым syncId, сервис вернёт ранее созданную сущность, а не создаст вторую. У документов по этому полю ещё и можно фильтровать.
Когда агент создаёт заказ, приёмку или списание, происходит следующее:
- Хелпер ставит syncId в сам payload. Повтор того же вызова несёт тот же ключ, а новый черновик — новый.
- Если ответ потерян, агент не сообщает об ошибке, а ищет документ по syncId.
- Нашёл — запись состоялась, агент продолжает с найденным документом.
- Не нашёл — исход всё ещё неизвестен: сервис мог не принять запись или не закончить её. Агент не говорит ни «создано», ни «не создано» и называет единственный безопасный ход: тот же вызов с тем же payload.
С массовыми правками механика другая. Обновления перечитываются по id и досылаются меньшими пачками. Созданные записи сначала ищутся фильтром, и только потом решается, что отправлять заново.
И даже успешную запись агент перечитывает и сверяет позиции, количества и ссылки с запросом. Документ, созданный не таким, для нас — провал записи, а не успех.
Ozon: «успешно» — ещё не итог
Проверять надо не только потерянный ответ, но и полученный.
У Ozon есть заявки покупателей на скидку. Когда агент одобряет пачку заявок, API отвечает счётчиком успешных, но цена, которую Ozon в итоге утвердил, может отличаться от отправленной. Поэтому после записи агент перечитывает затронутые заявки и показывает то, что реально стоит в кабинете. Если перечитанных меньше, чем «успешных», он ищет шире, а не считает запись подтверждённой.
Отказ по правам: читать узко
Последний случай — сервис ответил, но отказом. Тут важно понять, от чего именно отказ.
В МойСклад права «создавать» и «проводить» документ разделены. Если у ключа есть первое и нет второго, на создание проведённой приёмки сервис отвечает 403 с кодом 1092. Смысл узкий: провести нельзя, создать можно.
До недавнего времени наш агент читал этот отказ широко: «нет доступа к разделу» — и бросал задачу. Причина была банальной: код 1092 отсутствовал в нашей таблице восстановлений и проваливался в общий ролевой вердикт, а тот ещё и запрещал соседние пути. Инструкция «повтори с applicable=false» у нас была, но агент до неё не доходил. Официальная таблица ошибок МойСклад тут не помогает: у 1092 описание скопировано от соседнего кода.
Теперь на 1092 агент повторяет запрос с applicable=false, получает непроведённый черновик и говорит прямо: документ создан, провести его нужно вам.
И отдельно: там, где право выдать нельзя, агент не отправляет человека его выдавать. У приложения из маркетплейса решений МойСклад права на проведение нет в принципе — набор прав задан дескриптором приложения, и ни администратор, ни пользователь его не расширят. Совет «попросите администратора» в таком случае стоит клиенту второго обращения в поддержку. Честный итог — черновик и одна кнопка «Провести».
Почему это важнее, чем кажется
В подкасте Latent Space основатель TypeSafe AI Диогу Алмейда сформулировал парадокс: модели берутся за задачи уровня «премии тысячелетия», но до сих пор не автоматизируют базовую работу. В разборе модели Jev от Browserbase есть точная формулировка той же мысли: ответ может убедить человека и всё равно не годиться для автоматизации без присмотра.
Разрыв между демо и продакшеном во многом состоит из таких мест: потерянных ответов, частично применённых пачек, отказов, которые значат не то, что написано, и «успешно», за которым стоит другая цена. В свежем опросе разработчиков Zapier пять навыков, которые они считают главными на будущее, сводятся к одному — решить, безопасно ли это, правильно ли и можно ли это выпускать. Для агентов, которые пишут в чужие системы, это и есть основная работа.
Что спросить у поставщика ИИ-агента
- Что агент делает, если на запись не пришёл ответ? Хороший ответ: считает исход неизвестным и проверяет. Плохой: повторяет или сообщает об ошибке.
- Кто ставит ключ идемпотентности — код или модель? Правильно — код, один на операцию.
- Правило повтора общее или своё в каждом сценарии? Общее, с доп. проверками там, где у сервиса свои механизмы.
- Проверяет ли агент результат после успешной записи? «Сервис ответил 200» — не проверка.
- Что будет при нехватке прав? Хорошо: сделает, что можно, например черновик, и скажет, что осталось вам.
- Что агент скажет, если не знает, чем закончилась запись? Честное «исход неизвестен, вот как проверить», а не «готово».
Самая быстрая проверка — попросить агента создать тестовый документ и спросить, что он сделает, если ответ не придёт. Если слышите «попробую ещё раз», главное вы узнали.
Источники: RFC 9110, §9.2.2 Idempotent Methods; документация ЮKassa об идемпотентности; Stripe API, Idempotent requests; МойСклад JSON API 1.2, назначение поля syncId; Latent Space, «Jev: System One models for Prod, not God»; Browserbase, «What is Jev?»; Zapier, опрос разработчиков, сентябрь 2026.