Надёжная автоматизация процесса: очереди, статусы, повторы и человек
Клиент сообщает: «Деньги за подписку списались, но доступ не открылся».
Система проверяет платёж, получает статус confirmed, проверяет подписку и получает inactive. Языковая модель правильно замечает расхождение и предлагает передать случай специалисту. На демонстрации всё выглядит готовым.
Затем происходит обычный сбой: работник перезапускается между проверкой подписки и сохранением результата. Или запрос на активацию подписки достигает сервиса, но ответ теряется. Или сотрудник одобряет действие через четыре часа, когда состояние клиента уже изменилось.
Хороший ответ модели не устраняет эти ситуации. Надёжность процесса определяется тем, может ли система после прерывания установить, что уже произошло, и безопасно продолжить работу. Для этого нужны сохраняемое состояние, очередь задач, правила повторов, контроль действий с побочным эффектом и понятная роль человека.
Разберём архитектуру на том же обращении о платеже и подписке, которое проходит через учебный цикл.
Что именно должно быть надёжным
Прежде чем выбирать очередь или платформу для процессов, полезно сформулировать требования к результату.
Для нашего обращения нужны как минимум четыре свойства:
- Обращение не теряется. После принятия заявки в системе остаётся запись, по которой можно увидеть её текущий этап.
- Работа продолжается после сбоя. Если исполнитель остановился, другой исполнитель сможет подобрать задачу.
- Действие не выполняется повторно из-за повтора задачи. Два получения одного сообщения не должны создавать два возврата или две независимые активации подписки.
- Система не заявляет о результате раньше подтверждения. «Активация запрошена» и «подписка активна» означают разные состояния.
Первые два свойства относятся к продвижению процесса. Последние два защищают его корректность. Можно построить систему, которая всегда быстро повторяет сбойную задачу, но при этом дважды списывает деньги. Можно построить систему, которая никогда не делает лишних действий, но оставляет заявки навсегда в ожидании. Надёжность требует обеих сторон.
Справка. Побочный эффект - изменение вне текущего вычисления: запись в базе, отправка письма, создание возврата, активация подписки. Повторить вычисление текста обычно можно. Повторить побочный эффект без проверки часто нельзя.
Для дальнейшего разбора примем конкретное правило: при подтверждённом платеже и неактивной подписке система готовит предложение о восстановлении доступа. Сотрудник проверяет его и подтверждает действие. Возврат средств в этом сценарии автоматически не создаётся.
Итог раздела:
- Успешный ответ LLM - только часть результата процесса.
- Надёжный процесс должен сохранять продвижение, переживать повторы и подтверждать фактические изменения.
Разделяем событие, задачу и состояние
Эти три понятия легко смешать, хотя у них разные роли.
Событие сообщает, что что-то произошло: ticket_created, approval_granted, subscription_activated. Оно описывает факт и имеет идентификатор.
Задача просит выполнить работу: проверить платёж, подготовить решение, отправить уведомление. Задача может быть выдана исполнителю несколько раз, если предыдущая попытка не завершилась подтверждённо.
Состояние процесса отвечает на вопрос, где сейчас находится конкретное обращение. Например, CHECKING, WAITING_APPROVAL или COMPLETED. Это не текст последнего сообщения в очереди, а сохранённая запись, которую приложение может прочитать независимо от исполнителя.
Для обращения T-4821 можно использовать такую схему:
| Состояние | Что уже известно | Что должно произойти дальше |
|---|---|---|
RECEIVED |
Обращение принято и сохранено | Поставить проверку в очередь |
CHECKING |
Проверки запущены | Получить факты из платёжной системы и сервиса подписок |
WAITING_APPROVAL |
Расхождение подтверждено, действие предложено | Дождаться решения сотрудника |
EXECUTING |
Конкретное действие разрешено | Выполнить его и проверить результат |
COMPLETED |
Результат подтверждён, клиент уведомлён | Работа закончена |
NEEDS_ATTENTION |
Автоматически продолжить нельзя | Передать дело оператору |
Технический тайм-аут при проверке платежа не обязательно требует отдельного главного состояния PAYMENT_TIMEOUT. Процесс может оставаться в CHECKING, а у задачи проверки будут номер попытки, время следующего запуска и последняя ошибка. Так бизнес-этап не смешивается с состоянием отдельного исполнителя.
Состояние нужно менять только по разрешённым переходам. Например, переход WAITING_APPROVAL -> EXECUTING допустим после подтверждения конкретного действия. Переход RECEIVED -> COMPLETED без проверок должен вызывать ошибку.
В базе это можно защитить условным обновлением:
UPDATE process_cases
SET state = 'EXECUTING',
version = version + 1
WHERE id = :case_id
AND state = 'WAITING_APPROVAL'
AND version = :expected_version;
Если обновлена одна строка, переход состоялся. Если ни одной, состояние уже изменилось или другой исполнитель опередил текущего. Поле version позволяет обнаруживать конкурирующие изменения; такой приём называют оптимистической блокировкой. После неудачного обновления нельзя просто повторить команду с новой версией: сначала нужно заново прочитать дело и решить, актуально ли действие.
Итог раздела:
- Очередь хранит работу, а база хранит проверяемое состояние процесса.
- Переходы состояний должны проверяться приложением, включая ситуации, когда два исполнителя работают одновременно.
Как принять обращение и гарантировать постановку задачи
Рассмотрим начало процесса. API принимает обращение, записывает его в базу и отправляет сообщение в очередь.
Если сначала записать обращение, а затем отправить сообщение, приложение может упасть между этими действиями. В базе останется RECEIVED, но никто не начнёт проверку. Если поменять порядок, в очередь может попасть задача для обращения, которое не удалось сохранить.
Это проблема двух изменений в разных системах. Обычная транзакция базы данных не охватывает одновременно запись в базе и произвольную отправку в брокер сообщений.
Один из способов решить её - транзакционный outbox. В одной транзакции приложение сохраняет обращение и запись «нужно отправить событие». Отдельный отправитель читает таблицу outbox и публикует сообщения в очередь:
Транзакция базы:
сохранить обращение T-4821
сохранить outbox-событие CheckCaseRequested(T-4821)
зафиксировать транзакцию
Отдельный отправитель:
прочитать неопубликованное outbox-событие
отправить его в очередь
отметить публикацию
Теперь обращение и намерение начать его обработку появляются вместе или не появляются вовсе. Но отправитель может опубликовать сообщение и остановиться до отметки об отправке. После перезапуска он опубликует его ещё раз. Поэтому outbox обеспечивает надёжную передачу при условии, что получатель умеет работать с дубликатами. Этот подход описан, в частности, в документации Debezium по outbox.
На входе тоже нужен устойчивый идентификатор. Если система поддержки повторно доставит событие о создании обращения, уникальный external_ticket_id не позволит создать два независимых процесса для одного тикета.
Справка. Outbox решает разрыв между записью в своей базе и публикацией события. Он не делает все последующие внешние действия автоматически однократными.
Итог раздела:
- Запись процесса и намерение поставить его в очередь нужно связывать одной транзакцией.
- Даже при outbox повторная доставка остаётся нормальным случаем, который надо обрабатывать.
Что очередь гарантирует, а чего не гарантирует
Очередь отделяет приём обращения от его обработки. API может быстро сохранить заявку, а работник проверит платёж позже. Несколько работников позволяют обрабатывать много обращений параллельно.
Обычно работник получает сообщение, на время обработки оно скрывается от других работников, затем работник подтверждает успешное завершение. Если он остановился до подтверждения, сообщение снова становится доступным. У стандартных очередей Amazon SQS, например, доставка выполняется как минимум один раз; дубликаты возможны, а строгий порядок доставки не гарантирован. Документация SQS прямо предлагает проектировать получателей с учётом повторов.
Отсюда важный сценарий:
- Работник получил задачу проверки
T-4821. - Проверил платёж и сохранил результат в базе.
- Упал до подтверждения сообщения очереди.
- Очередь выдала ту же задачу другому работнику.
Второй работник не должен считать сообщение новым обращением. Он читает сохранённое состояние и либо продолжает с нужного этапа, либо завершает повторную задачу без изменения результата. Для этого сообщение содержит идентификатор процесса и этапа, а не только текст обращения.
Время невидимости сообщения должно учитывать длительность обработки. Если оно истечёт раньше, чем первый работник закончит задачу, второй может начать её параллельно. Но и длинное время невидимости не даёт абсолютной гарантии отсутствия повторов. Эти особенности описаны в руководстве SQS по visibility timeout. Поэтому защита состояния в базе всё равно нужна.
Подтверждать сообщение следует после сохранения результата обработки. Если подтвердить его до записи и затем упасть, задача исчезнет из очереди, а результат не появится в базе.
Итог раздела:
- Повторное получение задачи является штатным сценарием.
- Подтверждение сообщения, проверка текущего состояния и защита от конкурирующих переходов решают разные задачи.
Где в процессе находится модель
В предыдущей главе модель выбирала инструменты и предлагала следующие шаги. В надёжном процессе её работа становится одним из этапов, который имеет вход, выход и критерий проверки.
Для T-4821 модель получает ограниченный набор фактов:
{
"ticket_id": "T-4821",
"customer_claim": "Деньги списались, доступ не открылся",
"payment": {
"status": "confirmed",
"observed_at": "2026-10-02T10:15:00Z"
},
"subscription": {
"status": "inactive",
"observed_at": "2026-10-02T10:15:02Z"
}
}
Она может вернуть структурированное предложение:
{
"decision": "propose_restore_access",
"reason": "Платёж подтверждён, подписка неактивна",
"evidence": ["payment.status", "subscription.status"],
"requires_approval": true
}
Это предложение, а не новый статус подписки. Приложение проверяет формат, допустимое значение decision, наличие подтверждающих фактов и бизнес-правило. Если данных недостаточно, модель должна вернуть «нужна дополнительная проверка», а не достраивать недостающие факты.
Для повторной обработки полезно сохранить версию инструкции модели, версию схемы ответа и идентификаторы использованных результатов. Иначе после обновления подсказки или модели старое обращение может неожиданно получить другое решение без видимой причины.
Повтор вызова LLM тоже требует осмысленного правила. Тайм-аут или временная недоступность сервиса могут оправдывать новую попытку. Неподходящий ответ модели может оправдывать ограниченную попытку исправления. Но бесконечно отправлять тот же запрос при систематической ошибке схемы бессмысленно; такое дело нужно передать человеку или исправить приложение.
Итог раздела:
- Результат модели следует сохранять как версионированное предложение с опорой на конкретные факты.
- Бизнес-правила и допустимые переходы проверяются кодом после ответа модели.
Как устроить повторные попытки
Повтор нужен только там, где ошибка может исчезнуть без изменения данных или программы. К таким случаям относятся кратковременный сетевой сбой, ограничение частоты запросов или временная недоступность сервиса.
Если платёжный API ответил «обращение относится к другому аккаунту», повтор через секунду не исправит проблему. Если схема ответа модели противоречит контракту после нескольких попыток, также нужен разбор причины, а не бесконечный цикл.
У задачи полезно хранить:
attempt- номер попытки;last_error- последнюю диагностированную ошибку;next_run_at- время следующего запуска;deadline_at- момент, после которого автоматическое ожидание прекращается.
Чтобы не обрушивать восстановившийся сервис одновременными повторами, применяют увеличивающуюся задержку. Упрощённая формула:
Задержка = min(предел, базовая задержка × 2^(номер попытки − 1)).
min выбирает меньшее из двух значений. Если базовая задержка равна 5 секундам, а предел 60 секундам, последовательность начинается с 5, 10, 20 и 40 секунд; затем задержка достигает предела. На практике добавляют небольшой случайный разброс, чтобы множество задач не просыпалось одновременно.
У повтора есть предел числа попыток и общий срок ожидания. После него задача попадает в отдельный разбор. В инфраструктуре для этого может использоваться очередь неуспешных сообщений (dead-letter queue): туда перемещаются сообщения, которые не удалось обработать после заданного числа получений. Такой механизм описан в документации SQS. При этом нахождение сообщения в dead-letter queue не должно быть единственным признаком проблемы: обращение в базе тоже переводят в NEEDS_ATTENTION, чтобы сотрудник видел его.
Самая важная развилка возникает после тайм-аута операции записи. Тайм-аут не сообщает, выполнилась операция или нет. Повторять её можно только по правилам конкретного внешнего API либо после сверки фактического состояния.
Итог раздела:
- Повторяют временные ошибки, а не любые ошибки подряд.
- У каждой задачи должны быть предел попыток, срок обработки и путь к человеку.
- При неизвестном результате записи сначала выясняют состояние операции.
Идемпотентность: как пережить повтор без повторного действия
Идемпотентная операция при повторе с тем же смыслом не создаёт нового эффекта. Проверка статуса обычно близка к этому свойству: два чтения не создают две подписки. Активация, возврат или отправка сообщения требуют специальной защиты.
Предположим, сотрудник одобрил восстановление доступа. Приложение создаёт локальную запись действия:
action_id: A-901
case_id: T-4821
type: activate_subscription
target_subscription_id: S-77
status: PENDING
idempotency_key: restore-access:A-901
Ключ restore-access:A-901 должен оставаться тем же при повторных попытках того же действия. Если при каждом повторе создавать новый ключ, защита от дубликатов исчезнет.
Далее исполнитель:
- Проверяет, что действие одобрено и ещё актуально.
- Помечает локальную попытку исполнения.
- Вызывает сервис подписок с тем же ключом идемпотентности, если сервис его поддерживает.
- Сохраняет ответ и проверяет фактическое состояние подписки.
- Только после подтверждения переводит действие в
CONFIRMED.
Если вызов завершился тайм-аутом, локальный статус становится UNKNOWN, а не FAILED. Затем приложение запрашивает состояние операции по ключу или проверяет подписку в исходной системе. Лишь после этого оно решает, повторять ли вызов. Для API, которые поддерживают идемпотентные запросы, это основной механизм безопасных повторов; пример такого контракта есть в документации Stripe.
Идемпотентный ключ внешнего сервиса не отменяет собственной защиты приложения. Нужно также не создавать два локальных действия из двух одинаковых событий подтверждения. Для этого approval_id или action_id делают уникальными, а переход к исполнению защищают условным обновлением состояния.
Если внешний сервис не поддерживает идемпотентность и не позволяет проверить результат, автоматический повтор действия с высокой ценой ошибки может быть недопустим. Тогда состояние UNKNOWN требует ручной сверки.
Итог раздела:
- Повторное сообщение и повторный внешний запрос защищаются на разных уровнях.
- Неизвестный результат нельзя автоматически приравнивать к неудаче.
- Гарантия «ровно один эффект» требует контроля всего пути до внешней системы, а не только настройки очереди.
Как встроить решение человека
В нашем примере одобрение восстановления доступа может занять минуты или часы. Работник очереди не должен всё это время удерживать задачу и ждать открытого соединения с интерфейсом сотрудника.
После подготовки предложения приложение сохраняет:
- точное действие и объект изменения;
- факты, на которых основано предложение;
- версию дела;
- время подготовки и срок актуальности;
- того, кто должен принять решение.
Дело переходит в WAITING_APPROVAL. Работник завершает свою задачу. Когда сотрудник нажимает «Подтвердить», приложение записывает решение как новое событие и ставит следующую задачу в очередь. Такой процесс «остановить, сохранить, продолжить после решения» поддерживается и в специализированных системах выполнения агентов; см. механизм human-in-the-loop.
Одобрение должно относиться к конкретной версии предложения. Если сотрудник подтверждал активацию подписки S-77, нельзя незаметно использовать то же одобрение для другого аккаунта, суммы или действия. Для этого можно сохранить неизменяемый снимок параметров либо их хэш и сверять его при исполнении.
Перед внешним вызовом приложение снова проверяет критические факты. За время ожидания клиент мог получить доступ через другой канал, платёж мог быть возвращён, а подписка могла измениться. Если условия больше не выполняются, одобрение не превращается в автоматическое действие. Дело возвращается на пересмотр.
Точно так же отказ сотрудника должен быть полноценным переходом состояния. Он может означать запрос дополнительных данных, ручную обработку или завершение без изменения подписки. Система не должна оставлять дело бесконечно в WAITING_APPROVAL, если срок рассмотрения истёк.
Итог раздела:
- Ожидание человека хранится как состояние, а не как работающий процесс в памяти.
- Подтверждение привязано к конкретному действию и проверяется ещё раз непосредственно перед исполнением.
Полный путь обращения при сбоях
Посмотрим, как все части взаимодействуют на одном деле.
| Момент | Событие | Состояние и реакция системы |
|---|---|---|
| 10:00 | Пришло обращение T-4821 |
В одной транзакции созданы дело RECEIVED и запись outbox |
| 10:01 | Сообщение доставлено работнику | Дело переходит в CHECKING; работник проверяет платёж и подписку |
| 10:02 | Платёж confirmed, подписка inactive |
Факты сохранены с источником и временем получения |
| 10:03 | Модель предложила восстановить доступ | Правило проверено; дело перешло в WAITING_APPROVAL |
| 14:10 | Сотрудник одобрил конкретное действие | Решение сохранено; новая задача поставлена в очередь |
| 14:11 | Исполнитель повторно проверил подписку | Она всё ещё неактивна; действие перешло в EXECUTING |
| 14:12 | Запрос на активацию завершился тайм-аутом | Результат помечен UNKNOWN, повтор вслепую не выполняется |
| 14:13 | Проверка по сервису подписок показала active |
Действие помечено CONFIRMED; готовится уведомление |
| 14:14 | Отправка уведомления временно не удалась | Активация остаётся подтверждённой; повторяется только отправка |
| 14:16 | Уведомление доставлено | Дело переходит в COMPLETED |
На шаге 14:14 видно важное свойство процесса: неудачная отправка сообщения не должна отменять успешную активацию подписки. Эти действия относятся к разным системам. У них нет общей транзакции, которая могла бы автоматически вернуть всё назад. Если в другом сценарии требуется обратное действие, его проектируют отдельно как компенсацию: например, явно отменяют резервирование после неудачи следующего шага. Компенсация сама тоже может завершиться ошибкой и требует учёта.
Такие длительные последовательности можно реализовать через собственные таблицы состояний и очередь либо через движок процессов. Например, Temporal сохраняет историю выполнения и позволяет продолжать процесс после сбоев. Выбор платформы меняет объём инфраструктурного кода, но не снимает обязанности определить бизнес-состояния, права, идемпотентность внешних действий и критерии завершения.
Итог раздела:
- После сбоя система продолжает с подтверждённого состояния, а не начинает дело заново.
- Каждый внешний эффект учитывается отдельно; частичный успех является нормальным состоянием длительного процесса.
Что наблюдать в эксплуатации
Надёжность нельзя оценить только по числу обращений со статусом COMPLETED. Система может закрывать простые случаи и незаметно копить сложные в ожидании.
Минимальный набор показателей:
- число дел в каждом состоянии и возраст самого старого дела;
- время ожидания в очереди, обработки и одобрения человеком по отдельности;
- доля задач, требующих повтора, и причины повторов;
- число дел в
NEEDS_ATTENTIONи сообщений в dead-letter queue; - число внешних действий со статусом
UNKNOWN; - случаи отклонения устаревшего одобрения;
- расхождения между локальным статусом
CONFIRMEDи фактическим состоянием внешнего сервиса; - доля решений модели, отклонённых проверкой правил или сотрудником.
Полное время решения обращения можно разложить так:
Tрешения = Tочередь + Tавтоматическая работа + Tожидание человека + Tвнешнее исполнение + Tуведомление.
Каждое T - время соответствующего этапа. Если автоматическая работа занимает 20 секунд, а подтверждение сотрудника четыре часа, ускорение модели на две секунды почти не изменит время для клиента. Разложение помогает улучшать настоящий узкий участок процесса.
Для диагностики нужно связывать записи общими идентификаторами: case_id, task_id, action_id и идентификатором вызова внешнего API. Журнал должен показывать, какой переход был запрошен, какой состоялся и на каких фактах основан. При этом тексты обращений, персональные и платёжные данные не стоит без необходимости размножать по логам и сообщениям очереди.
Проверять архитектуру полезно не только успешным сценарием, но и принудительными сбоями: остановить работник после внешнего вызова, повторно доставить событие, задержать одобрение, сделать платёжный API недоступным, изменить подписку между проверкой и исполнением. Именно такие испытания показывают, можно ли восстановить ход дела.
Итог раздела:
- Наблюдать нужно движение дела, ошибки переходов и фактические внешние эффекты.
- Проверка надёжности требует сценариев с прерыванием и повторами.
Когда достаточно простой схемы, а когда нужен движок процессов
Для небольшого процесса с несколькими короткими этапами часто достаточно базы данных, outbox, очереди и работников. Такой вариант даёт полный контроль, но команда самостоятельно реализует состояния, повторы, таймеры и восстановление.
Если процесс живёт днями, ждёт человека, содержит множество веток и обращается к нескольким сервисам, движок длительных процессов может уменьшить объём служебного кода. Его всё равно придётся связать с бизнес-правилами и внешними API. Нельзя считать, что наличие движка само по себе делает активацию подписки или возврат средств безопасными при повторе.
Есть и более простой вариант: если все проверки укладываются в один короткий запрос, внешних изменений нет и результат не обязан переживать длительное ожидание, отдельная очередь может оказаться лишней. Архитектуру следует усложнять по требованиям к восстановлению и цене ошибки.
Общая граница остаётся неизменной: модель предлагает решение, процесс фиксирует этапы, приложение проверяет переходы, а внешняя система подтверждает выполненное действие. Когда эти роли разделены, обращение можно продолжить после перезапуска, безопасно разобрать при неясном результате и объяснить человеку, что именно произошло.