Python & AI Tutorials Logo
LangChain & LangGraph

18. Мультиагентные системы — паттерн супервизора

Вспомните агента службы поддержки, которого мы создали в главе 16. У него был ровно один инструмент — поиск заказа — так что когда клиент спрашивал о заказе, агент сообщал статус доставки. Имея всего один инструмент, он попросту не мог выбрать неправильный.

Теперь представьте, что мы развиваем этого агента в настоящий продакшн-сервис. Одного поиска заказа недостаточно. Нам потребуется отмена заказа, отслеживание отправления, изменение адреса, запросы на обмен, запросы на возврат, проверка права на возврат средств, обработка возврата средств, проверка склада, выдача купонов, поиск баллов, создание тикетов поддержки и многое другое. Механика проста: продолжаем добавлять инструменты и дописывать бизнес-правила в системный промпт.

Но по мере того как инструменты и правила накапливаются, возникают три проблемы.

  • Выбор инструмента становится менее точным. На каждом шаге модель читает все описания инструментов и решает, какой из них вызвать. По мере добавления инструментов, которые принимают одинаковые входные данные и пересекаются по назначению — например, запрос на обмен и запрос на возврат — вероятность выбрать неправильный растёт.

  • Контекст заполняется информацией, которая вам сейчас не нужна. Даже при обработке возврата средств схема инструмента проверки склада, правила выдачи купонов, процедура запроса на обмен и всё остальное едут вместе с каждым вызовом. Вы отправляете полный набор схем инструментов и бизнес-правила каждого домена всякий раз. Когда контекст переполнен материалом, не относящимся к текущей задаче, важные части оказываются погребены, и точность страдает.

  • Становится трудно вносить изменения. Изменение одного правила возврата означает редактирование промпта, где правила всех доменов переплетены вместе, и вы не можете быть уверены, что изменение не аукнется в обменах или доставке. Если у разных доменов разные владельцы, проблема только усугубляется.

Эта глава учит одному способу справиться с этим. Вместо того чтобы нагромождать всё больше инструментов и правил на одного агента, мы разделяем работу — один агент на домен. Продолжая сценарий службы поддержки из главы 16, мы создадим команду из агента, который занимается только поиском заказов, и агента, который занимается только возвратами. У каждого свои инструменты и свой промпт. Затем мы добавим ещё одного агента, чтобы им руководить — этот руководитель называется супервизором(supervisor). Конструкция с несколькими агентами вроде этой — это мультиагентная система(multi-agent system), а устройство, при котором супервизор командует остальными, — это паттерн супервизора(supervisor pattern).

Мультиагентные системы всё же имеют свою цену. Поскольку супервизору приходится решать, какому исполнителю делегировать на каждом шаге, вызовов LLM становится больше, а это означает больше задержек и больше затрат. Так что если у вас не так много инструментов и правил, тянуться к мультиагентной системе вообще не нужно.

Вот план. В 18.1 мы рассмотрим, что такое мультиагентная система и как она работает. В 18.2 мы создадим агентов-исполнителей. В 18.3 мы вручную создадим супервизора с помощью объекта Command из LangGraph. В 18.4 мы обернём исполнителей как инструменты и пересоберём ту же команду с гораздо меньшим объёмом кода.

18.1) Понимание мультиагентных систем

18.1.1) Что такое мультиагентная система

Давайте спроектируем агента, который может обработать запрос: «Мой заказ так и не пришёл — если что-то не так, верните деньги». Мы могли бы построить его на всём, что уже узнали: прикрепить к одному агенту инструмент поиска заказа и инструмент возврата и позволить агенту работать в цикле, пока задача не будет выполнена. Один агент ищет заказ, подтверждает, что доставка не удалась, видит этот результат, запрашивает возврат и пишет итоговый ответ.

