Мультиагентные системы: роли, оркестрация, критики, верификация
В статье про агентов и 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 из продвинутого промптинга — берите дебаты только там, где голосование по ответам не работает (длинные, неструктурированные артефакты).
критерии пройдены?} C -- нет, правки --> G C -- да --> OUT[Артефакт] end subgraph DEB["Debate"] direction TB D1[Агент A] --> X[Обмен ответами] D2[Агент B] --> X D3[Агент C] --> X X --> AGG[Агрегатор] end
Роли: что реально работает, а что косплей
Индустрия любит промпты вида «Ты — Senior Product Manager с 15-летним опытом». Работает не биография, а три вещи: узкий мандат, урезанный набор инструментов и явный формат выхода.
| Роль | Что даёт | Мандат | Инструменты | Выход |
|---|---|---|---|---|
| Планировщик | Декомпозиция, приоритеты | «Разбей на 3–7 независимых подзадач» | нет | JSON-список задач |
| Исполнитель | Работа с внешним миром | «Ответь на один конкретный вопрос» | поиск, чтение, API | сводка ≤ 300 слов + источники |
| Критик | Поиск дефектов | «Найди нарушения этих N критериев» | нет | список находок с severity |
| Верификатор | Проверка фактов/инвариантов | «Подтверди каждое утверждение источником» | поиск, тесты, линтер | verdict + evidence |
| Синтезатор | Сборка финала | «Собери из сводок, не добавляя нового» | нет | финальный артефакт |
| Супервизор | Остановка и эскалация | «Реши: продолжать, переделать, эскалировать» | нет | enum-решение |
Три эмпирических правила по ролям:
- Критик и генератор — разные вызовы, а лучше разные модели. Генератор, которого попросили «проверь себя», систематически подтверждает собственный ответ. Это не мнение — см. Huang et al., «Large Language Models Cannot Self-Correct Reasoning Yet», arXiv:2310.01798: без внешней обратной связи самокоррекция на reasoning-задачах ухудшает результат.
- У критика не должно быть контекста генератора. Он видит артефакт и критерии — и всё. Если он видит цепочку рассуждений автора, он на неё «залипает».
- Роли без урезания инструментов бесполезны. Если «критику» доступны те же 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, даже когда проигрывает по суммарным токенам. Для интерактивных сценариев это часто решающий фактор.
Что реально снижает счёт:
- Кэширование промптов. Системный промпт воркера идентичен для всех N воркеров — с
cache_controlвы платите за него полную цену один раз, дальше ~0.1×. При 5 воркерах и системном промпте в 6K токенов это экономит порядка 60 % входных токенов на воркерах. Детали — в статье про продакшн. - Роутинг моделей по ролям. Планирование и синтез — Opus, исполнение — Sonnet, классификация и роутинг — Haiku.
- Бюджет как жёсткий инвариант. Не «попросим модель быть экономной», а счётчик в оркестраторе, который останавливает цикл.
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.
Типичные ошибки
- Начинать с мультиагентности. Сначала один агент, замеры, узкое место. Мультиагентность — ответ на конкретный измеренный дефицит, а не стартовая архитектура.
- Считать роли главным. Главное — изоляция контекста, узкий мандат и точка проверки. Персона в промпте — косметика.
- Критик на той же истории, что и генератор. Он подтвердит всё что угодно.
- Верификация только через LLM. Схема, тесты, поиск цитаты в источнике — сначала. LLM-судья — на остаток.
- Свободный текст между агентами. Типизированный контракт, обязательные поля
statusиopen_questions. - Передавать наверх сырые результаты инструментов. Тогда вы получаете все минусы мультиагентности и ни одного плюса: контекст оркестратора раздувается ровно как у монолита.
- Отсутствие бюджета и потолка итераций. Каждая петля — со счётчиком и путём эскалации.
- Параллелить взаимозависимые подзадачи. Если результаты нельзя склеить конкатенацией — не параллельте.
- Не логировать роль в трейсе. Отладка мультиагентной системы без
agent_roleв спане — археология. - Игнорировать частичный успех.
status: "partial"иopen_questionsдолжны доезжать до пользователя, а не молча теряться в синтезе.
Мини-итог
Мультиагентная система — это способ купить параллелизм и изоляцию контекста ценой токенов и надёжности. Сделка выгодна, когда подзадачи независимы и read-only, а результаты складываются конкатенацией; невыгодна, когда есть общее изменяемое состояние.
Проверочный список перед тем, как вводить второго агента:
- Одноагентная версия построена и измерена, узкое место известно.
- Подзадачи действительно независимы — результат одной не нужен другой.
- Между агентами — типизированный контракт со
status,evidence,open_questions. - Есть инвариантная верификация на коде (схема, цитаты, тесты) до всякого LLM-критика.
- Критик — отдельный вызов, видит только артефакт и чеклист.
- Каждая петля ограничена по числу итераций и имеет путь эскалации.
- Бюджет (доллары, вызовы, время) — жёсткий инвариант в коде оркестратора.
- Трейс содержит роль,
task_idи родительский спан для каждого вызова. - Выход субагентов с внешним вводом трактуется как данные, а не инструкции.
И главный вопрос, который стоит задавать себе на каждом ревью архитектуры: «этот агент поднимает надёжность системы или просто добавляет ещё один множитель меньше единицы?»
Источники
- Anthropic, «How we built our multi-agent research system» — практика, цифры по токенам, разбор ограничений.
- Anthropic, «Building effective agents» — каталог паттернов: routing, orchestrator-workers, evaluator-optimizer.
- Cognition, «Don’t Build Multi-Agents» — аргументированный контрапункт.
- Cemri et al., «Why Do Multi-Agent LLM Systems Fail?», arXiv:2503.13657 — таксономия MAST из 14 режимов отказа.
- Huang et al., «LLMs Cannot Self-Correct Reasoning Yet», arXiv:2310.01798 — почему критик должен быть внешним.
- Shinn et al., «Reflexion», arXiv:2303.11366 и Madaan et al., «Self-Refine», arXiv:2303.17651 — петли улучшения с обратной связью.
- Du et al., «Multiagent Debate», arXiv:2305.14325 — дебаты и их эффект на фактологию.
- Wu et al., «AutoGen», arXiv:2308.08155, Hong et al., «MetaGPT», arXiv:2308.00352, Xia et al., «Agentless», arXiv:2407.01489 — фреймворки и отрезвляющее сравнение с простым пайплайном.
- Liu et al., «Lost in the Middle», arXiv:2307.03172 — почему изоляция контекста работает.
- Документация LangGraph — графовая оркестрация с чекпоинтами.
Что дальше
Мы научились строить систему из нескольких агентов, но каждый раз подключали инструменты вручную — своими схемами, своими адаптерами, своим кодом на каждую интеграцию. Дальше — про стандарт, который решает именно эту проблему: Model Context Protocol: зачем нужен, архитектура, серверы, инструменты и ресурсы.