ИИ-агенты и prompt engineering Мультиагентные системы: роли, оркестрация, критики, верификация
0%

Мультиагентные системы: роли, оркестрация, критики, верификация

Мультиагентные системы: роли, оркестрация, критики, верификация

В статье про агентов и ReAct мы собрали один цикл «рассуждение → действие → наблюдение». Эта статья — про то, что происходит, когда одного такого цикла не хватает, и вы заводите второй, третий и десятый.

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

Интуиция: три причины, по которым один агент ломается

Одиночный агент — это цикл, где вся история (системный промпт, определения инструментов, все результаты вызовов, все рассуждения) живёт в одном окне контекста и перечитывается на каждом шаге. Отсюда три независимых источника деградации.

1. Контекст переполняется, и качество падает раньше, чем кончаются токены. Даже при окне в 1M токенов модель хуже находит информацию в середине длинного контекста — классический результат Liu et al., «Lost in the Middle», arXiv:2307.03172. Агент, который сделал 18 вызовов поиска, тащит за собой сотню килотокенов сырья, из которого релевантны единицы процентов. Стоимость каждого следующего шага растёт линейно по длине истории, а точность — падает.

2. Инструментов становится слишком много. При 5 инструментах модель выбирает уверенно. При 40 — начинает путать похожие (search_docs против search_code против grep_repo), а определения занимают 10–15K токенов в каждом запросе. Это лечится tool search, но и он не бесконечен.

3. Задача содержит независимые ветви. «Собери профили 20 конкурентов» — это 20 полностью независимых подзадач. Последовательный агент потратит на них 20 раундов; параллельные субагенты — один раунд по стенным часам.

Куда девается контекст: один агент против оркестратора с субагентами

Ключевая мысль картинки: мультиагентность — это в первую очередь механизм управления контекстом, и только во вторую — «разделение труда». Роли типа «ты аналитик, а ты копирайтер» дают меньше, чем изоляция окна и явная точка проверки на стыке.

Что говорят замеры

Единственная опубликованная крупная цифра, на которую стоит опираться, — из инженерного блога Anthropic про их research-систему: мультиагентная конфигурация (Claude Opus как лид + Claude Sonnet как субагенты) обошла одиночного Opus на внутреннем бенчмарке исследовательских запросов на 90.2 %. Там же — цена: агенты тратят примерно в 4 раза больше токенов, чем чат, а мультиагентные системы — примерно в 15 раз («How we built our multi-agent research system»).

И самое честное наблюдение из той же статьи: на BrowseComp объём потраченных токенов объясняет около 80 % дисперсии результата. То есть значительная часть выигрыша мультиагентности — это не магия координации, а просто «мы разрешили системе потратить в 15 раз больше вычислений». Тот же вывод другими словами: если вашу задачу можно решить одним агентом с большим effort и хорошим RAG — сделайте так, это дешевле.

Контрапункт, который тоже стоит прочитать целиком: Cognition, «Don’t Build Multi-Agents». Их два принципа — «делись контекстом целиком» и «действия несут неявные решения» — объясняют, почему параллельные субагенты хорошо работают в исследовании (ветви независимы, результат — текст) и плохо в написании кода (ветви взаимозависимы, субагент B принимает решение, противоречащее решению субагента A, и склеить их уже нельзя).

Отсюда практическое правило:

Мультиагентность окупается, когда подзадачи read-only, независимы и их результаты складываются конкатенацией. Она вредит, когда подзадачи меняют общее состояние и требуют согласованных решений.

Топологии оркестрации

Разберём четыре, которые реально применяются в проде.

1. Orchestrator-workers

Лид-агент декомпозирует задачу, порождает субагентов с узким мандатом, собирает их сводки, синтезирует ответ. Это базовая рабочая лошадка — описана как отдельный паттерн в Anthropic, «Building effective agents».

Работает, когда: подзадачи известны только во время выполнения, независимы и read-only. Пример: исследование, аудит кодовой базы, сбор данных.

Ломается, когда: лид не может внятно сформулировать мандат («изучи эту тему» вместо «найди выручку компании X за 2024 год из отчётности, верни число и ссылку») — субагенты дублируют работу и приносят кашу.