Мультиагентная система(multi-agent system) — это структура, в которой несколько агентов делят эту работу между собой. Вы разделяете агентов по доменам и ставите над ними супервизора, и каждый агент держит только те инструменты, которые ему нужны. Агент, которого мы только что набросали, например, естественным образом делится на агента поиска заказа и агента возврата. Супервизор сначала вызывает агента поиска заказа, чтобы проверить статус доставки; когда в ответ приходит, что доставка не удалась, он вызывает агента возврата, чтобы обработать возврат; затем он собирает оба результата, чтобы ответить клиенту. То, что раньше было одним агентом, вызывающим инструменты по очереди, стало супервизором, вызывающим агентов по очереди.

Если бы было всего два инструмента, не было бы причины разделять таким образом. Одного агента было бы достаточно, а добавление супервизора только добавило бы вызовов LLM. Но как мы видели во введении, когда инструментов становится много, агент может выбрать неправильный, его контекст заполняется информацией, не относящейся к текущей задаче, а его промпт становится трудно менять.

Разделение решает эти проблемы. Агент поиска заказа видит лишь несколько инструментов, связанных с заказами, так что выбор из списка десятков сжимается до выбора из горстки. Его промпт содержит только правила поиска заказа, так что политика возврата, условия купонов и другие вопросы, не относящиеся к поиску заказа, не заполняют его контекст. А когда вам нужно изменить правило возврата, вы трогаете только агента возврата, так что изменение не доходит до остальных.

Агент поиска заказа и агент возврата здесь ничем не примечательны. Это тот же тип агента, что вы создали в главе 16. Вы создаёте их, передавая модель, инструменты и промпт в create_agent, и вызываете их через invoke. Просто они охватывают меньшую область.

Так что же делает супервизор? Агент поиска заказа и агент возврата не знают о существовании друг друга. Каждый просто делает свою работу; ни один не знает, кто должен идти первым. В примере выше вызвать сначала поиск заказа и — только увидев его результат — вызвать возврат было решением супервизора.

Кто кого вызывает и когда. Это то, что мы называем оркестрацией.

18.1.2) Паттерн супервизора

Паттерн супервизора(supervisor pattern) — это структура, в которой единый центральный супервизор оркеструет несколько агентов-исполнителей. Он следует этим правилам:

  • Супервизор никогда не делает работу сам. Он не ищет заказы и не обрабатывает возвраты. Он лишь решает, кому передать управление, а затем собирает возвращённые результаты в итоговый ответ.
  • Исполнители никогда не вызывают друг друга. Агент поиска заказа никогда не вызывает агента возврата напрямую. Каждый путь проходит через супервизора.
  • Только супервизор общается с клиентом. Исполнители докладывают супервизору, а не клиенту.

Так как же супервизор решает, какого агента вызвать? Решает LLM. Супервизор читает весь разговор на текущий момент и выносит суждение. Если он ещё не знает статус заказа, он вызывает агента поиска заказа; как только он подтвердил, что доставка не удалась и нужен возврат, он вызывает агента возврата.

Супервизор повторяет это суждение, пока запрос пользователя не будет выполнен. Он вызывает агента, получает отчёт, перечитывает разговор — теперь, когда отчёт добавлен, — и решает, какого агента вызвать следующим.

Этот цикл имеет ту же структуру, что и цикл, который вы построили в главе 14.

  • Думать — прочитать разговор на текущий момент и решить, какого агента вызвать.
  • Действовать — запустить выбранного агента.
  • Наблюдать — взять отчёт агента и добавить его в разговор.

В главе 14 вы вызывали инструменты; здесь вы вызываете агентов. Это единственное, что меняется.

делегировать

делегировать

отчёт

отчёт

готово

Запрос клиента

Супервизор

Агент поиска заказа

Агент возврата

Ответ клиенту

Обратите внимание, как стрелки замыкаются обратно на супервизора. Когда исполнитель заканчивает, он докладывает супервизору, а супервизор читает этот отчёт и решает следующий шаг.

Именно супервизор завершает цикл. Как только он решает, что запрос клиента полностью обработан, он выдаёт итоговый ответ и останавливается.

18.2) Создание агентов-исполнителей

Давайте создадим двух агентов-исполнителей, которых мы спроектировали в 18.1: исполнитель поиска заказа, который проверяет статус заказа, и исполнитель возврата, который обрабатывает возвраты. Супервизора мы создадим в 18.3.

