Múltiples tools en paralelo
Objetivo
Al terminar esta lección sabrás cómo el modelo puede emitir varios tool_use blocks en un solo turno cuando la pregunta del usuario requiere información independiente, cómo tu código debe ejecutar esas tools (idealmente en paralelo) y devolver múltiples tool_result blocks en un solo mensaje user, y cuándo desactivar este comportamiento con disable_parallel_tool_use.
Concepto
El modelo puede llamar varias tools en un mismo turno
Si el usuario pregunta algo que requiere información de fuentes independientes, el modelo puede emitir múltiples tool_use blocks en el mismo content[]:
user: "¿Qué clima y hora hay en Tokio?"
↓
assistant (un solo turno):
content: [
{type: "tool_use", id: "toolu_A", name: "get_weather", input: {city: "Tokio"}},
{type: "tool_use", id: "toolu_B", name: "get_time", input: {city: "Tokio"}}
]
stop_reason: "tool_use"El modelo identificó que las dos tools son independientes (no hace falta el resultado de una para llamar a la otra) y propuso las dos simultáneamente. Esto reduce latencia: en vez de 2 roundtrips (pregunta → tool A → result A → tool B → result B → respuesta), hacés 1 roundtrip con 2 resultados.
Cómo responder con múltiples tool_results
En el mensaje siguiente (role: "user"), devolvés todos los tool_result blocks dentro de un único content array:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_A",
"content": "{\"temperature\": 18, \"conditions\": \"clear\"}"
},
{
"type": "tool_result",
"tool_use_id": "toolu_B",
"content": "{\"time\": \"2026-04-12T23:45+09:00\"}"
}
]
}Reglas:
- Un único mensaje
usercon todos los tool_result blocks. - Cada
tool_use_iddebe matchear su tool_use correspondiente. - El orden de los tool_result blocks NO tiene que coincidir con el orden de los tool_use — el ID es lo que los emparreja.
- Si olvidás uno solo, la API devuelve error.
Cuándo el modelo paraleliza y cuándo no
Paraleliza cuando:
- Las tools no tienen dependencia entre sí.
- El input de una no depende del output de la otra.
- Ejemplos:
get_weather(Tokio) + get_time(Tokio),get_user(123) + get_orders(123).
No paraleliza (secuencial) cuando:
- El output de la primera tool es input de la segunda.
- Ejemplo:
search_user(email) → idy despuésget_orders(id). El modelo primero llamasearch_user, espera el result, y recién entonces llamaget_orders.
No hace falta que hagas nada especial para habilitar el paralelismo — es el comportamiento default. Vas a verlo aparecer naturalmente cuando las tools sean ortogonales.
Ejecutar en paralelo en tu código
Si el modelo propuso 2 tool_use, tu código puede ejecutarlas en paralelo con Promise.all (o asyncio.gather en Python):
const toolUseBlocks = resp.content.filter((b: any) => b.type === "tool_use");
const results = await Promise.all(
toolUseBlocks.map(async (block) => {
const output = await executeTool(block.name, block.input);
return {
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(output),
};
})
);
messages.push({ role: "user", content: results });Esto aprovecha el paralelismo del modelo en tu lado: no hay razón para ejecutar get_weather y get_time en serie si son independientes.
disable_parallel_tool_use
Si querés forzar al modelo a emitir un solo tool_use por turno, pasá:
{
"tool_choice": {
"type": "auto",
"disable_parallel_tool_use": true
}
}Combinable con auto, any o tool. Útil cuando:
- Tus tools tienen efectos secundarios y querés auditar una a la vez.
- La segunda tool normalmente depende del resultado de la primera, y no querés que el modelo "se adelante".
- Estás debugeando y necesitás determinismo.
Default: false (paralelismo permitido).
Ejecución real
Request con 2 tools independientes:
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-haiku-4-5",
"max_tokens": 500,
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
},
{
"name": "get_time",
"description": "Get current local time for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
],
"messages": [
{"role": "user", "content": "¿Qué clima y hora hay en Tokio?"}
]
}'Respuesta real:
{
"content": [
{
"type": "tool_use",
"id": "toolu_01TJ...",
"name": "get_weather",
"input": {"city": "Tokyo"}
},
{
"type": "tool_use",
"id": "toolu_015jr...",
"name": "get_time",
"input": {"city": "Tokyo"}
}
],
"stop_reason": "tool_use",
"usage": { "input_tokens": 629, "output_tokens": 90 }
}Dos tool_use blocks en un solo turno. Observá: el modelo "entendió" que ambas tools son independientes y las propuso juntas.
Paso 2 — Ejecutar ambas en paralelo y responder:
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-haiku-4-5",
"max_tokens": 500,
"tools": [ ... mismas tools ... ],
"messages": [
{"role": "user", "content": "¿Qué clima y hora hay en Tokio?"},
{
"role": "assistant",
"content": [
{"type": "tool_use", "id": "toolu_01TJ...", "name": "get_weather", "input": {"city": "Tokyo"}},
{"type": "tool_use", "id": "toolu_015jr...", "name": "get_time", "input": {"city": "Tokyo"}}
]
},
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_01TJ...", "content": "{\"temp\": 18, \"conditions\": \"clear\"}"},
{"type": "tool_result", "tool_use_id": "toolu_015jr...", "content": "{\"time\": \"23:45 JST\"}"}
]
}
]
}'Respuesta final:
{
"content": [{
"type": "text",
"text": "En Tokio hay 18°C con cielo despejado y son las 23:45 hora local (JST)."
}],
"stop_reason": "end_turn"
}Paso 3 — Loop completo con paralelización
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
function getWeather(city: string) {
return { temp: 18, conditions: "clear", city };
}
function getTime(city: string) {
return { time: "23:45 JST", city };
}
async function executeTool(name: string, input: any) {
// Cada tool puede tener su propia implementación asíncrona
switch (name) {
case "get_weather": return getWeather(input.city);
case "get_time": return getTime(input.city);
default: throw new Error(`Unknown tool: ${name}`);
}
}
async function runAgent(question: string) {
const tools = [
{ name: "get_weather", description: "Get current weather.",
input_schema: { type: "object" as const, properties: { city: { type: "string" } }, required: ["city"] } },
{ name: "get_time", description: "Get current local time.",
input_schema: { type: "object" as const, properties: { city: { type: "string" } }, required: ["city"] } },
];
const messages: any[] = [{ role: "user", content: question }];
while (true) {
const resp = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
tools,
messages,
});
messages.push({ role: "assistant", content: resp.content });
if (resp.stop_reason === "end_turn") {
return (resp.content.find((b: any) => b.type === "text") as any)?.text ?? "";
}
if (resp.stop_reason === "tool_use") {
const toolUses = resp.content.filter((b: any) => b.type === "tool_use");
// Ejecutar todas en paralelo
const results = await Promise.all(
toolUses.map(async (block: any) => ({
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(await executeTool(block.name, block.input)),
}))
);
messages.push({ role: "user", content: results });
continue;
}
throw new Error(`Unexpected stop_reason: ${resp.stop_reason}`);
}
}
console.log(await runAgent("¿Qué clima y hora hay en Tokio?"));Output:
En Tokio hay 18°C con cielo despejado y son las 23:45 hora local (JST).El Promise.all es la clave: si get_weather tarda 300ms y get_time tarda 200ms, el total es ~300ms, no 500ms.
Anti-patterns
- ❌ Ejecutar las tools en serie cuando el modelo las propuso en paralelo. Desperdicia la optimización del modelo. Siempre usá
Promise.all/asyncio.gather. - ❌ Responder con mensajes
userseparados por cada tool_result. La API requiere que TODOS los tool_result del turno estén en un solo mensajeusercon múltiples content blocks. - ❌ Olvidar algún tool_result. Si el modelo emitió 3 tool_use, tu
usermessage debe tener 3 tool_result. Omitir uno → error de API. - ❌ Mezclar orden y pensar que importa. El matching se hace por
tool_use_id, no por posición. El orden es libre — pero por legibilidad, matcheá el orden. - ❌ Usar
disable_parallel_tool_use: truepor reflejo. Si tus tools son idempotentes y ortogonales, permitir paralelo reduce latencia y tokens (menos turnos). - ❌ Asumir que el modelo SIEMPRE paralelizará tools ortogonales. Es un hint del modelo, no una garantía. A veces emite una sola. Tu loop debe funcionar en ambos casos.
Recap
- El modelo puede emitir varios
tool_useblocks en el mismo turno cuando las tools son independientes. - Respondés con UN solo mensaje
userque contiene todos lostool_resultblocks, matcheados portool_use_id. - Ejecutá las tools en paralelo (
Promise.all/asyncio.gather) para aprovechar la optimización. disable_parallel_tool_use: truefuerza 1 tool_use por turno — solo usalo si hay dependencias o efectos secundarios delicados.- El patrón es transparente: tu loop con
filter(type === "tool_use")+Promise.allfunciona tanto con 1 como con N tools en paralelo.
Fuente oficial: platform.claude.com/docs/en/build-with-claude/tool-use/implementationEjercicio: