Зачем останавливаться перед узлом
Представь агента с инструментами: один из них умеет удалять файлы, другой — отправлять деньги, третий — публиковать пост. Полностью доверять LLM такие действия страшно: достаточно одной галлюцинации, чтобы случилось непоправимое. Нужна контрольная точка: перед тем как агент реально выполнит инструмент, человек должен увидеть, что именно запланировано, и дать добро.
Причём правило простое и безусловное: «всегда останавливайся перед узлом tools». Городить ради этого interrupt() с условиями внутри узла избыточно. LangGraph даёт декларативный способ: указать при компиляции, перед какими узлами граф обязан вставать на паузу. Это interrupt_before.
Как и динамическое прерывание, interrupt_before работает только с checkpointer + thread_id. Пауза — это сохранённый checkpoint перед узлом; возобновление — продолжение из него. Без памяти останавливаться и продолжать невозможно.
interrupt_before при компиляции
Точка прерывания задаётся одним аргументом compile() — списком имён узлов, перед которыми нужно вставать на паузу. Сами узлы трогать не надо: их код не меняется, прерывание навешивается снаружи, структурно.
from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(
checkpointer=MemorySaver(),
interrupt_before=["tools"], # ← пауза ПЕРЕД узлом tools, всегда
)
Теперь, когда поток управления подходит к узлу tools, граф останавливается прямо перед его выполнением. Узел ещё не отработал — а значит, ничего необратимого не случилось. Состояние (включая запланированный вызов инструмента, который придумала LLM) сохранено в checkpoint и доступно для проверки.
Цикл: пауза → проверка → возобновление
Работа с паузой состоит из трёх шагов: запустить до прерывания, осмотреть состояние, продолжить (или нет). Возобновляют статическое прерывание вызовом invoke(None, config) — None вместо входа означает «не добавляй ничего, просто продолжи с checkpoint».
config = {"configurable": {"thread_id": "approve-1"}}
# 1. Запуск — граф доходит до tools и ВСТАЁТ перед ним
graph.invoke({"messages": [{"role": "user", "content": "Удали лог-файлы"}]}, config)
# 2. Смотрим, на чём остановились и что запланировано
snapshot = graph.get_state(config)
print(snapshot.next) # ('tools',) — дальше пойдёт tools
planned = snapshot.values["messages"][-1] # последнее сообщение LLM с tool_calls
print(planned.tool_calls) # [{'name': 'delete_files', 'args': {...}}]
# 3a. ОДОБРИТЬ — продолжаем с того же места
graph.invoke(None, config) # None = «возобнови», tools выполнится
# 3b. ОТКЛОНИТЬ — просто не возобновляем (или уводим в END — см. ниже)
Признак паузы — непустой snapshot.next: если там есть имя узла, граф ждёт перед ним. Когда граф дошёл до END, next пустой. По этому полю приложение понимает, нужно ли показывать человеку запрос на подтверждение.
Тонкость на стыке двух механизмов. Статическое прерывание возобновляют invoke(None, config) (просто «продолжай»). Динамический interrupt() из прошлого урока — Command(resume=значение) (передать ответ внутрь узла). Не перепутай: статическому прерыванию нечего «вернуть» — узел перед паузой ещё не запускался.
Отклонить и отредактировать
Одобрение — это просто invoke(None). А что если человек не согласен? Два сценария.
Отклонить. Самый прямой способ — не возобновлять граф вовсе. Но обычно агенту надо сообщить об отказе, чтобы он не завис. Тогда правят состояние через update_state — например, дописывают «инструмент отклонён», и LLM на следующем шаге придумает другой план.
Отредактировать. Раз граф стоит перед узлом, можно поменять то, что узел получит на вход. Через update_state человек правит запланированный вызов (скажем, сужает удаление до одного файла), и только потом возобновляет.
# Человек посмотрел план и хочет вмешаться, пока граф на паузе перед tools
# Вариант «отклонить»: дописываем отказ, агент перепланирует
graph.update_state(
config,
{"messages": [{"role": "tool", "content": "Отклонено пользователем",
"tool_call_id": planned.tool_calls[0]["id"]}]},
)
graph.invoke(None, config) # agent увидит отказ и предложит иное
# Вариант «отредактировать»: заменяем аргументы запланированного вызова,
# затем возобновляем — узел tools выполнит уже исправленное действие
# (детали редактирования сообщений LLM — в уроке «Редактирование состояния»)
Вся ценность interrupt_before в том, что узел ещё не выполнился. Поэтому отклонить или поправить действие безопасно — необратимого ещё не произошло. Сравни с interrupt_after (следующий урок): там узел уже отработал, и человек ревьюит результат, а не план.
Полный пример: агент с подтверждением инструментов
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
def delete_files(pattern: str) -> str:
return f"Удалены файлы по маске {pattern}" # ⚠️ необратимо!
class State(TypedDict):
messages: Annotated[list, add_messages]
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([delete_files])
def agent(state: State) -> dict:
return {"messages": [llm.invoke(state["messages"])]}
def tools(state: State) -> dict:
call = state["messages"][-1].tool_calls[0]
out = delete_files(**call["args"])
return {"messages": [{"role": "tool", "content": out,
"tool_call_id": call["id"]}]}
def route(state: State) -> str:
return "tools" if state["messages"][-1].tool_calls else END
builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tools)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", route, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")
# ⬇️ ключевая строка: пауза перед каждым выполнением инструментов
graph = builder.compile(checkpointer=MemorySaver(), interrupt_before=["tools"])
config = {"configurable": {"thread_id": "ops-1"}}
graph.invoke({"messages": [{"role": "user", "content": "Удали *.log"}]}, config)
# Агент решил вызвать delete_files — но граф встал ПЕРЕД tools
print(graph.get_state(config).next) # ('tools',)
print(graph.get_state(config).values["messages"][-1].tool_calls)
# Человек одобряет — только теперь файлы реально удаляются
graph.invoke(None, config)
print(graph.get_state(config).values["messages"][-1].content)
Когда что: статическое или динамическое
| Критерий | interrupt_before (статическое) | interrupt() (динамическое) |
|---|---|---|
| Где задаётся | в compile(), снаружи узла |
в коде узла |
| Когда срабатывает | всегда перед узлом | по условию (можно в if) |
| Несёт payload человеку | нет (смотрят состояние) | да (аргумент interrupt) |
| Как возобновить | invoke(None, config) |
Command(resume=...) |
| Лучший случай | «всегда спрашивай перед X» | диалог/ввод данных в узле |
Типичные ошибки
Без checkpointer и thread_id паузе негде сохраниться — прерывание просто не сработает. Сначала память, потом точки прерывания.
Статическое прерывание продолжают invoke(None, config). Command(resume=...) — это для динамического interrupt(). Перепутаешь — граф не возобновится как ожидалось.
После invoke всегда смотри get_state(config).next. Непусто — граф на паузе, нужно подтверждение. Пусто — дошли до END. Иначе примешь остановку на подтверждении за готовый ответ.
interrupt_before=["tool"] вместо ["tools"] — прерывание молча не сработает, потому что такого узла нет. Имя должно точно совпадать с тем, что в add_node.
Шпаргалка
# 1. Пауза задаётся при компиляции (нужен checkpointer)
graph = builder.compile(checkpointer=saver, interrupt_before=["tools"])
cfg = {"configurable": {"thread_id": "t-1"}}
# 2. Запуск → граф встаёт ПЕРЕД узлом tools (узел ещё не выполнен)
graph.invoke(inp, cfg)
# 3. Проверяем паузу и запланированное действие
snap = graph.get_state(cfg)
snap.next # ('tools',) — ждём подтверждения
snap.values["messages"][-1].tool_calls # что собирается сделать агент
# 4. Решение человека:
graph.invoke(None, cfg) # ✓ одобрить → выполнить узел
graph.update_state(cfg, {...}) # ✏️ поправить состояние, потом invoke(None)
# ✗ отклонить → дописать отказ / не возобновлять
# Помни:
# • resume статического прерывания = invoke(None), НЕ Command(resume)
# • пауза «перед» = действие ещё не случилось → безопасно отклонять/править
# • всегда проверяй get_state().next, чтобы не спутать паузу с финалом
Практическое задание
Поставь человека перед опасным действием:
Задание: агент с подтверждением перед инструментом
- Собери ReAct-агента с инструментом
send_email(to, text)(просто печатает «письмо отправлено»). Скомпилируй сMemorySaverиinterrupt_before=["tools"]. - Запусти запрос, который заставит агента вызвать инструмент. Убедись, что
get_state().next == ('tools',), а письмо ещё не «отправлено». - Выведи запланированный
tool_calls— покажи, что именно агент собирается сделать. - Одобри через
invoke(None, config)и проверь, что инструмент выполнился. - Во втором треде отклони действие: через
update_stateдопиши tool-сообщение «отклонено» и возобнови — убедись, что агент перепланировал, а письмо не ушло. - Со звёздочкой: добавь к графу узел
db_writeи прерывайся перед обоими опасными узлами:interrupt_before=["tools", "db_write"].