К курсу

Цикл агента

Один шаг цикла агентаПрактика: один шаг циклаЦикл в кодеПрактика: цикл в кодеОстановка и повторыПрактика: остановка и повторыЧто решает только кодПрактика: что решает код

Надёжность в проде

Таймаут и ключ идемпотентностиПрактика: таймаут и ключ идемпотентностиСказал «готово», а ничего не произошлоПрактика: ложный успехУстойчивый агент: checkpoint и продолжениеПрактика: устойчивый агент

Устройство агентов

Workflow или агентПрактика: workflow или агентReAct и plan-and-executeПрактика: ReAct и plan-and-executePlan-and-SolveПрактика: Plan-and-SolveReflexionПрактика: ReflexionОдин агент или ройПрактика: один агент или рой

Контекст и стоимость

Почему растёт счёт за агентаПрактика: токены и кэшКонтекст-инженерияПрактика: управление окномПамять агентаПрактика: что агент хранит

Инструменты и протоколы

MCPПрактика: MCPОписания инструментов и инъекцииПрактика: описания инструментов и инъекцииКод вместо набора инструментовПрактика: код вместо набора инструментовНавыки агента и A2AПрактика: навыки агента и A2A

Фреймворки

LangGraph или обычный циклПрактика: LangGraph или обычный циклAgent SDK: что вы покупаетеПрактика: agent SDK

Агенты для кода

Агент для кода по спецификацииПрактика: агент для кода по спецификации
←→ENВойти
Агенты › Надёжность в продеDeep dive

Устойчивый агент: checkpoint и продолжение

Опирается наСказал «готово», а ничего не произошло

Этот блок объясняет, что остаётся, когда процесс с агентом умирает посреди работы, и что происходит с шагами, которые уже затронули внешний мир.

Возврат на $420 в Northwind требует одобрения менеджера. В прогоне шесть шагов: найти заказ 8890, проверить правила, написать покупателю, что возврат на рассмотрении, ждать одобрения, вернуть деньги, подтвердить. В 18:00 прогон доходит до ожидания. В 02:00 сервер перезапускается из-за выкатки. В 09:00 менеджер одобряет.

Процесса уже нет, а вместе с ним пропали переменные, история сообщений в памяти и знание о том, какие шаги закончены. Без подстраховки прогон потерян или начнётся заново и ещё раз напишет покупателю.

Блокнот

Коллега на телефоне гоняет вас по поручениям, и вы ведёте блокнот. Закончив дело, записываете строку: что сделали и чем кончилось. Вы заболели и ушли домой. Следующий на смене открывает блокнот, видит три законченные строки, не повторяет их и берётся за четвёртую.

Durable execution (устойчивое выполнение) — это такой блокнот и то, что передаёт его следующей смене. Среда выполнения записывает ход работы вне процесса, и новый процесс продолжает по этой записи.

Процесс 1 умирает на шаге 4. Процесс 2 берёт шаги с 1 по 3 из журнала и работает дальше с шага 4.
Процесс 1 умирает на шаге 4. Процесс 2 берёт шаги с 1 по 3 из журнала и работает дальше с шага 4.

Самая маленькая версия:

def step(run_id, name, fn):
    saved = journal.find(run_id, name)    # a database table
    if saved is not None:
        return saved.result               # finished before: fn does not run
    result = fn()
    journal.write(run_id, name, result)
    return result

Момент между делом и записью

Посмотрите на две строки после if. Вы отправили письмо и упали раньше, чем записали это в блокнот. Следующая смена строки не видит и отправляет письмо ещё раз.

Действие произошло, а записи нет. Новому воркеру приходится выполнить шаг снова.
Действие произошло, а записи нет. Новому воркеру приходится выполнить шаг снова.

Записать строку сначала тоже не выход. Сбой после такой записи пропустил бы возврат, которого не было. Поэтому обычно запись идёт после работы, и такой порядок даёт гарантию at-least-once: каждый шаг выполняется один раз или больше.

