Tool calling paralelo en agentes IA: 60% menos latencia
El agente de presupuestos funciona. Lo desplegaste, el cliente lo prueba, y después de unos segundos aparece la respuesta. Cuatro segundos, exactamente. Cada vez.
Lo que ocurre por dentro: el agente llama al CRM para obtener el historial del cliente, espera la respuesta, llama al sistema de inventario para verificar stock, espera, llama a la API de precios para calcular la tarifa, espera. Tres llamadas en serie. Tres esperas acumuladas.
No es un problema del modelo. Es un problema de arquitectura.
Por qué el tool calling secuencial es el default y por qué destroza la latencia
Cuando implementas tool calling por primera vez, el patrón más natural es este:
// El patrón que parece correcto pero acumula latencia
const clientHistory = await getClientHistory(clientId); // 1.2s
const stockStatus = await checkStock(productIds); // 1.4s
const pricing = await getPrice(productId, volume); // 0.9s
// Total: 3.5s — y eso si todo va bien
Lógicamente tiene sentido. Llamas a una función, esperas el resultado, sigues. El problema es que estas tres llamadas no dependen la una de la otra. No necesitas el historial del cliente para consultar el stock. No necesitas el stock para obtener el precio. Las estás serializando por defecto, no por necesidad.
En producción, cada 100ms de latencia que añades a una interacción tiene un coste real: usuarios que abandonan, conversaciones que se sienten lentas, agentes que parecen menos capaces de lo que son.
Cómo la API de Claude señala llamadas paralelas
Antes de ver el código del lado del servidor, entender qué pasa en el modelo es importante. Cuando diseñas bien el system prompt y las descripciones de las tools, Claude puede devolver múltiples bloques tool_use en una sola respuesta.
Esto es el modelo diciéndote: "puedo resolver estas tres cosas al mismo tiempo". La respuesta de la API tiene esta forma:
{
"content": [
{
"type": "tool_use",
"id": "toolu_01",
"name": "getClientHistory",
"input": { "clientId": "c-4821" }
},
{
"type": "tool_use",
"id": "toolu_02",
"name": "checkStock",
"input": { "productIds": ["p-100", "p-101"] }
},
{
"type": "tool_use",
"id": "toolu_03",
"name": "getPrice",
"input": { "productId": "p-100", "volume": 5 }
}
],
"stop_reason": "tool_use"
}
El modelo ya hizo el trabajo de identificar qué puede paralelizar. El error es no aprovecharlo.
Implementación con Promise.all() y manejo de errores
El patrón correcto para ejecutar esos tres bloques tool_use de forma paralela:
import Anthropic from "@anthropic-ai/sdk";
type ToolUseBlock = {
type: "tool_use";
id: string;
name: string;
input: Record<string, unknown>;
};
type ToolResult = {
type: "tool_result";
tool_use_id: string;
content: string;
};
async function executeToolsInParallel(
toolUseBlocks: ToolUseBlock[]
): Promise<ToolResult[]> {
const results = await Promise.allSettled(
toolUseBlocks.map(async (block) => {
const result = await callTool(block.name, block.input);
return {
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(result),
};
})
);
return results.map((r, i) => {
if (r.status === "fulfilled") return r.value;
// Si una tool falla, devolvemos error pero el agente puede continuar
return {
type: "tool_result" as const,
tool_use_id: toolUseBlocks[i].id,
content: JSON.stringify({ error: r.reason?.message ?? "Tool failed" }),
};
});
}
Nótese el uso de Promise.allSettled en lugar de Promise.all. La diferencia es crítica: Promise.all cancela todo si una sola tool falla. Promise.allSettled devuelve el resultado de cada tool independientemente — el agente recibe el error como un tool_result con contenido de error y puede decidir cómo manejarlo en el siguiente turno.
El loop completo: agent turn con tools paralelas
async function runAgentWithParallelTools(
userMessage: string,
tools: Anthropic.Tool[]
): Promise<string> {
const client = new Anthropic();
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools,
messages,
});
// ¿Terminó el agente?
if (response.stop_reason === "end_turn") {
const text = response.content.find((b) => b.type === "text");
return text?.type === "text" ? text.text : "";
}
// Extraer todos los bloques tool_use
const toolUseBlocks = response.content.filter(
(b): b is ToolUseBlock => b.type === "tool_use"
);
if (toolUseBlocks.length === 0) break;
// Ejecutar en paralelo — aquí está la diferencia
const toolResults = await executeToolsInParallel(toolUseBlocks);
// Añadir el turno del asistente y los resultados
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
}
return "";
}
Con este patrón, si el modelo devuelve tres bloques tool_use, los ejecutas con Promise.allSettled y el tiempo total es max(1.2, 1.4, 0.9) = 1.4s, no 1.2 + 1.4 + 0.9 = 3.5s.
Cuándo NO paralelizar: dependencias entre tools
El paralelo no es siempre la respuesta correcta. Hay casos donde la segunda tool necesita el resultado de la primera:
// Este flujo NO puede paralelizar — paso 2 depende de paso 1
const reservationId = await createReservation(data); // primero
const confirmation = await sendConfirmation(reservationId); // necesita el ID anterior
Cuando el modelo entiende que hay dependencia, devuelve los bloques tool_use en turnos separados — uno por turno — y el loop natural lo maneja correctamente. Si notas que estás forzando el paralelo en casos dependientes, el síntoma es que los resultados son incorrectos o el agente entra en bucle.
La regla práctica: si la tool B necesita un valor de salida de la tool A, deben ir en turnos separados. Si son independientes, Claude los agrupará solo — solo asegúrate de procesarlos en paralelo cuando lleguen.
Diseñar el system prompt para favorecer el batching
El modelo batchea tools cuando entiende que puede hacerlo. Puedes favorecerlo en el system prompt:
Cuando necesites múltiples datos independientes (historial de cliente,
stock, precios), llama a todas las herramientas relevantes en el mismo
turno en lugar de hacerlo una a una. Agrupa las llamadas independientes.
Esta instrucción no cambia la lógica del agente — solo indica al modelo que no necesita ser conservador con el número de tool calls por turno. En benchmarks internos, esta instrucción sola reduce el número de turnos necesarios en un 30-40% en agentes con cuatro o más tools disponibles.
El resultado en producción
Este patrón está en el núcleo de los agentes de integración IA que implementamos para clientes. Un agente de presupuestos que antes tardaba 4-5 segundos en generar una respuesta pasa a tardar 1.5-2 segundos — sin cambiar el modelo, sin aumentar el presupuesto de API.
La diferencia no es solo de rendimiento. Un agente que responde en 1.5 segundos se percibe como capaz y rápido. Uno que tarda 5 segundos se percibe como lento aunque la respuesta sea exactamente la misma.
Si estás construyendo agentes con acceso a múltiples sistemas — CRM, ERP, inventario, precios, historial — y todavía serializas las llamadas, estás dejando latencia en la mesa en cada interacción. Si quieres ver cómo aplicar este patrón en tu caso concreto, hablamos.
Escríbeme por WhatsApp y vemos qué parte de tu agente está acumulando latencia innecesaria.