Агент-исполнитель — это просто обычный агент, которого вы создали в главе 16. Мы быстро создадим их с помощью create_agent.

Сначала настроим данные о заказах, которые будут общими для обоих исполнителей.

python
ORDERS = {
    "12345": {"item": "Wireless Earbuds", "amount": 89,
              "status": "in_transit", "status_text": "В пути (прибудет завтра)"},
    "67890": {"item": "Mechanical Keyboard", "amount": 129,
              "status": "delivered", "status_text": "Доставлен"},
    "24680": {"item": "Noise-Cancelling Headphones", "amount": 249,
              "status": "delivery_failed", "status_text": "Доставка не удалась (возвращён — получатель не найден)"},
}

У исполнителя поиска заказа всего один инструмент.

python
from langchain.tools import tool
from langchain.agents import create_agent
 
@tool
def get_order_status(order_id: str) -> str:
    """Ищет товар, оплаченную сумму и статус доставки по номеру заказа."""
    order = ORDERS.get(order_id)
    if order is None:
        return f"Заказ {order_id} не найден."
 
    return (f"Заказ {order_id}: {order['item']}, "
            f"${order['amount']:,}, статус: {order['status_text']}")
 
 
order_agent = create_agent(
    name="order_expert",
    model="openai:gpt-5.4-mini",
    tools=[get_order_status],
    system_prompt=(
        "Вы специалист по поиску заказов. Найдите статус заказа и ответьте.\n"
        "Включите в итоговый ответ номер заказа, товар, оплаченную сумму и статус доставки.\n"
        "Не судите о том, оправдан ли возврат, и никак не упоминайте возвраты. Ваша роль — только поиск заказа и сообщение о статусе."
    ),
)

У исполнителя возврата два инструмента: один решает, имеет ли заказ право на возврат, а другой фактически обрабатывает возврат.

python
@tool
def check_refund_eligibility(order_id: str) -> str:
    """Определяет, имеет ли заказ право на возврат. Право имеют только заказы с неудавшейся доставкой."""
    order = ORDERS.get(order_id)
    if order is None:
        return f"Заказ {order_id} не найден."
 
    if order["status"] == "delivery_failed":
        return f"Заказ {order_id} имеет право на возврат (причина: неудавшаяся доставка)."
 
    return f"Заказ {order_id} не имеет права на возврат (текущий статус: {order['status_text']})."
 
 
@tool
def issue_refund(order_id: str) -> str:
    """Обрабатывает возврат. Всегда подтверждайте право на возврат через check_refund_eligibility, прежде чем вызывать это."""
    order = ORDERS.get(order_id)
    if order is None:
        return f"Заказ {order_id} не найден."
 
    return (f"Возврат выполнен: ${order['amount']:,} по заказу {order_id} "
            f"будет возвращён в течение 3–5 рабочих дней. (номер одобрения: RF-{order_id})")
 
 
refund_agent = create_agent(
    name="refund_expert",
    model="openai:gpt-5.4-mini",
    tools=[check_refund_eligibility, issue_refund],
    system_prompt=(
        "Вы специалист по обработке возвратов.\n"
        "Всегда сначала подтверждайте право на возврат через check_refund_eligibility, и только "
        "затем вызывайте issue_refund.\n"
        "Если вы обработали возврат, включите в итоговый ответ сумму и номер одобрения.\n"
        "Если заказ не имеет права на возврат, не обрабатывайте его — вместо этого сообщите причину."
    ),
)

Мы дали обоим исполнителям name. Именно это имя create_supervisor в 18.5 использует как имя узла и имя инструмента передачи управления.

Четыре вещи, которые нужны системному промпту исполнителя

Системный промпт исполнителя строится из четырёх элементов — принципов, которые Anthropic вывела, создавая собственную мультиагентную исследовательскую систему. Когда они слабы, исполнители дублируют работу, оставляют задачи невыполненными или не находят нужную им информацию.

