Один и тот же boilerplate
Напомним ReAct-цикл: LLM рассуждает и решает вызвать инструмент → инструмент исполняется → результат возвращается модели → она рассуждает дальше → ... → пока не сформулирует ответ. Узел agent уникален (там твоя модель и промпт), а вот узел tools и маршрутизатор — всегда одинаковые:
# Этот код мы писали руками в уроке про conditional edges:
def tools(state):
results = []
for call in state["messages"][-1].tool_calls: # перебрать вызовы
fn = TOOLS_BY_NAME[call["name"]] # найти инструмент
try:
out = fn.invoke(call["args"]) # исполнить
except Exception as e:
out = f"Ошибка: {e}" # обработать ошибку
results.append({"role": "tool", "content": str(out),
"tool_call_id": call["id"]})
return {"messages": results}
def route(state):
return "tools" if state["messages"][-1].tool_calls else END
Перебор вызовов, поиск инструмента по имени, обработка ошибок, упаковка в ToolMessage, проверка на наличие tool_calls — всё это не зависит от конкретного агента. LangGraph даёт это из коробки.
ToolNode и tools_condition
Два готовых блока из langgraph.prebuilt заменяют рукописный шаблон один в один:
ToolNode(tools)— узел-исполнитель. Берётtool_callsиз последнего сообщения, вызывает нужные инструменты (хоть несколько за раз, параллельно), ловит ошибки и возвращаетToolMessageс результатами.tools_condition— готовая функция-маршрутизатор. Смотрит последнее сообщение: естьtool_calls→ ведёт в узелtools, нет → вEND.
Сборка графа из готовых блоков
Это рекомендуемый «средний» уровень: ты по-прежнему явно строишь граф (видишь структуру, можешь вмешаться), но рутину отдаёшь готовым блокам. Используем MessagesState из урока про reducers — состояние с единственным полем messages.
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
def get_weather(city: str) -> str:
"""Погода в городе."""
return f"В {city} +18°C, ясно."
tools = [get_weather]
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)
def agent(state: MessagesState) -> dict:
return {"messages": [llm.invoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode(tools)) # ← готовый исполнитель
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition) # ← готовый router
builder.add_edge("tools", "agent") # цикл
graph = builder.compile()
result = graph.invoke({"messages": [{"role": "user", "content": "Погода в Москве?"}]})
print(result["messages"][-1].content)
tools_condition по умолчанию маршрутизирует в узел с именем "tools" (или в END). Поэтому ToolNode добавляют под именно этим именем — тогда готовый router сработает без настройки. Хочешь другое имя — придётся передать карту исходов вручную.
Сравни с уроком про conditional edges: граф тот же, но рукописные tools и route ушли. ToolNode вдобавок умеет то, что мы писали бы сами: исполнять несколько инструментов из одного ответа параллельно и аккуратно обрабатывать ошибки исполнения.
Что умеет ToolNode
ToolNode — не просто «вызвать функцию». Несколько важных свойств:
from langgraph.prebuilt import ToolNode
# По умолчанию handle_tool_errors=True — ошибка инструмента
# вернётся модели как ToolMessage, и агент сможет среагировать.
tool_node = ToolNode(tools)
# Можно подменить текст ошибки или отключить «мягкую» обработку
tool_node = ToolNode(tools, handle_tool_errors="Инструмент недоступен, попробуй иначе")
create_react_agent: весь паттерн одной строкой
Если нужен стандартный ReAct без кастомной структуры графа — есть хелпер create_react_agent. Он собирает ровно такой граф (agent ↔ ToolNode с tools_condition) внутри и возвращает готовый скомпилированный агент.
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_react_agent(
model="openai:gpt-4o-mini", # модель
tools=[get_weather], # инструменты
prompt="Ты вежливый ассистент.", # системный промпт (опционально)
checkpointer=MemorySaver(), # память (опционально)
)
# Тот же интерфейс Runnable — invoke / stream
out = agent.invoke(
{"messages": [{"role": "user", "content": "Погода в Москве?"}]},
{"configurable": {"thread_id": "1"}},
)
print(out["messages"][-1].content)
create_react_agent — когда нужен типовой ReAct: быстрый старт, прототип, стандартный агент с инструментами и памятью. Сборка из ToolNode + tools_condition — когда нужна нестандартная структура: дополнительные узлы (ревью, маршрутизация по типу запроса), human-in-the-loop в неочевидных местах, своя логика состояния. Начинай с хелпера, переходи на ручную сборку, когда упрёшься в его рамки.
Кастомизация create_react_agent
Хелпер не «всё или ничего» — он принимает множество параметров, покрывающих частые нужды, прежде чем придётся строить граф вручную.
from pydantic import BaseModel
class Answer(BaseModel):
city: str
temperature: int
agent = create_react_agent(
model="openai:gpt-4o-mini",
tools=[get_weather],
prompt="Ты метеоролог.",
checkpointer=MemorySaver(),
interrupt_before=["tools"], # human-in-the-loop: пауза перед инструментами
response_format=Answer, # структурированный ответ (Pydantic-схема)
)
# Динамический промпт (например, подставить имя пользователя из state) —
# передают функцию вместо строки; для совсем нестандартного — собирают граф руками.
Агент из create_react_agent — обычный скомпилированный граф. К нему применимо всё из прошлых разделов: checkpointer и thread_id для памяти, interrupt_before для подтверждений, трейсинг LangSmith через окружение. Это не «чёрный ящик», а собранный за тебя граф со знакомым интерфейсом.
Типичные ошибки
tools_condition по умолчанию ищет узел "tools". Назовёшь узел иначе и не передашь карту исходов — маршрутизация не найдёт его. Либо имя "tools", либо явная карта в add_conditional_edges.
При ручной сборке модель должна знать про инструменты: llm.bind_tools(tools). Без этого LLM не сгенерирует tool_calls, и ToolNode будет нечего исполнять. (create_react_agent делает это сам.)
Без обратного ребра ToolNode исполнит инструмент и граф остановится, не вернув результат модели. ReAct — это цикл: tools всегда ведёт обратно в agent.
create_react_agent отлично покрывает типовой случай, но если архитектура требует своих узлов и нестандартной маршрутизации — не воюй с параметрами хелпера, собери граф из ToolNode и tools_condition руками.
Шпаргалка
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition, create_react_agent
# ── Уровень 1: сборка из готовых блоков (гибко) ──
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools) # bind_tools обязателен!
builder = StateGraph(MessagesState)
builder.add_node("agent", lambda s: {"messages": [llm.invoke(s["messages"])]})
builder.add_node("tools", ToolNode(tools)) # узел ДОЛЖЕН зваться "tools"
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition) # → "tools" или END
builder.add_edge("tools", "agent") # цикл!
graph = builder.compile()
# ── Уровень 2: хелпер (быстро) ──
agent = create_react_agent(
model="openai:gpt-4o-mini", tools=tools,
prompt="...", checkpointer=MemorySaver(),
interrupt_before=["tools"], # HITL
response_format=MySchema, # структурированный вывод
)
# Выбор:
# • типовой ReAct → create_react_agent
# • нестандартные узлы/маршруты → ручная сборка из ToolNode + tools_condition
# • ToolNode сам: параллельные вызовы, ошибки, ToolMessage с tool_call_id
Практическое задание
Перепиши агента на готовые блоки:
Задание: ReAct на ToolNode и хелпере
- Возьми ручного ReAct-агента из урока про conditional edges и перепиши узел
toolsи маршрутизатор наToolNode+tools_condition. Убедись, что поведение не изменилось. - Добавь второй инструмент и запрос, на который LLM захочет вызвать оба сразу. Проверь, что ToolNode вернул два
ToolMessage. - Сделай инструмент, бросающий исключение, и убедись, что агент получает сообщение об ошибке и продолжает работу, а не падает.
- Собери тот же агент через
create_react_agentс системным промптом иMemorySaver; сравни объём кода. - Добавь в хелпер
interrupt_before=["tools"]и проверь, что агент встаёт на подтверждение перед инструментом (как в уроке про interrupt_before). - Со звёздочкой: задай
response_formatPydantic-схемой и получи структурированный финальный ответ вместо свободного текста.