Phase 13 - Lesson 03
Llamadas de Herramientas en Paralelo y Streaming con Herramientas
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Tres consultas de clima independientes serializadas representan tres viajes de ida y vuelta (round trips). Ejecútalas en paralelo y el tiempo total se reduce al de la llamada individual más lenta. Todos los proveedores de frontera ahora emiten múltiples llamadas de herramientas en un solo turno. El beneficio es real; la implementación es sutil. Esta lección aborda ambas mitades: la distribución paralela (fan-out) y la remontada de argumentos transmitidos por streaming, con énfasis en la trampa de correlación de IDs.
Type: Build Languages: Python (stdlib, thread pool + streaming harness) Prerequisites: Phase 13 · 02 (function calling deep dive) Time: ~75 minutos
Objetivos de Aprendizaje
- Explicar por qué existe
parallel_tool_calls: truey cuándo desactivarlo. - Correlacionar fragmentos de argumentos transmitidos (streamed argument chunks) con el ID de llamada de herramienta correcto durante la distribución paralela (parallel fan-out).
- Reensamblar cadenas de
argumentsparciales en un JSON completo sin realizar un análisis (parsing) prematuro. - Ejecutar un benchmark de clima de tres ciudades que demuestre la latencia secuencial vs paralela.
El Problema
Sin llamadas paralelas, un agente que responde a "cómo está el clima en Bengaluru, Tokio y Zúrich" hace lo siguiente:
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
Tres viajes de ida y vuelta (round trips) del LLM, cada uno de los cuales también paga la latencia del ejecutor. Aproximadamente 4x el tiempo de reloj ideal (wall-clock time).
Con llamadas 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
Un viaje de ida y vuelta del LLM. El tiempo del ejecutor es el máximo de los tres, no la suma. Los benchmarks de producción en OpenAI, Anthropic y Gemini muestran una reducción del 60 al 70 por ciento en el tiempo de reloj para cargas de trabajo de distribución (fan-out).
El precio es la complejidad de la correlación. Cuando las tres llamadas se completan fuera de orden, los resultados deben llevar el tool_call_id correspondiente para que el modelo pueda alinearlos. Cuando los resultados se transmiten por streaming, se deben ensamblar los fragmentos parciales de argumentos en un JSON completo antes de la ejecución. Gemini 3 agregó IDs únicos en parte para resolver un problema del mundo real donde dos llamadas paralelas a la misma herramienta eran indistinguibles.
El Concepto
Habilitar el paralelismo
- OpenAI.
parallel_tool_calls: trueactivado por defecto. Establécelo enfalsepara forzar el modo serial. - Anthropic. Paralelo mediante
disable_parallel_tool_use: false(por defecto en Claude 3.5 y versiones superiores). Establécelo entruepara serial. - Gemini. Siempre con capacidad paralela;
tool_config.function_calling_config.mode = "AUTO"permite que el modelo decida.
Desactiva el paralelismo cuando las herramientas tengan dependencias de orden (create_file y luego write_file), cuando la salida de una llamada informe la entrada de otra, o cuando el limitador de tasa (rate limiter) no pueda manejar la distribución (fan-out).
Correlación de IDs
Cada llamada que emite el modelo tiene un id. Cada resultado que devuelve el host debe incluir el mismo ID. Sin esto, los resultados son ambiguos.
- OpenAI.
tool_call_iden cada mensaje con el rol tool. - Anthropic.
tool_use_iden cada bloquetool_result. - Gemini.
iden cadafunctionResponse(Gemini 3 y superior; Gemini 2 realizaba la correspondencia por nombre, lo que fallaba para llamadas paralelas con el mismo nombre).
Ejecución de llamadas de forma concurrente
El host ejecuta el ejecutor de cada llamada en su propio hilo (thread), corrutina o worker remoto. El entorno más simple utiliza un pool de hilos; la producción utiliza asyncio con asyncio.gather o concurrencia estructurada. El orden de finalización es impredecible: el ID es el identificador.
Un error común: responder con los resultados en el orden de la lista de llamadas en lugar del orden de finalización. Esto generalmente funciona porque el modelo solo se preocupa por el tool_call_id, pero si un resultado se pierde o se duplica, el envío fuera de orden dificulta la depuración. Es preferible responder en orden de finalización con IDs explícitos.
Streaming de llamadas de herramientas
Cuando el modelo transmite por streaming, los arguments llegan en partes. Tres flujos separados de fragmentos para tres llamadas paralelas se intercalan en la transmisión. Necesitas un acumulador por ID.
Formato por proveedor:
- OpenAI. Cada fragmento es
choices[0].delta.tool_calls[i].function.arguments(cadena parcial). El fragmento lleva elindex(posición en la lista de llamadas). Acumulas por índice, lees oidcuando aparece por primera vez y analizas el JSON cuandofinish_reason = "tool_calls". - Anthropic. Los eventos de flujo son
message_start, luego uncontent_block_startpor bloque con tipotool_use(que contiene id, nombre, entrada vacía). Los eventoscontent_block_deltallevan fragmentos deinput_json_delta. Elcontent_block_stopcierra cada bloque. - Gemini.
streamFunctionCallArguments(Gemini 3 y superior) emite fragmentos con unfunctionCallIdpara que las llamadas se intercalen de forma limpia. Antes de Gemini 3, el streaming devolvía una llamada completa a la vez.
JSON parcial y la trampa del análisis prematuro
No se pueden analizar los arguments hasta que estén completos. Un JSON parcial como {"city": "Beng no es válido y provocará un error. La validación correcta es la señal de fin de llamada del proveedor: finish_reason = "tool_calls" de OpenAI, content_block_stop de Anthropic o el evento de fin de flujo de Gemini. Solo entonces intenta ejecutar json.loads. Un enfoque más robusto utiliza un analizador JSON incremental que genera eventos a medida que se completa la estructura; la guía de streaming de OpenAI recomienda esto para una UX que muestra un indicador de "pensando" en tiempo real. El conteo de llaves no es confiable como prueba de completitud (las llaves dentro de cadenas de texto entre comillas o contenido escapado causan falsos positivos) y solo debe usarse como una heurística informal de depuración.
Finalización fuera de orden
call_A: fast API, returns first
call_B: slow API, returns second
call_C: median API, returns third
La respuesta del host aún debe citar los 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: ...}]
El orden en la respuesta no importa para la corrección en OpenAI o Anthropic. Gemini acepta cualquier orden siempre que los IDs coincidan.
Benchmark: secuencial vs paralelo
El entorno en code/main.py simula tres ejecutores con latencias de 400, 600 y 800 ms. El modo secuencial se ejecuta en 1800 ms en total. El modo paralelo se ejecuta en max(400, 600, 800) = 800 ms. La diferencia es constante, no proporcional, por lo que el ahorro crece con la cantidad de herramientas.
Advertencia del mundo real: las llamadas paralelas sobrecargan las APIs downstream. Una distribución (fan-out) de 10 vías hacia un servicio con límite de tasa (rate-limited) fallará. La Fase 13 · 17 cubre la contrapresión (backpressure) a nivel de gateway; las semánticas de reintento están planificadas para una fase futura.
Tiempo de reloj en la distribución de streaming
Si el modelo en sí transmite por streaming, puedes comenzar la ejecución tan pronto como los argumentos de una llamada estén completos, en lugar de esperar a que todas las llamadas finalicen. Esta es una optimización que OpenAI documenta pero que no todos los SDKs exponen. El entorno de esta lección lo hace: tan pronto como el flujo simulado genera un objeto de argumento completo, el host inicia esa llamada.
Uso
code/main.py consta de dos partes. La primera ejecuta tres llamadas simuladas de clima de forma secuencial y paralela utilizando concurrent.futures.ThreadPoolExecutor e imprime el tiempo de reloj. La segunda mitad reproduce una respuesta de streaming simulada (fragmentos de arguments para tres llamadas paralelas intercaladas en un solo flujo) y los reensambla por ID con StreamAccumulator. Sin LLM, sin red, solo la lógica de reensamblado.
Qué observar:
- El temporizador secuencial alcanza 1.8 segundos. El temporizador paralelo alcanza 0.8 segundos con las mismas latencias simuladas.
- El acumulador maneja los fragmentos que llegan fuera de orden almacenándolos en un buffer por ID y analizando el JSON solo cuando el de cada llamada está completo.
- El ejecutor se inicia tan pronto como se finalizan los argumentos de un ID, no después de que terminen todos los flujos.
Entregable
Esta lección produce outputs/skill-parallel-call-safety-check.md. Dado un registro de herramientas, la skill audita qué herramientas son seguras para paralelizar, cuáles tienen dependencias de orden y cuáles abrumarían los límites de tasa downstream, devolviendo un registro revisado con flags parallel_safe por herramienta.
Ejercicios
Ejecuta
code/main.pyy varía las latencias simuladas. Confirma que la relación entre paralelo y secuencial es de aproximadamentemax/sum(las ejecuciones reales se desvían ligeramente de lo ideal debido a la planificación de hilos, la serialización y la sobrecarga del entorno). ¿En qué distribución de latencia deja de importar el paralelismo?Extiende el acumulador para manejar un caso de "llamada cancelada a mitad de flujo" descartando su buffer y emitiendo un evento
cancelled. ¿Qué proveedor documenta este caso explícitamente? Verifica la semántica decontent_block_stopde Anthropic y el comportamiento definish_reason: "length"de OpenAI.Reemplaza el pool de hilos con
asyncio.gather. Realiza un benchmark de ambos. Deberías ver pequeñas mejoras en asíncrono debido al menor costo de cambio de contexto, pero solo si los ejecutores realizan E/S (I/O) real.Elige dos herramientas que NO deban paralelizarse (por ejemplo,
create_filey luegowrite_file). Agrega un grafo deordering_dependencyal registro y restringe la distribución paralela según ese grafo. Este es el mecanismo mínimo para una planificación consciente de dependencias, que una fase futura de ingeniería de agentes formalizará.Lee la sección de llamadas de funciones paralelas de OpenAI y los documentos de
disable_parallel_tool_usede Anthropic. Identifica el único tipo de herramienta del mundo real donde Anthropic recomienda deshabilitar el paralelismo. (Pista: mutaciones consecuentes en el mismo recurso).
Términos Clave
| Término | Qué dice la gente | Qué significa realmente |
|---|---|---|
| Llamadas de herramientas en paralelo | "Distribución (fan-out) en un solo turno" | El modelo emite múltiples llamadas de herramientas en un solo mensaje del asistente |
parallel_tool_calls |
"La bandera (flag) de OpenAI" | Habilitar o deshabilitar la emisión de llamadas múltiples |
disable_parallel_tool_use |
"La inversa de Anthropic" | Bandera de exclusión voluntaria (opt-out); por defecto, el paralelismo está habilitado |
| ID de llamada de herramienta | "Identificador de correlación" | Identificador por llamada que el mensaje de resultado debe reflejar |
| Acumulador | "Buffer de flujo (stream)" | Buffer de cadena por ID para fragmentos parciales de arguments |
| Finalización fuera de orden | "Más rápido primero" | Las llamadas paralelas terminan en un orden impredecible; los IDs son el pegamento |
| Grafo de dependencia | "Restricciones de orden" | Herramientas cuyas salidas alimentan las entradas de otras herramientas; no se pueden paralelizar |
| Trampa del análisis prematuro | "JSON.parse explotó" | Intentar analizar una cadena de arguments incompleta |
streamFunctionCallArguments |
"Característica de Gemini 3" | Fragmentos de argumentos transmitidos por stream con un ID único por llamada |
| Respuesta en orden de finalización | "No esperes por todas" | Responder con los resultados a medida que llegan, indexados por ID |
Lecturas Adicionales
- OpenAI — Parallel function calling — comportamiento por defecto y bandera de exclusión (opt-out)
- Anthropic — Tool use: implementing tool use —
disable_parallel_tool_usey procesamiento por lotes de resultados - Google — Gemini function calling parallel section — llamadas paralelas correlacionadas por ID a partir de Gemini 3
- OpenAI — Streaming responses with tools — reensamblado de argumentos fragmentados para flujos de OpenAI
- Anthropic — Streaming messages —
content_block_deltaconinput_json_delta