Мультиагентные системы для бизнеса: когда одного агента мало

Мультиагентные системы для бизнеса: когда один ИИ-агент — не предел

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

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

Мультиагентная система — это не просто несколько промптов

Начнём с определения, потому что термин размытый. Мультиагентная система — это несколько специализированных агентов, у каждого своя узкая зона ответственности, свой промпт и свои инструменты, а решение «кто сейчас работает» принимает маршрутизатор (router). Ключевое здесь — не «мы делаем несколько вызовов LLM в скрипте», а то, что у каждого агента отдельная «голова», принимающая решения в своей области.

Полезно разложить путь к мультиагентности на три уровня, от самого слабого к самому сильному.

Уровень 1. Один агент, длинный промпт. Это анти-паттерн, из которого мы убегаем. Одна модель, один разросшийся системный промпт: «ты консультант, отвечай на вопросы о компании, но если спрашивают про заказ, найди его в базе, а если клиент ругается, эскалируй, и не забудь проверить, имеет ли он право на эту операцию». Такой промпт быстро собирается и хорошо смотрится в демо, но в проде забывает половину правил и путает инструкции для разных ситуаций.

Уровень 2. Один граф, router → специализированные узлы. По-прежнему один StateGraph и одно общее состояние, но каждый «специалист» это отдельный узел со своим системным промптом и своими инструментами. Маршрутизатор в начале графа решает, в какую ветку отправить диалог. Это уже настоящая специализация: узел orders не знает ничего про справочные ответы, а узел info не умеет создавать заказы, и это хорошо.

Уровень 3. Граф из графов. У каждого специалиста здесь всё своё: схема состояния, набор инструментов, при желании даже своя модель, а сам он полностью самостоятельный, скомпилированный агент. Такой агент вставляется как узел в родительский граф. Вот это и есть «настоящая» мультиагентная система для всего, что сложнее пары сценариев.

Разница между уровнями 2 и 3 не косметическая. На втором уровне все узлы делят одно состояние, и вы не можете дать узлу orders собственную форму памяти или свою модель, не задев остальных. На третьем каждый агент изолирован, и это открывает независимую разработку, тестирование и подбор модели под задачу. Дальше разберём оба уровня на коде.

Шаг 1: router внутри одного графа (специализация без изоляции)

Самый практичный первый шаг, это маршрутизатор, который на основе последних сообщений диалога выбирает ветку. Проще всего сделать это через structured output: модель возвращает не свободный текст, а строго одно из заранее описанных значений.

class DialogRoutes(BaseModel):
    routes: Literal["ORDERS", "INFO", "CHAT"] = "CHAT"

def router(state: State) -> str:
    system = "Определи по истории диалога маршрут: ORDERS / INFO / CHAT"
    structured_llm = llm.with_structured_output(schema=DialogRoutes)
    decision = structured_llm.invoke([SystemMessage(content=system)] + state["messages"][-4:])
    return decision.routes

Обратите внимание: в маршрутизатор мы отдаём не всю историю, а только последние несколько сообщений (state["messages"][-4:]). Этого достаточно, чтобы понять текущее намерение, и заодно экономит токены. Дальше подключаем маршрутизатор к графу через условные рёбра:

graph_builder.add_conditional_edges(START, router, {
    "ORDERS": "orders",
    "INFO": "info",
    "CHAT": "chat",
})
graph_builder.add_conditional_edges("orders", should_continue, ["orders_tool_node", END])
graph_builder.add_edge("orders_tool_node", "orders")

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

def info(state: State) -> State:
    system = "Ты знаток нашей компании, ответь на вопрос клиента по базе знаний."
    return {"messages": [llm.invoke([SystemMessage(content=system)] + state["messages"])]}

def orders(state: State) -> State:
    system = "Ты консультант по заказам. Можешь искать товары и оформлять заказы."
    llm_with_tools = llm.bind_tools([find_product, place_order])
    return {"messages": [llm_with_tools.invoke([SystemMessage(content=system)] + state["messages"])]}

Видно главное отличие от «одного агента на всё»: промпт узла info не знает ничего про заказы, а промпт узла orders не пытается быть справочником. Каждый занят своим, и его можно править, не боясь сломать соседа.

