Phase 13 - Lesson 03

Chamadas de Ferramentas em Paralelo e Streaming com Ferramentas

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Três consultas de clima independentes serializadas representam três viagens de ida e volta (round trips). Execute-as em paralelo e o tempo total se reduz ao da chamada única mais lenta. Todos os principais provedores de fronteira agora emitem várias chamadas de ferramentas em um único turno. O retorno é real; a implementação é sutil. Esta lição aborda ambas as metades: a distribuição paralela (fan-out) e a remontagem de argumentos transmitidos por streaming, com ênfase na armadilha de correlação de IDs.

Type: Build Languages: Python (stdlib, thread pool + streaming harness) Prerequisites: Phase 13 · 02 (function calling deep dive) Time: ~75 minutos

Objetivos de Aprendizado

  • Explicar por que parallel_tool_calls: true existe e quando desativá-lo.
  • Correlacionar partes de argumentos transmitidos (streamed argument chunks) ao ID correto da chamada de ferramenta durante a distribuição paralela (parallel fan-out).
  • Remontar strings de arguments parciais em um JSON completo sem realizar o parsing prematuro.
  • Executar um benchmark de clima de três cidades que demonstra latência sequencial vs paralela.

O Problema

Sem chamadas paralelas, um agente respondendo a "qual é o clima em Bengaluru, Tóquio e Zurique" faz o seguinte:

user -> LLM
LLM -> call get_weather(Bengaluru)
host -> run executor, reply with result
LLM -> call get_weather(Tokyo)
host -> run executor, reply with result
LLM -> call get_weather(Zurich)
host -> run executor, reply with result
LLM -> final text answer

Três viagens de ida e volta (round trips) do LLM, cada uma das quais também paga a latência do executor. Aproximadamente 4x o tempo ideal de relógio (wall-clock time).

Com chamadas paralelas:

user -> LLM
LLM -> call get_weather(Bengaluru); call get_weather(Tokyo); call get_weather(Zurich)
host -> run all three executors concurrently, reply with three results
LLM -> final text answer

Uma viagem de ida e volta do LLM. O tempo do executor é o máximo dos três, não a soma. Benchmarks de produção na OpenAI, Anthropic e Gemini mostram de 60 a 70 por cento de redução no tempo de relógio em cargas de trabalho de distribuição (fan-out).

O preço é a complexidade de correlação. Quando as três chamadas terminam fora de ordem, seus resultados devem carregar o tool_call_id correspondente para que o modelo possa alinhá-los. Quando os resultados são transmitidos via streaming, você deve remontar fragmentos parciais de argumentos em um JSON completo antes de executar. O Gemini 3 adicionou IDs exclusivos em parte para resolver um problema do mundo real em que duas chamadas paralelas à mesma ferramenta eram indistinguíveis.

O Conceito

Habilitando o paralelismo

  • OpenAI. parallel_tool_calls: true ativado por padrão. Defina como false para forçar o modo serial.
  • Anthropic. Paralelo via disable_parallel_tool_use: false (padrão no Claude 3.5 e superior). Defina como true para serial.
  • Gemini. Sempre capaz de paralelismo; tool_config.function_calling_config.mode = "AUTO" permite que o modelo decida.

Desative o paralelismo quando as ferramentas tiverem dependências de ordenação (create_file e depois write_file), quando a saída de uma chamada informar a entrada de outra ou quando o limitador de taxa (rate limiter) não puder lidar com a distribuição (fan-out).

Correlação de IDs

Cada chamada que o modelo emite possui um id. Cada resultado que o host retorna deve incluir o mesmo ID. Sem isso, os resultados ficam ambíguos.

  • OpenAI. tool_call_id em cada mensagem com a função (role) tool.
  • Anthropic. tool_use_id em cada bloco tool_result.
  • Gemini. id em cada functionResponse (Gemini 3 e superior; o Gemini 2 correspondia pelo nome, o que causava falhas para chamadas paralelas de mesmo nome).

Executando chamadas concorrentemente

