Почему «история чата» не является состоянием
В transcript есть слова, но нет строгой гарантии, что письмо уже отправлено, approval ещё действует, а лимит не исчерпан. После обрезки контекста или рестарта модель может заново предложить выполненный шаг. Авторитетное workflow state хранится в базе или durable orchestrator, а модели передаётся только его безопасная проекция.
State machine нужна не для ограничения рассуждения. Она отделяет гибкое планирование от жёстких бизнес-инвариантов: модель предлагает событие, runtime проверяет схему, права и допустимость перехода.
Состояние, событие, переход и guard
Состояние описывает, где находится процесс. Событие сообщает факт или команду. Переход связывает исходное и новое состояние. Guard проверяет условия: действующий approval, остаток бюджета, версия объекта, права и дедлайн.
from: awaiting_approval
event: approval_granted
guards:
- approval.scope == pending_action.scope
- approval.expires_at > now
- resource.version == expected_version
to: executing_action
Минимальный набор состояний
Не ограничивайтесь pending / done / failed. Для production обычно нужны accepted, planning, waiting_input, awaiting_approval, queued, executing, retry_wait, compensating и terminal states.
| Класс | Пример | Можно продолжить автоматически |
|---|---|---|
| Active | planning, executing | да, в пределах lease |
| Waiting | approval, input, retry_wait | только по событию |
| Uncertain | effect_unknown | сначала reconciliation |
| Terminal | succeeded, cancelled, expired | нет |
Контракт workflow state
Запись включает workflow_id, tenant и actor из доверенной identity, тип и версию workflow, status, state_version, общий deadline, budget, pending action, completed steps, approvals и ссылки на артефакты. Большие документы хранятся отдельно; state содержит immutable reference, hash и классификацию.
Каждое поле имеет тип и владельца. Модель не может самостоятельно изменить tenant, увеличить budget или записать approved=true.
Модель предлагает событие, runtime применяет его
Ответ модели приводится к JSON Schema: proposed_event, аргументы, evidence и uncertainty. Runtime отклоняет неизвестное событие, проверяет guard и только затем атомарно записывает переход. Объяснение модели полезно для ревьюера, но не является доказательством разрешения.
Детерминированные правила — обязательная эскалация, лимит платежа, запрет чужого tenant — остаются в коде или policy engine. Модель работает внутри разрешённого участка.
Side effects выполняются отдельно
Вызов LLM, базы, браузера и внешнего API не должен происходить внутри транзакции перехода. State machine создаёт команду activity с idempotency key. Activity выполняет эффект, возвращает typed result, а reducer применяет новое событие.
- Зарезервировать переход и outbox event.
- Доставить activity worker.
- Выполнить действие с timeout и ключом.
- Сохранить receipt.
- Применить success, retryable failure или unknown outcome.
Optimistic locking против двух workers
Два события могут прийти одновременно: пользователь отменяет задачу, пока worker завершает шаг. Обновляйте запись условием WHERE state_version = expected. Победитель увеличивает версию, проигравший перечитывает состояние и заново проверяет переход.
Lock на весь длинный LLM-вызов создавать нельзя. Используйте короткий lease выполнения и version check при фиксации результата.
Timeout — это событие
Время ожидания approval, исполнения шага и всего workflow различается. Timer после срабатывания создаёт событие approval_expired или step_timed_out; дальше автомат выбирает безопасный переход. Таймер должен переживать рестарт, поэтому локальный sleep() worker недостаточен.
Поздний ответ после timeout не применяется вслепую: runtime проверяет текущую версию и решает, можно ли использовать артефакт.
Retries и состояние неизвестного исхода
Временная ошибка переводит workflow в retry_wait с номером попытки, next_attempt_at и причиной. Но timeout внешнего необратимого действия означает effect_unknown, а не обычный retry. Сначала запросите состояние у провайдера по idempotency key или external ID.
Переход из unknown должен быть явным: подтверждён успех, подтверждён отказ или ручная сверка. Так система не маскирует двойной платёж под надёжный повтор.
Approval как одноразовое ограниченное событие
Approval привязывается к hash точного действия, resource version, actor, scope и сроку. Если модель поменяла получателя, сумму или текст публикации, старое подтверждение недействительно. После успешного перехода approval помечается использованным.
Ожидание человека не держит процесс в памяти. Checkpoint позволяет возобновить workflow на другом worker и показать ревьюеру те же evidence и preview.
Отмена и graceful stop
Событие cancel запрещает запуск новых side effects, но не гарантирует мгновенную остановку внешнего API. State machine переходит в cancelling, отзывает capabilities, ждёт завершения активной activity ограниченное время и выполняет нужную компенсацию. Итог различает «отменено без эффектов» и «частично выполнено».
Saga и компенсации
Многошаговый процесс между сервисами не откатывается одной SQL-транзакцией. Для компенсируемых шагов задайте обратное бизнес-действие: снять бронь, отозвать публикацию, создать возврат. После pivot — точки невозврата — оставшиеся шаги должны быть идемпотентно доведены до согласованного состояния.
Компенсация тоже может сломаться, поэтому имеет собственные retries, deadline, аудит и terminal state compensation_failed.
История событий и snapshot
Event log позволяет объяснить, как workflow пришёл к состоянию, а snapshot ускоряет загрузку. Snapshot всегда восстанавливается из проверенной последовательности событий и содержит версию reducer. Не редактируйте прошлое событие ради исправления: добавьте корректирующее событие с причиной и actor.
Версионирование живых workflow
Длинный процесс может пережить несколько релизов. Перестановка шагов или переименование состояния способно сломать replay. Используйте версию definition, совместимые reducers и миграции, протестированные на сохранённых histories. Старый execution должен быть закреплён за совместимым worker или пройти явный upgrade transition.
Перед деплоем проигрывайте production histories на новой версии и проверяйте одинаковую последовательность команд.
Наблюдаемость
Для каждого перехода пишите structured event: from, event, to, workflow version, state version, guard decision, actor, trace ID и latency в предыдущем состоянии. Метрики: число зависших executions по state, возраст старейшего waiting, retry rate, invalid transitions, optimistic conflicts, compensation failures и terminal outcomes.
Алерт «много running» слабее сигнала «p95 времени в executing превысил SLO» или «effect_unknown не сверяется 15 минут».
Тестирование автомата
Проверяйте не только happy path. Генерируйте допустимые последовательности событий и доказывайте инварианты: terminal state не выходит наружу, side effect имеет один business key, approval нельзя использовать дважды, budget никогда не растёт по ответу модели.
- worker падает после эффекта до commit;
- два результата приходят параллельно;
- approval истекает в момент исполнения;
- cancel пересекается с success;
- старое событие приходит после миграции;
- компенсация сама падает.
Пошаговый rollout
- Нарисуйте текущий workflow и все внешние эффекты.
- Определите terminal и uncertain states.
- Запишите таблицу переходов и guards.
- Вынесите state из transcript в хранилище.
- Добавьте optimistic locking и outbox.
- Сделайте activities идемпотентными.
- Запустите shadow reducer на реальных traces.
- Переведите один низкорисковый сценарий.
- Инъецируйте падения и проверьте recovery.
- Расширяйте только после метрик и разбора конфликтов.
Главный принцип
LLM хорошо работает с неоднозначностью: понимает цель, извлекает факты и предлагает план. State machine хорошо защищает инварианты: что уже сделано, кто разрешил действие и какой переход допустим. Production-агент соединяет оба свойства, не пытаясь превратить prompt в базу данных и систему транзакций.