Узел orders дополнительно умеет вызывать инструменты (find_product, place_order), поэтому вокруг него крутится маленький цикл «модель → вызов инструмента → снова модель», который завершается, когда модель перестаёт запрашивать инструменты. Функция should_continue просто смотрит, есть ли в последнем сообщении вызовы инструментов:

def should_continue(state: State) -> Literal["orders_tool_node", END]:
    if state["messages"][-1].tool_calls:
        return "orders_tool_node"
    return END

Чтобы посмотреть, как это работает на живом примере, покрутите симуляцию: выберите обращение клиента и запустите маршрутизацию. Router классифицирует запрос и передаст его нужному специалисту.

Честный разбор компромисса. Этот подход уже решает главную боль: «один промпт делает слишком много». Но все узлы по-прежнему живут в одном State. Пока специалисты простые и близкие по смыслу, это нормально и даже удобно: общее состояние это общая история диалога, ничего лишнего. Ограничения начинаются, когда специалисту нужна собственная форма памяти, собственный набор инструментов или его хочется разрабатывать и тестировать полностью отдельно от остальных. Тогда пора на следующий уровень. Если вы ещё не собирали ни одного агента на LangGraph, начните с разбора базового графа в статье про LangGraph-агента для 1С: там подробно про состояние, узлы и инструменты.

Шаг 2: граф из графов, независимые агенты как узлы

Вот центральная идея статьи. В LangGraph скомпилированный граф сам по себе можно вставить как узел в другой граф. То есть специалист тут не функция, а целый самостоятельный агент, который снаружи выглядит просто: принимает состояние, возвращает состояние.

Соберём для примера систему триажа обращений в поддержку: верхний граф-диспетчер (triage) решает, кому отдать обращение, а специалисты, это отдельные скомпилированные графы. Сначала посмотрим, как выглядит один суб-агент. Это обычный StateGraph со своей схемой состояния, который мы компилируем и возвращаем наружу как готовый объект:

class OrdersState(TypedDict):
    request: str
    result: str

def build_orders_agent():
    g = StateGraph(OrdersState)
    g.add_node("lookup", orders_lookup)   # свои узлы, свои инструменты
    g.add_edge(START, "lookup")
    g.add_edge("lookup", END)
    return g.compile()                     # наружу — готовый агент

Обратите внимание: у orders своя схема состояния OrdersState, а не общий State из шага 1. Диспетчер про её внутренности ничего не знает. Сам узел-диспетчер, это функция, которая кладёт в состояние решение о маршруте:

def triage_node(state: TopState) -> TopState:
    text = state["request"].lower()
    if "верн" in text or "жалоб" in text:
        return {"decision": "escalation"}
    if "заказ" in text:
        return {"decision": "orders"}
    return {"decision": "end"}

В реальной системе triage_node вызывал бы LLM со structured output, здесь для наглядности простые правила. А дальше собирается родительский граф, в который скомпилированные агенты вставляются как узлы:

# Каждый специалист — самостоятельный, скомпилированный граф
orders_agent = build_orders_agent()      # свой State, свои инструменты, свой промпт
escalation_agent = build_escalation_agent()

def route_after_triage(state: TopState) -> str:
    return state["decision"]  # "orders" | "escalation" | "end"

top_graph = StateGraph(TopState)
top_graph.add_node("triage", triage_node)
top_graph.add_node("orders", orders_agent)          # скомпилированный граф как узел!
top_graph.add_node("escalation", escalation_agent)  # ещё один
top_graph.add_conditional_edges("triage", route_after_triage, {
    "orders": "orders", "escalation": "escalation", "end": END,
})
top_graph.add_edge("orders", END)
top_graph.add_edge("escalation", END)
compiled = top_graph.compile()

Что это даёт по сравнению с шагом 1:

  • Независимая тестируемость. Суб-агента orders можно вызвать напрямую с его собственной формой состояния, не поднимая весь родительский граф. Пишете тест ровно на одну обязанность.
  • Разные модели под разные задачи. Для orders подойдёт дешёвая быстрая модель, там простая логика поиска. Для escalation, где нужно аккуратно разобрать конфликт, можно поставить более сильную рассуждающую модель. На одном общем состоянии так не сделать.
  • Независимая разработка. Разные люди или команды владеют разными специалистами и не лезут во внутренности друг друга. Родительскому графу важно только одно: узел принимает TopState на вход и отдаёт TopState на выход. Что там внутри, это уже его дело.