Элементorder_agentrefund_agent
РольВы специалист по поиску заказовВы специалист по обработке возвратов
Указания по инструментамВсегда сначала подтверждайте через check_refund_eligibility, и только затем вызывайте issue_refund
Формат выводаВключите в итоговый ответ номер заказа, товар, оплаченную сумму и статус доставкиЕсли вы обработали возврат, включите в итоговый ответ сумму и номер одобрения
Границы задачиНе судите о том, оправдан ли возврат, и никак не упоминайте возвраты. Ваша роль — только поиск заказа и сообщение о статусе.Если заказ не имеет права на возврат, не обрабатывайте его — сообщите причину

Роль фиксирует в одном предложении, кто этот исполнитель. Закрепление его идентичности словами «Вы специалист по поиску заказов» удерживает модель сосредоточенной на её собственной работе и снижает вероятность того, что она забредёт в чужую.

Указания по инструментам. Пишите это, когда есть что-то, что схема инструмента сама по себе не может передать — например, порядок и условия, при которых инструменты должны использоваться. Если добавить к схеме нечего, можно оставить это.

Формат вывода и границы задачи имеют огромное значение в мультиагентной среде.

Формат вывода. Итоговый ответ исполнителя — это не ответ клиенту, а отчёт, предоставляемый супервизору. Всё, что там не написано, никогда не доходит до супервизора. Если исполнитель находит сумму с помощью инструмента, но оставляет её за рамками итогового ответа, у супервизора нет способа об этом узнать.

Границы задачи. Это область, которая определяет, как далеко исполнитель может зайти и что он не должен делать. Исполнитель поиска заказа должен только искать — никогда не возвращать. Именно поэтому мы не дали ему инструмент возврата. Но одного лишь удержания инструмента недостаточно, потому что модель всё равно может сказать: «Доставка не удалась, поэтому я оформлю вам возврат» — вообще без инструмента. Если это предложение дойдёт до супервизора, супервизор может решить, что возврат уже в процессе, и никогда не вызвать исполнителя возврата. Поэтому промпт также говорит: «Не судите о том, оправдан ли возврат, и никак не упоминайте возвраты» — блокируя его даже от упоминания возвратов на словах.

Выбор моделей

Мы используем gpt-5.4-mini для исполнителей и gpt-5.4 для супервизора. Исполнитель выполняет простую работу — вызвать несколько инструментов в заданном порядке, так что небольшой модели вполне достаточно. Супервизору приходится читать весь разговор и решать, кого вызвать следующим, так что ему нужна модель побольше. Возможность выбирать модель для каждого агента, подобранную под сложность его работы, — ещё одно преимущество разделения.

Теперь мы можем добавить супервизора.

18.3) Создание супервизора вручную

Давайте создадим супервизора вручную. На практике вы чаще всего будете использовать подход, при котором фреймворк делает это за вас (рассмотрен в следующем разделе), но чтобы понять, что происходит под капотом, вам нужно один раз построить его самому.

Как мы видели в 18.1, то, что делает супервизор, — это один цикл: вызвать исполнителя, прочитать отчёт и решить, кого вызвать следующим — или стоит ли остановиться — снова и снова.

18.3.1) Передачи управления и Command

Чтобы этот цикл вращался, управление должно переходить туда-сюда между супервизором и исполнителями. Супервизор передаёт управление — «этот исполнитель идёт следующим» — а когда исполнитель заканчивает, он передаёт управление обратно супервизору. Эта передача управления от одного узла к другому называется передачей управления(handoff).

Передаче управления нужны два фрагмента информации: куда идти (пункт назначения) и что передать (полезная нагрузка). Пункт назначения всегда обязателен; полезная нагрузка включается только когда есть что передать. В LangGraph узел указывает и то, и другое, возвращая Command.

python
from typing import Literal
from langgraph.graph import MessagesState
from langgraph.types import Command
 
def some_node(state: MessagesState) -> Command[Literal["refund_expert_proxy"]]:
    return Command(
        goto="refund_expert_proxy",             # куда: узел, который запустить следующим
        update={"messages": [...]},             # что: отчёт исполнителя (полезная нагрузка, добавляемая в State)
    )

