Alejandro Rioja.
AI Agents Operations

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

Alejandro Rioja
Alejandro Rioja
6 мин чтения
TL;DR

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

Бесплатная рассылка

Каждую среду. 28 400+ читателей. Никакой воды.

Содержание

Обновлено в июне 2026 года.

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

Взгляд оператора: Большинство из моих 100+ агентов — одношаговые. Те, что не такие, — конвейеры, которые классифицируют, затем обогащают, затем действуют, — стали надёжными лишь тогда, когда я перестал думать «цепочка промптов» и начал думать «очередь задач с LLM-работниками». Это архитектура, а не промпт-инжиниринг.

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

Паттерн 1: ставьте долговечную очередь между каждым агентом

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

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

typescript
// Агент A закончил и передаёт через очередь — без прямого вызова B
await env.ENRICH_QUEUE.send({
  traceId,
  type: "enrich",
  payload: classifierResult,
});
// Задача A выполнена. B заберёт это независимо.

В Cloudflare я использую Workers Queues именно для этого — те же примитивы, что стоят за стеком агентов, который я использую. Очередь даёт вам четыре вещи бесплатно: буферизацию (B может лежать, не теряя работу), повторы (неудавшиеся сообщения доставляются заново), обратное давление (всплеск встаёт в очередь, а не роняет систему) и развязку (масштабируйте или передеплойте B, не касаясь A). Каждая из них — то, что иначе пришлось бы строить вручную и сделать неправильно.

Паттерн 2: держите состояние вне модели, всегда

Самый частый мультиагентный баг — предполагать, что модель что-то помнит между шагами. Не помнит. Каждый вызов модели не имеет состояния; единственная память — то, что вы кладёте в промпт. Поэтому источник истины для «где эта задача в конвейере» должен жить в базе данных, а не в разговоре.

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

typescript
interface JobState {
  traceId: string;
  stage: "classified" | "enriched" | "acted" | "done" | "failed";
  data: Record<string, unknown>;
  attempts: number;
  updatedAt: number;
}

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

Паттерн 3: делайте каждую передачу идемпотентной

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

Решение — сделать действия идемпотентными с помощью ключа:

typescript
async function handleEnrich(msg: QueueMessage, env: Env) {
  const job = await getJob(env, msg.traceId);
  if (job.stage !== "classified") {
    // Уже обработано дальше этого этапа — это дубликат доставки. Пропустить.
    return;
  }
  const result = await enrich(job.data);
  await advanceJob(env, msg.traceId, "enriched", result);
  await env.ACT_QUEUE.send({ traceId: msg.traceId, type: "act" });
}

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

Паттерн 4: оркестратор против хореографии — выбирайте осознанно

Есть два способа связать поток, и правильный выбор зависит от сложности.

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

Оркестрация (центральный координатор): один оркестратор владеет потоком, по очереди вызывает каждого агента и решает, что дальше, на основе результатов. Весь поток живёт в одном читаемом месте, а логика ветвления явная. Цена — центральный компонент, который сам должен быть долговечным: если собственное состояние оркестратора не вынесено наружу (Паттерн 2), он становится единой точкой отказа.

Моё правило: хореография, пока ветвление не станет сложным, затем долговечный оркестратор. Линейный трёхэтапный конвейер — это хореография. Поток с условной маршрутизацией, параллельным fan-out и объединениями требует оркестратора, чьё состояние живёт в базе данных, чтобы он мог возобновиться после сбоя.

Паттерн 5: fan-out, fan-in без потери кусочков

Когда одна задача порождает N параллельных подзадач (обогатить 50 записей, обобщить 20 документов), и нужно дождаться их всех перед продолжением, вам нужно объединение (join). Хитрость — счётчик в состоянии задачи:

  1. Родитель ставит в очередь N дочерних сообщений и пишет expected: N, completed: 0 в запись задачи.
  2. Каждый потомок делает свою работу и атомарно инкрементирует completed.
  3. Потомок, доводящий completed до равенства с expected, ставит в очередь следующий этап.

Атомарный инкремент несущий — без него два потомка, заканчивающие одновременно, могут оба решить, что они не последние, и объединение никогда не сработает. Используйте счётчик, который хранилище может инкрементировать атомарно, или транзакцию. Этот паттерн позволяет распараллелить дорогую середину конвейера (часто дешёвую для Haiku работу — см. расчёт стоимости Haiku против Sonnet), сохраняя чистое объединение в конце.

Что бы я пропустил

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

Резюме: агенты — это работники без состояния, очереди — долговечный хребет, состояние живёт в базе данных, и каждая передача безопасна для двойного выполнения. Вот и вся игра.

Часто задаваемые вопросы

Должны ли агенты вызывать друг друга напрямую или идти через очередь?

Через очередь. Прямые вызовы связывают агентов — сбой или медлительность одного распространяется на другого, и вы не можете независимо масштабировать или передеплоивать. Долговечная очередь даёт вам буферизацию, повторы, обратное давление и развязку бесплатно.

Где должно жить мультиагентное состояние?

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

Как помешать агенту сработать дважды по одной и той же задаче?

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

Нужен ли мне мультиагентный фреймворк?

Обычно нет. Долговечные очереди, таблица состояния и ключи идемпотентности покрывают большинство продакшен-нужд примитивами, которые ваша платформа уже предоставляет. Принимайте фреймворк только тогда, когда столкнётесь с конкретной проблемой, которую он решает уникально, а не по умолчанию.

Читать дальше

Получайте ИИ-руководство на почту

Каждую среду. 28 400+ читателей. Никакой воды.

↵ — все результаты esc esc — закрыть