Значит, шаг с внешним эффектом должен быть безопасен при повторе. Для платежа это ключ идемпотентности из блока про таймауты, и сохранить ключ надо до попытки. Если шаг при каждом запуске создаёт новый ключ, второй запуск отправит другой. Temporal тоже рекомендует делать свои Activity идемпотентными, а LangGraph велит строить узлы так, чтобы повторное выполнение было безопасным.

Одно слово, два значения

«Replay» значит разное в двух системах, которые вы можете встретить.

Temporal

LangGraph

Что сохраняется

Event History у Workflow, включая результаты Activity

Checkpoint состояния графа на каждом super-step

После сбоя

Код Workflow выполняется снова и сверяется с историей

invoke(None, config) загружает последний checkpoint и повторяет незаконченную работу

Что значит replay

Обычное восстановление. Activity остаются вне пути replay

Запуск с более раннего checkpoint_id. Узлы до него пропускаются, узлы после него выполняются снова, с вызовами LLM и API

Правило для вашего кода

Код Workflow должен быть детерминированным. Вызовы API и LLM кладите в Activity, а время и случайные числа берите из безопасных для replay API самого SDK

Checkpoint стоят между super-step, поэтому узел может выполниться снова с первой строки

Пауза внутри узла

interrupt() в LangGraph ставит прогон на паузу ради человека. При возобновлении узел начинается с начала, поэтому код выше interrupt() выполняется второй раз.

Письмо и пауза в одном узле, и письмо уходит дважды. В двух узлах между ними стоит checkpoint.
Письмо и пауза в одном узле, и письмо уходит дважды. В двух узлах между ними стоит checkpoint.

Вынесите эффект из узла с паузой:

def notify(state):
    send_email(state["order_id"], template="refund_under_review")
    return {"notified": True}

def approval(state):
    decision = interrupt({
        "order_id": state["order_id"],
        "amount_cents": state["amount_cents"],
    })
    return {"approved": decision}

Ответ приходит позже как graph.invoke(Command(resume=True), config) с тем же thread_id. Обернуть эффект в task LangGraph тоже можно: завершённые task возвращают сохранённые результаты. Само письмо после сбоя всё равно может повториться, как и любой шаг.

Где лежит блокнот

Блокнот, оставленный внутри процесса, умирает вместе с ним. InMemorySaver в LangGraph держит checkpoint в оперативной памяти, и при перезапуске они пропадают. В продакшене нужен постоянный checkpointer, например PostgresSaver.

LangGraph также позволяет выбрать, когда блокнот записывается:

durability

Когда пишет

Сбой посреди прогона

"exit"

Только при выходе из графа

Промежуточное состояние пропадает

"async"

Пока идёт следующий шаг

Небольшой риск потерять checkpoint

"sync"

До начала следующего шага

Записан каждый checkpoint, ценой некоторой скорости

Пауза может длиться днями

Не держите процесс живым ради менеджера. Состояние лежит в базе, а ответ приходит через Command(resume=...) или, в Temporal, через Signal.

Мир тем временем меняется. К 09:00 коллега мог уже вернуть деньги по заказу 8890 вручную. Поэтому после долгого ожидания перечитайте заказ, доступную к возврату сумму и права того, кто одобрил.

Код тоже мог измениться. LangGraph продолжает с сохранённым состоянием и тем графом, который вы скомпилировали сейчас. В Temporal изменение Workflow Definition, от которого зависят идущие выполнения, может дать ошибку недетерминированности, и для этого есть инструменты версионирования.

Durable execution защищает от умершего процесса. Контекстное окно модели в длинном прогоне всё равно заполняется, и это отдельная задача.

Вывод

Глубже

The definitive guide to Durable Execution (Temporal)Что забирает сбой и как прогон продолжается в других процессах.Checkpointers (LangGraph)Как продолжить thread, режимы durability и ловушка хранения в памяти.Interrupts (LangGraph)Что выполняется снова при возобновлении.Workflow Definition (Temporal)Детерминизм, replay и версионирование с примерами того, что ломается.
‹ Назад
Предложить улучшениеНашёл ошибку