Conteúdo da trilha
RefatoraçãoMovendo controle

Paralelizar Etapas Independentes

Parallelize Independent Steps

Paralelizar Etapas Independentes (Parallelize Independent Steps) é uma refatoração de IA para sistemas com LLM, do grupo Movendo controle. Rodar ao mesmo tempo as chamadas que não dependem uma da outra.

Elemento
12/35
Nível
Iniciante
Leitura
5 min
Atualizado
Nesta página 8
  1. 01Problema
  2. 02Solução
  3. 03Antes e depois
  4. 04Por que funciona
  5. 05Passo a passo seguro
  6. 06Riscos
  7. 07Desafio
  8. 08Teste seu entendimento
  9. ··Perguntas rápidas

Problema

Chamadas que não dependem umas das outras rodam em fila, e o usuário espera a soma de todas. É o smell Fila Indiana de Chamadas.

Imagine um copiloto para atendentes de banco. Quando o cliente manda uma mensagem no WhatsApp, o sistema prepara quatro coisas antes de sugerir a resposta: a intenção da mensagem, um resumo do histórico do cliente no CRM, a política que se aplica ao caso e o sentimento do cliente. Cada uma é uma chamada de modelo. O código chama uma, espera, chama a próxima, espera.

Nenhuma dessas quatro precisa do resultado da outra. Todas leem só a conversa ou o histórico. Mesmo assim, o atendente fica olhando um spinner enquanto elas terminam uma depois da outra.

Solução

Desenhe o grafo de dependências do pipeline e rode ao mesmo tempo todos os passos que não esperam resultado de outro. A latência passa a ser a do caminho mais longo, não a soma.

Antes e depois

-intent    = await classify_intent(transcript)
-customer  = await summarize_customer(crm_history)
-policy    = await find_policy(transcript)
-sentiment = await detect_sentiment(transcript)
+intent, customer, policy, sentiment = await asyncio.gather(
+    classify_intent(transcript),
+    summarize_customer(crm_history),
+    find_policy(transcript),
+    detect_sentiment(transcript),
+)
 reply = await generate_reply(intent, customer, policy, sentiment)

Antes: quatro passos em fila.

flowchart LR
  IN(["1. Entrada"]):::io --> A(["Intenção"]):::llm
  A --> B(["Cliente"]):::llm
  B --> C(["Política"]):::llm
  C --> D(["Sentimento"]):::llm
  D --> R(["2. Resposta"]):::llm

Depois: os quatro ao mesmo tempo, e a resposta quando o último termina.

flowchart LR
  IN(["1. Entrada"]):::io --> A(["Intenção"]):::llm
  IN --> B(["Cliente"]):::llm
  IN --> C(["Política"]):::llm
  IN --> D(["Sentimento"]):::llm
  A --> R(["2. Resposta"]):::llm
  B --> R
  C --> R
  D --> R
  1. A entrada é distribuída para todos os passos que só dependem dela.
  2. O passo que precisa de todos espera o mais lento, não a soma.

Exemplo trabalhado

Suponha estas durações, medidas no trace do copiloto (números ilustrativos):

Passo Duração Depende de
Intenção 0,8 s conversa
Cliente 2,4 s histórico do CRM
Política 1,5 s conversa
Sentimento 0,6 s conversa
Resposta 1,8 s os quatro acima

Em fila, o atendente espera 0,8 + 2,4 + 1,5 + 0,6 + 1,8 = 7,1 segundos. Em paralelo, a primeira fase dura o tempo do passo mais lento (2,4 s) e depois vem a resposta (1,8 s). Total: 4,2 segundos.

O número de chamadas é o mesmo, o número de tokens é o mesmo e o texto final é o mesmo. Só o relógio mudou.

Checagem +20 XP de primeiraDepois de paralelizar a primeira fase do copiloto, a latência caiu de 7,1 s para 4,2 s. O time quer chegar a 3 s. Qual é o próximo alvo?

Por que funciona

Uma chamada a modelo passa quase todo o tempo esperando o provedor gerar tokens. O processo que chamou fica parado. Enquanto espera uma, ele pode disparar as outras. Para passos independentes, não há razão de ordem: o resultado é o mesmo, e o relógio corre uma vez só.

Esse tipo de espera é o caso clássico de trabalho limitado por I/O (entrada e saída). Você não precisa de mais CPU nem de threads extras. Um asyncio.gather em Python ou um Promise.all em TypeScript bastam, porque o programa só está esperando a rede.

O ganho tem um limite claro: o caminho crítico. Se o passo mais lento leva 2,4 segundos, a fase paralela leva perto disso, não menos. Por isso, depois de paralelizar, o próximo alvo é o passo mais lento: um modelo menor, uma entrada menor ou cache.

A ideia aparece com nome próprio no post “Building effective agents” da Anthropic (dezembro de 2024), como o workflow de paralelização. Ele tem duas variantes. Em Seccionamento, cada chamada cuida de uma parte diferente da tarefa, como no copiloto. Em Votação, várias chamadas fazem a mesma tarefa e você compara as respostas. Quando a mesma operação se repete sobre muitos itens, o padrão é Map-Reduce.