Этот пример я прогнал целиком: топология с вложенными скомпилированными графами компилируется и корректно маршрутизирует обращения (запрос про заказ уходит в orders, конфликтный уходит в escalation, а «спасибо» завершает диалог), даже если тела узлов заменить на заглушки без реального обращения к LLM. Именно так и стоит отлаживать граф из графов: сначала убедиться, что топология правильная, а уже потом подключать модели.

Как агенты общаются между собой? В этом паттерне не напрямую, а через состояние родительского графа. Диспетчер кладёт в общее состояние то, что нужно специалисту (текст обращения, номер заказа, признак приоритета), специалист отрабатывает и возвращает результат обратно в состояние, откуда его забирает следующий шаг. Прямого «агент зовёт агента» здесь нет намеренно: так проще отлаживать систему, весь обмен виден в одном месте, в состоянии, а не растворён в перекрёстных вызовах. Это самый простой и предсказуемый способ связи. Более хитрые схемы, общая память, обмен сообщениями между агентами, стоит вводить только тогда, когда без них действительно не обойтись.

Отдельно про отладку. Как только графы начинают вкладываться друг в друга, держать топологию в голове становится тяжело. Возьмите за привычку рисовать её: graph.get_graph().draw_mermaid_png() отдаёт картинку структуры графа. Увидеть схему глазами гораздо надёжнее, чем верить, что рёбра расставлены правильно. На вложенных графах это особенно ценно: одна забытая связь вроде add_edge("orders", END) превращается в зависший диалог, который на картинке заметен сразу, а в логах его почти не видно. Тот же приём помогает и при код-ревью: схема графа объясняет архитектуру новому человеку быстрее, чем чтение кода узел за узлом.

У суб-агентов есть свой набор инструментов: поиск заказа, создание тикета, обращение к учётной системе. Это отдельный слой. Чтобы агент мог безопасно ходить в «1С», нужен коннектор с валидацией аргументов. Как его поднять, разобрано в статье про MCP-сервер для 1С: MCP как раз и есть тот стандартный «разъём», через который любой из этих агентов подключается к инструментам.

Хотите собрать такую систему под свои задачи? На курсе «ChatGPT для 1С» есть отдельная группа уроков «ИИ-агенты на LangGraph», от простого графа с маршрутизатором до композиции независимых агентов, и практические проекты, которые как раз выстраиваются в такую композицию.

SGR: router без полного графа, если это overkill

Не всякая мультиагентность требует тяжёлой инфраструктуры графов. Часто встречается более лёгкий вариант, schema-guided reasoning (SGR): вместо полноценного узла-маршрутизатора в графе делается один вызов llm.with_structured_output(RouterResponse), где RouterResponse, это размеченное объединение возможных следующих действий, а исполняется всё в обычном цикле, а не в графе.

class NextAction(BaseModel):
    action: Literal["lookup_order", "escalate", "answer"]
    reason: str

decision = llm.with_structured_output(NextAction).invoke(messages)
# дальше обычный if/elif по decision.action

Это тот самый «решение на 80%», когда вам ещё не нужны независимое состояние и инструменты у суб-агентов, но уже хочется, чтобы модель явно выбирала следующий шаг, а не решала всё в одном промпте. Смысл упомянуть SGR в том, чтобы снять ложное ожидание: мультиагентность не обязательно LangGraph и вложенные графы. Иногда достаточно одного структурированного вызова и цикла. Полный граф стоит доставать тогда, когда специалистам действительно нужна изоляция.

Конвейер и параллельные специалисты: router не единственная форма

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

Конвейер (pipeline). Порядок специалистов заранее известен и не меняется, поэтому маршрутизатор здесь просто не нужен. Хороший пример: голосовой агент. Обращение проходит через фиксированную цепочку специализированных шагов «распознать речь → сформировать ответ → синтезировать голос → передать клиенту», и каждый шаг умеет только своё. В одном из наших голосовых агентов конвейер устроен так:

# STT → LLM streaming → TTS streaming → отправка клиенту (шаги пересекаются во времени)
transcription_text = await self.transcribe_audio(audio_data)
llm_stream = answer_question_stream(self.messages, transcription_text)
tts_stream = self.tts_client.synthesize_stream(llm_stream)
async for audio_chunk in tts_stream:
    await self.send_status(status="audio_chunk", audio=audio_base64, chunk_index=chunk_count)

