Устойчивый агент: checkpoint и продолжение
Этот блок объясняет, что остаётся, когда процесс с агентом умирает посреди работы, и что происходит с шагами, которые уже затронули внешний мир.
Возврат на $420 в Northwind требует одобрения менеджера. В прогоне шесть шагов: найти заказ 8890, проверить правила, написать покупателю, что возврат на рассмотрении, ждать одобрения, вернуть деньги, подтвердить. В 18:00 прогон доходит до ожидания. В 02:00 сервер перезапускается из-за выкатки. В 09:00 менеджер одобряет.
Процесса уже нет, а вместе с ним пропали переменные, история сообщений в памяти и знание о том, какие шаги закончены. Без подстраховки прогон потерян или начнётся заново и ещё раз напишет покупателю.
Блокнот
Коллега на телефоне гоняет вас по поручениям, и вы ведёте блокнот. Закончив дело, записываете строку: что сделали и чем кончилось. Вы заболели и ушли домой. Следующий на смене открывает блокнот, видит три законченные строки, не повторяет их и берётся за четвёртую.
Durable execution (устойчивое выполнение) — это такой блокнот и то, что передаёт его следующей смене. Среда выполнения записывает ход работы вне процесса, и новый процесс продолжает по этой записи.
Самая маленькая версия:
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 выполняется снова и сверяется с историей | |
Что значит replay | Обычное восстановление. Activity остаются вне пути replay | Запуск с более раннего |
Правило для вашего кода | Код Workflow должен быть детерминированным. Вызовы API и LLM кладите в Activity, а время и случайные числа берите из безопасных для replay API самого SDK | Checkpoint стоят между super-step, поэтому узел может выполниться снова с первой строки |
Пауза внутри узла
interrupt() в LangGraph ставит прогон на паузу ради человека. При возобновлении узел начинается с начала, поэтому код выше interrupt() выполняется второй раз.
Вынесите эффект из узла с паузой:
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 также позволяет выбрать, когда блокнот записывается:
| Когда пишет | Сбой посреди прогона |
|---|---|---|
| Только при выходе из графа | Промежуточное состояние пропадает |
| Пока идёт следующий шаг | Небольшой риск потерять checkpoint |
| До начала следующего шага | Записан каждый checkpoint, ценой некоторой скорости |
Пауза может длиться днями
Не держите процесс живым ради менеджера. Состояние лежит в базе, а ответ приходит через Command(resume=...) или, в Temporal, через Signal.
Мир тем временем меняется. К 09:00 коллега мог уже вернуть деньги по заказу 8890 вручную. Поэтому после долгого ожидания перечитайте заказ, доступную к возврату сумму и права того, кто одобрил.
Код тоже мог измениться. LangGraph продолжает с сохранённым состоянием и тем графом, который вы скомпилировали сейчас. В Temporal изменение Workflow Definition, от которого зависят идущие выполнения, может дать ошибку недетерминированности, и для этого есть инструменты версионирования.
Durable execution защищает от умершего процесса. Контекстное окно модели в длинном прогоне всё равно заполняется, и это отдельная задача.
Вывод
- Прогон переживает сбой, когда его ход записан вне процесса.
- Сбой между действием и записью повторяет шаг, поэтому каждый шаг с эффектом должен быть безопасен при повторе, обычно через сохранённый ключ идемпотентности.
- Уточните, что значит replay в вашем движке, и держите эффекты подальше от кода, который выполняется заново.
- После долгой паузы перечитайте то, что могло измениться.