2. Pipeline (конвейер)

Фиксированная цепочка: извлечение → нормализация → обогащение → форматирование. Каждый шаг — отдельный промпт, возможно на разной модели.

Это не «агенты» в сильном смысле — это workflow, и в 80 % случаев это то, что вам нужно. Детерминированная топология, каждый шаг тестируется отдельно, стоимость предсказуема.

3. Evaluator-optimizer (генератор + критик)

Один агент производит артефакт, второй оценивает по явным критериям и возвращает список правок, первый переделывает. Цикл до прохождения критериев или до лимита итераций.

4. Debate (дебаты)

N агентов независимо решают задачу, видят ответы друг друга, пересматривают своё решение, агрегатор выбирает финальный. Метод из Du et al., «Improving Factuality and Reasoning in Language Models through Multiagent Debate», arXiv:2305.14325.

Даёт заметный прирост на фактологии и арифметике, но стоит $O(N \cdot R)$ вызовов при N агентах и R раундах. На практике почти всегда проигрывает по цене/качеству более дешёвому self-consistency из продвинутого промптинга — берите дебаты только там, где голосование по ответам не работает (длинные, неструктурированные артефакты).

Роли: что реально работает, а что косплей

Индустрия любит промпты вида «Ты — Senior Product Manager с 15-летним опытом». Работает не биография, а три вещи: узкий мандат, урезанный набор инструментов и явный формат выхода.

Роль Что даёт Мандат Инструменты Выход
Планировщик Декомпозиция, приоритеты «Разбей на 3–7 независимых подзадач» нет JSON-список задач
Исполнитель Работа с внешним миром «Ответь на один конкретный вопрос» поиск, чтение, API сводка ≤ 300 слов + источники
Критик Поиск дефектов «Найди нарушения этих N критериев» нет список находок с severity
Верификатор Проверка фактов/инвариантов «Подтверди каждое утверждение источником» поиск, тесты, линтер verdict + evidence
Синтезатор Сборка финала «Собери из сводок, не добавляя нового» нет финальный артефакт
Супервизор Остановка и эскалация «Реши: продолжать, переделать, эскалировать» нет enum-решение

Три эмпирических правила по ролям:

  1. Критик и генератор — разные вызовы, а лучше разные модели. Генератор, которого попросили «проверь себя», систематически подтверждает собственный ответ. Это не мнение — см. Huang et al., «Large Language Models Cannot Self-Correct Reasoning Yet», arXiv:2310.01798: без внешней обратной связи самокоррекция на reasoning-задачах ухудшает результат.
  2. У критика не должно быть контекста генератора. Он видит артефакт и критерии — и всё. Если он видит цепочку рассуждений автора, он на неё «залипает».
  3. Роли без урезания инструментов бесполезны. Если «критику» доступны те же 24 инструмента, он начнёт доделывать работу вместо проверки.

Протокол коммуникации: главный источник багов

Самая частая ошибка в мультиагентных системах — не плохая топология, а свободный текст между агентами. Агент A написал «нашёл несколько релевантных документов», агент B интерпретировал как «нашёл всё, что нужно», и всё поехало.

Работа Cemri et al., «Why Do Multi-Agent LLM Systems Fail?», arXiv:2503.13657 построила таксономию MAST из 14 режимов отказа, сгруппированных в три категории:

Категория Что это Примеры режимов
Specification Плохо поставленная задача/роль нарушение спецификации, потеря роли, потеря истории диалога
Inter-agent misalignment Сбой координации обрыв разговора, игнорирование ввода другого агента, дублирование работы, утаивание информации
Task verification Провал проверки преждевременное завершение, отсутствующая или поверхностная верификация

Показательно, что примерно треть всех отказов приходится на последнюю категорию — то есть на отсутствие нормальной проверки, а не на «плохие агенты».

Лечится это одним приёмом: межагентный контракт — типизированная структура, а не текст.

"""Типизированный контракт между агентами. Проверяется валидатором,
а не «выглядит нормально»."""
from typing import Literal
from pydantic import BaseModel, Field, HttpUrl


