Multi-agent em Laravel: 3 padrões testados em produção (Orchestrator, Hierarchical, Swarm)
Multi-agent em Laravel: 3 padrões testados em produção (Orchestrator, Hierarchical, Swarm)
A Anthropic publicou que o sistema de pesquisa multi-agent deles supera o Claude Opus 4 sozinho em 90,2%. Tem dev lendo isso e saindo correndo para "agentizar" tudo no PHP. Resultado: 4 chamadas paralelas pro mesmo modelo, sem estado, sem trace, sem retry. Isso não é multi-agent. É paralelização disfarçada — e quando quebra em produção, ninguém sabe onde olhar.
O ponto não é se multi-agent funciona. Funciona. O ponto é qual padrão usar e como rodar isso dentro de uma stack Laravel sem virar um Frankenstein de jobs.
Neste post mostro os três padrões que sobreviveram a um projeto Laravel em produção: Orchestrator-Worker, Hierarchical e Swarm. Tem código real (Prism PHP + PrismAgents + Bus::batch), tem o anti-padrão que todo mundo cai, e tem o número que mais importa: o custo medido entre eles. Spoiler — o hierarchical sai 30% mais barato que o orchestrator flat quando você tem mais de 4 especialistas.
TL;DR
- O que é: três arquiteturas multi-agent rodando em produção num projeto Laravel real.
- Stack: Laravel 11, Prism PHP, PrismAgents, Redis Queue, State Machine em Eloquent, Trace ID propagado.
- Modelos: Claude Sonnet 4 nos orquestradores, Haiku 4.5 nos workers (reservar Opus pra casos específicos).
- Custo medido: hierarchical com modelos mistos = ~30% menos tokens cobrados que orchestrator flat com Sonnet em tudo.
- Aviso: multi-agent consome ~15× mais tokens que chat normal. Se o seu caso resolve com um prompt bem-feito, não complica.
Por que multi-agent (e por que cuidado)
Anthropic mostrou o ganho técnico: contexto isolado por subagente, paralelização real, decomposição dinâmica de tarefa. O lead agent define plano e delega; cada worker tem janela de contexto própria e devolve só o relevante. Em pesquisa aberta, o ganho é absurdo.
Mas a mesma matéria avisa: agente único já gasta 4× mais token que chat; multi-agent vai pra 15×. E os early failures deles foram coisas como "spawning 50 subagents for simple queries" e workers duplicando a mesma busca.
Tradução: multi-agent é arquitetura, não enfeite. Você só ganha quando:
- A tarefa não cabe num contexto sem perder qualidade.
- Existem subtarefas independentes que podem rodar em paralelo de verdade.
- Você tem como avaliar o resultado de cada agente isoladamente.
Sem isso, é dinheiro queimado e debugging impossível.
Stack base — Job, Queue, State Machine, Trace ID
Antes de qualquer padrão, o esqueleto. Os três compartilham a mesma fundação no Laravel.
1. AgentRun como agregado. Cada execução vira uma linha no banco com estado, trace ID, entrada, saída parcial e custo acumulado.
// database/migrations/2026_05_20_create_agent_runs_table.php
Schema::create('agent_runs', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->string('trace_id')->index(); // propagado entre todos os jobs
$table->string('pattern'); // orchestrator | hierarchical | swarm
$table->string('agent_name');
$table->string('status'); // pending|running|waiting_human|completed|failed
$table->json('input');
$table->json('output')->nullable();
$table->unsignedInteger('tokens_input')->default(0);
$table->unsignedInteger('tokens_output')->default(0);
$table->ulid('parent_run_id')->nullable()->index();
$table->timestamps();
});
2. State machine simples em Eloquent. Sem package mágico. Estado é um campo string + transições explícitas no model. Se você precisa de algo mais sério, spatie/laravel-model-states entrega — mas pra começar, basta:
final class AgentRun extends Model
{
public function transitionTo(string $next): void
{
$allowed = [
'pending' => ['running'],
'running' => ['waiting_human', 'completed', 'failed'],
'waiting_human' => ['running', 'failed'],
];
if (! in_array($next, $allowed[$this->status] ?? [], true)) {
throw new \DomainException("Transição inválida: {$this->status} → {$next}");
}
$this->update(['status' => $next]);
}
}
3. Trace ID propagado. ULID gerado no entry point, viaja em todos os jobs filhos, vai pro log estruturado e pra tabela prism_agent_traces (que o PrismAgents cria automaticamente). Sem isso, debug de fluxo multi-agent é adivinhação.
abstract class AgentJob implements ShouldQueue
{
public function __construct(
public string $traceId,
public string $parentRunId,
) {}
public function handle(): void
{
Log::withContext(['trace_id' => $this->traceId]);
$this->run();
}
abstract protected function run(): void;
}
Com esse esqueleto, os três padrões viram variação de quem dispara quem.
Padrão 1 — Orchestrator-Worker
"A central LLM dynamically breaks tasks into subtasks, delegates to worker LLMs, synthesizes results." — Anthropic, Building Effective Agents.
Esse é o ponto de entrada de todo projeto multi-agent: um gerente que decide e delega para especialistas. Cada worker tem instrução estreita e janela de contexto isolada. O gerente é o único que vê o todo.
Quando usar: três a cinco especialistas, fluxo previsível mas com decisão dinâmica de qual chamar. Exemplo real: pipeline de análise de contrato — um agente extrai cláusulas, outro classifica risco, outro gera resumo executivo. O orquestrador chama os três quando faz sentido, pula quando não.
Em PHP com PrismAgents, o handoff vira asTool():
use Grpaiva\PrismAgents\Agent;
use Grpaiva\PrismAgents\PrismAgents;
use Prism\Prism\Enums\Provider;
$clauseExtractor = Agent::as('clause_extractor')
->withInstructions('Extraia cláusulas relevantes do contrato. Devolva JSON estrito.')
->using(Provider::Anthropic, 'claude-haiku-4-5')
->withHandoffDescription('Extrai cláusulas de contratos PT-BR.');
$riskClassifier = Agent::as('risk_classifier')
->withInstructions('Classifique cada cláusula em baixo/médio/alto risco com justificativa curta.')
->using(Provider::Anthropic, 'claude-haiku-4-5')
->withHandoffDescription('Classifica risco de cláusulas extraídas.');
$summarizer = Agent::as('summarizer')
->withInstructions('Gere um sumário executivo em até 200 palavras.')
->using(Provider::Anthropic, 'claude-haiku-4-5')
->withHandoffDescription('Resume análises pra board.');
$orchestrator = Agent::as('contract_lead')
->withInstructions(<<<'PROMPT'
Você coordena análise de contratos. Sempre extraia cláusulas primeiro.
Só classifique risco se o contrato tiver mais de 3 cláusulas relevantes.
Termine com um sumário se o usuário pediu resumo.
PROMPT)
->using(Provider::Anthropic, 'claude-sonnet-4-6')
->withTools([
$clauseExtractor->asTool(),
$riskClassifier->asTool(),
$summarizer->asTool(),
]);
$result = PrismAgents::run($orchestrator, $request->contract_text);
A peça que falta no exemplo do README é a fila. Em produção a chamada acima fica dentro de um AgentJob:
final class RunContractAnalysisJob extends AgentJob
{
public function __construct(
string $traceId,
string $parentRunId,
public string $contractText,
) {
parent::__construct($traceId, $parentRunId);
}
protected function run(): void
{
$run = AgentRun::findOrFail($this->parentRunId);
$run->transitionTo('running');
try {
$result = PrismAgents::run(
ContractOrchestratorFactory::make(),
$this->contractText,
);
$run->update([
'output' => $result->toArray(),
'tokens_input' => $result->usage->promptTokens,
'tokens_output' => $result->usage->completionTokens,
]);
$run->transitionTo('completed');
} catch (\Throwable $e) {
$run->update(['output' => ['error' => $e->getMessage()]]);
$run->transitionTo('failed');
throw $e;
}
}
}
O orquestrador é síncrono dentro do job, mas o job em si é async. Se a análise demora 40s, o usuário não fica esperando — você notifica via WebSocket/broadcasting quando o AgentRun muda pra completed.
O detalhe que dói depois: o orquestrador chama os workers em sequência por padrão (uma decisão por vez no loop do LLM). Se você quer paralelizar dois workers independentes, precisa pedir explicitamente no prompt ou quebrar em jobs separados com Bus::batch. Isso entra no padrão hierarchical adiante.
Padrão 2 — Hierarchical
Quando o orchestrator começa a engasgar (mais de 5 especialistas, ou quando os especialistas viram subdomínios com 2-3 agentes cada), o flat vira árvore.
Estrutura: um supervisor no topo, sub-orquestradores no meio, workers nas folhas. A LangGraph chama isso de hierarchical teams e recomenda quando você passa de 6 agentes.
top_supervisor (Sonnet)
├── legal_team (Sonnet)
│ ├── clause_extractor (Haiku)
│ └── risk_classifier (Haiku)
└── finance_team (Sonnet)
├── tax_extractor (Haiku)
└── payment_terms (Haiku)
O truque do custo está aí. Top supervisor toma decisão de roteamento — não precisa escrever ensaios. Workers fazem trabalho pesado de extração — também não precisam. O modelo caro fica reservado pros sub-orquestradores que decidem qual worker chamar dentro do time deles, e ainda assim só onde faz diferença.
No nosso projeto, a métrica saiu assim numa amostra de 200 análises:
| Padrão | Modelo dominante | Tokens médios/run | Custo relativo |
|---|---|---|---|
| Orchestrator flat | Sonnet em tudo | ~38k | 1.00× |
| Hierarchical | Sonnet topo + Haiku folhas | ~41k | 0.71× |
Mais tokens, menos custo. Isso só acontece porque Haiku custa uma fração de Sonnet e cobre 80% do volume.
Implementação no Laravel: cada sub-orquestrador vira um Agent cujo asTool() é exposto ao supervisor. Recursão pura. O que muda é a estratégia de fan-out: quando o supervisor identifica que dois times podem trabalhar em paralelo, você dispara Bus::batch em vez de deixar o LLM decidir uma chamada por vez.
$legalTeamRun = new RunSubOrchestratorJob($traceId, $parentId, 'legal', $contract);
$financeTeamRun = new RunSubOrchestratorJob($traceId, $parentId, 'finance', $contract);
Bus::batch([$legalTeamRun, $financeTeamRun])
->name("hier:{$traceId}")
->then(fn (Batch $batch) => MergeSubOrchestratorResultsJob::dispatch(
$traceId,
$parentId,
))
->dispatch();
Os dois sub-orquestradores rodam em workers diferentes, em paralelo, com contextos isolados. O then consolida no merge final. É aqui que o trace_id salva sua vida — sem ele, ler o Telescope vira terapia.
Padrão 3 — Swarm
Sem supervisor. Agentes pares conversam entre si e passam controle via handoff explícito. A OpenAI lançou o conceito no Swarm (hoje incorporado ao Agents SDK) e o ponto-chave é: o contexto viaja junto com a transferência.
Quando usar: conversas longas com domínios bem definidos onde a transferência não é hierárquica. Caso clássico: customer support — triage_agent recebe, decide se é billing_agent ou tech_agent, e quem está dirigindo a conversa muda. Não tem ninguém "acima" coordenando — o próprio agente atual decide pra quem passa.
No Laravel, sem framework nativo de swarm, dá pra implementar com Events + estado compartilhado:
final class SwarmConversation extends Model
{
protected $casts = [
'messages' => 'array',
'shared_state' => 'array',
];
public function activeAgent(): string
{
return $this->current_agent;
}
public function handoff(string $toAgent, array $stateDelta = []): void
{
$this->update([
'current_agent' => $toAgent,
'shared_state' => array_merge($this->shared_state, $stateDelta),
]);
event(new AgentHandedOff($this->trace_id, $this->id, $toAgent));
}
}
Cada agente expõe um tool handoff_to_{agent_name} que, ao ser chamado pelo LLM, atualiza o estado e dispara o próximo job:
$billingAgent = Agent::as('billing_agent')
->withInstructions('Resolva questões de cobrança. Se a dúvida virar técnica, chame handoff_to_tech.')
->using(Provider::Anthropic, 'claude-sonnet-4-6')
->withTools([
HandoffTool::to('tech_agent')->describedAs('Passa para o time técnico.'),
HandoffTool::to('triage_agent')->describedAs('Volta para triagem.'),
]);
O listener de AgentHandedOff enfileira RunSwarmTurnJob com o próximo agente. A conversa vira uma sequência de jobs, cada um com seu próprio agente ativo, mas todos lendo do mesmo shared_state.
Honestidade brutal: swarm é o padrão que menos uso. Maioria dos casos que parecem swarm são na verdade orchestrator com handoff dinâmico — o triagem é um orquestrador simples. Vale quando a conversa precisa transitar entre domínios sem que um deles seja "dono" do fluxo. Em interfaces de chat com personas distintas, faz sentido. Em pipeline de análise, é overengineering.
O anti-padrão clássico — "4 prompts em paralelo"
Já vi essa cena em mais de um code review:
// ❌ Isso NÃO é multi-agent. É só paralelização.
$responses = collect(['extract', 'classify', 'summarize', 'translate'])
->map(fn ($task) => Http::pool(fn ($pool) => [
$pool->post('claude.api', ['prompt' => "Task {$task}: {$input}"]),
]));
Quatro chamadas paralelas pro mesmo modelo, sem estado, sem trace, sem retry coordenado, sem decisão dinâmica. O que isso é, na taxonomia da própria Anthropic, é o padrão de parallelization — útil quando você de fato quer N visões da mesma entrada, mas não tem nada de "agente" aqui.
Por que dá ruim em produção:
- Uma falha não invalida as outras — e ninguém sabe se o resultado parcial vale.
- Sem
AgentRunpor chamada, custo agregado é um chute. - Retry vira "manda tudo de novo" em vez de "retoma do passo que falhou".
- Quando o produto pede uma etapa nova, você reescreve o orquestrador inteiro.
A diferença entre paralelização e orquestração é que orquestração tem um agente decidindo. Se quem decide é um match em PHP, você não tem agente. Tem só requests assíncronos com nome bonito.
Limitações e pontos de atenção
- Custo escala não-linear. O número da Anthropic (15× tokens) é otimista pra casos abertos. Em pipelines fechados dá pra ficar em 5-8×, mas faça medição antes de prometer ROI.
- Loops infinitos. O Agent SDK não traz loop guard nativo. Coloque limite numérico de iterações e detector de repetição. Sempre.
- Debugging. Sem trace ID propagado, três níveis de hierarquia viram pesadelo. Telescope ajuda, mas é o seu
trace_idno log estruturado que resolve. - Latência. Paralelizar workers ajuda, mas o orquestrador continua sendo um loop de LLM que toma decisões em série. Não espere SLA de API REST.
- Estado humano-na-loop. O estado
waiting_humanda state machine existe porque metade dos casos reais precisa de aprovação no meio do fluxo. Planeje desde o dia um.
FAQ rápido
Posso rodar tudo num único job síncrono? Pode, mas a primeira vez que o lead agent chamar 4 workers em sequência, seu job vai estourar timeout. Use job orquestrador + jobs filhos com Bus::batch ou Bus::chain.
Como meço custo real? A tabela prism_agent_traces do PrismAgents guarda token count por agente. Cruze com a tabela de preços do provider e exporte pro Grafana. Sem isso, "barato" é fé.
Qual padrão começar? Orchestrator-worker, sempre. É o mais fácil de entender, de testar e de jogar fora se descobrir que single-agent resolvia. Hierarchical só quando você tiver 5+ especialistas estáveis. Swarm só quando o produto pedir conversa entre personas — não como otimização.
Funciona sem PrismAgents? Funciona — você reimplementa o asTool() na mão usando Prism PHP puro e expõe agentes como tools no ToolBuilder. PrismAgents só economiza boilerplate e te dá tracing de graça.
Conclusão
Multi-agent em Laravel não é mágica nem moda. É uma arquitetura específica que ganha quando o caso pede: contexto isolado, decisão dinâmica, paralelização real. Os três padrões cobrem o espectro — orchestrator pra começar, hierarchical pra escalar barato, swarm quando o produto realmente exige conversa entre iguais. Tudo amarrado pelo mesmo esqueleto Laravel: Job, Queue, State Machine, Trace ID. O resto é engenharia normal.
Se você está construindo esse tipo de coisa em produção e quer trocar pancada técnica com gente fazendo o mesmo, é exatamente o assunto que rola na Beer and Code, a melhor comunidade de AI engineering em português, com grupo no WhatsApp aberto pra quem está construindo IA em produção — devs PHP/Laravel, Python e engenheiros de IA brasileiros trocando experiência de LLM, agente, RAG, MCP e harness na prática.
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.
Você também pode gostar
Multi-agent com Claude: separando search, judge e writer (e quando isso é overengineering)
Quando vale a pena quebrar o agente único em sub-agentes especializados (search, judge, writer) e quando isso vira complexidade desnecessária. Padrão de orquestração com Claude, custo real em tokens e quando voltar para single-agent.
Hands-on: meu primeiro Pull Request 100% gerado por agente em Laravel (com diff e revisão)
Liguei o agente, fui tomar café e voltei 43 minutos depois com um PR de 380 linhas em 9 arquivos. Case study real com harness Laravel + Claude Agent SDK + sandbox isolado, a task escolhida, o loop cronometrado de 43 min em 12 iterações, o diff comentado, os 3 bugs que escaparam pro code review humano, custo total em USD e o veredito sobre soltar isso em produção. Repositório público no final.
Agente que pesquisa antes de agir: multi-tool + RAG em Laravel com pgvector
Como construir um agente em Laravel que decide quando buscar e quando responder direto. Arquitetura completa com Prism PHP, pgvector e a lógica de orquestração que separa demo de produto.
30 perguntas de entrevista para AI engineer (e como eu respondo cada uma)
30 perguntas reais (10 técnicas, 10 de arquitetura, 10 comportamentais) de entrevistas para AI engineer em maio de 2026. Pra cada uma: resposta curta de 30s, resposta de senior de 2min, e o red flag que entrega o junior. Mais 5 perguntas reversas pra filtrar empresa sem maturidade de IA.