O host executa o executor de cada chamada em sua própria thread, corrotina ou worker remoto. O harness mais simples usa um pool de threads; a produção usa asyncio com asyncio.gather ou concorrência estruturada. A ordem de conclusão é imprevisível — o ID é o identificador.

Um bug comum: responder com os resultados na ordem da lista de chamadas em vez da ordem de conclusão. Isso geralmente funciona porque o modelo só se importa com o tool_call_id, mas se um resultado for perdido ou duplicado, o envio fora de ordem torna a depuração mais difícil. Prefira responder na ordem de conclusão com IDs explícitos.

Streaming de chamadas de ferramentas

Quando o modelo transmite por streaming, os arguments chegam em pedaços. Três fluxos separados de pedaços para três chamadas paralelas se intercalam na transmissão. Você precisa de um acumulador por ID.

Formato por provedor:

  • OpenAI. Cada parte é choices[0].delta.tool_calls[i].function.arguments (string parcial). A parte carrega o index (posição na lista de chamadas). Você acumula por índice, lê o id quando ele aparece pela primeira vez e analisa o JSON quando finish_reason = "tool_calls".
  • Anthropic. Os eventos de stream são message_start, depois um content_block_start por bloco com tipo tool_use (contendo id, nome, input vazio). Os eventos content_block_delta carregam partes de input_json_delta. O content_block_stop fecha cada bloco.
  • Gemini. O streamFunctionCallArguments (Gemini 3 e superior) emite partes com um functionCallId para que as chamadas se intercalem de forma limpa. Antes do Gemini 3, o streaming retornava uma chamada completa de cada vez.

JSON parcial e a armadilha do parsing prematuro