class Evidence(BaseModel):
    """Одно подтверждённое утверждение с источником."""
    claim: str = Field(max_length=400, description="Утверждение, одно предложение")
    source: HttpUrl = Field(description="URL, откуда взято утверждение")
    quote: str = Field(max_length=600, description="Дословная цитата из источника")


class WorkerReport(BaseModel):
    """Единственный формат, в котором субагент возвращает результат наверх."""
    task_id: str
    status: Literal["completed", "partial", "failed"]
    summary: str = Field(max_length=1500, description="Сводка для оркестратора")
    evidence: list[Evidence] = Field(default_factory=list, max_length=12)
    # Явное поле для «я не знаю» — иначе модель заполнит пробел выдумкой
    open_questions: list[str] = Field(default_factory=list, max_length=5)
    tokens_used: int = 0

Три поля здесь несут основную нагрузку. status не даёт оркестратору принять частичный результат за полный. evidence с обязательной цитатой почти полностью убивает галлюцинации на стыке (модели трудно выдумать дословную цитату под URL, когда её просят приложить). open_questions даёт легальный способ сказать «не нашёл» — без такого поля модель заполняет пустоту правдоподобным мусором.

Рабочий оркестратор на Python

Соберём orchestrator-workers целиком: лид планирует, воркеры выполняют параллельно, критик проверяет синтез. Модели разнесены по цене — дорогая на планирование и синтез, дешёвая на исполнение.

"""Orchestrator-workers на Anthropic SDK: план → параллельные воркеры → синтез → критик."""
import asyncio
import json
from typing import Literal

import anthropic
from pydantic import BaseModel, Field

client = anthropic.AsyncAnthropic()

LEAD_MODEL = "claude-opus-4-8"      # план и синтез — качество решает
WORKER_MODEL = "claude-sonnet-5"    # исполнение — много вызовов, важна цена
CRITIC_MODEL = "claude-opus-4-8"    # критик не должен быть слабее генератора


class SubTask(BaseModel):
    id: str
    question: str = Field(description="Один конкретный вопрос, ответ проверяем")
    success_criteria: str = Field(description="Как понять, что подзадача решена")


class Plan(BaseModel):
    reasoning: str
    subtasks: list[SubTask] = Field(min_length=1, max_length=7)


PLANNER_SYSTEM = """Ты — планировщик исследовательской системы.

Разбей запрос на 1–7 ПОЛНОСТЬЮ НЕЗАВИСИМЫХ подзадач: результат одной
не должен требоваться для выполнения другой.

Правила:
- Каждая подзадача — ОДИН конкретный вопрос с проверяемым ответом.
  Плохо: "изучи рынок". Хорошо: "найди выручку Acme Corp за 2024 финансовый год".
- Если задача по своей природе последовательная — верни ОДНУ подзадачу.
  Ложное распараллеливание хуже, чем его отсутствие.
- Не дублируй: две подзадачи не должны приводить к одному источнику."""


async def make_plan(query: str) -> Plan:
    """Шаг 1: декомпозиция. Structured output гарантирует разбор ответа."""
    response = await client.messages.parse(
        model=LEAD_MODEL,
        max_tokens=4000,
        thinking={"type": "adaptive"},          # планирование выигрывает от рассуждения
        output_config={"effort": "high"},
        system=PLANNER_SYSTEM,
        messages=[{"role": "user", "content": query}],
        output_format=Plan,
    )
    return response.parsed_output


WORKER_SYSTEM = """Ты — исполнитель одной подзадачи. У тебя нет доступа к общему
плану и к работе других исполнителей — это намеренно.

Отвечай ТОЛЬКО на поставленный вопрос. Каждое фактическое утверждение
подкрепляй дословной цитатой из источника.
Если ответа нет — status="partial" и запиши, чего не хватило, в open_questions.
Выдумывать источники запрещено: лучше пустой evidence, чем ложный."""


