16. Готовые компоненты и многоветвевая маршрутизация
В главе 15 мы собрали граф агента вручную — узел модели, узел инструментов и условное ребро, которое решает, продолжать ли цикл или остановиться. По сути, это та самая стандартная структура для агентов, вызывающих инструменты, поэтому LangChain и LangGraph поставляют её в виде готовых компонентов, которые можно использовать вместо того, чтобы каждый раз писать один и тот же каркас с нуля.
В первой половине этой главы мы заново создадим агента из главы 15, используя готовые компоненты. Мы заменим узел выполнения инструментов и функцию маршрутизации на ToolNode и tools_condition, а в конце заменим всю сборку графа единственным вызовом create_agent. Вы увидите, что поведение остаётся идентичным главе 15, тогда как код заметно сокращается.
Во второй половине мы скомбинируем готовые компоненты с подходом ручной сборки графа из главы 15, чтобы построить более сложного агента. Агент, которого мы построим, направляет каждый запрос к отдельному обработчику — сложные консультации отправляются к высокопроизводительной модели, а на простые вопросы отвечает более дешёвая, меньшая модель. Это многоветвевая структура, в которой путь обработки расходится в зависимости от типа запроса.
16.1) Готовые компоненты и create_agent
В этом разделе мы заменим функцию tool_node и функцию should_continue из графа главы 15 готовыми компонентами ToolNode и tools_condition. После этого мы вообще пропустим ручную сборку и создадим весь граф единственным вызовом create_agent. На каждом шаге стоит обращать внимание на то, что код становится короче, тогда как поведение агента остаётся идентичным главе 15.
16.1.1) ToolNode и tools_condition
ToolNode — это готовый узел, который выполняет вызов инструментов за вас. Когда последнее сообщение в состоянии (AIMessage, возвращённое LLM) содержит tool_calls, он запускает запрошенные инструменты и добавляет результаты в messages в виде объектов ToolMessage. Он выполняет ту же работу, что и функция tool_node, которую мы написали в главе 15. Кроме того, когда LLM запрашивает несколько инструментов одновременно, он запускает их параллельно.
Он также поддерживает обработку исключений во время выполнения инструментов. Если задать ToolNode(tools, handle_tool_errors=True), граф не упадёт даже тогда, когда инструмент вызовет исключение. Исключение преобразуется в ToolMessage с деталями ошибки, которое передаётся LLM, чтобы она увидела сбой и повторила попытку с исправленными аргументами.
ToolNode создаётся передачей ему списка инструментов. Мы будем использовать те же два инструмента из главы 15.
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Получить текущую погоду для города."""
fake_data = {"Tokyo": "18°C, облачно", "Cairo": "31°C, солнечно"}
return fake_data.get(city, f"Нет данных о погоде для {city}.")
@tool
def calculate(expression: str) -> str:
"""Вычислить простое арифметическое выражение. Пример: '3 * 21'."""
return str(eval(expression)) # Внимание: eval() представляет угрозу безопасности. Не используйте в продакшене.
tools = [get_weather, calculate]
# Словарь tool_map + функция tool_node из главы 15 заменены этой единственной строкой
tool_node = ToolNode(tools)ToolNode строит внутреннее сопоставление имён с инструментами из списка инструментов, точно так же, как tool_map из главы 15. Во время выполнения он находит каждый инструмент по имени, запрошенному LLM, и вызывает его. Иными словами, словарь tool_map, цикл for по tool_calls и код, который строит и собирает объекты ToolMessage — всё это теперь находится внутри ToolNode.
tools_condition — это готовая функция маршрутизации, которая заменяет функцию should_continue из главы 15. Давайте снова посмотрим на should_continue из главы 15.
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
"""Решить, выполнять ли инструменты или завершить граф."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return ENDОна возвращала "tool_node" (зарегистрированное имя нашего узла инструментов), когда последнее сообщение содержало tool_calls, и END в противном случае. tools_condition работает точно так же, с одним отличием в имени, которое она возвращает. Тогда как should_continue была написана для возврата "tool_node" — имени, которое мы зарегистрировали в нашем графе — tools_condition жёстко возвращает "tools".
Как мы узнали в разделе 15.2.4, значение, возвращаемое функцией маршрутизации, — это имя следующего узла для выполнения. Если узла с таким именем в графе не существует, маршрутизация завершается неудачей. Поэтому при использовании tools_condition узел инструментов должен быть зарегистрирован под именем "tools".
from langgraph.prebuilt import ToolNode, tools_condition
builder.add_node("tools", ToolNode(tools)) # Регистрируем под именем "tools"
builder.add_conditional_edges("llm_call", tools_condition) # Маршрутизирует к "tools" или ENDТаким образом, когда tools_condition возвращает "tools", происходит подключение именно к тому ToolNode, который мы только что зарегистрировали.
Теперь давайте заново соберём полный граф из главы 15, включая все оставшиеся части. Инструменты, состояние и узел llm_call не изменились по сравнению с главой 15.
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Получить текущую погоду для города."""
fake_data = {"Tokyo": "18°C, облачно", "Cairo": "31°C, солнечно"}
return fake_data.get(city, f"Нет данных о погоде для {city}.")
@tool
def calculate(expression: str) -> str:
"""Вычислить простое арифметическое выражение. Пример: '3 * 21'."""
return str(eval(expression)) # Внимание: eval() представляет угрозу безопасности. Не используйте в продакшене.
tools = [get_weather, calculate]
class AgentState(MessagesState):
llm_calls: int
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
def llm_call(state: AgentState):
"""Вызвать LLM и вернуть её ответ."""
response = model_with_tools.invoke(state["messages"])
return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1,
}
builder = StateGraph(AgentState)
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools)) # ToolNode вместо функции tool_node из главы 15
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition) # tools_condition вместо should_continue из главы 15
builder.add_edge("tools", "llm_call")
agent = builder.compile()Сравните это с кодом главы 15. Словарь tool_map, функция tool_node и функция should_continue — все они исчезли. Работу, которую они выполняли, теперь берут на себя ToolNode(tools) и tools_condition. Узел инструментов зарегистрирован как "tools", чтобы соответствовать имени, к которому маршрутизирует tools_condition. Давайте запустим его с тем же вопросом, что и в главе 15.
result = agent.invoke({
"messages": [HumanMessage(content="Узнай температуру в Каире, затем умножь это число на 3.")],
"llm_calls": 0,
})
print(result["messages"][-1].content)
print(f"\nВсего вызовов LLM: {result['llm_calls']}")Вывод:
Текущая температура в Каире: 31°C. Умноженная на 3 = 93.
Всего вызовов LLM: 3Результат идентичен главе 15. Агент узнаёт погоду, выполняет вычисление и выдаёт итоговый ответ — сохраняется то же самое поведение, тогда как код, который нам нужно писать и поддерживать, сократился.
Что делать, если вы хотите зарегистрировать узел инструментов под именем, отличным от "tools"? В этом случае передайте словарь сопоставления в качестве третьего аргумента add_conditional_edges, указав, к какому узлу должно подключаться каждое возвращаемое значение из tools_condition. Поскольку tools_condition возвращает либо "tools", либо END, вы используете их в качестве ключей и сопоставляете с целевыми узлами. Например, если вы регистрируете узел инструментов как "run_tools":
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
"llm_call",
tools_condition,
{"tools": "run_tools", END: END} # возврат "tools" → узел run_tools, возврат END → завершение
)ToolNode и tools_condition заменяют отдельные части графа — самые рутинные — но добавление узлов и их соединение по-прежнему остаётся на нас. Могли бы мы передать и эту сборку тоже? Именно это и делает create_agent.
16.1.2) create_agent
create_agent — это фабричная функция LangChain, которая берёт на себя всю сборку графа для агента, вызывающего инструменты. Передайте ей модель и список инструментов, и она построит граф с той же структурой, что мы собрали в 16.1.1, уже скомпилированный и готовый к запуску. Давайте попробуем.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
@tool
def get_weather(city: str) -> str:
"""Получить текущую погоду для города."""
fake_data = {"Tokyo": "18°C, облачно", "Cairo": "31°C, солнечно"}
return fake_data.get(city, f"Нет данных о погоде для {city}.")
@tool
def calculate(expression: str) -> str:
"""Вычислить простое арифметическое выражение. Пример: '3 * 21'."""
return str(eval(expression)) # Внимание: eval() представляет угрозу безопасности. Не используйте в продакшене.
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
system_prompt="Вы полезный помощник.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Узнай температуру в Каире, затем умножь это число на 3.")],
})
print(result["messages"][-1].content)Вывод:
Текущая температура в Каире: 31°C. Умноженная на 3 = 93.Ни определения состояния, ни функций узлов, ни add_node или add_edge. Единственный вызов create_agent сделал всё это, и результат идентичен 16.1.1.
Внутри происходит именно то, что мы уже знаем. create_agent создаёт узел вызова LLM из переданной вами модели, строит ToolNode из списка инструментов и соединяет их ребром tools_condition и ребром возврата в цикл. В результате получается граф со структурой цикла, идентичной той, что мы собрали в 16.1.1.
Давайте рассмотрим параметры. create_agent на самом деле не нов для нас — мы кратко использовали его в главе 11 при построении разговорного RAG, но не разбирали параметры подробно. Давайте пройдёмся по ним по очереди.
model: LLM, которую будет использовать агент. Самый простой подход — передать строку провайдера, такую как"openai:gpt-5-mini". Если вам нужно настроить параметры модели напрямую, передайте инициализированный экземпляр модели, напримерChatOpenAI(model="gpt-5-mini"). Внутри узел вызова LLM использует эту модель.tools: Список инструментов, которые может использовать агент. Внутри из них строитсяToolNode.system_prompt: Поведенческие инструкции для агента. Оно добавляется в начало списка сообщений как системное сообщение при каждом вызове LLM.checkpointer: Сохраняет состояние разговора, чтобы агент мог запоминать предыдущие ходы. Это тот же параметр, который мы использовали сInMemorySaver()иthread_idв главе 11 для реализации многоходовых разговоров. Мы подробно рассмотрим, как это работает, в главе 17.response_format: Используйте это, когда вы хотите получить итоговый ответ агента в виде структурированного вывода. Передайте модель Pydantic (та же концепция, что и в главе 7), и валидированный объект будет доступен вresult["structured_response"].middleware: Регистрирует функции, которые будут выполняться в определённых точках цикла выполнения агента. Это тот параметр, который мы использовали для регистрацииtrim_old_messagesв главе 11. Мы подробно объясним его ниже.
Давайте посмотрим, как response_format работает на практике.
from pydantic import BaseModel
from langchain.agents import create_agent
class WeatherReport(BaseModel):
city: str
temperature: str
condition: str
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
response_format=WeatherReport,
)
result = agent.invoke({
"messages": [HumanMessage(content="Какая погода в Токио?")],
})
print(result["structured_response"])Вывод:
city='Tokyo' temperature='18°C' condition='cloudy'Агент вызвал инструмент get_weather, а затем организовал информацию в объект WeatherReport, соответствующий схеме.
middleware
Цикл агента имеет отдельные стадии. Он вызывает LLM, выполняет инструменты, снова вызывает LLM — эти стадии повторяются. Middleware позволяет вставлять ваши собственные функции до или после этих стадий. Вы указываете момент времени с помощью декоратора: @before_model означает непосредственно перед вызовом LLM, а @after_model означает сразу после того, как LLM ответит. Зарегистрируйте функцию в параметре middleware, и она будет выполняться в указанной точке каждый раз.
Мы уже использовали middleware в главе 11. Мы декорировали функцию trim_old_messages с помощью @before_model и зарегистрировали её — поскольку она выполнялась перед каждым вызовом LLM, она могла обрезать список сообщений каждый раз.
Давайте построим middleware, которое печатает количество сообщений непосредственно перед каждым вызовом LLM, чтобы мы могли точно увидеть, когда оно выполняется.
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
"""Печатает количество сообщений непосредственно перед каждым вызовом LLM."""
print(f"[before_model] Собираемся вызвать LLM, текущие сообщения: {len(state['messages'])}")
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
middleware=[log_llm_call],
)
result = agent.invoke({
"messages": [HumanMessage(content="Узнай температуру в Каире, затем умножь это число на 3.")],
})
print(result["messages"][-1].content)Вывод:
[before_model] Собираемся вызвать LLM, текущие сообщения: 1
[before_model] Собираемся вызвать LLM, текущие сообщения: 3
[before_model] Собираемся вызвать LLM, текущие сообщения: 5
Текущая температура в Каире: 31°C. Умноженная на 3 = 93.Middleware log_llm_call выполнилось три раза. LLM была вызвана три раза при обработке запроса пользователя, и middleware выполнялось непосредственно перед каждым вызовом. Количество сообщений говорит нам о состоянии в каждой точке: перед первым вызовом был только вопрос пользователя (HumanMessage) — 1 сообщение. После каждой итерации цикла добавлялись AIMessage с запросом вызова инструмента и ToolMessage с результатом, увеличивая до 3, затем до 5.
Стоит знать одну вещь: до LangChain 1.0 эту роль выполняла функция под названием
create_react_agentна стороне LangGraph, и сейчас она устарела. Если вы видитеfrom langgraph.prebuilt import create_react_agentв старых руководствах или блог-постах, понимайте, что это предыдущая версияcreate_agent, который вы изучаете сейчас.
Мы заново создали агента из главы 15 лаконично, используя ToolNode, tools_condition и create_agent. В следующем разделе мы скомбинируем эти готовые компоненты с ручной сборкой графа, чтобы построить более сложного агента.
16.2) Построение многоветвевого агента
Многоветвевой агент, которого мы построим в этом разделе, сначала определяет, с каким видом запроса он имеет дело, а затем обрабатывает каждый вид с помощью отдельной модели или отдельного набора инструментов. Мы соберём общий граф вручную, используя подход из главы 15, и будем использовать create_agent для тех частей, которым нужен цикл вызова инструментов.
16.2.1) Требования и проектирование
Мы построим агента поддержки клиентов, упомянутого во введении к этой главе. Вот требования:
- Простые запросы («Какие у вас часы работы?») → Недорогая маленькая модель отвечает напрямую.
- Сложные консультации («Мой заказ пришёл повреждённым — мне стоит запросить обмен или возврат?») → Отвечает высокопроизводительная модель.
- Поиск заказов («Каков статус доставки заказа #12345?») → Обрабатывает агент с инструментом поиска заказов.
Каждый тип запроса требует отдельной настройки модели и инструментов, поэтому нам нужен граф, который сначала классифицирует каждый запрос, а затем направляет его к правильному обработчику. Вот структура:
Когда поступает запрос, узел classify определяет, какого он типа, и записывает результат в состояние. Затем условное ребро считывает записанный тип из состояния и направляет к соответствующему узлу. simple_handler обрабатывает простые запросы, а complex_handler занимается сложными консультациями. order_agent использует инструмент поиска заказов, чтобы проверить статус доставки и ответить. Поскольку order_agent — это стандартный узел, вызывающий инструменты, мы построим его с помощью create_agent. Теперь давайте построим каждую часть по порядку.
16.2.2) Узел классификации и функция маршрутизации
Сначала давайте определим состояние. Мы добавим поле intent для хранения результата классификации. Три типа будут представлены значениями "simple", "complex" и "order".
from langgraph.graph import MessagesState
class State(MessagesState):
intent: str # Результат классификации: "simple", "complex", "order"Далее идёт узел классификации. Он использует LLM, чтобы определить, к какому из трёх типов относится запрос пользователя, и записывает результат в intent. Мы получаем результат классификации с помощью структурированного вывода, который мы изучили в главе 7. Когда мы объявляем поле intent схемы с типом Literal, ответ LLM ограничивается одним из объявленных значений.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class IntentRoute(BaseModel):
"""Результат классификации запроса клиента."""
intent: Literal["simple", "complex", "order"] = Field(
description=(
"simple: общие вопросы, такие как часы работы или приветствия. "
"complex: консультации, требующие тщательного рассуждения, такие как споры или возвраты. "
"order: запросы на поиск конкретного заказа."
)
)
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
def classify(state: State):
"""Классифицировать тип запроса клиента."""
question = state["messages"][-1].content
result = classifier_llm.invoke(
f"Классифицируй запрос клиента.\n\nЗапрос: {question}"
)
return {"intent": result.intent}Мы использовали самую маленькую модель (gpt-5.4-nano) для классификации. Решение «какого типа этот запрос?» — простая задача, которая не требует высокопроизводительной модели. А поскольку узел классификации — это шлюз, через который проходит каждый запрос, предпочтительнее дешёвая и быстрая модель.
Далее идёт функция маршрутизации. Она просто возвращает результат классификации, хранящийся в состоянии.
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
"""Определить следующий узел на основе результата классификации."""
return state["intent"]Узел classify уже решил, какой узел должен выполняться следующим, и записал это в intent, поэтому функция маршрутизации просто возвращает это значение как есть.
16.2.3) Узлы-обработчики по типам
Теперь давайте построим обработчики для каждого из трёх типов запросов.
Обработчик простых запросов вызывает маленькую модель один раз. В реальном агенте поддержки клиентов вы бы применили RAG для поиска ответов во внутренних документах, но мы оставили обработчик простым, чтобы сосредоточиться на теме этой главы.
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
def simple_handler(state: State):
"""Отвечать на простые запросы маленькой моделью."""
response = simple_llm.invoke(state["messages"])
return {"messages": [response]}Обработчик сложных консультаций использует высокопроизводительную модель. По той же причине, что и обработчик простых запросов, мы оставили его простым — он просто генерирует ответ.
complex_llm = ChatOpenAI(model="gpt-5.4")
def complex_handler(state: State):
"""Отвечать на сложные консультации высокопроизводительной моделью."""
response = complex_llm.invoke(state["messages"])
return {"messages": [response]}Обработчик поиска заказов должен использовать инструмент поиска заказов, что означает, что ему нужен цикл вызова инструментов. Поскольку его структура идентична стандартному агенту, вызывающему инструменты, мы построим его с помощью create_agent.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Найти статус доставки заказа по номеру заказа."""
fake_data = {"12345": "В пути, ожидается завтра", "67890": "Доставлено"}
return fake_data.get(order_id, f"Заказ {order_id} не найден.")
order_agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[get_order_status],
)16.2.4) Сборка и запуск графа
Давайте соединим все узлы, которые мы построили, в граф. Мы поместим classify в начальную точку, соединим его с тремя обработчиками через условное ребро и настроим каждый обработчик на завершение после того, как он закончит.
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent) # Регистрируем граф create_agent как узел
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
agent = builder.compile()Давайте запустим по одному запросу каждого типа и посмотрим, какой обработчик его обрабатывает.
from langchain_core.messages import HumanMessage
for question in [
"Какие у вас часы работы?",
"Мой заказ пришёл повреждённым. Мне стоит запросить обмен или возврат?",
"Каков статус доставки заказа 12345?",
]:
result = agent.invoke({"messages": [HumanMessage(content=question)]})
print(f"В: {question}")
print(f"[{result['intent']}] О: {result['messages'][-1].content}\n")Вывод:
В: Какие у вас часы работы?
[simple] О: У меня нет фиксированных часов работы — я доступен круглосуточно.
...
В: Мой заказ пришёл повреждённым. Мне стоит запросить обмен или возврат?
[complex] О: Если ваш заказ пришёл повреждённым, вы, как правило, имеете право на **замену/обмен или полный возврат средств**.
...
В: Каков статус доставки заказа 12345?
[order] О: Заказ 12345 **в пути** и **ожидается завтра**.Каждый запрос был обработан по своему пути. simple_handler ответил на простой запрос маленькой моделью, complex_handler ответил на сложную консультацию высокопроизводительной моделью, а order_agent вызвал инструмент get_order_status, чтобы ответить на поиск заказа.