Você não pode analisar os arguments até que estejam completos. Um JSON parcial como {"city": "Beng não é válido e causará um erro. A validação correta é o sinal de fim de chamada do provedor: finish_reason = "tool_calls" da OpenAI, content_block_stop da Anthropic ou o evento de fim de transmissão do Gemini. Somente então tente executar json.loads. Uma abordagem mais robusta usa um parser JSON incremental que gera eventos à medida que a estrutura é concluída; o guia de streaming da OpenAI recomenda isso para uma UX que mostra um indicador de "pensando" em tempo real. A contagem de chaves é não confiável como teste de completude (chaves dentro de strings entre aspas ou conteúdo escapado causam falsos positivos) e deve ser usada apenas como uma heurística informal de depuração.

Conclusão fora de ordem

call_A: fast API, returns first
call_B: slow API, returns second
call_C: median API, returns third

A resposta do host ainda deve citar os IDs:

[{role: "tool", tool_call_id: "call_A", content: ...},
 {role: "tool", tool_call_id: "call_B", content: ...},
 {role: "tool", tool_call_id: "call_C", content: ...}]

A ordem na resposta não importa para a correção na OpenAI ou Anthropic. O Gemini aceita qualquer ordem, desde que os IDs correspondam.

Benchmark: sequencial vs paralelo

O harness em code/main.py simula três executores com latências de 400, 600 e 800 ms. O modo sequencial executa em 1800 ms no total. O modo paralelo executa em max(400, 600, 800) = 800 ms. A diferença é constante, não proporcional, de modo que a economia cresce com a quantidade de ferramentas.

Ressalva do mundo real: chamadas paralelas sobrecarregam APIs downstream. Uma distribuição (fan-out) de 10 vias para um serviço com limite de taxa (rate-limited) irá falhar. A Fase 13 · 17 cobre o backpressure em nível de gateway; semânticas de repetição estão planejadas para uma fase futura.

Tempo de relógio na distribuição de streaming

Se o próprio modelo realiza streaming, você pode iniciar a execução assim que os argumentos de uma chamada estiverem completos, em vez de esperar que todas as chamadas sejam finalizadas. Essa é uma otimização que a OpenAI documenta, mas nem todos os SDKs expõem. O harness desta lição faz isso: assim que o fluxo simulado gera um objeto de argumento completo, o host inicia essa chamada.

Use

code/main.py tem duas metades. A primeira executa três chamadas de clima simuladas de forma sequencial e paralela usando concurrent.futures.ThreadPoolExecutor e imprime o tempo de relógio. A segunda metade reproduz uma resposta de streaming falsa — partes de arguments para três chamadas paralelas intercaladas em um único fluxo — e as remontam por ID com o StreamAccumulator. Sem LLM, sem rede, apenas a lógica de remontagem.

O que observar:

  • O temporizador sequencial chega a 1,8 segundos. O temporizador paralelo chega a 0,8 segundos sob as mesmas latências simuladas.
  • O acumulador lida com a chegada de partes fora de ordem fazendo o buffering por ID e analisando o JSON apenas quando o de cada chamada estiver completo.
  • O executor inicia assim que os argumentos de um ID são finalizados, não depois que todos os streams terminam.

Entregue

Esta lição produz outputs/skill-parallel-call-safety-check.md. Dado um registro de ferramentas, a skill audita quais ferramentas são seguras para paralelizar, quais possuem dependências de ordenação e quais sobrecarregariam os limites de taxa downstream — retornando um registro revisado com flags parallel_safe por ferramenta.

Exercícios

  1. Execute code/main.py e varie as latências simuladas. Confirme que a razão entre paralelo e sequencial é de aproximadamente max/sum (as execuções reais desviam-se ligeiramente do ideal devido ao agendamento de threads, serialização e sobrecarga do harness). Em qual distribuição de latência o paralelismo deixa de ter importância?

  2. Estenda o acumulador para tratar o caso em que a "chamada foi cancelada no meio do fluxo" descartando seu buffer e emitindo um evento cancelled. Qual provedor documenta esse caso explicitamente? Verifique a semântica de content_block_stop da Anthropic e o comportamento de finish_reason: "length" da OpenAI.

  3. Substitua o pool de threads por asyncio.gather. Faça o benchmark de ambos. Você deve ver pequenos ganhos no modo assíncrono devido ao menor custo de troca de contexto, mas apenas se os executores realizarem E/S (I/O) real.

  4. Escolha duas ferramentas que NÃO devem ser paralelizadas (por exemplo, create_file e depois write_file). Adicione um grafo de ordering_dependency ao registro e controle a distribuição paralela baseando-se nesse grafo. Esse é o mecanismo mínimo para agendamento ciente de dependências, que uma fase futura de engenharia de agentes formalizará.

  5. Leia a seção de chamadas paralelas de funções da OpenAI e os documentos de disable_parallel_tool_use da Anthropic. Identifique o único tipo de ferramenta do mundo real onde a Anthropic recomenda desativar o paralelismo. (Dica: mutações consequentes no mesmo recurso.)

Termos-Chave

Termo O que dizem O que realmente significa
Chamadas de ferramentas em paralelo "Distribuição (fan-out) em um único turno" O modelo emite várias chamadas de ferramentas em uma única mensagem do assistente
parallel_tool_calls "A flag da OpenAI" Habilitar ou desabilitar a emissão de múltiplas chamadas
disable_parallel_tool_use "O inverso da Anthropic" Flag de desativação (opt-out); o padrão é o paralelismo ativado
ID da chamada de ferramenta "Identificador de correlação" Identificador por chamada que a mensagem de resultado deve ecoar
Acumulador "Buffer de fluxo (stream)" Buffer de string por ID para partes parciais de arguments
Conclusão fora de ordem "Mais rápido primeiro" Chamadas paralelas terminam em ordem imprevisível; IDs são o elo de ligação
Grafo de dependência "Restrições de ordenação" Ferramentas cujas saídas alimentam as entradas de outras ferramentas; não podem ser paralelizadas
Armadilha do parsing prematuro "JSON.parse explodiu" Tentar analisar uma string de arguments incompleta
streamFunctionCallArguments "Recurso do Gemini 3" Partes de argumentos transmitidas por stream com um ID exclusivo por chamada
Resposta em ordem de conclusão "Não espere por todas" Responder com os resultados conforme chegam, indexados pelo ID

Leitura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).