async def run_worker(task: SubTask, tools: list[dict]) -> "WorkerReport":
    """Шаг 2: один субагент = одно изолированное окно контекста."""
    response = await client.messages.parse(
        model=WORKER_MODEL,
        max_tokens=8000,
        thinking={"type": "adaptive"},
        output_config={"effort": "medium"},     # исполнение дешевле планирования
        system=WORKER_SYSTEM,
        tools=tools,                            # УЗКИЙ набор: только поиск и чтение
        messages=[{
            "role": "user",
            "content": (
                f"task_id: {task.id}\n"
                f"Вопрос: {task.question}\n"
                f"Критерий готовности: {task.success_criteria}"
            ),
        }],
        output_format=WorkerReport,
    )
    return response.parsed_output


async def orchestrate(query: str, tools: list[dict]) -> dict:
    """Полный цикл. Возвращает финальный ответ и телеметрию."""
    plan = await make_plan(query)

    # Параллельный запуск. return_exceptions=True — падение одного воркера
    # не должно ронять всю систему: оркестратор увидит частичный результат.
    results = await asyncio.gather(
        *(run_worker(t, tools) for t in plan.subtasks),
        return_exceptions=True,
    )

    reports = [r for r in results if isinstance(r, WorkerReport)]
    failures = [r for r in results if isinstance(r, Exception)]

    if not reports:
        raise RuntimeError(f"Все {len(plan.subtasks)} воркеров упали: {failures}")

    # Наверх идут ТОЛЬКО сводки, а не сырые результаты вызовов инструментов.
    # Это и есть главный выигрыш архитектуры — окно оркестратора остаётся маленьким.
    digest = "\n\n".join(
        f"### {r.task_id} [{r.status}]\n{r.summary}\n"
        f"Источники: {', '.join(str(e.source) for e in r.evidence) or 'нет'}\n"
        f"Открытые вопросы: {'; '.join(r.open_questions) or 'нет'}"
        for r in reports
    )

    synthesis = await client.messages.create(
        model=LEAD_MODEL,
        max_tokens=8000,
        thinking={"type": "adaptive"},
        output_config={"effort": "high"},
        system=(
            "Собери финальный ответ ИСКЛЮЧИТЕЛЬНО из предоставленных сводок. "
            "Не добавляй фактов, которых нет в сводках. Если сводки противоречат "
            "друг другу — покажи противоречие явно, не выбирай молча. "
            "Если подзадача имеет status=partial — отметь пробел в ответе."
        ),
        messages=[{"role": "user", "content": f"Запрос: {query}\n\nСводки:\n{digest}"}],
    )

    answer = next(b.text for b in synthesis.content if b.type == "text")
    return {
        "answer": answer,
        "subtasks": len(plan.subtasks),
        "failed_workers": len(failures),
        "worker_tokens": sum(r.tokens_used for r in reports),
        "orchestrator_tokens": synthesis.usage.input_tokens + synthesis.usage.output_tokens,
    }

Что здесь принципиально:

  • Сводки, а не сырьё. В окно оркестратора попадают summary, а не результаты вызовов инструментов. Именно это делает архитектуру дешевле, чем один агент со всей историей.
  • Разные модели на разных ролях. Планирование и синтез — самое дорогое по последствиям, исполнение — самое массовое по объёму. Разнести их по моделям обычно снижает счёт в 2–3 раза без потери качества.
  • return_exceptions=True. Падение одного субагента — норма, а не катастрофа. Система должна деградировать частично.
  • Воркеры не видят план. Не из принципа, а потому что общий план в контексте воркера провоцирует «а дай-ка я заодно решу и соседнюю подзадачу» — прямая дорога к дублированию из таксономии MAST.

Асимптотика: при $n$ подзадачах — $O(n)$ вызовов модели, но глубина по стенным часам $O(1)$ вместо $O(n)$ у последовательного агента. Память оркестратора — $O(n \cdot s)$, где $s$ — размер сводки (обычно 1–2K токенов), против $O(n \cdot d)$ у монолита, где $d$ — сырой объём данных на подзадачу (десятки тысяч токенов).

Критики: где они помогают, а где вредят

Петля «генератор → критик → генератор» — самый недооценённый и одновременно самый неправильно применяемый паттерн.

Она работает, когда у критика есть внешний сигнал. Self-Refine, arXiv:2303.17651 показал прирост в среднем ~20 % по набору задач, но ключ там — конкретность критериев. Reflexion, arXiv:2303.11366 поднял pass@1 на HumanEval до 91 % — но только потому, что сигнал шёл от запуска тестов, а не от мнения модели о себе.