Passo a passo seguro

  1. Meça. Rode o pipeline com trace e anote a duração de cada passo e a latência total no p50 e no p95.
  2. Desenhe as dependências. Para cada passo, liste o que ele lê. Os que só leem a entrada original são candidatos.
  3. Confirme que não há dependência escondida: estado compartilhado, ordem de escrita num banco, efeitos colaterais.
  4. Troque a sequência por execução simultânea (asyncio.gather, Promise.all, ou o recurso do seu framework).
  5. Trate falhas parciais: decida se a falha de um passo derruba tudo ou se o pipeline segue com um valor padrão.
  6. Limite a concorrência se o número de chamadas simultâneas puder estourar o rate limit.
  7. Garanta que o trace mostre os spans sobrepostos, com o mesmo pai.
  8. Meça de novo. A latência total deve ficar perto da do passo mais lento, e a qualidade não deve mudar.

Erro comum: a dependência que só existe no prompt

Quem começa costuma olhar só o código. Mas às vezes a dependência mora no texto. Imagine que o prompt de find_policy diz “considere a intenção identificada acima”, e o código monta esse prompt com a variável intent. No código em fila, funcionava. No paralelo, intent ainda não existe quando a política começa.

Para pegar isso, faça a lista de dependências a partir das variáveis que entram em cada prompt, não a partir da ordem em que os passos aparecem no arquivo.

Riscos

  • Dependência escondida. Um passo que usava o resultado de outro por um caminho indireto (uma variável global, um cache) passa a ler valor velho. Rodar o eval antes e depois detecta.
  • Rate limit. Mais chamadas simultâneas podem gerar erros 429. Use um limite de concorrência (um semáforo) e retry com espera.
  • Falha que derruba tudo. Com asyncio.gather padrão, o primeiro erro sobe para quem chamou e você perde os outros resultados, mesmo que as outras chamadas continuem rodando e sendo cobradas. Promise.all se comporta igual. Se o pipeline pode seguir sem um dos passos, use return_exceptions=True ou Promise.allSettled e trate cada resultado.
  • Custo igual, expectativa errada. Paralelizar não economiza dinheiro. Se o objetivo é custo, procure outra refatoração, como Introduzir Cache.

A conta do rate limit é simples. Se o copiloto atende 50 conversas ao mesmo tempo e cada uma dispara quatro chamadas juntas, o pico vira 200 chamadas simultâneas, contra 50 antes. O consumo total por minuto não muda, mas os picos ficam quatro vezes mais altos. É o pico que bate no limite do provedor.

Checagem +20 XP de primeiraDepois da troca para asyncio.gather, o passo Política às vezes falha por timeout. Quando isso acontece, o atendente não recebe sugestão nenhuma, embora os outros três passos tenham terminado. O que você ajusta?

Desafio

Quais destes passos de um revisor de contratos podem rodar em paralelo?

1. extrair_texto(pdf)
2. identificar_partes(texto)
3. listar_clausulas(texto)
4. classificar_risco(cada cláusula)
5. checar_partes_em_lista_restritiva(partes)
6. escrever_parecer(riscos, checagem)
Ver solução

Depois do passo 1, os passos 2 e 3 rodam em paralelo, porque ambos leem só o texto. O passo 4 roda em paralelo por cláusula (map) e ao mesmo tempo que o passo 5, que só depende das partes. O passo 6 espera 4 e 5.

texto = extrair_texto(pdf)
parallel { partes = identificar_partes(texto); clausulas = listar_clausulas(texto) }
parallel {
    riscos = parallel_map(classificar_risco, clausulas)
    checagem = checar_partes_em_lista_restritiva(partes)
}
parecer = escrever_parecer(riscos, checagem)

Um detalhe: o passo 5 só precisa das partes, não das cláusulas. Se identificar as partes for bem mais rápido que listar as cláusulas, você pode começar a checagem assim que as partes chegarem, sem esperar o passo 3. É o mesmo princípio aplicado com mais precisão: cada passo começa quando as entradas dele estão prontas.

Teste seu entendimento

Checagem +20 XP de primeiraUm pipeline tem três chamadas independentes de 1 s, 1 s e 3 s, seguidas de uma chamada final de 2 s que usa as três. Qual é a latência depois de paralelizar as três primeiras?

Checagem +20 XP de primeiraSeu gerente pede para paralelizar o pipeline porque a fatura do provedor subiu 40% no mês. O que você responde?

Checagem +20 XP de primeiraDois passos parecem independentes no código: gerar_resumo(ticket) e gerar_titulo(ticket). Mas o prompt de título inclui a frase 'baseie-se no resumo abaixo' e recebe a variável resumo. O que fazer?

Checagem +20 XP de primeiraDepois de paralelizar, o sistema começa a receber erros 429 nos horários de pico, embora o volume diário de chamadas não tenha mudado. Por quê?

Perguntas rápidas

O que é Paralelizar Etapas Independentes?

Paralelizar Etapas Independentes (Parallelize Independent Steps) é uma refatoração de IA para sistemas com LLM, do grupo Movendo controle. Rodar ao mesmo tempo as chamadas que não dependem uma da outra.

Que problema Paralelizar Etapas Independentes resolve?

Trata Fila Indiana de Chamadas.

Aonde Paralelizar Etapas Independentes leva?

Ao padrão de projeto Seccionamento e Map-Reduce de Documentos.

Para ir além

Relações

Smells que trata 1
Buscar