Orkas Orkas
Главная Блог Архитектура
Архитектура

Оркестрация нескольких агентов на практике: как Orkas запускает ведущего агента и его субагентов

Оркестрация нескольких агентов в Orkas изнутри: ведущий агент превращает запрос в план, распределяет субагентов по зависимостям, передаёт контекст между шагами и восстанавливается после сбоев.

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

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

Коротко Ведущий агент и его субагенты в одной рабочей папке Так Orkas распределяет работу сегодня: Commander привлекает специалистов, запускает их параллельно или последовательно, а вы наблюдаете за процессом.
Скачать Orkas — бесплатно

Ведущий агент и субагенты

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

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

План — это граф, а не сценарий

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

interface PlanStep {
  index: number;            // 1-based, stable, never renumbered
  title: string;            // human-readable, shown in the UI
  assignee: string;         // who runs it: "user" | "commander" | a sub-agent
  input?: string;           // the dispatch payload — a template (see below)
  wait_for?: number[];      // upstream step indexes; defaults to [index - 1]
  on_failure?: "abort_plan" | "continue" | "ask_commander";

  // --- runtime state, owned by the orchestrator, NOT written by the model ---
  status: "pending" | "in_progress" | "done" | "failed" | "skipped" | "blocked";
  output_summary?: string;  // short summary of what the step produced
  output_files?: string[];  // files the step produced
  failure_reason?: string;
}

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

Кто отвечает за достоверность: оркестратор, а не модель

Ещё раз посмотрите на разделение в этой структуре. При первом составлении плана модель задаёт замысел — названия, исполнителей, входные данные и зависимости. Но всё, что относится к состоянию выполненияstatus, output_summary, failure_reason — находится исключительно под контролем оркестратора. Модель один раз предлагает план; она никогда не может сама пометить свои шаги как "выполненные".

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

Запуск готовых шагов

Когда план создан, его продвигает небольшой механизм — исполнитель. Его основная операция: "Найти готовые шаги и запустить их". Шаг готов, если он всё ещё находится в состоянии pending и каждый шаг, которого он ждёт, достиг конечного, достаточно успешного состояния:

function findReadySteps(plan): PlanStep[] {
  return plan.steps.filter((s) => {
    if (s.status !== "pending") return false;
    const deps = s.wait_for ?? (s.index > 1 ? [s.index - 1] : []);
    return deps.every((d) => isTerminal(plan.step(d).status)); // done or skipped
  });
}

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

Задачи передаются в том же чате, а не по отдельному каналу

Вот решение, обеспечивающее прозрачность системы: для запуска шага не используется скрытый RPC-канал. В ту же групповую беседу, за которой наблюдает пользователь, отправляется сообщение от ведущего агента с @-упоминанием субагента. Для субагента получение задачи ничем не отличается от обращения в чате: он просто выполняет свой обычный ход. Нет второго пути выполнения, который нужно синхронизировать с первым.

Исполнители бывают трёх типов, и задачи передаются им немного по-разному:

  • Субагент — обычный случай. Исполнитель находит идентификатор агента по имени и публикует @<agent> <rendered input> от имени Commander. Субагент получает сообщение и выполняет полный ход агента.
  • Пользователь — если шаг действительно требует участия человека, он превращается в форму и приостанавливает план (подробнее ниже). Последующие шаги не выполняются, пока пользователь не ответит.
  • Сам Commander — для шагов обобщения или принятия решений ("прочитай всё выше и напиши сводку"). Это внутренний сигнал пробуждения без лишнего видимого пользователю сообщения; ведущий агент просто выполняет ход с собранным контекстом.

Передача контекста от шага к шагу

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

// step 3.input, as written by the lead agent
"Using the findings below, draft the launch note.\n\n{{step_1.output_summary}}"

// what the sub-agent actually receives at dispatch time
"Using the findings below, draft the launch note.\n\n- Market is growing ~20% YoY; two incumbents…"

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

Последовательно по умолчанию — и почему

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

Причина — корректность при конкурентном выполнении. Представьте двух субагентов, завершающих работу почти одновременно. Оба завершения запускают согласование; оба процесса читают план; оба видят один и тот же следующий шаг в состоянии pending — и оба запускают его. Теперь один шаг выполняется дважды. Чтобы исключить это, каждый цикл чтения, изменения и запуска в конкретной беседе выполняется последовательно под блокировкой этой беседы:

// all state-changing paths for one conversation run under one mutex
planLock(uid, cid).runExclusive(async () => {
  const plan = await readPlan(uid, cid);
  applyOutcomeOfFinishedStep(plan);   // mark done / failed / skipped
  await dispatchReady(plan);          // dispatch the next ready step
});

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

Когда шаг завершается неудачно

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

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

Если ошибка не временная, указанная для шага политика on_failure определяет дальнейшие действия команды:

  • abort_plan — шаг настолько важен, что без него всё последующее бессмысленно. Он помечается как неудачный, затем каскадно каждому ещё ожидающему шагу присваивается статус skipped. План корректно останавливается, а не продолжает строиться на отсутствующей основе.
  • continue — этот шаг был необязательным. Ему присваивается статус skipped, а последующие шаги продолжаются так, будто он просто не дал результата.
  • ask_commander (по умолчанию) — ни слепая остановка, ни слепое продолжение. Шаг помечается как неудачный, а ведущий агент пробуждается, чтобы разобраться в случившемся и решить: повторить иначе, найти обходной путь или остановиться и спросить пользователя.

Стоит отдельно отметить шестой статус: blocked. В ходе работы субагент может понять, что ему нужно что-то, что может предоставить только пользователь, и показать форму или вопрос. Шаг не завершается ошибкой: он переходит в состояние blocked, и весь план приостанавливается. Как только пользователь отвечает, исполнитель согласует состояние, и команда продолжает ровно с того места, где остановилась. Заблокированный план приостановлен, а не сломан.

Каждый шаг — полноценный запуск агента

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

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

Несколько важных решений

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

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

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

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

В заключение

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

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