goto — это пункт назначения; update — полезная нагрузка. Аннотация возвращаемого типа Command[Literal["refund_expert_proxy"]] заранее перечисляет пункты назначения, куда может пойти этот узел. Как этот Command фактически используется, мы увидим в следующем разделе, где будем строить узел супервизора и прокси исполнителей.

18.3.2) Создание цикла супервизора

Сама структура проста. Мы создаём один узел супервизора и столько прокси исполнителей, сколько нам нужно. Прокси исполнителя — это узел, который вызывает назначенного ему агента-исполнителя от имени супервизора. Точка входа — это супервизор, а каждый прокси исполнителя, закончив работу, возвращается к супервизору — образуя цикл. Command, который возвращает каждый узел, — это то, что решает, куда управление пойдёт следующим.

goto=order_expert_proxy

goto=refund_expert_proxy

invoke

invoke

goto

goto

goto=END

START

supervisor

order_expert_proxy

refund_expert_proxy

order_agent

refund_agent

END

Сплошные линии — это передачи управления между узлами (goto); пунктирные линии — прокси исполнителя, вызывающий своего агента-исполнителя (invoke). Супервизор передаёт управление прокси исполнителя, а прокси исполнителя, закончив, возвращается к супервизору. Когда супервизор решает FINISH, он выходит на END.

Теперь давайте превратим эту картину в код. Сначала класс Route. Route — это схема для получения ответа супервизора в виде структурированного ответа, когда мы спрашиваем LLM, какой прокси исполнителя вызвать следующим. Если бы LLM ответила свободным текстом на естественном языке, было бы трудно понять, какой прокси исполнителя запускать. Route содержит, какой прокси исполнителя вызвать следующим (next), и причину, по которой было принято такое решение (reason).

python
from typing import Literal
from pydantic import BaseModel, Field
 
class Route(BaseModel):
    reason: str = Field(description="Причина этого решения.")
    next: Literal["order_expert_proxy", "refund_expert_proxy", "FINISH"] = Field(
        description="Узел-исполнитель, который запустить следующим. FINISH, если запрос полностью обработан."
    )

Есть причина, по которой reason объявлен раньше next. Структурированный вывод генерируется в том порядке, в котором поля появляются в схеме, так что с reason первым LLM пишет своё рассуждение прежде, чем выбрать исполнителя. Сначала рассуждать, а потом решать — это даёт лучший выбор. Переверните порядок — поставьте next первым — и LLM выберет исполнителя ещё до того, как вообще рассуждала, а затем задним числом подгонит обоснование под выбор, который, возможно, уже сделала неправильно.

Далее узел супервизора.

python
from typing import Literal
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
supervisor_llm = ChatOpenAI(model="gpt-5.4")
 
SUPERVISOR_PROMPT = (
    "Вы супервизор команды службы поддержки. Вы управляете двумя исполнителями:\n"
    "- order_expert_proxy: ищет статус заказа.\n"
    "- refund_expert_proxy: проверяет право на возврат и обрабатывает возвраты.\n"
    "Чтобы решить, нужен ли возврат, вы должны сначала проверить статус заказа.\n"
    "Назначайте задачу одному исполнителю за раз и отвечайте FINISH, как только запрос полностью обработан."
)
 
 
def supervisor(
    state: MessagesState,
) -> Command[Literal["order_expert_proxy", "refund_expert_proxy", "__end__"]]:
    messages = [{"role": "system", "content": SUPERVISOR_PROMPT}, *state["messages"]]
    decision = supervisor_llm.with_structured_output(Route).invoke(messages)
    print(f"[supervisor] → {decision.next} ({decision.reason})")
 
    if decision.next == "FINISH":
        final = supervisor_llm.invoke(
            [{"role": "system", "content": "На основе разговора на текущий момент напишите ответ клиенту."},
             *state["messages"]]
        )
        return Command(goto=END, update={"messages": [final]})   # передаём управление на END
 
    return Command(goto=decision.next)                            # передаём управление прокси исполнителя