Она вредит, когда критик — это тот же контекст с другим системным промптом. Обзор Huang et al., arXiv:2310.01798: при внутренней самокоррекции модель чаще портит правильный ответ, чем чинит неправильный.

Практическая иерархия сигналов для критика — от самого надёжного к самому шаткому:

Сигнал Надёжность Стоимость Применимость
Компилятор / типчекер детерминированная ~0 код
Юнит-тесты, линтер детерминированная ~0 код
Схема / валидатор (Pydantic, JSON Schema) детерминированная ~0 любой структурный выход
Проверка по источнику (цитата есть в документе?) высокая ~0 (строковый поиск) RAG, research
Отдельная модель + явный чеклист средняя 0.01 $–0.05 за проверку текст, документы
«Проверь себя» тем же вызовом низкая, часто отрицательная дёшево почти нигде

Отсюда правило проектирования: всё, что можно проверить кодом, проверяйте кодом, и только остаток отдавайте LLM-критику. Не наоборот.

"""Критик с явным чеклистом и жёстким ограничением на количество итераций."""
from typing import Literal
from pydantic import BaseModel, Field


class Finding(BaseModel):
    criterion: str = Field(description="Какой пункт чеклиста нарушен")
    severity: Literal["blocker", "major", "minor"]
    evidence: str = Field(description="Дословный фрагмент артефакта с проблемой")
    fix: str = Field(description="Конкретное действие для исправления")


class Review(BaseModel):
    verdict: Literal["pass", "revise", "reject"]
    findings: list[Finding] = Field(default_factory=list, max_length=10)


CRITIC_SYSTEM = """Ты — критик. Ты НЕ пишешь и НЕ переписываешь артефакт.
Ты проверяешь его строго по чеклисту ниже, пункт за пунктом.

Для каждой находки приводи ДОСЛОВНЫЙ фрагмент артефакта.
Находка без дословной цитаты недействительна и должна быть отброшена.

verdict:
- pass    — нет находок severity=blocker или major
- revise  — есть исправимые проблемы
- reject  — артефакт нужно делать заново, точечные правки не спасут

Чеклист:
{checklist}"""


async def critique_loop(
    generate,                  # async () -> str, генерирует/переделывает артефакт
    deterministic_checks,      # (str) -> list[str], компилятор/тесты/схема
    checklist: str,
    max_rounds: int = 3,       # больше 3 почти никогда не даёт прироста
) -> tuple[str, list[Review]]:
    artifact = await generate(feedback=None)
    history: list[Review] = []

    for round_no in range(max_rounds):
        # Сначала — бесплатные детерминированные проверки.
        # LLM-критика запускаем ТОЛЬКО если код уже доволен.
        hard_errors = deterministic_checks(artifact)
        if hard_errors:
            artifact = await generate(feedback="\n".join(hard_errors))
            continue

        response = await client.messages.parse(
            model=CRITIC_MODEL,
            max_tokens=4000,
            thinking={"type": "adaptive"},
            output_config={"effort": "high"},
            system=CRITIC_SYSTEM.format(checklist=checklist),
            # Критик видит ТОЛЬКО артефакт — без рассуждений автора,
            # без истории предыдущих раундов, без плана.
            messages=[{"role": "user", "content": f"<artifact>\n{artifact}\n</artifact>"}],
            output_format=Review,
        )
        review = response.parsed_output
        history.append(review)

        if review.verdict == "pass":
            return artifact, history
        if review.verdict == "reject" and round_no == max_rounds - 1:
            break

        feedback = "\n".join(
            f"[{f.severity}] {f.criterion}\nПроблема: {f.evidence}\nИсправить: {f.fix}"
            for f in review.findings
        )
        artifact = await generate(feedback=feedback)

    return artifact, history

Обратите внимание на max_rounds = 3. Эмпирика по петлям критики стабильна: первый раунд даёт основной прирост, второй — заметный, третий — маргинальный, начиная с четвёртого система чаще всего входит в осцилляцию (правит A, ломает B, правит B, ломает A). Если после трёх раундов verdict != "pass" — это сигнал эскалации к человеку, а не повод крутить цикл дальше.

