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: true y 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 arguments parciales 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: true activado por defecto. Establécelo en false para forzar el modo serial.
  • Anthropic. Paralelo mediante disable_parallel_tool_use: false (por defecto en Claude 3.5 y versiones superiores). Establécelo en true para 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_id en cada mensaje con el rol tool.
  • Anthropic. tool_use_id en cada bloque tool_result.
  • Gemini. id en cada functionResponse (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 el index (posición en la lista de llamadas). Acumulas por índice, lees o id cuando aparece por primera vez y analizas el JSON cuando finish_reason = "tool_calls".
  • Anthropic. Los eventos de flujo son message_start, luego un content_block_start por bloque con tipo tool_use (que contiene id, nombre, entrada vacía). Los eventos content_block_delta llevan fragmentos de input_json_delta. El content_block_stop cierra cada bloque.
  • Gemini. streamFunctionCallArguments (Gemini 3 y superior) emite fragmentos con un functionCallId para 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

  1. Ejecuta code/main.py y varía las latencias simuladas. Confirma que la relación entre paralelo y secuencial es de aproximadamente max/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?

  2. 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 de content_block_stop de Anthropic y el comportamiento de finish_reason: "length" de OpenAI.

  3. 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.

  4. Elige dos herramientas que NO deban paralelizarse (por ejemplo, create_file y luego write_file). Agrega un grafo de ordering_dependency al 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á.

  5. Lee la sección de llamadas de funciones paralelas de OpenAI y los documentos de disable_parallel_tool_use de 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

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