До сих пор узлы возвращали только изменённое State. Но узел супервизора возвращает Command. Когда узел возвращает Command, LangGraph делает две вещи: применяет содержимое update к State и запускает узел, названный в goto, следующим. В коде выше мы помещаем имя прокси исполнителя, извлечённое из decision.next, в goto, так что узел, который выбрала LLM — decision.next — и запускается.

Далее прокси исполнителей. Прокси исполнителя делает свою работу и возвращает управление супервизору — именно поэтому он передаёт управление через goto="supervisor".

Работа прокси исполнителя проста: вызвать своего агента-исполнителя через invoke.

python
def order_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
    result = order_agent.invoke(state)
    last = result["messages"][-1]
    return Command(
        goto="supervisor",
        update={"messages": [HumanMessage(content=last.content, name="order_expert")]},
    )
 
 
def refund_expert_proxy(state: MessagesState) -> Command[Literal["supervisor"]]:
    result = refund_agent.invoke(state)
    last = result["messages"][-1]
    return Command(
        goto="supervisor",
        update={"messages": [HumanMessage(content=last.content, name="refund_expert")]},
    )

Обратите внимание, что мы извлекаем только итоговое сообщение исполнителя и передаём его супервизору. Супервизору нужен только вывод; ему не нужно знать, сколько раз исполнитель вызывал инструменты внутри себя.

18.3.3) Соединение и запуск графа

Мы регистрируем три узла и подключаем к супервизору только точку входа. Каждый другой переход решается Command каждого узла, так что больше рёбер не нужно.

python
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("order_expert_proxy", order_expert_proxy)
builder.add_node("refund_expert_proxy", refund_expert_proxy)
builder.add_edge(START, "supervisor")
 
team = builder.compile()
 
result = team.invoke(
    {"messages": [HumanMessage(
        content="Заказ 24680 всё ещё не пришёл. Если что-то не так, пожалуйста, верните деньги."
    )]},
    config={"recursion_limit": 15},
)

Вывод:

[supervisor] → order_expert_proxy (Нужно проверить статус заказа, прежде чем принимать решение о возврате.)
[supervisor] → refund_expert_proxy (Неудавшаяся доставка подтверждена, так что проверяем право на возврат и обрабатываем его.)
[supervisor] → FINISH (Проверка заказа и обработка возврата обе завершены.)

Супервизор сначала направил к поиску заказа. Когда пришёл отчёт о том, что доставка не удалась, он направил к возврату, а когда пришёл отчёт о завершении возврата, он закончил. Он выбирал каждый следующий пункт назначения, читая отчёт предыдущего агента.

Прокси исполнителя передаёт всё общее State своему исполнителю через invoke(state), так что каждый исполнитель видит весь разговор на текущий момент. С всего двумя исполнителями это нормально, но по мере роста исполнителей и разговора каждый исполнитель в итоге читает сообщения, которые не имеют ничего общего с его собственной работой. Мы решим это по-другому в следующем разделе.

18.4) Делегирование исполнителям через инструменты

Построив внутренности супервизора вручную в 18.3, давайте теперь пересоберём ту же команду тем способом, который рекомендуется для реальных проектов. Этот подход не требует нового API. Вы превращаете каждого исполнителя в инструмент с помощью @tool и даёте эти инструменты агенту-супервизору. Мы просто создадим агента-супервизора с помощью create_agent.

Ключевая идея умещается в одно предложение: супервизор сам по себе просто агент, и каждый исполнитель становится инструментом, который супервизор вызывает.

вызов инструмента lookup_order

вызов инструмента handle_refund

строка результата

строка результата

Клиент

Агент-супервизор

order_agent

refund_agent

Ответ клиенту

При таком взгляде супервизор имеет ту же структуру, что и агент с вызовом инструментов, которого вы создали в главе 16. Просто он держит высокоуровневые инструменты, которые вызывают агентов, вместо низкоуровневых инструментов вроде get_order_status.

18.4.1) Обёртывание исполнителей как инструментов

Мы используем order_agent и refund_agent из 18.2 без изменений. Всё, что мы делаем, — оборачиваем каждого в функцию @tool.

python
from langchain.tools import tool
 
# order_agent и refund_agent — это исполнители из 18.2
 
