Tool calling en paralelo: cuándo sí, cuándo no y cómo devolver resultados
Resumen
Un turno del modelo puede pedir varias tools a la vez. OpenAI lo apaga con parallel_tool_calls: false; Claude con disable_parallel_tool_use. Gemini exige devolver todas las respuestas. Esta guía fija el loop, el lote de resultados y cuándo forzar una sola llamada.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
El error más caro en un agente no es “el modelo no llamó la tool”. Es llamar tres tools en un turno y tratarlas como una. OpenAI documenta que la respuesta puede traer cero, una o varias llamadas y pide asumir varias. Claude, por defecto, puede devolver varios bloques tool_use en el mismo turno. Gemini, en Vertex, dice que si el modelo propone llamadas paralelas hay que devolver todas las respuestas.
Esta guía no rediseña contratos de tools —eso está en function calling confiable— ni elige modelo —eso está en mejores modelos para tool calling. Aquí el foco es el lote: cómo ejecutarlo, cómo devolverlo y cuándo apagar el paralelo.
Qué es “paralelo” (y qué no)
Paralelo en la API significa: un solo assistant turn pide N tools. No significa que tu runtime las tenga que lanzar a la vez.
Claude lo deja explícito: el API no prescribe orden. Puedes usar Promise.all / asyncio.gather, correrlas en serie o mezclar. Lo que sí exige es un tool_result por cada tool_use, todos juntos en el siguiente mensaje de usuario, emparejados por tool_use_id, y antes de cualquier texto en ese mensaje.
Gemini (Vertex) usa el mismo contrato: si el prompt es “clima en Boston y San Francisco”, el modelo puede proponer varias function_call; la app debe devolver todas las function_response.
OpenAI: tool_calls es un array. Cada item tiene id. El follow-up lleva un tool message por id. parallel_tool_calls: false fuerza cero o una tool por turno.

Cuándo sí: lecturas independientes
Corre en paralelo (en tu proceso) solo si todas estas son ciertas:
- Las tools son lectura o idempotentes (
get_weather,get_order,search_docs). - No comparten estado mutable (mismo saldo, mismo archivo, mismo lock).
- El orden no cambia el resultado.
- Un fallo de una no invalida a las otras.
Ejemplo seguro: “compara el clima en Madrid, Lima y Ciudad de Guatemala”. Tres get_weather. Tres HTTP. Promise.allSettled. Tres resultados, aunque uno 500.
Claude recomienda prompt explícito en Claude 4+: “whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously”. Eso sube la probabilidad de lote; no te obliga a ejecutarlo concurrente.
Cuándo no: side effects y orden
Apaga el paralelo en el modelo (no solo en tu executor) cuando hay:
- Escritura:
refund,charge,send_message,create_ticket. - Dependencia:
get_customer→create_ticketcon elid. - Computer use / browser use en Claude: si el turno trae un batch de esas tools, Claude pide secuencia en el orden del array y parar al primer fallo.
OpenAI: parallel_tool_calls: false. En modelos tipo gpt-4.1-nano la propia doc recomienda apagarlo porque a veces duplica la misma tool.
Claude: disable_parallel_tool_use: true dentro de tool_choice, no como flag suelto.
tool_choice.type | Con disable_parallel_tool_use: true |
|---|---|
auto (default) | Como máximo una tool. Puede responder solo texto. |
any o tool | Exactamente una tool. |
Gemini (Vertex function_calling_config.mode): AUTO decide; ANY fuerza una o más function calls; NONE prohíbe tools. No hay un flag gemelo de parallel_tool_calls; el control es el modo + devolver el lote completo.
El loop mínimo (OpenAI Chat Completions)
Asume array. No indexas [0].
const msg = completion.choices[0].message;
messages.push(msg);
for (const call of msg.tool_calls ?? []) {
if (call.type !== "function") continue;
const args = JSON.parse(call.function.arguments);
const output = await runTool(call.function.name, args); // o allSettled
messages.push({
role: "tool",
tool_call_id: call.id,
content: output,
});
}
OpenAI: tools built-in no entran en el mismo batch paralelo que functions de usuario. Si mezclas web search nativo y tus functions, no esperes un lote mixto.
Para schema de argumentos, usa strict: true —mismo mecanismo que structured outputs. Paralelo mal parseado es N fallos, no uno.
Claude: un resultado por tool_use, aunque no hayas corrido la tool
Si corres el lote en serie y la primera escritura falla, igual devuelves tool_result para las que saltaste:
{
"type": "tool_result",
"tool_use_id": "toolu_02",
"is_error": true,
"content": "Not executed: the preceding write_file call failed."
}
is_error: true también cubre 500 de red. No tragues el error en silencio: Claude reintenta o explica. Server tools (web search, etc.) las maneja Anthropic; no les inventes is_error. Excepción documentada: si un server tool cae en el mismo grupo paralelo que una client tool, aplica el fallback de stop reason de su overview.
Executor: allSettled, no all
Promise.all cancela el lote entero si una peta. El modelo espera N resultados. Usa allSettled (o return_exceptions en asyncio) y serializa el error en el content.
const settled = await Promise.allSettled(
calls.map((c) => runTool(c.name, c.args)),
);
return settled.map((r, i) =>
r.status === "fulfilled"
? ok(calls[i].id, r.value)
: err(calls[i].id, String(r.reason)),
);
Rate limits: N HTTP a la vez pueden ser N 429. Eso se mitiga con reintentos y cola, no con más paralelo.

Checklist
- El handler itera todas las
tool_calls/tool_use/function_call. - Cada resultado lleva el
idoriginal. - Lecturas independientes:
allSettled. Escrituras: serie, oparallel_tool_calls: false/disable_parallel_tool_use: true. - Si saltas una llamada, igual devuelves
is_error(Claude) o un tool message de error (OpenAI). - Menos de ~20 tools visibles al inicio del turno (sugerencia blanda de OpenAI). Más superficie = peor elección y lotes más raros.
- Tests del executor sin LLM: un fixture con 3 calls, uno que falla. Ver tests de tools.
- Guardrail de side effect: guardrails input/output/tools delante de
refund/send.
FAQ
¿El modelo “ejecuta” en paralelo? No. Propone. Tú ejecutas.
¿Puedo devolver solo las que salieron bien? No en Claude ni en Gemini Vertex. OpenAI también espera un tool message por id.
¿Apago el paralelo siempre en producción? No. Apágalo en money paths. Déjalo en lecturas. Mide latencia del turno, no “el modelo es lento”.
¿Function calling y MCP? MCP sigue siendo tools con schema. El lote es el mismo problema: N calls, N results.
Si estás armando el primer agente y todavía no tienes este loop, empieza por la ruta de instalación y recién después paralelizas lecturas. El lote sin ids es peor que una tool lenta.
Lecturas relacionadas
Sigue explorando Herramientas y otras piezas para builders.

Max turns y recursion_limit: cómo cortar un agente que no para

Truncar tool results en agentes: recortar el payload, no el contrato

Structured outputs: JSON confiable para agentes de IA
