Orkas Orkas
Início Blog Arquitetura
Arquitetura

Orquestração multiagente na prática: como o Orkas gere um agente líder e os seus subagentes

Por dentro da orquestração multiagente do Orkas: um agente líder transforma um pedido num plano, atribui tarefas a subagentes conforme as dependências, transmite contexto entre etapas e recupera de falhas.

O artigo anterior abordava como tornar um agente fiável: ciclo de execução, encaminhamento de ferramentas, compactação de contexto, sessões resistentes a falhas. Essa camada responde à pergunta "como é que um único agente realiza uma tarefa sem falhar?" Este artigo trata da camada acima: o que acontece quando um agente não é suficiente e um trabalho precisa de ser dividido por uma equipa.

É isto que as pessoas geralmente querem dizer com orquestração multiagente: um agente líder, responsável pela conversa, divide um pedido em partes, entrega cada parte a um subagente especializado, espera que as partes necessárias terminem antes de iniciar a seguinte, transmite os resultados e mantém tudo sob controlo quando uma etapa falha. O Orkas executa tudo isto na máquina do utilizador. Veja abaixo como esta camada de orquestração é construída: o código foi limpo e generalizado, mas a estrutura é real.

Em resumo Um agente líder e os seus subagentes, num só espaço de trabalho É assim que o Orkas distribui o trabalho hoje: o Commander recruta especialistas, executa-os em paralelo ou em série, e pode acompanhar o processo.
Descarregar o Orkas — grátis

Agente líder e subagentes

O modelo mental é uma equipa pequena com uma cadeia de comando clara. Um agente líder (a que chamamos comandante) é responsável pela conversa e pelo contexto geral. Não faz todo o trabalho sozinho; a sua função é decidir o que precisa de ser feito, em que ordem e por quem. Os subagentes são especialistas — cada um configurado com o seu próprio prompt de sistema, as ferramentas que pode utilizar e o seu próprio conjunto de competências. Um subagente é bom numa parte do trabalho e é invocado quando essa parte surge.

Duas coisas fazem com que isto seja mais do que uma expressão da moda. Primeiro, os subagentes e as competências são unidades de primeira classe, e não truques improvisados: um subagente é um agente real, configurado separadamente, e atribuir-lhe uma tarefa é uma transferência real com o seu próprio contexto. Em segundo lugar, a coordenação não é deixada às boas intenções do modelo – é orientada por um artefacto explícito cuja fidelidade é assegurada pelo sistema, e não pelo modelo. Esse artefacto é o plano.

Um plano é um grafo, não um script

Quando o agente principal decide que um pedido precisa de mais do que uma etapa, escreve um plano. O plano não é prosa de formato livre nem uma lista de verificação linear – é um pequeno grafo de dependências (um DAG). Cada nó é uma etapa, e cada etapa contém tudo o que o orquestrador precisa para a distribuir:

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;
}

O campo wait_for é o que transforma uma lista num grafo. Por predefinição, uma etapa espera pela etapa anterior (uma cadeia simples), mas pode declarar que depende de várias etapas anteriores - "resumir" pode esperar tanto por "pesquisar o mercado" como por "pesquisar concorrentes". Isto é um losango, não uma linha, e o orquestrador trata-o como tal.

Quem detém a verdade: o orquestrador, não o modelo

Veja novamente a divisão nesta estrutura. O modelo preenche a intenção — títulos, responsáveis, entradas, dependências — quando escreve o plano pela primeira vez. Mas tudo sobre o estado de execução — status, output_summary, failure_reason — pertence exclusivamente ao orquestrador. O modelo propõe o plano uma vez; nunca marca as suas próprias etapas como "concluídas".

Esta separação é deliberada e é a decisão mais importante de toda a conceção. Um modelo de linguagem é perfeitamente capaz de anunciar alegremente "etapa 3 concluída" quando a etapa 3 falhou, ou de perder a noção de quais as etapas que ainda estão pendentes a meio de uma longa conversa. Se o estado existisse na cabeça do modelo, o plano afastar-se-ia da realidade. Ao tornar o estado um artefacto estruturado que apenas o executor escreve – e escreve apenas em resposta a uma etapa realmente concluída – o plano permanece um espelho preciso do que realmente aconteceu. O modelo decide a forma do trabalho; o ambiente de execução decide o que é verdade sobre o seu progresso.

Distribuir as etapas que estão prontas

Quando existe um plano, um pequeno mecanismo — o executor — fá-lo avançar. A operação principal é “encontrar as etapas que estão prontas e distribuí-las”. Uma etapa está pronta quando ainda está pending quando todas as etapas pelas quais espera atingiram um estado terminal, com sucesso suficiente:

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
  });
}