@tool
def lookup_order(request: str) -> str:
    """Ищет товар заказа, оплаченную сумму и статус доставки. Используйте это, когда нужно узнать статус заказа.
 
    Вход: запрос на поиск на естественном языке (например, 'Скажи мне статус доставки заказа 24680').
    """
    print("[tool call] lookup_order")
    print(f"    request: {request}")
 
    result = order_agent.invoke({"messages": [{"role": "user", "content": request}]})
    return result["messages"][-1].content
 
 
@tool
def handle_refund(request: str) -> str:
    """Проверяет право на возврат и обрабатывает возврат. Используйте это, когда клиент хочет возврат, и вы уже подтвердили статус заказа.
 
    Вход: запрос на возврат на естественном языке. Включите номер заказа и статус доставки, подтверждённый поиском.
    (например, 'Заказ 24680 в статусе неудавшейся доставки. Обработай возврат, если он имеет право.')
    """
    print("[tool call] handle_refund")
    print(f"    request: {request}")
 
    result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})
    return result["messages"][-1].content

Изменились три вещи.

  • Описания инструментов заменяют логику маршрутизации. В 18.3 мы вручную писали SUPERVISOR_PROMPT и схему Route, чтобы сообщить супервизору список его исполнителей и его варианты выбора. Здесь эту работу делают докстринги инструментов. LLM супервизора читает описания инструментов и решает, когда что вызвать.

  • Каждый исполнитель стартует с чистого контекста. Поставленная бок о бок с 18.3, разница очевидна.

    python
    # 18.3 (граф вручную): передаёт всё общее State
    result = refund_agent.invoke(state)
     
    # 18.4 (делегирование через инструмент): передаёт только описание задачи, написанное супервизором
    result = refund_agent.invoke({"messages": [{"role": "user", "content": request}]})

    В 18.4 исполнитель возврата получает всего одно предложение, описывающее его задачу. Он никогда не видит исходную формулировку клиента, рассуждения супервизора или историю вызовов инструментов другого исполнителя. Даже с десятью исполнителями и сотней ходов разговора контекст каждого исполнителя — это по-прежнему всего лишь то самое описание задачи.

  • В обмен супервизор берёт на себя работу по передаче информации. Поскольку исполнитель не может видеть историю разговора, всё, что ему нужно, должно быть упаковано в строку request супервизором. Именно поэтому докстринг handle_refund прямо прописывает: «Включите номер заказа и статус доставки, подтверждённый поиском». Без этого указания супервизор мог бы передать только "Обработай возврат", оставив исполнителя возврата в неведении, о каком заказе вообще идёт речь.

18.4.2) Сборка и запуск супервизора

При этом подходе супервизор тоже просто агент. Ни Command, ни схема Route не нужны.

python
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
 
TOOL_SUPERVISOR_PROMPT = (
    "Вы супервизор команды службы поддержки.\n"
    "Чтобы решить, нужен ли возврат, вы должны сначала проверить статус заказа.\n"
    "Не делайте работу сами — делегируйте исполнителям.\n"
    "Исполнители не могут видеть этот разговор. Когда делегируете, поместите всё, что им нужно, в запрос.\n"
    "Когда вся работа сделана, синтезируйте результаты исполнителей в ответ клиенту."
)
 
supervisor_agent = create_agent(
    model="openai:gpt-5.4",
    tools=[lookup_order, handle_refund],
    system_prompt=TOOL_SUPERVISOR_PROMPT,
)
 
result = supervisor_agent.invoke(
    {"messages": [HumanMessage(
        content="Заказ 24680 всё ещё не пришёл. Если что-то не так, пожалуйста, верните деньги."
    )]}
)
 
print("\n\n[final response]")
print(result["messages"][-1].content)

Конечный результат тот же, что и в 18.3.

[tool call] lookup_order
    request: Клиент говорит, что заказ 24680 ещё не пришёл. Чтобы решить, нужен ли
             возврат, пожалуйста, дай мне товар, оплаченную сумму и текущий статус доставки заказа 24680.
