Lição 3: Paralelização: seccionamento e votação
Objetivos de aprendizado:
- Distinguir as duas variantes da paralelização — seccionamento e votação —, entender o que cada uma resolve e respeitar a fronteira traçada por “saídas agregadas programaticamente”
- Usar
Promise.all e um pool de concorrência próprio para implementar seccionamento, garantindo que a agregação passe referências em vez de cargas
- Calcular os três custos do fan-out (resultados inundando o contexto, tetos reais de concorrência nos produtos, o multiplicador de N× tokens da votação) e usá-los para decidir se uma proposta deve ser paralelizada
Pré-requisitos: Concluir as Lições 1 e 2, ter o wrapper runAgent() da Lição 2 | Anterior: << Lição 2: Encadeie, roteie: encadeamento e roteamento | Próxima: Lição 4 >>
Doze documentos, uma cadeia, uma hora na fila
A cadeia da Lição 2 agora funciona: esboço → gate → rascunho → gate → conferir terminologia. Troque por prompts voltados à revisão — extrair pontos-chave, sugerir mudanças, verificar termos — e a forma da cadeia não muda. Rode em um documento: seis ou sete minutos.
Aí o time de produto larga um diretório na sua mesa: 12 documentos, cada um precisando da mesma passada de revisão.
Você escreve um for, dá o start e vai fazer café. Uma hora depois você volta. O log parou no documento 9. O documento 10 está extraindo pontos-chave.
Durante aquela hora, a máquina passou a maior parte do tempo esperando. Esperando a resposta da API do documento 1 antes de mandar a requisição do documento 2. Esperando o documento 2 terminar os quatro estágios antes de o documento 3 ter a sua vez. Faça uma pergunta prática: a conclusão da revisão do documento 3 depende de uma única palavra do resultado do documento 2?
Não. São 12 documentos independentes. Os relatórios de revisão deles não se importam com quem termina primeiro. No encadeamento, a espera tem um motivo — a entrada da etapa seguinte é a saída da anterior. Aqui não existe esse motivo. Essas 12 execuções só estão enfileiradas porque um for as colocou na fila.
O curso 6 desta série já cobriu os padrões de colaboração de fan-out e agregação e como funciona a votação de múltiplas perspectivas1. Esta lição transforma isso em código e acerta as contas: fan-out não é de graça. A velocidade é real, e o custo também.
Definição: dá para rodar ao mesmo tempo, com as saídas agregadas programaticamente
Comece pela formulação original. LLMs às vezes conseguem trabalhar em uma tarefa simultaneamente e ter suas saídas agregadas programaticamente. Este fluxo de trabalho é a paralelização, com duas variações principais1:
- Seccionamento: quebrar uma tarefa em subtarefas independentes rodadas em paralelo1. Revisar 12 documentos é seccionamento.
- Votação: rodar a mesma tarefa várias vezes para obter saídas diversas1. Ter três perspectivas avaliando o mesmo texto é votação.
Quando usar: quando as subtarefas divididas puderem ser paralelizadas por velocidade, ou quando múltiplas perspectivas ou tentativas forem necessárias para resultados de maior confiança1. Há um acréscimo fácil de pular, mas valioso — para tarefas complexas com múltiplas considerações, LLMs geralmente têm desempenho melhor quando cada consideração é tratada por uma chamada de LLM separada, permitindo atenção focada em cada aspecto específico1. Traduzindo: seccionamento e votação não são só economizadores de tempo. Enfiar “jurídico, segurança, marca” em um prompt só, versus ter três chamadas cada uma cuidando de uma coisa, produz qualidades diferentes.
Mais uma meia-frase a fixar: saídas agregadas programaticamente. Depois que os resultados abertos em fan-out voltam, é o seu código que julga, filtra e resume — não outra chamada de modelo para ler os 12 relatórios e escrever um resumo. Deixar o modelo agregar é um padrão diferente. O orquestrador da Lição 4 faz exatamente esse trabalho. Trace a linha com clareza aqui. A documentação atual da plataforma Claude lista Parallelization sob orquestração multiagente: abrir subtarefas independentes em fan-out simultaneamente (buscar em múltiplas fontes, analisar arquivos separados) e ter o coordenador sintetizando os resultados2 — note que naquela versão é o coordenador que faz a agregação, enquanto esta lição escreve a versão de agregação programática1. Mesma palavra, mas quem agrega são duas coisas diferentes.
Seccionamento: troque o for loop por Promise.all
A versão serial fica assim, com o tempo total sendo a soma dos 12:
A versão com seccionamento muda uma linha, e o tempo total se aproxima do mais lento:
O runAgent() continua na mesma posição de wrapper da Lição 2 — um loop de harness completo, ramificando internamente pelo stop_reason. As respostas do stub desta lição se completam todas em um turno (end_turn), então a versão do script final omite o ramo de ferramenta como simplificação. Ao conectar em um cliente de verdade ou ao precisar de ferramentas, traga de volta, sem mudanças, a versão da Lição 2 que despacha ferramentas. A paralelização não altera nenhuma linha desse loop em si. Ela só para de fazer esses loops ficarem na fila.
A agregação acontece na linha seguinte, feita por este código:
Estas três linhas não contêm uma segunda chamada de modelo. filter, reduce, uma conferência de limiar — tudo código determinístico. Os mesmos 12 relatórios entram, a mesma conclusão de uma linha sai, toda vez. Esse é o benefício de manter a agregação em código: as 12 chamadas do fan-out são não determinísticas, a etapa de junção é determinística. Quando algo quebra, você sabe de qual lado suspeitar.
O Promise.all tem um temperamento que convém conhecer de antemão: se uma promise qualquer rejeitar, o await inteiro rejeita, e mesmo que as outras 11 tenham terminado, você não consegue os resultados delas. Revise 12 documentos, o documento 7 pega um 500 e o lote inteiro é desperdiçado — os outros 11 rodaram à toa. Esse custo é desarrazoado. Ou troque para Promise.allSettled, ou, como no exercício desta lição, embrulhe cada worker em try/catch para coletar as falhas como registros — todo caminho de fan-out deveria conseguir falhar de forma independente.
Por que paralelização não é só sobre velocidade
Se paralelização fosse só “a mesma coisa feita mais cedo”, seria um truque de desempenho, não valeria uma lição própria. O motivo de verdade está do lado do contexto.
A retrospectiva da Anthropic sobre o sistema multiagente de pesquisa deles é direta: a essência da busca é compressão — destilar percepções de um corpus vasto. Subagentes facilitam a compressão ao operar em paralelo com as suas próprias janelas de contexto, explorando aspectos diferentes da pergunta simultaneamente antes de condensar os tokens mais importantes para o agente líder de pesquisa. Cada subagente também fornece separação de responsabilidades — ferramentas, prompts e trajetórias de exploração distintas —, o que reduz a dependência do caminho e permite investigações minuciosas e independentes3.
Abra essas duas frases. O fan-out compra pelo menos três coisas:
- Capacidade de janela. Eles escreveram esse julgamento arquitetural como conclusão: distribuir trabalho entre agentes com janelas de contexto separadas acrescenta capacidade para raciocínio paralelo3. A Lição 1 cobriu isso: o que realmente bate no teto não é o tamanho da janela, é “um loop só” como forma. O fan-out contorna os limites de janela única não esticando a janela, mas abrindo várias.
- Separação de responsabilidades. Três subagentes carregando ferramentas e prompts diferentes naturalmente não vão poluir uns aos outros.
- Menos dependência do caminho. Em um loop só, o julgamento da etapa 3 é enviesado pela formulação da etapa 2. Três trajetórias independentes não compartilham o mesmo viés.
A documentação atual da plataforma aponta na mesma direção: múltiplos agentes podem agir em paralelo com o próprio contexto isolado, o que ajuda a melhorar a qualidade da saída e também pode melhorar o tempo até a conclusão2. Note que a qualidade vem primeiro.
Do lado da velocidade eles deram um número, com um contexto que precisa ser copiado junto: os primeiros agentes deles executavam buscas sequenciais, o que era dolorosamente lento. Por velocidade, eles introduziram dois tipos de paralelização: (1) o agente líder sobe de 3 a 5 subagentes em paralelo em vez de em série; (2) os subagentes usam 3 ou mais ferramentas em paralelo. Essas mudanças cortaram o tempo de pesquisa em até 90% para consultas complexas3.
Esse número precisa ser usado com os seus três qualificadores: é um número de latência, não de qualidade; está limitado a consultas complexas (consultas simples não têm muito o que paralelizar); e vem do sistema deles. Quanto você economiza trocando for por Promise.all depende de quanto das suas subtarefas são genuinamente independentes, de quão lento é cada caminho e de onde a concorrência encontra gargalo — o resto desta lição é sobre isso.
Primeiro custo: a agregação come de volta o contexto que você economizou
Ao abrir em fan-out, todo mundo olha para “quantos caminhos rodam de uma vez”. As quebras normalmente acontecem no caminho de volta.
A documentação de subagentes do Claude Code coloca esse custo na mesa: quando os subagentes terminam, os resultados deles voltam para a sua conversa principal. Rodar muitos subagentes que devolvem, cada um, resultados detalhados pode consumir contexto significativo4. Subagentes existem para proteger o contexto da conversa principal — mantendo exploração e implementação fora da sua conversa principal4 —, mas quando o que volta é pesado demais, a proteção se inverte.
A mesma retrospectiva deu um remédio, e deu de forma específica: em vez de exigir que os subagentes comuniquem tudo através do agente líder, implemente sistemas de artefatos em que agentes especializados possam criar saídas que persistem de forma independente. Os subagentes chamam ferramentas para guardar o trabalho deles em sistemas externos e depois passam referências leves de volta ao coordenador3.
Traduza isso para um mantra na hora de escrever código: passe referências, não cargas.
Qual é o tamanho da diferença? O script de exercício desta lição dá um número real: 8 saídas em disco somam 2.120 bytes, e o que volta para a agregação são só 716 bytes; e essa proporção se abre rapidamente conforme os documentos crescem — relatórios dez vezes mais longos, e o que passa de volta continua sendo um resumo de uma linha mais um caminho. Quem precisar do texto inteiro que leia a partir do caminho.
Esse caminho compra outras coisas de quebra. A documentação de fluxos de trabalho menciona que o runtime rastreia o resultado de cada agente conforme a execução avança, e é isso que torna uma execução retomável dentro da mesma sessão. Um fluxo de trabalho que abre o trabalho em fan-out entre muitos agentes pequenos, portanto, preserva mais progresso do que um agente longo5. As saídas aterrissam fora, deixando registro linha a linha — a parte de manter registro é o mesmo princípio da observabilidade do curso 11 desta série. A parte de “interrompido no meio não recomeça do zero” é território do curso 9, “fazendo tarefas longas sobreviverem à interrupção”.
Segundo custo: concorrência nunca é ilimitada
No momento em que você escreve Promise.all(docs.map(...)), você está de fato dizendo “concorrência = tamanho do array”. Array de 12, tudo bem. Array de 200, é outra história.
Olhe os tetos de três produtos reais:
- Claude Code: por padrão, quando 20 subagentes estão rodando em uma sessão, subir mais um com a ferramenta Agent falha com
Concurrent subagent limit reached, e a mensagem de erro diz explicitamente ao Claude para não tentar de novo4.
- Runtime de fluxos de trabalho do Claude Code: até 16 agentes concorrentes, menos quando o Claude Code tem menos CPUs disponíveis (inclusive dentro de um contêiner com CPU limitada)5; 1.000 agentes no total por execução5.
- Managed Agents: no máximo 25 threads concorrentes são suportadas. O coordenador pode chamar várias cópias de um mesmo agente do elenco, criando várias threads associadas a um agente2.
Três times diferentes, três implementações diferentes, todos estabeleceram tetos, e os números nem são grandes. Esse fato em si é material didático: fan-out ilimitado é um acidente, não uma otimização. (A Lição 4 vai cobrir um caso real de quebra — os primeiros agentes subiam 50 subagentes para uma consulta simples3. Lá você vai descobrir que “quem decide quantos subir” é mais espinhoso do que “qual é o teto”.)
A forma mais barata de limitar o fluxo é o lote:
Funciona, mas tem efeito de balde: cada lote espera o mais lento dele terminar antes de começar o próximo. De três documentos, um é especialmente longo, e os outros dois caminhos ficam só esperando.
Um pool de concorrência não tem esse problema — fixe N “faixas”, e cada faixa pega o próximo item de um cursor compartilhado assim que termina o trabalho atual, sempre com N em voo:
Umas dez linhas, sem dependências. O cursor++ é seguro no JavaScript de thread única — código síncrono entre dois await não pode ser interrompido. Não há risco de duas faixas pegarem o mesmo índice. No exercício você vai acrescentar try/catch para que a falha de um caminho não derrube o lote inteiro.
Quanto deve valer limit? Não há resposta universal. É a interseção entre a sua cota de API, a capacidade dos serviços a jusante e a duração de um caminho. Mas preencher um número específico versus não preencher nenhum são dois tipos de engenharia.
Terceiro custo: votação paga N× tokens
A definição de votação é uma frase: rodar a mesma tarefa várias vezes para obter saídas diversas1. O código é curto:
A agregação aqui continua sendo feita por código — filter mais um limiar. Quantos votos definem o limiar é uma decisão de produto, fixada no código, alterável a qualquer momento, auditável. Isso não deveria ficar por conta da improvisação do modelo. Cenários de alto risco podem ajustar o limiar para “um veto basta”; cenários de baixo risco podem exigir os três votos para barrar.
A contabilidade é direta: vote N vezes, pague N× tokens. Coloque esse dinheiro ao lado do multiplicador da Lição 1 — pelos dados deles, agentes usam cerca de 4× os tokens de interações de chat, e sistemas multiagente cerca de 15×. Sistemas multiagente, portanto, precisam de tarefas valiosas o bastante para cobrir o custo desse ganho de desempenho3. A etiqueta de preço da votação de três perspectivas é aquele 4× de agente único vezes mais 3. Não multiplique o 15× por 3 — aquele 15× já inclui a contabilidade do fan-out.
Então votação não é “rodar mais algumas vezes para dormir tranquilo”. Ela precisa comprar algo concreto. A documentação de fluxos de trabalho é mais clara: mover o plano para dentro do código também permite que um fluxo de trabalho aplique um padrão de qualidade repetível, e não apenas rode mais agentes — ele pode fazer agentes independentes revisarem adversarialmente os achados uns dos outros antes de serem reportados, ou rascunhar um plano a partir de vários ângulos e pesá-los uns contra os outros, de modo que você obtenha um resultado mais confiável do que o de uma passada única5.
“Agentes independentes revisando uns aos outros” é a mesma regra do curso 10 desta série: quem executa não julga o próprio trabalho. Ao fazer o modelo se autoconferir no mesmo contexto, na maior parte das vezes ele vai defender o que acabou de produzir. Troque para um caminho de contexto independente, troque os prompts, e aí talvez ele realmente pegue problemas. O que a votação e a revisão por pares compram não é “maioria”, é independência.
Note uma fronteira: três chamadas a um mesmo modelo não são três juízes independentes. Elas compartilham os mesmos vieses de treinamento. A votação consegue filtrar ruído de amostragem e lacunas de atenção de uma passada única. Ela não consegue filtrar vieses sistemáticos. Não a trate como um mecanismo que produz verdade só por votar.
A régua: independência é pré-requisito, não opcional
Todos os benefícios desta lição repousam sobre um pré-requisito que já apareceu repetidamente e vale destacar: as subtarefas precisam realmente não depender umas das outras.
A documentação do Claude Code, ao discutir vários subagentes investigando ao mesmo tempo, acrescenta uma frase específica: cada subagente explora a sua área de forma independente, e depois o Claude sintetiza os achados. Isso funciona melhor quando os caminhos de pesquisa não dependem uns dos outros4. A condição inversa está escrita na retrospectiva multiagente: alguns domínios exigem que todos os agentes compartilhem o mesmo contexto ou envolvem muitas dependências entre agentes, e esses domínios hoje não são uma boa escolha para sistemas multiagente. Por exemplo, a maior parte das tarefas de programação envolve menos tarefas genuinamente paralelizáveis do que pesquisa, e agentes LLM ainda não são muito bons em coordenar e delegar a outros agentes em tempo real3.
Para julgar se uma proposta deve ser paralelizada, faça uma pergunta: o caminho 2 precisa esperar a conclusão do caminho 1 para saber o que fazer?
- Precisa esperar → esta não é a forma da paralelização. A saída da etapa anterior é a entrada da seguinte — isso é o encadeamento da Lição 2.
- Não precisa esperar → seccionamento.
- Mesma coisa, mas você quer vários julgamentos independentes → votação.
O primeiro caso é o mais fácil de embaçar: existe uma dependência, mas você força o fan-out porque “provavelmente vai dar certo”. O resultado são vários caminhos de agente escrevendo cada um a sua conclusão, sem saber dos outros. Na agregação, você suaviza as contradições na mão — o tempo economizado vai todo para a suavização, e ainda por cima você pagou tokens a mais.
Fronteira: a decomposição desta lição é predefinida
Fixe uma palavra — ela é a entrada da Lição 4.
Em todos os exemplos desta lição, quem definiu as subtarefas? Você. Doze documentos, você os leu do diretório. Três perspectivas, você as fixou em um array. Antes de o código rodar, quantos caminhos abrem em fan-out e o que cada um faz já está tudo resolvido. Isso se chama decomposição predefinida.
O oposto: o modelo decide em quantas partes e o que cada uma faz. A fonte original trata essa diferença como o divisor de águas entre dois padrões — no fluxo de trabalho orquestrador-workers, um LLM central decompõe tarefas dinamicamente, delega-as a LLMs workers e sintetiza seus resultados1. Embora seja topograficamente semelhante à paralelização, a diferença-chave é a flexibilidade dele — as subtarefas não são predefinidas, e sim determinadas pelo orquestrador com base na entrada específica1.
Então a fronteira entre os dois não é “quantos caminhos rodam de uma vez”, e sim “quem escreveu aquele array”. O array é seu, é esta lição. O array é gerado pelo modelo na hora, é a lição seguinte. O roteamento da lição passada já entregou uma decisão ao modelo (qual ramo). A lição seguinte entrega um pedaço maior: a própria decomposição.
💻 Exercícios
Recapitulação
- A definição de paralelização é “LLMs às vezes conseguem trabalhar em uma tarefa simultaneamente e ter suas saídas agregadas programaticamente”, e as duas variantes são seccionamento (quebrar uma tarefa em subtarefas independentes rodadas em paralelo) e votação (rodar a mesma tarefa várias vezes para obter saídas diversas)1
- As condições dela são: as subtarefas podem ser paralelizadas por velocidade, ou são necessárias múltiplas perspectivas e tentativas para maior confiança. Para tarefas complexas com múltiplas considerações, tratar cada uma com uma chamada separada, com atenção focada, geralmente tem desempenho melhor1
- O fan-out compra mais do que velocidade: subagentes operando em paralelo com as próprias janelas de contexto exploram e comprimem de volta os tokens mais importantes, trazendo também separação de responsabilidades (ferramentas, prompts e trajetórias de exploração distintas) e menos dependência do caminho3. Distribuir trabalho entre agentes com janelas de contexto separadas acrescenta capacidade para raciocínio paralelo3
- O número da aceleração vem com contexto: o sistema de pesquisa deles introduziu paralelização em dois níveis (o líder sobe de 3 a 5 subagentes em paralelo, e os subagentes usam 3 ou mais ferramentas em paralelo), cortando o tempo de pesquisa em até 90% para consultas complexas3 — este é um número de latência do sistema deles, não um número de qualidade
- A agregação é o primeiro custo: os resultados dos subagentes voltam para a conversa principal, e muitos subagentes devolvendo, cada um, resultados detalhados consomem contexto significativo4. A solução são sistemas de artefatos — os subagentes guardam a saída em sistemas externos e passam de volta ao coordenador apenas referências leves3
- Concorrência nunca é ilimitada: o Claude Code usa por padrão 20 subagentes concorrentes e falha com um explícito “não tente de novo” quando excedido4. O runtime de fluxos de trabalho suporta até 16 agentes concorrentes (menos quando a CPU é limitada), com teto de 1.000 por execução5. Os Managed Agents vão até 25 threads concorrentes2
- O custo da votação é N× tokens, e precisa ser colocado junto naquele multiplicador — pelos dados deles, agentes ficam em cerca de 4× o chat, e sistemas multiagente em cerca de 15× o chat. O valor da tarefa precisa ser alto o bastante para cobrir isso3. O que ela deve comprar é um padrão de qualidade repetível, como agentes independentes revisando adversarialmente, ou rascunhar a partir de vários ângulos e depois pesar5
- Independência é pré-requisito: vários subagentes investigando ao mesmo tempo funciona melhor quando os caminhos de pesquisa não dependem uns dos outros4. Domínios que exigem contexto compartilhado ou muitas dependências hoje não são uma boa escolha3 — havendo dependência, volte à forma do encadeamento
- A decomposição desta lição é toda predefinida (você escreveu o array). Subtarefas não predefinidas, e sim determinadas pelo orquestrador com base na entrada específica, são a diferença-chave entre orquestrador-workers e paralelização1, e também o tema da próxima lição
>> Lição 4: Orquestrador-workers: tornar a própria decomposição dinâmica