Верификация: последняя миля

Критик проверяет форму и логику. Верификация — про соответствие реальности. Это разные вещи, и их регулярно путают.

Три уровня верификации, которые стоит реализовать в любой серьёзной системе:

1. Инвариантная (код, ноль вызовов LLM). Схема соблюдена; все URL из evidence действительно встречались в результатах инструментов; каждая quote буквально присутствует в тексте источника; числа в итоге совпадают с числами в сводках. Ловит 60–70 % галлюцинаций на стыках практически бесплатно:

def verify_grounding(report: WorkerReport, tool_outputs: dict[str, str]) -> list[str]:
    """Инвариантная проверка: ни одного вызова LLM, миллисекунды на отчёт."""
    errors: list[str] = []
    corpus = "\n".join(tool_outputs.values())

    for ev in report.evidence:
        if str(ev.source) not in tool_outputs:
            errors.append(f"Источник {ev.source} не встречался ни в одном вызове инструмента")
            continue
        # Нормализуем пробелы: модели любят переформатировать цитату
        needle = " ".join(ev.quote.split())
        haystack = " ".join(tool_outputs[str(ev.source)].split())
        if needle not in haystack:
            errors.append(f"Цитата не найдена в источнике {ev.source}: {ev.quote[:80]}…")

    if report.status == "completed" and not report.evidence:
        errors.append("status=completed без единого источника — подозрение на выдумку")

    return errors

2. Перекрёстная. Два независимых воркера с разными формулировками отвечают на один критичный вопрос; расхождение — сигнал, а не шум. Дороже вдвое, применяйте точечно — к 2–3 самым важным фактам, а не ко всему.

3. LLM-as-judge по рубрике. Последняя линия, для того, что нельзя проверить кодом. Подробно — в статье про оценку и бенчмарки. Здесь важно одно: судья должен получать рубрику с бинарными критериями, а не «оцени качество от 1 до 10».

Почему верификация важнее топологии — видно из простой арифметики.

$$P_{\text{success}} = \prod_{i=1}^{n} p_i$$

При надёжности одного шага $p = 0.97$ и цепочке из 40 шагов вероятность пройти без единой ошибки — меньше 30 %.

Вероятность довести цепочку до конца без единой ошибки

Отсюда неочевидный вывод: добавление агента в систему по умолчанию снижает её надёжность. Оправданным оно становится только тогда, когда этот агент либо поднимает $p$ остальных шагов (критик, верификатор), либо разрывает длинную цепочку на короткие независимые с валидацией на стыке.

Полный протокол взаимодействия

Обратите внимание на шаг 9–11: верификатор отклонил одну цитату, и оркестратор переспросил только по отклонённому пункту, а не перезапустил воркера целиком. Гранулярный ретрай — разница между системой, которая укладывается в бюджет, и системой, которая сжигает его на повторах.

Стоимость: считаем честно

Возьмём типовой исследовательский запрос и посчитаем по прайсу Anthropic (Opus 4.8 — 5 $/25 $ за 1M входных/выходных токенов, Sonnet 5 — 3 $/$15, Haiku 4.5 — 1 $/5 $).

Конфигурация Вызовов Вход, K ток. Выход, K ток. Цена Latency Качество (внутр. рубрика)
Один агент, Sonnet 5, effort=high 12 640 22 2.25 $ 95 с 6.1 / 10
Один агент, Opus 4.8, effort=xhigh 14 810 31 4.83 $ 160 с 7.4 / 10
Orchestrator (Opus) + 4 воркера (Sonnet) 21 390 48 2.34 $ 55 с 8.0 / 10
То же + верификатор (код) 21 390 48 2.34 $ 58 с 8.5 / 10
То же + критик (Opus, 2 раунда) 25 470 66 3.62 $ 82 с 8.9 / 10
Дебаты, 3 × Opus, 2 раунда + агрегатор 31 1 950 74 11.60 $ 210 с 9.0 / 10

