Почему «история чата» не является состоянием

В 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.

КлассПримерМожно продолжить автоматически
Activeplanning, executingда, в пределах lease
Waitingapproval, input, retry_waitтолько по событию
Uncertaineffect_unknownсначала reconciliation
Terminalsucceeded, 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 применяет новое событие.

  1. Зарезервировать переход и outbox event.
  2. Доставить activity worker.
  3. Выполнить действие с timeout и ключом.
  4. Сохранить receipt.
  5. Применить 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

  1. Нарисуйте текущий workflow и все внешние эффекты.
  2. Определите terminal и uncertain states.
  3. Запишите таблицу переходов и guards.
  4. Вынесите state из transcript в хранилище.
  5. Добавьте optimistic locking и outbox.
  6. Сделайте activities идемпотентными.
  7. Запустите shadow reducer на реальных traces.
  8. Переведите один низкорисковый сценарий.
  9. Инъецируйте падения и проверьте recovery.
  10. Расширяйте только после метрик и разбора конфликтов.

Главный принцип

LLM хорошо работает с неоднозначностью: понимает цель, извлекает факты и предлагает план. State machine хорошо защищает инварианты: что уже сделано, кто разрешил действие и какой переход допустим. Production-агент соединяет оба свойства, не пытаясь превратить prompt в базу данных и систему транзакций.

Нужна ли state machine для простого чат-бота?
Для чата без внешних действий достаточно состояния сессии. Автомат становится особенно полезен, когда есть долгие задачи, approvals, retries, несколько workers или необратимые tool calls.
Может ли модель сама выбрать новое состояние?
Она может предложить типизированное событие. Разрешённый переход и критические поля применяет runtime после schema, policy и guard checks.
Где хранить состояние?
В транзакционной базе или durable workflow engine. Transcript и векторная память могут дополнять state, но не заменяют его как источник истины.
Чем retry отличается от повторного запуска workflow?
Retry повторяет конкретную безопасную activity с тем же business key. Полный перезапуск заново принимает решения и без checkpoints способен продублировать уже выполненные эффекты.
Что делать после timeout внешнего действия?
Перейти в состояние неизвестного исхода и выполнить reconciliation по idempotency key или external reference. Не повторять необратимую команду вслепую.
Как обновлять автомат без остановки старых задач?
Версионировать definition и reducers, прогонять сохранённые histories, закреплять старые executions за совместимым кодом либо выполнять явную миграцию состояния.
← Все статьи блога