[tool call] handle_refund
    request: Заказ 24680 — это Noise-Cancelling Headphones, $249, и его статус доставки
             подтверждён как 'Доставка не удалась (возвращён — получатель не найден)'. Клиент
             запрашивает возврат, так что проверь право на возврат и обработай его, если он имеет право.
 
 
[final response]
Я проверил, и заказ 24680 был в статусе неудавшейся доставки (возвращён — получатель не найден).
 
Он получил право на возврат, и я завершил возврат.
- Товар: Noise-Cancelling Headphones
- Сумма возврата: $249
- Номер одобрения возврата: RF-24680
 
В зависимости от вашего способа оплаты обычно требуется несколько рабочих дней, чтобы возврат поступил.

Но посмотрите на вызовы инструментов, которые супервизор сделал по пути — именно там проявляется отличие от 18.3. Посмотрите на request в handle_refund. Супервизор сам обобщил более ранний результат поиска и написал описание задачи. Исполнитель возврата получает только это одно предложение. Там, где 18.3 передавал исполнителю весь разговор и позволял ему копаться в нём, здесь супервизор выбирает только то, что нужно, и передаёт это дальше.

18.4.3) Добавление памяти супервизору с помощью чекпоинтера

Утверждение, что супервизор — это обычный агент, означает, что чекпоинтинг, который вы изучили в главе 17, работает на нём как есть.

python
from langgraph.checkpoint.memory import InMemorySaver
 
supervisor_agent = create_agent(
    model="openai:gpt-5.4",
    tools=[lookup_order, handle_refund],
    system_prompt=TOOL_SUPERVISOR_PROMPT,
    checkpointer=InMemorySaver(),      # чекпоинтер ставится только на агента верхнего уровня
)
 
config = {"configurable": {"thread_id": "cs-1"}}
 
supervisor_agent.invoke(
    {"messages": [HumanMessage(content="Где мой заказ 12345?")]},
    config,
)
follow_up = supervisor_agent.invoke(
    {"messages": [HumanMessage(content="Сколько он стоил?")]},
    config,
)
print(follow_up["messages"][-1].content)

Вывод:

Ваш заказ 12345, Wireless Earbuds, стоил $89.

Супервизор корректно понимает его в уточняющем вопросе как заказ 12345 из предыдущего хода. Чекпоинтер работает на супервизоре точно так же.

Не прикрепляйте чекпоинтер к агентам-исполнителям (order_agent, refund_agent). Если вы это сделаете, исполнитель перенесёт результаты своего предыдущего вызова в текущий, что может помешать текущей задаче. Без него исполнитель работает на основе не более чем запроса супервизора. Для субагентов это рекомендуемое значение по умолчанию.

18.5) create_supervisor: форма, которую вы встретите в legacy-коде

В существующих кодовых базах и старых руководствах вы столкнётесь с хелпером create_supervisor из пакета langgraph-supervisor. Дайте ему список агентов и промпт, и он построит весь граф супервизора за вас.

python
from langgraph_supervisor import create_supervisor
from langchain_openai import ChatOpenAI
 
workflow = create_supervisor(
    agents=[order_agent, refund_agent],   # у каждого агента должно быть задано имя
    model=ChatOpenAI(model="gpt-5.4"),
    prompt="Назначайте проверки заказов order_expert, а возвраты refund_expert.",
)
app = workflow.compile()

create_supervisor — это хелпер, который собирает команду супервизора одним вызовом функции (подход с передачей управления из 18.3). Однако не используйте его в новых проектах. Это legacy, который LangChain больше не рекомендует, и внутри он зависит от create_react_agent, который был объявлен устаревшим в v1 (удаление запланировано на v2). LangChain рекомендует супервизора на основе инструментов, которого вы изучили в 18.4.


В этой главе мы взяли агента, который жил внутри одного графа, и расширили его в команду — разделённую по доменам, оркеструемую супервизором. Мы построили ту же команду тремя способами: граф с Command вручную в 18.3, подход с делегированием через инструменты в 18.4 и legacy-хелпер create_supervisor в 18.5. Для реальной работы сделайте подход с делегированием через инструменты вашим значением по умолчанию.