Цифры иллюстративные — на вашей задаче они будут другими, — но соотношения устойчивы и стоят того, чтобы их запомнить:

  • Входных токенов у оркестратора меньше, чем у монолита, хотя вызовов больше. Именно потому, что наверх идут сводки. Это контринтуитивно и это главный экономический аргумент за архитектуру.
  • Верификатор на коде — бесплатный прирост качества. Строчка $2.34 → $2.34, качество 8.0 → 8.5. Всегда делайте это первым.
  • Критик стоит ~55 % надбавки за ~0.4 балла. Оправдано, когда цена ошибки высокая.
  • Дебаты стоят в 3+ раза дороже критика за 0.1 балла. Почти всегда не оправдано.
  • Мультиагентность выигрывает по latency, даже когда проигрывает по суммарным токенам. Для интерактивных сценариев это часто решающий фактор.

Что реально снижает счёт:

  1. Кэширование промптов. Системный промпт воркера идентичен для всех N воркеров — с cache_control вы платите за него полную цену один раз, дальше ~0.1×. При 5 воркерах и системном промпте в 6K токенов это экономит порядка 60 % входных токенов на воркерах. Детали — в статье про продакшн.
  2. Роутинг моделей по ролям. Планирование и синтез — Opus, исполнение — Sonnet, классификация и роутинг — Haiku.
  3. Бюджет как жёсткий инвариант. Не «попросим модель быть экономной», а счётчик в оркестраторе, который останавливает цикл.
class Budget:
    """Бюджет — свойство системы, а не пожелание в промпте."""

    def __init__(self, max_usd: float, max_calls: int, max_wall_s: float):
        self.max_usd, self.max_calls, self.max_wall_s = max_usd, max_calls, max_wall_s
        self.spent_usd, self.calls, self.started = 0.0, 0, time.monotonic()

    def charge(self, usage, price_in: float, price_out: float) -> None:
        """price_* — цена за 1M токенов. Кэш-чтение ≈ 0.1× от входной цены."""
        self.spent_usd += (
            usage.input_tokens * price_in
            + (usage.cache_read_input_tokens or 0) * price_in * 0.1
            + usage.output_tokens * price_out
        ) / 1_000_000
        self.calls += 1

    def check(self) -> None:
        if self.spent_usd > self.max_usd:
            raise BudgetExceeded(f"бюджет ${self.max_usd} исчерпан: ${self.spent_usd:.2f}")
        if self.calls > self.max_calls:
            raise BudgetExceeded(f"лимит вызовов {self.max_calls} исчерпан")
        if time.monotonic() - self.started > self.max_wall_s:
            raise BudgetExceeded(f"лимит времени {self.max_wall_s}s исчерпан")

Когда мультиагентность — плохая идея

Конкретные антипаттерны, которые встречаются чаще всего:

Роли ради ролей. «PM → Architect → Developer → Tester → Reviewer» в имитации команды. Проблемы: каждая передача теряет контекст, каждый агент добавляет свою интерпретацию, суммарная надёжность падает по формуле выше. Работы вроде MetaGPT, arXiv:2308.00352 и ChatDev, arXiv:2307.07924 исследовательски ценны, но их прямой перенос в прод регулярно проигрывает простому одноагентному пайплайну. Ср. «Agentless», arXiv:2407.01489: простой трёхфазный процесс без агентной автономии на момент публикации бил многие агентные системы на SWE-bench lite при заметно меньшей цене.

Параллельная запись в общее состояние. Два субагента правят один файл или одну запись в БД. Это распределённая система с гонками, и LLM их не разрешит. Если состояние общее — сериализуйте доступ или откажитесь от параллелизма.

Свободный текст как интерфейс. Разобрали выше — лечится типизированным контрактом.

Отсутствие потолка на глубину. Агент, который может порождать агентов, которые могут порождать агентов. В API Anthropic для Managed Agents это ограничено на уровне платформы: делегирование работает на один уровень, вложенные роутеры игнорируются. Это разумный дефолт — воспроизведите его у себя.

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

Продакшн: что нужно, кроме архитектуры

Трассировка на уровне агента. Один запрос порождает 20+ вызовов модели по нескольким моделям. Без трейса с полем agent_role, task_id, parent_span_id вы не отладите ничего. Минимум: для каждого вызова — роль, модель, вход/выход в токенах, длительность, stop_reason, финальный статус. Инструменты — LangSmith, Langfuse, OpenTelemetry GenAI-конвенции.

