Когда важен результат, а не план

Представь агента, который пишет ответы клиентам. Узел draft генерирует текст письма, узел send его отправляет. Останавливаться перед draft бессмысленно — там ещё нет текста, проверять нечего. Нам нужно увидеть готовый черновик — то есть остановиться после draft, но до send.

Это типичный «контроль качества»: узел что-то произвёл (черновик, результат поиска, план), и человек должен подтвердить, что результат годится, прежде чем агент пойдёт дальше. Здесь работает interrupt_after — пауза сразу за узлом.

ℹ️ Та же база, что и у interrupt_before

interrupt_after — близнец interrupt_before: тоже статическое прерывание, тоже задаётся в compile(), тоже требует checkpointer + thread_id и возобновляется через invoke(None, config). Отличие одно — сторона узла, на которой граф встаёт.

interrupt_after при компиляции

Задаётся симметрично — списком узлов, после которых граф обязан вставать на паузу. Разница с interrupt_before в одном слове в имени аргумента:

Пауза ПОСЛЕ узла — результат уже в состоянии
python
from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(
    checkpointer=MemorySaver(),
    interrupt_after=["draft"],      # ← пауза ПОСЛЕ узла draft
)

Теперь, когда узел draft отработал и записал результат в состояние, граф останавливается, не переходя к следующему узлу. Черновик уже лежит в состоянии — его можно прочитать, оценить и при необходимости поправить, прежде чем разрешить графу двигаться к send.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
START draft ✓ создал черновик interrupt_after send отправит письмо END Человек ревьюит результат state["draft"] уже готов ✓ invoke(None) ✏️ update_state ⏸ пауза + checkpoint resume → draft уже выполнен — на ревью готовый результат, send ещё не запускался

«До» против «после»: в чём разница

Оба прерывания статические и работают одинаково механически. Разница — что именно ревьюит человек и выполнился ли узел.

interrupt_before — ревью плана
  • пауза до узла
  • узел ещё не выполнен
  • смотрим что собирается сделать агент
  • для опасных узлов (действие необратимо)
interrupt_after — ревью результата
  • пауза после узла
  • узел уже выполнен
  • смотрим что узел произвёл
  • для генерирующих узлов (есть что проверить)
⚠️ Не ставь interrupt_after на необратимый узел

Логика простая: если узел делает что-то необратимое (отправка, платёж, удаление) — нужен interrupt_before, чтобы поймать его до выполнения. interrupt_after ставят на узел, который что-то создаёт для проверки. Поставишь interrupt_after на «отправку» — письмо уже улетит к моменту ревью, проверять будет поздно.

Цикл: выполнение → ревью → продолжение

Шаги те же, что у interrupt_before, но в состоянии уже лежит результат узла. Человек его читает, при желании правит через update_state и возобновляет граф через invoke(None, config).

Ревью и правка произведённого результата
python
config = {"configurable": {"thread_id": "review-1"}}

# 1. Запуск — draft отрабатывает, граф встаёт ПОСЛЕ него
graph.invoke({"topic": "извинение за задержку"}, config)

# 2. Результат уже в состоянии — читаем и оцениваем
snap = graph.get_state(config)
print(snap.next)               # ('send',) — дальше пойдёт send
print(snap.values["draft"])    # готовый текст письма для ревью

# 3a. ОДОБРИТЬ как есть — продолжаем
graph.invoke(None, config)     # send выполнится с текущим черновиком

# 3b. ПОПРАВИТЬ результат, затем продолжить
graph.update_state(config, {"draft": "Отредактированный человеком текст"})
graph.invoke(None, config)     # send отправит уже исправленный вариант

Поскольку правка делается до следующего узла, send увидит уже отредактированный draft. Это и есть «ревью результата перед продолжением»: узел произвёл черновик, человек довёл его до ума, и только потом он идёт дальше по графу.

Отклонить = вернуть на доработку

Если результат плох, можно не просто остановиться, а отправить граф на повторную генерацию. Через update_state дописывают замечание (например, ставят флаг needs_revision или добавляют комментарий в состояние), а условное ребро после draft по этому флагу возвращает поток обратно в draft. Получается петля «сгенерировал → ревью → доработал».

Полный пример: черновик письма с ревью

Агент пишет письмо, человек ревьюит перед отправкой
python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

class State(TypedDict):
    topic: str
    draft: str
    sent: bool

def draft(state: State) -> dict:
    resp = llm.invoke(f"Напиши короткое деловое письмо на тему: {state['topic']}")
    return {"draft": resp.content}

