O que é Orquestrador e Trabalhadores?
Orquestrador e Trabalhadores (Orchestrator-Workers) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Um modelo decide as subtarefas na hora e distribui para trabalhadores.
A Paula lidera a migração de um sistema legado, de um framework antigo para um novo. São quatrocentos arquivos. Alguns só precisam trocar os imports. Outros têm uma lógica cujo significado muda no framework novo. E alguns dependem uns dos outros, e precisam migrar juntos.
A primeira tentativa foi um agente só percorrendo os arquivos. Na metade, o contexto estava cheio, e as primeiras decisões, como tratar o novo sistema de eventos, tinham se perdido.
A segunda tentativa foi um script que mandava cada arquivo para uma chamada isolada. Rápido. Mas arquivos que dependiam uns dos outros saíam incoerentes.
O que funciona tem as duas partes. Alguém que olha o todo e decide como dividir. E operários que fazem cada parte, com foco. A divisão muda a cada módulo, então não dá para fixar no código. É esta aula.
Separe três papéis. O orquestrador lê a tarefa e o contexto, e produz uma lista de subtarefas, cada uma com uma instrução completa, e as dependências. Ele não faz o trabalho.
Os operários recebem uma subtarefa cada, fazem no próprio contexto e devolvem um resultado num formato combinado. Subtarefas sem dependência rodam em paralelo.
E o sintetizador junta os resultados, resolve conflitos e entrega o produto final. Muitas vezes é o próprio orquestrador, numa segunda chamada.
A diferença para o sectioning é quem decide a divisão. Lá, o seu código. Aqui, o modelo, olhando a entrada. Se você consegue escrever o loop que divide o trabalho antes de ver a entrada, você não precisa de orquestrador. E saiba o preço de antemão. Esse padrão compra tempo e cobertura com dinheiro. Times que rodam isso em escala relataram usar muitas vezes os tokens de um chat comum.
A tarefa chega, com o material necessário, ou acesso a ele.
O orquestrador analisa e decide a divisão.
O plano é um objeto estruturado. Subtarefas, a instrução de cada uma, dependências e um critério de pronto.
Os operários rodam em paralelo quando não há dependência, cada um com o seu contexto e orçamento.
O código valida cada resultado, com schema, testes ou compilação, antes de seguir.
O sintetizador junta tudo e resolve as inconsistências.
E o resultado sai, com o plano e os resultados parciais registrados no trace.
A Paula pede: migre o módulo de pagamentos para o framework novo. O orquestrador lê o módulo e devolve um plano.
Quatro subtarefas. Models e events, sem dependência. Depois service, que depende dos dois. Depois a API, que depende do service. Cada uma com um critério de pronto, como os testes de models passam.
O código transforma essas dependências em ondas. A onda um roda models e events, em paralelo. A onda dois roda o service, que recebe os dois resultados. A onda três roda a API. Quatro operários, três ondas e uma síntese no fim.
E repare nas convenções no topo. Eventos usam o novo barramento, nunca o emit antigo. Erros de domínio herdam de uma classe de erro de pagamento. Todo operário recebe isso.
Uma pergunta para você. Durante a migração, dois operários devolvem código que compila. Mas um trata erros com exceção, e o outro com código de retorno. A síntese não consegue conciliar. Onde está a correção mais barata?
No plano. O orquestrador fixa a convenção de erro, e todo operário recebe. Decisões que valem para várias partes moram no plano. Quando a convenção chega a todo operário antes de ele começar, as partes já nascem compatíveis.
Um sintetizador mais forte que reescreve um dos lados paga para refazer um trabalho que nasceu incoerente. E operários maiores também não ajudam. Cada um só vê a própria subtarefa, e não tem como saber o que o outro decidiu. A regra prática: toda decisão que mais de um operário precisa tomar vem do orquestrador, antes de qualquer operário começar.
A divisão é sempre a mesma. Por arquivo, por página, por seção. Aí divida no código.
A tarefa cabe num agente, com contexto limpo. Um orquestrador acrescenta custo e pontos de falha. Comece com um loop só e divida quando o contexto encher.
As partes dependem tanto umas das outras que nenhuma roda sozinha. O paralelismo some, e você paga o plano e a síntese sem ganhar tempo nenhum.
E os operários seriam papéis decorativos, passando texto adiante. Um analista, um revisor e um redator, lendo o mesmo contexto e reescrevendo a mesma resposta, não dividem trabalho nenhum.
Um caso rápido. O time quer um resumo semanal de trinta relatórios de loja. Cada relatório vira um parágrafo, e uma versão consolidada sai no fim. Alguém propõe um orquestrador. O que você responde? A divisão é fixa, um relatório por parte. Isso é um loop no código. Você ganha paralelismo e contexto limpo por relatório, sem a chamada do plano, e sem o risco de um plano ruim.
E um primo próximo. O plan then execute também começa com um plano. A diferença é quem executa. Lá, um agente só segue o plano passo a passo, no mesmo contexto. Aqui, cada subtarefa vai para um operário com contexto próprio, e as independentes rodam ao mesmo tempo. Use o primeiro quando os passos são sequenciais e precisam do que veio antes. Use este quando as partes cabem em contextos separados, e o volume justifica o paralelismo.
Vamos dar um preço, com números ilustrativos. O plano custa uns seis mil tokens, cada operário vinte mil, e a síntese quinze mil. Com quatro subtarefas, a tarefa gasta uns cento e um mil tokens. Se o orquestrador decidir por vinte subtarefas, o total passa de quatrocentos e vinte mil. E a qualidade não sobe necessariamente. Por isso o limite de subtarefas mora no prompt, e também no código. O modelo escolhe a divisão. O código decide quanto ela pode custar.
Os modos de falha. Um plano ruim, com subtarefas sobrepostas, ou uma parte faltando. Isso estraga tudo o que vem depois, então avalie o plano sozinho. Para vinte tarefas reais, escreva à mão as partes que um bom plano precisa cobrir. Parte faltando é falha de cobertura. Duas subtarefas mexendo no mesmo arquivo é sobreposição. E valide o plano no código também. Sem ciclos, sem dependência apontando para o nada.
Divisão demais. Dezenas de subtarefas triviais, com saídas curtíssimas e um custo total alto.
E incoerência. Operários tomando decisões diferentes sobre o mesmo problema. Você acabou de ver a correção. Convenções no plano.
Em produção, registre o plano como um span, com um span filho para cada operário. Coloque limite de subtarefas e de ondas por tarefa, com alerta quando baterem. E acompanhe as novas tentativas por subtarefa. Um tipo de subtarefa que sempre falha está pedindo um prompt de operário melhor.
Então, a ideia para levar. Use um orquestrador só quando a divisão depende do conteúdo. Deixe ele escrever um plano com instruções completas, dependências, critérios de pronto e convenções compartilhadas. Deixe o código rodar as ondas, validar cada resultado e limitar até onde a divisão pode ir.
Na próxima aula, a gente fecha a trilha de agentes com um sistema real que usa quase tudo o que você viu: um agente de código. Até lá.
Orchestrator-Workers
Orquestrador e Trabalhadores (Orchestrator-Workers) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Um modelo decide as subtarefas na hora e distribui para trabalhadores.
Um modelo orquestrador decide, em tempo de execução, em quais subtarefas dividir o trabalho, entrega cada uma a um trabalhador e junta os resultados. Serve para tarefas cuja divisão não dá para prever antes de olhar a entrada.
Um mestre de obras que olha a casa, decide quantos pedreiros e eletricistas chamar e confere tudo no fim.
Paula lidera a migração de um sistema legado de um framework antigo para um novo. São 400 arquivos. Alguns precisam só trocar imports; outros têm lógica de ciclo de vida que muda de semântica; alguns dependem de outros e precisam migrar juntos.
A primeira tentativa foi um agente único percorrendo os arquivos. Na metade, o contexto estava cheio e as decisões do começo (como tratar o novo sistema de eventos) tinham se perdido. A segunda tentativa foi um script que mandava cada arquivo para uma chamada isolada. Rápido, mas os arquivos que dependiam uns dos outros saíam incoerentes.
O que funciona tem as duas partes: alguém que olha o todo e decide como dividir, e trabalhadores que fazem cada parte com foco. A divisão muda a cada módulo, então não dá para fixá-la no código.
A pergunta que este padrão responde: como dividir dinamicamente uma tarefa grande entre vários modelos, mantendo coerência no resultado final?
Separe três papéis:
A diferença para o Seccionamento é quem decide a divisão: lá, o código; aqui, o modelo, olhando a entrada. Se você consegue escrever o for que divide o trabalho antes de ver a entrada, não precisa de orquestrador.
O post “Building effective agents” da Anthropic (dezembro de 2024) descreve este padrão com dois exemplos: produtos de código que mudam vários arquivos a cada pedido e buscas que juntam informação de várias fontes. Em junho de 2025, a Anthropic publicou como construiu o sistema de pesquisa do Claude com um agente líder que dispara subagentes em paralelo. O mesmo texto conta o custo: o sistema multiagente gastava cerca de 15 vezes mais tokens que uma conversa comum. O padrão compra tempo e cobertura com dinheiro.
Paula pede: “Migre o módulo payments para o framework novo”. O orquestrador lê a árvore do módulo e devolve este plano:
{
"conventions": [
"Eventos usam o novo EventBus; nunca chame emit() direto",
"Erros de domínio herdam de PaymentError"
],
"subtasks": [
{ "id": "models", "instruction": "Migre models/*.py para o novo ORM", "depends_on": [], "done_when": "testes de models passam" },
{ "id": "events", "instruction": "Troque emit() por EventBus em events.py", "depends_on": [], "done_when": "events.py compila" },
{ "id": "service", "instruction": "Migre service.py usando os models e eventos novos", "depends_on": ["models", "events"], "done_when": "testes de service passam" },
{ "id": "api", "instruction": "Atualize as rotas de api.py", "depends_on": ["service"], "done_when": "testes de api passam" }
]
}
O código transforma as dependências em ondas. A onda 1 roda models e events em paralelo. A onda 2 roda service, que recebe o resultado das duas. A onda 3 roda api. São quatro trabalhadores, três ondas e uma síntese no fim.
Repare em duas coisas. Cada instrução é autocontida: o trabalhador de events não precisa ler o resto do módulo para saber o que fazer. E o campo conventions leva para todos os trabalhadores as decisões que valem para o módulo inteiro.
Sem conventions, cada trabalhador decide sozinho como tratar o sistema de eventos. Um usa o EventBus, outro mantém emit() com um adaptador, e os dois resultados compilam. A síntese recebe duas soluções corretas e incompatíveis.
É o mesmo problema da primeira tentativa de Paula, só que em paralelo. A regra prática: toda decisão que mais de um trabalhador precisa tomar sai do orquestrador, no plano, antes de qualquer trabalhador começar.
flowchart TD
IN(["1. Tarefa"]):::io --> O(["2. Orquestrador planeja"]):::llm
O --> P[("3. Plano: subtarefas e dependências")]:::store
P --> W1(["4. Trabalhador"]):::llm
P --> W2(["4. Trabalhador"]):::llm
P --> W3(["4. Trabalhador"]):::llm
W1 --> V["5. Validar resultados"]:::tool
W2 --> V
W3 --> V
V --> S(["6. Sintetizar"]):::llm
S --> OUT(["7. Resultado"]):::io
schema Subtask { id: string, instruction: string, depends_on: list[string], done_when: string }
schema Plan { conventions: list[string], subtasks: list[Subtask] }
schema WorkResult { id: string, output: string, ok: bool, notes: string }
function orchestrate(task):
# 2 e 3. o modelo decide a divisão
plan = llm(ORCHESTRATOR_PROMPT, task) -> Plan
validate_dag(plan) # sem ciclos, sem ids soltos
trace.span("plan", plan)
results = {}
for wave in topological_waves(plan.subtasks):
# 4. cada onda roda em paralelo
parallel {
for st in wave:
ctx = plan.conventions + [st.instruction] + [results[d].output for d in st.depends_on]
results[st.id] = llm(WORKER_PROMPT, ctx) -> WorkResult
}
# 5. validar antes da próxima onda
for st in wave:
if not check(st.done_when, results[st.id]):
results[st.id] = retry_once(st, results)
budget.check()
# 6. sintetizar
return llm(SYNTH_PROMPT, task, plan, results) # 7
O Planejar e Executar também começa com um plano. A diferença está em quem executa. Lá, um único agente segue o plano passo a passo, no mesmo contexto. Aqui, cada subtarefa vai para um trabalhador com contexto próprio, e as independentes rodam ao mesmo tempo.
Use Planejar e Executar quando os passos são sequenciais e precisam do que veio antes. Use Orquestrador e Trabalhadores quando as partes cabem em contextos separados e o volume justifica o paralelismo.
Suponha que o plano custe 6 mil tokens, cada trabalhador 20 mil e a síntese 15 mil. Com 4 subtarefas, a tarefa gasta cerca de 101 mil tokens. Se o orquestrador decide dividir em 20 subtarefas, o total passa de 420 mil, e a qualidade não necessariamente sobe.
Por isso o limite de subtarefas fica no prompt e também no código. O modelo escolhe a divisão; o código decide quanto ela pode custar.
| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | 1 plano, N trabalhadores, 1 síntese, mais retries | spans plan, worker e synthesize |
| Custo | Proporcional ao número de subtarefas escolhido pelo modelo | número de worker por tarefa |
| Latência | Plano e síntese em série, trabalhadores em paralelo por onda | spans worker sobrepostos |
| Falha: plano ruim | Subtarefas sobrepostas ou faltando uma parte | resultado final com lacuna; plano sem item correspondente |
| Falha: divisão demais | Dezenas de subtarefas triviais | worker com saída muito curta e custo total alto |
| Falha: incoerência | Trabalhadores tomam decisões diferentes para o mesmo problema | conflitos resolvidos (ou não) na síntese |
{ "task": "Migre o módulo payments para o framework novo", "check": "compila e passa nos testes de payments", "max_subtasks": 12 }
Avalie o plano separado do resultado. Para 20 tarefas reais, escreva à mão a lista de partes que um bom plano precisa cobrir. Depois compare com os planos gerados: parte faltando é falha de cobertura, duas subtarefas mexendo no mesmo arquivo é sobreposição. Um plano ruim estraga tudo o que vem depois, então é ele que você corrige primeiro.
plan com o plano completo, spans worker filhos com a subtarefa e o resultado.| Prós | Contras |
|---|---|
| A divisão se adapta a cada entrada | Custo difícil de prever |
| Paralelismo reduz o tempo total | O orquestrador vira ponto único de falha |
| Cada trabalhador tem contexto limpo | Coerência entre partes exige uma síntese cuidadosa |
| Partes falhas podem ser refeitas sozinhas | Mais spans, mais prompts, mais coisas para avaliar |
As abas fazem o plano com saída estruturada, executam os trabalhadores em paralelo por onda e sintetizam. Python e TypeScript disparam as ondas com o runtime da linguagem. LangGraph dispara cada trabalhador com Send, e o estado junta os retornos.
Python. Use quando você quer o plano validado em código e os trabalhadores em asyncio.
import asyncio
import anthropic
from pydantic import BaseModel
client = anthropic.AsyncAnthropic()
class Subtask(BaseModel):
id: str
instruction: str
depends_on: list[str]
class Plan(BaseModel):
subtasks: list[Subtask]
async def plan(task: str) -> Plan: # 2 e 3
reply = await client.messages.parse(
model="claude-opus-5-5", max_tokens=8000,
system="Divida a tarefa em no máximo 12 subtarefas autocontidas. Liste dependências por id.",
messages=[{"role": "user", "content": task}], output_format=Plan,
)
return reply.parsed_output
async def work(st: Subtask, deps: dict[str, str]) -> str: # 4
context = "\n\n".join(f"Resultado de {d}:\n{deps[d]}" for d in st.depends_on)
reply = await client.messages.create(
model="claude-sonnet-5-5", max_tokens=16000,
system="Você executa uma subtarefa de um plano maior. Faça só o que foi pedido.",
messages=[{"role": "user", "content": f"{st.instruction}\n\n{context}"}],
)
return reply.content[-1].text
async def orchestrate(task: str) -> str:
p = await plan(task)
pending, results = {s.id: s for s in p.subtasks}, {}
while pending:
wave = [s for s in pending.values() if all(d in results for d in s.depends_on)]
if not wave:
raise ValueError("plano com ciclo ou dependência inexistente")
outputs = await asyncio.gather(*(work(s, results) for s in wave)) # paralelo
for s, out in zip(wave, outputs):
results[s.id] = out # 5. valide aqui (testes, schema) antes de seguir
del pending[s.id]
joined = "\n\n".join(f"[{k}]\n{v}" for k, v in results.items())
final = await client.messages.create( # 6
model="claude-opus-5-5", max_tokens=16000,
system="Junte os resultados num produto final coerente. Aponte conflitos.",
messages=[{"role": "user", "content": f"Tarefa: {task}\n\n{joined}"}],
)
return final.content[-1].textO que você ganhou: plano tipado, ondas em paralelo e erro claro para plano inválido. O que você perdeu: os trabalhadores aqui são chamadas únicas; para trabalho com ferramentas, troque work por um loop de agente.
TypeScript. Use quando a orquestração roda em Node e você quer Promise.all por onda.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const Plan = z.object({
subtasks: z.array(z.object({ id: z.string(), instruction: z.string(), depends_on: z.array(z.string()) })),
});
type Subtask = z.infer<typeof Plan>["subtasks"][number];
const text = (m: Anthropic.Message) => m.content.find((b) => b.type === "text")?.text ?? "";
async function work(st: Subtask, results: Map<string, string>): Promise<string> {
const context = st.depends_on.map((d) => `Resultado de ${d}:\n${results.get(d)}`).join("\n\n");
const reply = await client.messages.create({
model: "claude-sonnet-5-5", max_tokens: 16000,
system: "Você executa uma subtarefa de um plano maior. Faça só o que foi pedido.",
messages: [{ role: "user", content: `${st.instruction}\n\n${context}` }],
});
return text(reply);
}
export async function orchestrate(task: string): Promise<string> {
const { parsed_output: plan } = await client.messages.parse({
model: "claude-opus-5-5", max_tokens: 8000,
system: "Divida a tarefa em no máximo 12 subtarefas autocontidas. Liste dependências por id.",
messages: [{ role: "user", content: task }],
output_config: { format: zodOutputFormat(Plan) },
});
if (!plan) throw new Error("orquestrador não devolveu plano");
const results = new Map<string, string>();
let pending = plan.subtasks;
while (pending.length) {
const wave = pending.filter((s) => s.depends_on.every((d) => results.has(d)));
if (!wave.length) throw new Error("plano com ciclo ou dependência inexistente");
const outs = await Promise.all(wave.map((s) => work(s, results))); // 4. paralelo
wave.forEach((s, i) => results.set(s.id, outs[i])); // 5. valide aqui
pending = pending.filter((s) => !results.has(s.id));
}
const joined = [...results].map(([k, v]) => `[${k}]\n${v}`).join("\n\n");
const final = await client.messages.create({
model: "claude-opus-5-5", max_tokens: 16000,
system: "Junte os resultados num produto final coerente. Aponte conflitos.",
messages: [{ role: "user", content: `Tarefa: ${task}\n\n${joined}` }],
});
return text(final);
}O que você ganhou: o plano é validado pelo Zod e as ondas rodam em paralelo. O que você perdeu: o mesmo que em Python; retry e orçamento total ficam por sua conta.
Python + LangGraph. Use quando cada trabalhador é um nó e o paralelismo tem de aparecer no grafo, não num `gather`.
import operator
from typing import Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from langgraph.types import Send
from pydantic import BaseModel
from typing_extensions import TypedDict
class Task(BaseModel):
id: str
instruction: str
class Plan(BaseModel):
tasks: list[Task]
class State(TypedDict):
goal: str
plan: list[dict]
results: Annotated[list[str], operator.add]
report: str
planner = init_chat_model("anthropic:claude-opus-5-5", temperature=0).with_structured_output(Plan)
worker_model = init_chat_model("anthropic:claude-haiku-4-5", temperature=0)
synth = init_chat_model("anthropic:claude-opus-5-5", temperature=0)
def plan(state: State) -> dict:
# 2 e 3. o orquestrador só planeja
drafted = planner.invoke([
("system", "Divida a tarefa em subtarefas independentes. Não execute."),
("human", state["goal"]),
])
return {"plan": [t.model_dump() for t in drafted.tasks]}
def fan(state: State):
# 4. um Send por trabalhador; eles rodam em paralelo
return [Send("work", {"goal": task["instruction"]}) for task in state["plan"]]
def work(state: State) -> dict:
reply = worker_model.invoke([("human", state["goal"])])
return {"results": [reply.text]} # operator.add junta as listas
def synthesize(state: State) -> dict:
# 5 e 6. validar e juntar fica no seu código antes desta chamada, se o critério for teste
report = synth.invoke([("human", "\n".join(state["results"]))])
return {"report": report.text}
graph = StateGraph(State)
graph.add_node("plan", plan)
graph.add_node("work", work)
graph.add_node("synthesize", synthesize)
graph.add_edge(START, "plan")
graph.add_conditional_edges("plan", fan, ["work"])
graph.add_edge("work", "synthesize")
graph.add_edge("synthesize", END)
app = graph.compile()O que você ganhou: cada trabalhador é uma invocação separada no trace, com o retorno reduzido por operator.add. O que você perdeu: onda com dependência entre tarefas pede um segundo fan depois da validação; esta aba mostra a onda independente.
| Construção | Paralelismo | Nesta página |
|---|---|---|
asyncio ou Promise.all |
O runtime | Abas Python e TypeScript |
Send |
O grafo | Aba LangGraph |
| Um agente com várias ferramentas | O modelo escolhe a ordem | Forma errada para este padrão |
Responda as 6 perguntas para concluir a aula. Só assistir não conclui: a checagem é o que conta para a trilha e para o certificado.
Orquestrador e Trabalhadores (Orchestrator-Workers) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Composição. Um modelo decide as subtarefas na hora e distribui para trabalhadores.
Nos cenários reais Pesquisa profunda com citações, Agente de código e Migração de código legado.