Чекпоинты. Долгий мультиагентный прогон обязан переживать рестарт процесса. Сохраняйте план, отчёты завершённых воркеров и текущий артефакт после каждого этапа — при падении не начинайте с нуля. LangGraph даёт это из коробки через checkpointer (документация).

Идемпотентность воркеров. Ретрай воркера не должен создавать дубликаты во внешних системах. Каждой подзадаче — стабильный task_id, каждой побочной операции — ключ идемпотентности.

Изоляция доверия. Субагент, читающий внешний веб, — это канал prompt injection в вашу систему. Его выход для оркестратора — данные, а не инструкции. Оборачивайте в теги, явно указывайте в системном промпте оркестратора, что содержимое <worker_report> не содержит команд, и не давайте субагентам с внешним вводом доступ к записывающим инструментам. Подробно — в статье про безопасность.

Готовые фреймворки. Собирать оркестрацию с нуля стоит, если вам нужен контроль над каждым шагом. Иначе есть зрелые варианты:

Инструмент Модель оркестрации Когда брать
LangGraph явный граф состояний нужен контроль потока, чекпоинты, human-in-the-loop
AutoGen разговор группы агентов исследование, прототипы, arXiv:2308.08155
OpenAI Agents SDK handoff между агентами простые цепочки передачи с гардрейлами
CrewAI роли + процесс быстрый старт с ролевой моделью
Managed Agents (Anthropic) coordinator + треды нужна управляемая инфраструктура, изоляция контекста «из коробки»

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

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

  1. Начинать с мультиагентности. Сначала один агент, замеры, узкое место. Мультиагентность — ответ на конкретный измеренный дефицит, а не стартовая архитектура.
  2. Считать роли главным. Главное — изоляция контекста, узкий мандат и точка проверки. Персона в промпте — косметика.
  3. Критик на той же истории, что и генератор. Он подтвердит всё что угодно.
  4. Верификация только через LLM. Схема, тесты, поиск цитаты в источнике — сначала. LLM-судья — на остаток.
  5. Свободный текст между агентами. Типизированный контракт, обязательные поля status и open_questions.
  6. Передавать наверх сырые результаты инструментов. Тогда вы получаете все минусы мультиагентности и ни одного плюса: контекст оркестратора раздувается ровно как у монолита.
  7. Отсутствие бюджета и потолка итераций. Каждая петля — со счётчиком и путём эскалации.
  8. Параллелить взаимозависимые подзадачи. Если результаты нельзя склеить конкатенацией — не параллельте.
  9. Не логировать роль в трейсе. Отладка мультиагентной системы без agent_role в спане — археология.
  10. Игнорировать частичный успех. status: "partial" и open_questions должны доезжать до пользователя, а не молча теряться в синтезе.

Мини-итог

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

Проверочный список перед тем, как вводить второго агента:

  • Одноагентная версия построена и измерена, узкое место известно.
  • Подзадачи действительно независимы — результат одной не нужен другой.
  • Между агентами — типизированный контракт со status, evidence, open_questions.
  • Есть инвариантная верификация на коде (схема, цитаты, тесты) до всякого LLM-критика.
  • Критик — отдельный вызов, видит только артефакт и чеклист.
  • Каждая петля ограничена по числу итераций и имеет путь эскалации.
  • Бюджет (доллары, вызовы, время) — жёсткий инвариант в коде оркестратора.
  • Трейс содержит роль, task_id и родительский спан для каждого вызова.
  • Выход субагентов с внешним вводом трактуется как данные, а не инструкции.

И главный вопрос, который стоит задавать себе на каждом ревью архитектуры: «этот агент поднимает надёжность системы или просто добавляет ещё один множитель меньше единицы?»

Источники

Что дальше

Мы научились строить систему из нескольких агентов, но каждый раз подключали инструменты вручную — своими схемами, своими адаптерами, своим кодом на каждую интеграцию. Дальше — про стандарт, который решает именно эту проблему: Model Context Protocol: зачем нужен, архитектура, серверы, инструменты и ресурсы.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов