Перейти к содержимому
Экосистема AI Vibe

Разборы

Идемпотентность агента: хэш аргументов, replay и stable intent

Редакция 21 октября 2026 г.

Идемпотентность агента: хэш аргументов, replay и stable intent

Повторный запуск агента после сбоя не равен «тому же запросу»: модель перепишет письмо, хэш аргументов сменится, побочный эффект уйдёт второй раз. Для соло-разработчика с агентом в цикле tools и ретраев это контракт над недетерминированным выводом LLM; в разборе Anis Meziani про runtime ApowerB сформулировано жёстко: ключ идемпотентности должен отражать устойчивое намерение, а не сгенерированный текст.

Три развилки и классы аргументов tool-call

В ApowerB при падении run фиксируется, какие tools вызывались. Если вызывался любой tool, даже read, автоматический replay отклоняется с HTTP 409 и списком вызовов; форсировать повтор можно только вручную (run_main.py в репозитории). Это default-deny: агент не получает «бесплатный» второй заход после side effect.

Классический обход, sha256(json(args)), ломается на примере send_email к одному получателю: при replay модель меняет subject и body, хэш другой, система считает запрос новым, и дубликат письма проходит как «успешная» идемпотентность.

Третий путь, ключ из stable intent, задаёт вопрос не «уже отправил эти байты?», а «уже сделал это действие по этой причине к этой цели?» Для кампании единицей эффекта служит одно письмо, не весь run; предлагаемый ключ: (triggering_event, tool, recipient) без subject/body в ключе.

Meziani делит входы tool на три класса. Это рамка для любой обёртки вокруг LLM-вызовов, не только для ApowerB.

Класс Примеры В ключе?
Внешняя identity event id, message id, thread id, record id, нормализованный to Да
Детерминированно от входов хэш байтов файла Да
Сгенерировано моделью body, subject, summary, любой free text Никогда

build the key from stable intent, never from generated content.

В sketch-декораторе (пометка design sketch, not shipped) ключ собирается из trigger_event_id, имени эффекта и нормализованного получателя, не из полей, которые модель перефразирует при каждом replay.

Replay на границе эффекта, не всего run

Сценарий email-кампании на 100 писем: run падает после 40 отправок. При default-deny автоматический replay не стартует, оставшиеся 60 не уйдут, пока человек не вмешается. Если оператор форсирует replay, 40 адресатов получат второе письмо.

Здесь ломается привычка мыслить идемпотентностью на уровне run, а не эффекта. Для агентных пайплайнов с human gate до send (skill email-campaign у Meziani) replay и ключ должны жить на границе «одно действие к одной цели», иначе оркестратор выбирает между голодной очередью и дубликатами.

Shipped-код ApowerB, read-only tools и MCP

Meziani опирается на поведение в проде, не только на RFC.

  • Microsoft Graph webhooks: уникальный индекс (subscription_id, resource_id) отсекает повторную доставку (outlook.py).
  • BI datasets: dataset_id как UUIDv5 от owner, organization_id, project_id, folder, name, sheet и SHA-256 содержимого; повтор того же файла даёт reused: true; при soft delete и коллизии fallback на случайный ID после IntegrityError (bi_datasets.py).

Read-only tools Meziani считает кандидатами на «safe to replay» только если ограничение встроено в конструкцию, а не в флаг: Oracle (SET TRANSACTION READ ONLY и rollback), MongoDB (блок write-стадий $out/$merge), Redshift (проверка read-only SQL до выполнения). Иначе «чистый» GET перед send_email не спасает от ложной уверенности при replay.

Сторонний tool через MCP, если в тексте не объявлен как безопасный, трактуется как unsafe; replay остаётся под default-deny (псевдокод third_party_mcp_action в sketch). Для практики с агентом в IDE вывод прямой: нельзя полагаться на декларацию «read-only» у внешнего MCP без контракта уровня кода.

Если провайдер API поддерживает идемпотентность нативно, ключ должен доходить до API (пример заголовка Stripe Idempotency-Key в тексте Meziani). Sketch также описывает effects ledger, проброс ключа провайдеру и отказ для undeclared tools.

В комментариях на Dev.to (не shipped-фича ApowerB) читатель сравнивает подход с duplicate detection в Azure Service Bus на business ID; Meziani соглашается с аналогией «окно vs навсегда». Идеи из треда (action slug из workflow/tool definition, не из модели; outbox pending→confirmed) остаются на уровне дискуссии.

Четыре шага в своём агентном слое

Meziani не закрывает поля ключа per tool (два письма одному получателю vs один summary в Slack), ветвление replay при другом наборе получателей, TTL ledger и разрыв между side effect и записью в ledger, если у провайдера нет native idempotency.

Для разработчика с агентом в IDE практический вывод такой:

  1. Не хэшируйте полный JSON аргументов tool, если в нём есть LLM-generated text.
  2. Привязывайте ключ к внешней identity и событию-триггеру, а не к формулировке модели.
  3. Считайте единицей идемпотентности эффект (письмо, запись, webhook), не весь agent run.
  4. Любой необъявленный tool трактуйте как риск при автоматическом replay; MCP third-party без контракта в той же корзине.

В CTA Meziani указывает RFC на GitHub, документацию и Helm-чарт хостинга ApowerB; номер issue в тексте не зафиксирован.

Источники