Глава 10. Идемпотентность, повторы, лимиты запросов и границы отката¶
1. Почему самые дорогие сбои часто выглядят как “мы просто повторили вызов”¶
Когда агентная система начинает выполнять реальные действия, один из самых неприятных источников инцидентов оказывается очень прозаичным: повторный вызов.
Снаружи это часто выглядит безобидно:
- запрос завис;
- рантайм решил повторить;
- интеграция ответила нестабильно;
- агент попытался “помочь” и отправил действие еще раз.
Но в реальном мире это означает:
- два одинаковых тикета;
- два письма одному клиенту;
- повторную оплату;
- повторную запись в CRM;
- несколько изменений одного и того же объекта.
То есть проблема не в рассуждении модели как таковом, а в том, что слой выполнения не умеет безопасно жить в мире частичных сбоев.
2. Идемпотентность это не приятное дополнение, а базовая страховка¶
Идемпотентность нужна для простого вопроса: “Если система по ошибке повторит этот вызов, что произойдет?”
Для операций записи хороший ответ должен быть одним из двух:
- повторный вызов не меняет результат;
- система может надежно распознать дубль и не создать побочный эффект повторно.
Если такого свойства нет, любая нестабильность сети, тайм-аут или гонка между несколькими запусками начинает стоить слишком дорого.
3. Повтор без классификации ошибок только множит хаос¶
Очень плохая стратегия выглядит так: “если запрос не удался, повторить еще три раза”.
В эксплуатации это опасно, потому что не все ошибки одинаковы:
validation_failureпочти никогда не нужно повторять;permission_deniedнельзя чинить количеством повторов;retryable_failureкак раз может требовать backoff;side_effect_unknownтребует осторожности, а не слепого повтора.
То есть политика повторов должна зависеть не от эмоции “ну вдруг получится”, а от класса исхода.
4. Самый неприятный статус это side_effect_unknown¶
Есть особенно неприятная категория сбоев: ты уже не знаешь, произошел побочный эффект или нет.
Примеры:
- тайм-аут после отправки запроса во внешний сервис;
- соединение оборвалось после commit;
- адаптер упал, не сохранив подтверждение;
- внешний API ответил так, что итоговое состояние неясно.
Это именно тот случай, где наивный повтор особенно опасен. Иногда правильное поведение тут не “повторить”, а:
- проверить текущее состояние во внешней системе;
- выполнить сверку;
- запросить человека;
- остановить рабочий процесс и зафиксировать неопределенность.
Сквозной кейс: неизвестный итог тикета
В кейсе сортировки обращений поддержки самый опасный момент — не ошибка в рассуждении, а тайм-аут после вызова create_support_ticket. Если helpdesk мог уже создать тикет, повторный вызов без ключа идемпотентности превратит один клиентский запрос в два инцидента. Правильная ветка сначала ищет тикет по correlation ID, затем либо привязывает найденный результат к трассе, либо останавливает запуск и просит оператора подтвердить состояние.
4.1. Ветка восстановления тоже должна проектироваться явно¶
Свежие работы по сценариям отказов инструментов усиливают еще одну мысль: путь восстановления почти никогда не стоит оставлять как импровизацию слоя выполнения.
Полезно заранее определить:
- какие классы исходов требуют сверки, а не повтора;
- где частичный успех должен создавать следующую задачу;
- когда восстановление само требует подтверждения;
- какие ветки должны явно попадать в оценочный набор данных;
- где опасный путь восстановления лучше остановить, чем “помочь пользователю еще раз”.
Многие тяжелые инциденты происходят не в счастливом пути, а именно в плохо спроектированной ветке восстановления.
5. Лимиты запросов это тоже часть безопасного дизайна¶
Когда о лимитах запросов думают только как о проблеме производительности, слой выполнения недооценивает их архитектурную роль.
На деле лимиты запросов нужны еще и для того, чтобы:
- один сорвавшийся агент не заддосил внешнюю систему;
- циклическое планирование не превратилось в лавину вызовов инструментов;
- дорогая возможность не съедала весь бюджет;
- шторм повторов не убивал интеграцию.
Именно поэтому лимит полезно задавать не только на уровне всего сервиса, но и:
- на инструмент;
- на арендатора;
- на рабочий процесс;
- на класс риска.
6. Граница отката должна быть определена заранее¶
Очень опасно обнаруживать только в инциденте, что операция “вообще-то неоткатываемая”.
У каждой возможности записи полезно заранее понимать:
- можно ли отменить действие;
- можно ли безопасно повторить действие;
- где проходит точка невозврата;
- какое компенсирующее действие допустимо;
- когда нужна ручная сверка.
То есть граница отката — это не “потом посмотрим”. Это часть контракта инструмента и рабочего процесса.
После побочного эффекта слой выполнения должен различать безопасный повтор, сверку и остановку
flowchart TD
A["Запрос инструмента"] --> B["Выполнить действие записи"]
B --> C{"Исход известен?"}
C -->|Да, успех| D["Сохранить результат и продолжить"]
C -->|Ошибка, которую можно повторить| E["Повторить по политике с backoff"]
C -->|Неизвестный побочный эффект| F["Сверить или запросить проверку человеком"]
C -->|Ошибка валидации или доступа| G["Остановиться и показать ошибку"] 7. Хороший контракт выполнения хранит операционную семантику явно¶
Недостаточно знать только схему входа и форму ответа. Для опасных операций контракт должен хранить операционную семантику:
- идемпотентно ли действие;
- нужен ли ключ идемпотентности;
- какие ошибки допустимы для повтора;
- где предел повторов;
- какие лимиты на частоту вызова;
- что делать при неизвестном исходе;
- возможен ли откат или компенсирующее действие.
Ниже очень практичный шаблон:
tools:
create_ticket:
mode: write
idempotent: true
idempotency_key_required: true
retry:
max_attempts: 3
backoff: exponential
retry_on: ["retryable_failure"]
rate_limit:
per_tenant_per_minute: 20
rollback:
strategy: "none"
reconcile_on_unknown: true
send_email:
mode: write
idempotent: false
idempotency_key_required: false
retry:
max_attempts: 1
retry_on: []
rate_limit:
per_user_per_hour: 10
rollback:
strategy: "manual_only"
reconcile_on_unknown: true
Такой YAML помогает команде заранее признать неприятную правду: не все действия одинаково безопасны для автоматизации.
8. Простой кодовый пример логики решений для повторов¶
Ниже не движок политик эксплуатационного уровня, а понятный каркас. Его задача показать, что повтор должен зависеть от класса исхода, а не быть общим рефлексом.
from dataclasses import dataclass
@dataclass
class ExecutionOutcome:
status: str
attempts: int
max_attempts: int
def next_step(outcome: ExecutionOutcome) -> str:
if outcome.status in {"validation_failure", "permission_denied"}:
return "stop"
if outcome.status == "retryable_failure" and outcome.attempts < outcome.max_attempts:
return "retry_with_backoff"
if outcome.status == "side_effect_unknown":
return "reconcile"
if outcome.status == "success":
return "continue"
return "escalate"
Важен не код сам по себе, а то, что у системы появляется явная операционная таблица решений.
9. У цикла агента должны быть явные условия остановки¶
Еще один практический совет из гайда OpenAI, который очень полезно формализовать: у цикла запуска должны быть понятные условия остановки.1
Хороший рантайм завершает запуск не “когда модель вроде бы успокоилась”, а по явным причинам:
- получен финальный структурированный результат;
- больше не требуются вызовы инструментов;
- пришла неустранимая ошибка;
- достигнут лимит по шагам или бюджету;
- сработала граница подтверждения и нужен человек.
Это звучит скучно, но именно такие правила не дают агенту уйти в бесконечное планирование, бессмысленные повторы или декоративные переходы между инструментами.
10. Ключ идемпотентности должен быть частью протокола, а не опциональной договоренностью¶
Если инструмент записи формально “поддерживает идемпотентность”, но ключ:
- не обязателен;
- по-разному генерируется в разных местах;
- не доживает до пути повтора;
- не логируется,
то это почти не настоящая идемпотентность.
Хорошая практика:
- генерировать ключ на границе рабочего процесса или действия;
- передавать его через весь путь выполнения;
- логировать его в журнале аудита;
- использовать для сверки и расследований.
11. Частые ошибки¶
Проблемы здесь довольно типовые:
- повторы идут на ошибки, которые нельзя повторять;
- неизвестный исход трактуется как обычная ошибка;
- лимиты запросов ставят только на входе, но не на инструментах;
- откат обещан, но фактически не существует;
- ключ идемпотентности забывается между планировщиком и адаптером;
- агент получает слишком много свободы в повторных попытках.
Все это означает одно: слой выполнения еще не дорос до модели отказов эксплуатационного уровня.
12. Что сделать сразу¶
Сначала пройди по короткому списку и отдельно отметь все ответы «нет»:
- Есть ли у инструментов записи явная стратегия идемпотентности?
- Различает ли система
retryable_failureиside_effect_unknown? - Привязаны ли повторы к политике, а не к общему вспомогательному обработчику?
- Есть ли лимиты запросов на инструмент или на арендатора?
- Понимаешь ли ты границу отката для каждого опасного действия?
- Может ли рантайм делать сверку вместо слепого повтора?
- Видно ли ключ идемпотентности в трассах и журналах аудита?
Если на несколько вопросов подряд ответ “нет”, то следующая нестабильность интеграции почти наверняка превратится в дубль, шум или ручной разбор инцидента.
13. Что делать дальше¶
Часть IV уже закрывает базовый слой выполнения: контракты, песочницу, транспорт возможностей и дисциплину вокруг побочных эффектов. Дальше стоит переходить к наблюдаемости и надежности на уровне всей агентной системы.