Почему агенту тесно внутри одного HTTP-запроса
Долгая агентная операция редко похожа на обычный запрос к сайту. Нужно получить документы, вызвать модель, проверить результат, обратиться к CRM, дождаться подтверждения человека и только потом изменить учётную систему. Если держать всё это внутри одного HTTP-соединения, первый же тайм-аут превращает статус работы в философский вопрос: задача не выполнилась, выполняется или уже выполнилась, но ответ потерялся?
Самый неприятный вариант — автоматический повтор. Интерфейс не получил ответ и отправил запрос ещё раз, агент снова создал задачу, а исполнитель добросовестно завёл второй заказ. Машина была очень старательной; бухгалтерия почему-то не оценила энтузиазм.
Надёжная схема отделяет принятие задания от исполнения. API быстро сохраняет намерение, возвращает идентификатор работы и освобождает соединение. Дальше задача проходит через очередь, аренду исполнителя, контролируемые повторы и явные состояния. Модель помогает решить, что делать, но финансовые и операционные изменения выполняет детерминированный код.
Базовый поток
Для небольшого бизнеса достаточно семи компонентов:
1. API принимает команду и проверяет права пользователя.
2. В PostgreSQL создаётся запись `job` со стабильным ключом идемпотентности и статусом `queued`.
3. В той же транзакции появляется событие в таблице outbox.
4. Диспетчер публикует событие в Redis, RabbitMQ или другой брокер.
5. Worker получает задачу, берёт ограниченную по времени аренду и периодически продлевает её.
6. Каждый внешний эффект проходит через отдельный исполнитель с проверкой схемы, политики и уникального `operation_id`.
7. Пользователь видит состояние задачи и подтверждает необратимый шаг, если он действительно нужен.
Ответ API в такой схеме — не «всё готово», а `202 Accepted` с `job_id`. Клиент опрашивает статус или получает событие через WebSocket. Повторная отправка того же бизнес-намерения возвращает прежнюю работу, а не создаёт её близнеца.
Идемпотентность: повторить можно, удвоить нельзя
Celery предупреждает: сообщение может быть повторно доставлено после сбоя worker, а позднее подтверждение безопасно только для идемпотентной задачи. AWS в описании transactional outbox делает тот же практический вывод: даже надёжная доставка может давать дубликаты, поэтому потребитель должен отслеживать уже обработанные события.
Ключ идемпотентности строят из бизнес-намерения, а не из случайного UUID каждой попытки. Для команды «создать счёт по заказу 841» это может быть `invoice:create:order-841:v1`. В базе нужен уникальный индекс по этому ключу. Если пользователь дважды нажал кнопку или шлюз повторил запрос, система находит существующую запись и показывает её состояние.
Для каждого шага полезен свой ключ:
- `job_id` — вся бизнес-операция;
- `step_id` — конкретный этап, например извлечение реквизитов;
- `operation_id` — внешний эффект, например создание черновика счёта;
- `attempt` — номер технической попытки, который не меняет бизнес-смысл.
Нельзя считать, что «модель второй раз наверняка ответит тем же». Идемпотентность обеспечивается хранилищем и исполнителем, а не хорошим настроением генератора текста.
Outbox закрывает щель между базой и очередью
Классическая ошибка выглядит невинно: сначала сохранить заказ, затем отправить сообщение в брокер. Если процесс упал между двумя действиями, заказ есть, а задачи нет. Обратный порядок создаёт зеркальную проблему: сообщение ушло, но транзакция базы откатилась.
Transactional outbox записывает бизнес-изменение и событие в одной транзакции PostgreSQL. Отдельный процесс читает подтверждённые записи outbox и публикует их в брокер. Он тоже может отправить сообщение повторно, поэтому идемпотентный потребитель всё равно обязателен. Outbox не отменяет дубликаты; он не даёт событию бесследно провалиться в щель между двумя системами.
Если масштаб небольшой, отдельный брокер иногда не нужен на первом пилоте. PostgreSQL допускает `FOR UPDATE SKIP LOCKED`, чтобы несколько работников забирали разные строки из таблицы, не ожидая заблокированные. Документация прямо отмечает, что такой режим подходит для queue-like table, но даёт непоследовательное представление и не предназначен для обычной аналитики. Очередь работ и отчётная таблица должны оставаться разными задачами, даже если живут в одной базе.
Аренда, heartbeat и возврат незавершённой работы
Worker не должен навсегда «владеть» задачей. Он записывает `worker_id`, `lease_until` и время последнего heartbeat. Пока работа идёт, аренда продлевается. Если процесс умер, другой worker после истечения срока может вернуть задачу в очередь.
Срок аренды должен превышать нормальный интервал между heartbeat, но не быть настолько длинным, чтобы зависшая задача лежала часами. Для шага, который обычно занимает 30 секунд, можно начать с heartbeat раз в 10 секунд и аренды на 60–90 секунд, а затем настроить по измерениям. Это не универсальные нормативы, а стартовые параметры пилота.
Перед повтором новый исполнитель сверяет журнал шагов и внешнюю систему. Если счёт уже создан, он фиксирует успех существующего `operation_id`, а не создаёт новый. Система сначала примиряется с реальностью, потом действует — полезная привычка и для роботов, и для людей после понедельника.
Повторять только то, что может восстановиться
Повтор оправдан для временной сетевой ошибки, ограничения частоты или краткой недоступности зависимости. Ошибка схемы, запрещённое действие или отсутствующий обязательный реквизит сами по себе не исправятся на пятой попытке.
Политика повторов должна содержать:
- явный список временных ошибок;
- экспоненциальную задержку и jitter, чтобы все workers не проснулись одновременно;
- максимальное число попыток;
- общий временной бюджет задачи;
- dead-letter или очередь разбора для исчерпанных попыток;
- возможность безопасно отменить ещё не начавшийся шаг.
Celery отдельно предупреждает, что неограниченный requeue способен создать бесконечный цикл. Поэтому «повторять всегда» — не политика устойчивости, а способ превратить одну проблему в оптовую.
Где заканчивается модель и начинается исполнитель
LLM может классифицировать обращение, предложить план, извлечь поля и подготовить аргументы инструмента. Но перед изменением CRM, ERP или банка нужен детерминированный шлюз действий.
Он проверяет:
- разрешён ли инструмент этой роли и этому типу задачи;
- соответствует ли JSON строгой схеме;
- есть ли ссылка на исходный документ или запись;
- укладывается ли сумма, количество или скидка в допустимые пределы;
- не выполнялся ли такой `operation_id` раньше;
- требуется ли подтверждение человека.
Необратимые шаги лучше разделять на `prepare` и `commit`. Агент создаёт черновик и доказательства, человек видит различия и подтверждает, после чего исполнитель делает единственную запись. Кнопка подтверждения должна работать по версии черновика: если данные изменились, старое согласие нельзя применять к новой операции.
Экономика: считать принятую работу, а не запуски модели
Модельный пример: компания обрабатывает 300 заявок в день. Ручная маршрутизация и подготовка черновика занимают в среднем восемь минут — 40 человеко-часов. Если агент подготавливает пакет, а проверка занимает две минуты, остаётся около 10 человеко-часов. Потенциал экономии — 30 часов в день до вычета стоимости моделей, инфраструктуры, поддержки и разбора исключений.
Но повторный запуск, который создал дубль, не является производительностью. Для пилота полезны другие метрики:
- стоимость одной принятой человеком задачи;
- доля задач без ручного исправления;
- число подавленных дубликатов;
- возраст старейшей задачи в очереди;
- доля истёкших аренд;
- среднее и 95-й перцентиль числа попыток;
- время ожидания подтверждения;
- стоимость и время разбора dead-letter.
Цифры выше — расчётный пример, не результат конкретной компании. Реальную экономику определяют на собственном потоке и учитывают цену ошибки: один лишний платёж может съесть экономию от тысячи аккуратно обработанных писем.
Пилот на две недели
В первую неделю возьмите один процесс без автоматического внешнего действия. API создаёт `job`, worker готовит только черновик, человек принимает или исправляет его. Искусственно отключите worker во время выполнения, дважды отправьте одинаковую команду и временно сделайте зависимость недоступной.
Во вторую неделю добавьте один обратимый инструмент и один шаг с подтверждением. Проверьте истечение аренды, backoff, отмену, повтор после неизвестного ответа внешней системы и восстановление из outbox. Отдельно убедитесь, что старое подтверждение не применится к изменённому черновику.
Критерий готовности — не красивый сценарий без ошибок. Система должна доказуемо пережить повтор, падение worker и потерянный ответ без двойного бизнес-эффекта. Тогда агент становится сотрудником процесса, а не очень быстрым генератором одинаковых карточек.