Isto não é executado por um temporizador, mas em resposta a eventos. Sempre que uma etapa termina – um subagente devolve um resultado, o comandante conclui um turno de síntese – o executor faz a reconciliação: regista o resultado da etapa que acabou de ser concluída, verifica novamente o que ficou pronto em consequência e distribui essas tarefas. A orquestração é este ciclo de reconciliação, que se repete continuamente até não restar nenhuma etapa.

O envio passa pela mesma conversa, não por um canal paralelo

Esta é uma escolha que mantém o sistema fiel à realidade: a distribuição de uma etapa não usa um canal RPC oculto. Publica uma mensagem na mesma conversa de grupo que o utilizador está a acompanhar, do agente principal, @mencionando o subagente. Para o subagente, receber uma tarefa é indistinguível de ser interpelado na conversa – limita-se a executar o seu turno normal. Não existe um segundo caminho de execução que seja necessário manter sincronizado com o primeiro.

Um responsável pode ser de três tipos, e o envio é ligeiramente diferente para cada um:

  • Um subagente — o caso comum. O executor converte o nome do agente no respetivo id e publica @<agent> <rendered input> em nome do comandante. O subagente recebe a mensagem e executa um turno completo do agente.
  • O utilizador — quando uma etapa precisa realmente de intervenção humana, transforma-se num formulário e coloca o plano em pausa (mais sobre isto abaixo). Nada acontece até que o utilizador responda.
  • O próprio comandante — para etapas de síntese ou decisão ("leia tudo acima e escreva o resumo"). Esta é uma ativação privada que não publica uma mensagem redundante visível para o utilizador; o agente principal limita-se a executar um turno com o contexto recolhido disponível.

Passar contexto de uma etapa para a seguinte

Uma equipa só é útil se o trabalho fluir entre os seus membros. O mecanismo é o modelo input. Quando o agente líder escreve uma etapa, a entrada não é uma cadeia de texto fixa — pode fazer referência a resultados anteriores, e o executor resolve essas referências no momento do envio:

// 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…"

Observe o que é transmitido: output_summary, um breve resumo de cada etapa concluída — não a sua transcrição completa. Esta é uma decisão de gestão do orçamento e segue a mesma lógica da compactação de contexto da infraestrutura de execução. Se cada etapa subsequente herdasse o histórico completo, token por token, de tudo o que foi feito antes dela, o contexto aumentaria e o custo dispararia após apenas algumas transferências. Os resumos mantêm cada transferência barata e cada subagente concentrado naquilo de que realmente precisa das etapas anteriores, em vez de analisar como estas chegaram aos resultados. A mensagem inicial do utilizador e quaisquer anexos também são transmitidos, pelo que uma terceira etapa na cadeia ainda conhece o pedido original.

Sequencial por predefinição — e porquê

Poderá esperar que, quando várias etapas ficam prontas ao mesmo tempo — dois ramos de um losango, por exemplo — o orquestrador as execute todas em paralelo. Poderia fazê-lo; em vez disso, atualmente distribui uma etapa pronta de cada vez, a primeira por índice, e deixa as restantes a aguardar a próxima reconciliação. Conduzir uma equipa inteira estritamente um elemento de cada vez é uma escolha deliberada e conservadora, e vale a pena ser honesto sobre o motivo.

O motivo é garantir a correção em condições de simultaneidade. Imagine dois subagentes a terminar quase ao mesmo tempo. Ambas as conclusões desencadeiam uma reconciliação; ambos os processos de reconciliação leram o plano; ambos veem a mesma etapa subsequente ainda em pending - e ambos a distribuem. Agora a mesma etapa é executada duas vezes. Para tornar isto impossível, cada ciclo de leitura-modificação-envio numa determinada conversa é serializado através de um bloqueio por conversa:

// 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
});

O bloqueio garante que "registar o que terminou" e "decidir o que vem a seguir" acontecem como uma unidade indivisível, de modo que uma etapa posterior nunca possa ser distribuída duas vezes. Com este bloqueio implementado, distribuir as etapas uma de cada vez é a solução mais simples e claramente correta. A distribuição verdadeiramente paralela é uma extensão viável sobre esta base - mas a base é um executor serializado e sem condições de corrida, e essa ordem de prioridades (correção primeiro, rapidez depois) é o essencial.

Quando uma etapa corre mal

Ao executar numa máquina real com APIs de modelos externos, as falhas são rotineiras, e o orquestrador classifica-as em alguns casos em vez de tratar todos os erros da mesma forma.