def send(state: State) -> dict:
    print("ОТПРАВЛЕНО:", state["draft"])    # ⚠️ необратимо
    return {"sent": True}

builder = StateGraph(State)
builder.add_node("draft", draft)
builder.add_node("send", send)
builder.add_edge(START, "draft")
builder.add_edge("draft", "send")
builder.add_edge("send", END)

# ⬇️ пауза ПОСЛЕ draft — ревьюим черновик до отправки
graph = builder.compile(checkpointer=MemorySaver(), interrupt_after=["draft"])

config = {"configurable": {"thread_id": "mail-1"}}
graph.invoke({"topic": "извинение за задержку доставки"}, config)

# Черновик готов, но письмо ещё НЕ отправлено
print(graph.get_state(config).next)          # ('send',)
print(graph.get_state(config).values["draft"])

# Человек слегка правит и одобряет
graph.update_state(config, {"draft": "Уважаемый клиент, приносим извинения..."})
graph.invoke(None, config)                   # теперь send отправит исправленный текст

Комбинируем before и after

Оба аргумента можно задать одновременно — на разных узлах. Типичный «двойной контроль»: ревью сгенерированного результата и подтверждение перед необратимым действием.

Ревью результата + подтверждение действия в одном графе
python
graph = builder.compile(
    checkpointer=MemorySaver(),
    interrupt_after=["draft"],     # проверить, ЧТО сгенерировано
    interrupt_before=["send"],     # подтвердить ПЕРЕД отправкой
)
# Граф остановится дважды: после draft (ревью) и перед send (подтверждение)
ℹ️ Возобновление одинаковое

Сколько бы точек ни было, каждое возобновление — это invoke(None, config): оно продвигает граф до следующей паузы или до END. Приложение в цикле проверяет get_state().next: пока не пусто — показывает человеку очередной запрос и снова возобновляет.

Типичные ошибки

Ошибка 1: interrupt_after на необратимом узле

Поставишь паузу после узла-отправителя — действие уже произошло к моменту ревью. Для необратимого нужен interrupt_before; interrupt_after — для узлов, которые что-то создают.

Ошибка 2: ждать payload, как у interrupt()

Статическое прерывание ничего не «возвращает» наружу — человек сам читает результат из get_state().values. Payload отдаёт только динамический interrupt().

Ошибка 3: возобновление через Command(resume)

Как и interrupt_before, продолжается через invoke(None, config), а не Command(resume=...). Последнее — только для динамического interrupt().

Ошибка 4: правка не тех ключей

При update_state помни про reducers: для поля с add_messages переданное «допишется», а не заменит. Чтобы заменить результат, правь обычное поле (как draft) или учитывай правило слияния.

Шпаргалка

interrupt_after — всё в одном месте
python
# Пауза ПОСЛЕ узла (нужен checkpointer)
graph = builder.compile(checkpointer=saver, interrupt_after=["draft"])
cfg = {"configurable": {"thread_id": "t-1"}}

graph.invoke(inp, cfg)             # draft выполнился → пауза перед следующим узлом

snap = graph.get_state(cfg)
snap.next                          # ('send',) — ждём ревью
snap.values["draft"]              # ГОТОВЫЙ результат на проверку

graph.invoke(None, cfg)            # ✓ одобрить как есть
graph.update_state(cfg, {"draft": "..."})  # ✏️ поправить, потом invoke(None)

# before vs after:
#   interrupt_before — узел НЕ выполнен, ревьюим ПЛАН (опасные узлы)
#   interrupt_after  — узел ВЫПОЛНЕН, ревьюим РЕЗУЛЬТАТ (генерирующие узлы)

# можно вместе:
builder.compile(checkpointer=saver,
                interrupt_after=["draft"], interrupt_before=["send"])

# resume всегда invoke(None) — НЕ Command(resume)

Практическое задание

Поставь человека после генерации:

Задание: пост в соцсети с ревью

  1. Граф из двух узлов: generate (LLM пишет пост по теме в поле post) и publish (печатает «опубликовано»). Скомпилируй с MemorySaver и interrupt_after=["generate"].
  2. Запусти, проверь, что get_state().next == ('publish',), а пост ещё не «опубликован». Выведи сгенерированный текст.
  3. Одобри через invoke(None, config) — пост публикуется.
  4. Во втором треде через update_state подправь текст поста и затем возобнови — убедись, что опубликован исправленный вариант.
  5. Добавь к графу interrupt_before=["publish"] и проследи, что граф теперь останавливается дважды.
  6. Со звёздочкой: добавь поле approved: bool и условное ребро после generate: если человек через update_state поставил approved=False — вернуть в generate на повторную генерацию.

Что дальше