Ключевая деталь здесь не в самих шагах, а в том, что TTS не ждёт, пока LLM закончит отвечать целиком. Он озвучивает уже пришедшие куски текста, и следующий шаг стартует, не дожидаясь полного завершения предыдущего. По комментарию в коде этого проекта, именно такой streaming-конвейер сократил задержку ответа с 5–10 секунд до 2–3. Для голосового диалога это разница между «нормально» и «положил трубку». Маршрутизатор здесь бессмысленен: шаги идут строго друг за другом, а весь выигрыш в том, что они пересекаются во времени, а не в том, что кто-то выбирает следующий шаг.

Параллельные специалисты. Несколько агентов получают одну и ту же задачу одновременно, а не по очереди, а их ответы потом сводятся в один. Классический случай, оценка ответа несколькими независимыми судьями (LLM-as-a-judge): один агент проверяет точность фактов, второй смотрит на релевантность ответа вопросу, а отдельный шаг агрегирует их оценки. Для бизнес-автоматизации это скорее инструмент контроля качества, чем повседневный сценарий поддержки. Параллельность окупается, когда специалистов действительно можно запустить независимо и их выводы не противоречат друг другу логически. Иначе агрегация превращается в отдельную сложную задачу, которая съедает весь выигрыш по времени.

Три формы отвечают на разные вопросы. Router решает, кто работает дальше. Конвейер держит порядок специалистов фиксированным без всякого выбора. А параллель про то, можно ли вообще не ждать, а работать одновременно. Для большинства задач поддержки, продаж и работы с «1С» подходит router, поэтому ему посвящена основная часть статьи. Но если ваш сценарий на деле жёсткая последовательность шагов, не городите маршрутизатор. Конвейер проще и предсказуемее.

Когда мультиагентная система это правильный выбор (а когда нет)

Мультиагентность не самоцель. Она окупается на одних задачах и вредит на других.

Когда система из нескольких агентов уместна:

  • Обязанности действительно разные: с разными инструментами, разными знаниями и, что важно, разным уровнем риска. Классический пример: «отвечать на вопросы» и «менять данные в системе» почти всегда должны быть разными агентами с разными правами доступа. Агент, который только читает, физически не должен иметь инструментов для записи.
  • Специалистов нужно независимо разрабатывать, тестировать и выкатывать. Если над проектом работает несколько человек, разнесение по агентам снимает конфликты.

Когда мультиагентная система лишняя:

  • Задача на самом деле единый связный поток, который искусственно разрезали «потому что мультиагент звучит солиднее». Вы платите за координацию (ошибки маршрутизации, рассинхрон форм состояния между родителем и детьми) и ничего не получаете взамен.
  • Обязанностей одна-две, и они близкие. Здесь одного агента или максимум одного графа с маршрутизатором более чем достаточно.

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

Поиграйте с этим балансом на последнем виджете: включайте обязанности и смотрите, как ведут себя два подхода, один агент и команда специалистов.

Правило простое: начинайте с одного агента. Когда его промпт становится клубком противоречивых правил, найдите обязанность, которая приносит больше всего сложности и багов, и вынесите её первой в отдельный узел (шаг 1). К полноценному графу из графов (шаг 2) переходите только тогда, когда этому специалисту действительно понадобятся собственное состояние, свои инструменты или отдельная модель. Такой путь удерживает вас от переусложнения и оставляет систему честной: ровно столько агентов, сколько нужно задаче, и ни одним больше.

Мультиагентные системы хорошо ложатся на общую стратегию внедрения ИИ в бизнес-процессы: сначала простые сценарии, затем усложнение по мере роста требований. Если вы только выбираете, с чего начать автоматизацию на нейросетях, посмотрите обзор подходов к ИИ в 1С на 2026 год, он помогает понять, где вообще уместен агент, а где хватит более простого решения.

Собрать мультиагентную систему под свою учётную систему, от первого графа с маршрутизатором до композиции независимых агентов, можно на курсе «ChatGPT для 1С»: там весь путь разобран на реальных проектах, с коннекторами к «1С», валидацией и боевыми сценариями.

Частые вопросы