Агент, который только ищет и читает, ошибается безобидно: в худшем случае даёт неточный ответ. Совсем другое дело — агент, который отправляет письма клиентам, возвращает деньги или удаляет записи в базе. Одна неверно выбранная функция или неправильный аргумент, и ошибку уже не отменить. Именно поэтому в рабочих системах рискованные действия согласует человек.
Подход называется human-in-the-loop — «человек в контуре». Агент доходит до опасного шага, останавливается и показывает, что собирается сделать. Человек одобряет, исправляет аргументы или отклоняет действие, и агент продолжает работу с учётом решения. После урока вы сможете включить такую проверку в агента на create_agent и в собственный граф LangGraph.
Что уже умеем
В уроке 2.4 вы подключали к агенту checkpointer и передавали thread_id, чтобы агент помнил разговор. В уроке 2.5 добавляли middleware — дополнительные шаги вокруг вызова модели. В прошлом уроке собирали графы из узлов и рёбер. Сегодня соединим всё это: встроенное middleware будет ставить агента на паузу, checkpointer сохранит состояние на время паузы, а команда Command продолжит выполнение.
Что вы узнаете в рамках статьи
- как работает прерывание (interrupt) и зачем ему checkpointer;
- как настроить
HumanInTheLoopMiddlewareдля отдельных инструментов; - как прочитать, какое действие ждёт подтверждения;
- как продолжить работу через
Command(resume=...)с решениями approve, edit и reject; - как поставить паузу в собственном графе функцией
interrupt().
Ключевые термины
- Human-in-the-loop (HITL) — схема, в которой человек проверяет и согласует действия агента.
- Прерывание (interrupt) — остановка графа в определённой точке с сохранением состояния до решения человека.
- Возобновление (resume) — продолжение графа с того же места с переданным ответом человека.
- Решение (decision) — ответ человека на запрос: одобрить, отредактировать или отклонить действие.
- Command — объект LangGraph, через который передаются данные для возобновления.
Как устроена пауза
Когда агент доходит до действия, требующего подтверждения, LangGraph не держит программу в ожидании. Он сохраняет всё состояние графа в checkpointer под текущим thread_id, и вызов invoke сразу возвращает результат с информацией о прерывании. Человек может ответить через минуту или через день, в другом процессе или с другого сервера. Когда ответ готов, вы снова вызываете агента с тем же thread_id, и он продолжает с места остановки.
Отсюда два обязательных условия. Без checkpointer состояние негде хранить, и пауза невозможна. Без thread_id агент не поймёт, какой именно разговор продолжать. В учебных примерах подойдёт InMemorySaver, а в продакшене нужен постоянный checkpointer на базе данных, иначе после перезапуска сервера все ожидающие решения пропадут.
HumanInTheLoopMiddleware в create_agent
Для агентов на create_agent есть готовое middleware. В параметре interrupt_on перечисляются инструменты и правила: True — останавливаться и разрешать все варианты решений, False — выполнять без вопросов, словарь — тонкая настройка, например список разрешённых решений.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
@tool
def find_order(order_id: str) -> str:
"""Находит заказ по номеру и возвращает сумму и статус."""
return f"Заказ {order_id}: 4990 руб., доставлен"
@tool
def refund(order_id: str, amount: int) -> str:
"""Оформляет возврат денег по заказу."""
return f"Возврат {amount} руб. по заказу {order_id} оформлен"
@tool
def send_email(to: str, text: str) -> str:
"""Отправляет письмо клиенту."""
return f"Письмо для {to} отправлено"
agent = create_agent(
model="openai:gpt-6-luna",
tools=[find_order, refund, send_email],
system_prompt="Ты сотрудник поддержки интернет-магазина.",
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"find_order": False,
"refund": {"allowed_decisions": ["approve", "reject"]},
"send_email": True,
}
)
],
checkpointer=InMemorySaver(),
)
Здесь поиск заказа безопасен и выполняется сразу. Возврат денег можно только одобрить или отклонить: редактировать сумму оператору не разрешено. Письмо останавливается со всеми вариантами решений, включая правку текста.
Читаем запрос и возобновляем работу
Запустим агента. Он найдёт заказ без паузы, а на возврате остановится.
config = {"configurable": {"thread_id": "ticket-42"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Верните деньги за заказ A-17"}]},
config=config,
)
request = result["__interrupt__"][0].value
for action in request["action_requests"]:
print("Агент хочет вызвать:", action["name"], action["args"])
Что увидим в консоли: Агент хочет вызвать: refund {'order_id': 'A-17', 'amount': 4990}. Под ключом __interrupt__ лежит список прерываний; в значении есть action_requests с именем инструмента и аргументами и review_configs с разрешёнными решениями. Инструмент refund при этом ещё не выполнялся.
Решение передаётся через Command(resume=...) с тем же config. Для каждого действия из action_requests нужно указать одно решение в том же порядке.
from langgraph.types import Command
result = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config,
)
print(result["messages"][-1].content)
Что увидим: финальный ответ агента вроде «Возврат 4990 руб. по заказу A-17 оформлен». Два других формата решений выглядят так:
# Исправить аргументы перед выполнением (для send_email это разрешено)
{"type": "edit", "edited_action": {"name": "send_email",
"args": {"to": "anna@example.com", "text": "Исправленный текст"}}}
# Отклонить с пояснением для модели
{"type": "reject", "message": "Возврат не одобрен: заказ уже возмещён купоном."}
При отклонении инструмент не выполняется, а модель получает ваше сообщение и может сообщить об этом пользователю или предложить другое решение. Поэтому в тексте отказа стоит объяснить причину: модель лучше поймёт, что делать дальше. Если попытаться передать решение, которого нет в allowed_decisions, например edit для refund, middleware его не примет.
Пауза в собственном графе
В графе LangGraph точку остановки можно поставить в любом узле функцией interrupt(). Она сохраняет состояние и возвращает человеку переданное значение. При возобновлении узел запускается заново с начала, а interrupt() возвращает то, что пришло в Command(resume=...).
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
class Post(TypedDict):
draft: str
approved: bool
def review(state: Post) -> dict:
answer = interrupt({"question": "Публикуем пост?", "draft": state["draft"]})
return {"approved": answer == "да"}
builder = StateGraph(Post)
builder.add_node("review", review)
builder.add_edge(START, "review")
builder.add_edge("review", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "post-1"}}
paused = graph.invoke({"draft": "Скидки до 30%!", "approved": False}, config=config)
print(paused["__interrupt__"][0].value["question"])
final = graph.invoke(Command(resume="да"), config=config)
print(final["approved"])
Что увидим: сначала Публикуем пост?, затем True. Поскольку узел перезапускается целиком, не размещайте до interrupt() действия, которые нельзя повторять, например списание денег: при возобновлении они выполнятся второй раз.
Частые ошибки
- Забыть checkpointer. Без него прерывание невозможно: состояние негде хранить. Передавайте
checkpointerвcreate_agentили вcompile(). - Возобновлять с другим
thread_id. Агент начнёт новый разговор и не найдёт ожидающее действие. Сохраняйтеthread_idвместе с запросом на подтверждение. - Подтверждать всё подряд. Если ставить паузу на каждое чтение, оператор устанет и начнёт одобрять не глядя. Останавливайтесь только на действиях с последствиями.
- Выполнять необратимое до
interrupt(). Код узла до паузы выполнится повторно при возобновлении.
Наиболее частые вопросы
- Сколько агент может ждать решения? — Сколько угодно: состояние лежит в checkpointer, а программа не блокируется. Важно лишь, чтобы хранилище было постоянным.
- Что, если модель вызвала два опасных инструмента сразу? — В
action_requestsбудут оба действия, и вdecisionsнужно передать два решения в том же порядке. - Можно ли разрешить только одобрение и отказ? — Да, через
{"allowed_decisions": ["approve", "reject"]}для конкретного инструмента. - Где человек видит запрос? — Где угодно: в консоли, веб-интерфейсе, чате. Ваш код показывает
action_requestsи потом передаёт решение черезCommand.
Итоги
Поздравляем, ваш агент стал заметно безопаснее! Вы разобрались, как устроена пауза в LangGraph, почему без checkpointer и thread_id она невозможна, настроили HumanInTheLoopMiddleware для разных инструментов, научились читать запрос на подтверждение и отвечать на него одобрением, правкой или отказом. А с помощью interrupt() вы можете поставить точку проверки в любом месте собственного графа.
Дальше агенты будут работать командой: в следующем уроке вы соберёте мультиагентную систему, где главный агент распределяет задачи между специализированными помощниками.