Один и тот же boilerplate

Напомним ReAct-цикл: LLM рассуждает и решает вызвать инструмент → инструмент исполняется → результат возвращается модели → она рассуждает дальше → ... → пока не сформулирует ответ. Узел agent уникален (там твоя модель и промпт), а вот узел tools и маршрутизатор — всегда одинаковые:

Ручной tools-узел и router — это шаблон, который повторяется везде
python
# Этот код мы писали руками в уроке про 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.
100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
START agent твоя LLM + промпт tools_condition готовый ToolNode исполняет tool_calls END нет → END есть → tools результат → обратно в agent тот же ReAct-цикл, но tools и router — готовые блоки из langgraph.prebuilt

Сборка графа из готовых блоков

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

ReAct-граф на ToolNode + tools_condition
python
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»

tools_condition по умолчанию маршрутизирует в узел с именем "tools" (или в END). Поэтому ToolNode добавляют под именно этим именем — тогда готовый router сработает без настройки. Хочешь другое имя — придётся передать карту исходов вручную.

Сравни с уроком про conditional edges: граф тот же, но рукописные tools и route ушли. ToolNode вдобавок умеет то, что мы писали бы сами: исполнять несколько инструментов из одного ответа параллельно и аккуратно обрабатывать ошибки исполнения.

Что умеет ToolNode

ToolNode — не просто «вызвать функцию». Несколько важных свойств:

несколько вызовов
Если LLM попросила сразу 3 инструмента, ToolNode выполнит их параллельно и вернёт 3 ToolMessage.
обработка ошибок
По умолчанию исключение инструмента ловится и возвращается модели как сообщение об ошибке — агент может попробовать иначе.
правильный формат
Сам упаковывает результат в ToolMessage с нужным tool_call_id — модель корректно свяжет ответ с запросом.
Управление обработкой ошибок
python
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) внутри и возвращает готовый скомпилированный агент.

Готовый ReAct-агент в одну строку
python
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

Хелпер не «всё или ничего» — он принимает множество параметров, покрывающих частые нужды, прежде чем придётся строить граф вручную.

Частые расширения готового агента
python
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 через окружение. Это не «чёрный ящик», а собранный за тебя граф со знакомым интерфейсом.

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

Ошибка 1: ToolNode под именем не «tools»

tools_condition по умолчанию ищет узел "tools". Назовёшь узел иначе и не передашь карту исходов — маршрутизация не найдёт его. Либо имя "tools", либо явная карта в add_conditional_edges.

Ошибка 2: забыли bind_tools у модели

При ручной сборке модель должна знать про инструменты: llm.bind_tools(tools). Без этого LLM не сгенерирует tool_calls, и ToolNode будет нечего исполнять. (create_react_agent делает это сам.)

Ошибка 3: нет ребра tools → agent

Без обратного ребра ToolNode исполнит инструмент и граф остановится, не вернув результат модели. ReAct — это цикл: tools всегда ведёт обратно в agent.

Ошибка 4: брать хелпер там, где нужен граф

create_react_agent отлично покрывает типовой случай, но если архитектура требует своих узлов и нестандартной маршрутизации — не воюй с параметрами хелпера, собери граф из ToolNode и tools_condition руками.

Шпаргалка

ReAct с ToolNode — всё в одном месте
python
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 и хелпере

  1. Возьми ручного ReAct-агента из урока про conditional edges и перепиши узел tools и маршрутизатор на ToolNode + tools_condition. Убедись, что поведение не изменилось.
  2. Добавь второй инструмент и запрос, на который LLM захочет вызвать оба сразу. Проверь, что ToolNode вернул два ToolMessage.
  3. Сделай инструмент, бросающий исключение, и убедись, что агент получает сообщение об ошибке и продолжает работу, а не падает.
  4. Собери тот же агент через create_react_agent с системным промптом и MemorySaver; сравни объём кода.
  5. Добавь в хелпер interrupt_before=["tools"] и проверь, что агент встаёт на подтверждение перед инструментом (как в уроке про interrupt_before).
  6. Со звёздочкой: задай response_format Pydantic-схемой и получи структурированный финальный ответ вместо свободного текста.

Что дальше