Primeiro, pergunta se a falha foi meramente transitória — uma quebra de ligação, um limite de frequência, um problema. Se assim for, e a etapa ainda não tiver esgotado um pequeno orçamento de novas tentativas, será silenciosamente reposta no estado pending para que a próxima reconciliação a reenvie. (Isto fica acima das novas tentativas durante a execução da infraestrutura; o plano só é reenviado depois de se esgotarem as próprias tentativas do agente, com um limite máximo para que uma etapa realmente interrompida não possa ser repetida para sempre.)

Se a falha for real, a política on_failure declarada na etapa decidirá o que a equipa fará a seguir:

  • abort_plan — esta etapa era suficientemente importante para que nada posterior fizesse sentido sem ela. Assinale-a como falhada e propague o efeito em cascata: cada etapa ainda pendente é marcada como skipped. O plano termina de forma limpa, em vez de continuar a ser construído sobre uma base inexistente.
  • continue — esta etapa era opcional. Marque-a como skipped e deixe as etapas seguintes prosseguirem como se esta simplesmente não tivesse produzido nada.
  • ask_commander (a predefinição) — nem abortar às cegas nem continuar às cegas. Assinale a falha e ative o agente principal para ver o que aconteceu e decidir: tentar novamente de forma diferente, contornar o problema ou parar e perguntar ao utilizador.

Há um sexto estado que vale a pena destacar: blocked. Um subagente a meio da sua etapa pode perceber que precisa de algo que só o utilizador pode fornecer e apresentar um formulário ou uma pergunta. A etapa não falha — passa ao estado blocked e todo o plano fica em pausa. Assim que o utilizador responde, o executor faz a reconciliação e a equipa continua exatamente de onde parou. Um plano bloqueado é um plano em pausa, não um plano avariado.

Cada etapa é uma execução completa do agente

Vale a pena fechar o ciclo com o artigo anterior. Quando o orquestrador atribui uma etapa a um subagente, esse subagente não executa uma rotina simplificada - executa o ciclo completo da infraestrutura de execução: o seu próprio ciclo de execução em fluxo contínuo, as suas próprias chamadas de ferramentas, a sua própria janela de contexto com compactação, a sua própria sessão resistente a falhas. A orquestração situa-se precisamente sobre o ambiente de execução de um único agente; nunca interfere no seu interior. O agente principal decide a forma do trabalho e a ordem das transferências; cada subagente, assim que recebe a sua parte, é um agente completo por si só.

Estas camadas são a razão pela qual os dois artigos se complementam. A infraestrutura torna um agente fiável para uma tarefa. O orquestrador reúne vários agentes fiáveis numa equipa que pode assumir uma tarefa demasiado grande ou demasiado variada para qualquer um deles.

Algumas decisões importantes

O orquestrador controla o estado de execução, o modelo controla a intenção. O modelo propõe o plano; apenas o ambiente de execução marca as etapas como concluídas, falhadas ou ignoradas, e apenas em resposta a algo que está realmente a acontecer. Esta separação é o que mantém o plano como um espelho fiel da realidade, em vez da estimativa otimista do modelo.

Envio pela mesma conversa, não por um canal secundário. Uma etapa atribuída é apenas uma mensagem do agente principal para um subagente. Um único caminho de execução, nada oculto que possa ficar dessincronizado, e o utilizador pode observar a equipa a trabalhar na mesma conversa que já está a ler.

Resumos, não transcrições, entre as etapas. Cada transferência inclui um breve resumo dos resultados anteriores. Mantém os orçamentos de contexto controlados em cadeias longas e cada subagente concentrado no que precisa, não na forma como o anterior lá chegou.

Correção antes do paralelismo. Um bloqueio por conversa serializa cada ciclo de leitura-modificação-envio e as etapas são distribuídas uma de cada vez. A execução estritamente sequencial é a versão claramente livre de condições de corrida que provocam distribuições duplicadas; a distribuição paralela é uma otimização a acrescentar sobre uma base que já está correta.

Para concluir

A camada de orquestração do Orkas não tem nenhum algoritmo exótico na sua essência. O seu valor está em alguns limites firmemente mantidos: um plano que é um grafo de dependências em vez de um script; estado de execução controlado pelo ambiente de execução e não pelo modelo; envio que passa pela mesma conversa que o utilizador está a acompanhar; contexto que passa entre etapas sob a forma de resumos; e um executor que dá prioridade à correção sobre a simultaneidade. Cada um é simples por si só. Juntos, transformam um único agente fiável numa equipa que divide o trabalho, o transmite e recupera quando algo corre mal.

Se quiser conhecer a camada abaixo desta, leia como um único agente é concebido para funcionar de forma fiável. Se quiser conhecer a camada que faz com que cada agente melhore com o uso, leia como os agentes Orkas aprendem com o seu próprio trabalho. E se preferir dirigir esta camada em vez de a construir, o Orkas disponibiliza-a como orquestração de agentes de IA de código aberto que é executada na sua máquina.