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: trueexiste 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
argumentsparciais 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: trueativado por padrão. Defina comofalsepara forçar o modo serial. - Anthropic. Paralelo via
disable_parallel_tool_use: false(padrão no Claude 3.5 e superior). Defina comotruepara 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_idem cada mensagem com a função (role) tool. - Anthropic.
tool_use_idem cada blocotool_result. - Gemini.
idem cadafunctionResponse(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 oindex(posição na lista de chamadas). Você acumula por índice, lê oidquando ele aparece pela primeira vez e analisa o JSON quandofinish_reason = "tool_calls". - Anthropic. Os eventos de stream são
message_start, depois umcontent_block_startpor bloco com tipotool_use(contendo id, nome, input vazio). Os eventoscontent_block_deltacarregam partes deinput_json_delta. Ocontent_block_stopfecha cada bloco. - Gemini. O
streamFunctionCallArguments(Gemini 3 e superior) emite partes com umfunctionCallIdpara 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
Execute
code/main.pye varie as latências simuladas. Confirme que a razão entre paralelo e sequencial é de aproximadamentemax/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?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 decontent_block_stopda Anthropic e o comportamento definish_reason: "length"da OpenAI.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.Escolha duas ferramentas que NÃO devem ser paralelizadas (por exemplo,
create_filee depoiswrite_file). Adicione um grafo deordering_dependencyao 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á.Leia a seção de chamadas paralelas de funções da OpenAI e os documentos de
disable_parallel_tool_useda 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
- OpenAI — Parallel function calling — comportamento padrão e a flag de exclusão (opt-out)
- Anthropic — Tool use: implementing tool use —
disable_parallel_tool_usee loteamento de resultados - Google — Gemini function calling parallel section — chamadas paralelas correlacionadas por ID a partir do Gemini 3
- OpenAI — Streaming responses with tools — remontagem de argumentos fragmentados para streams da OpenAI
- Anthropic — Streaming messages —
content_block_deltacominput_json_delta