Guía10 min

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.

OpenAIAnthropicGemini
Varias llamadas a herramientas saliendo del mismo turno del modelo hacia handlers independientes

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.

Un turno del modelo emite varias llamadas; el runtime decide si las corre juntas o en serie

Cuándo sí: lecturas independientes

Corre en paralelo (en tu proceso) solo si todas estas son ciertas:

  1. Las tools son lectura o idempotentes (get_weather, get_order, search_docs).
  2. No comparten estado mutable (mismo saldo, mismo archivo, mismo lock).
  3. El orden no cambia el resultado.
  4. 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_customercreate_ticket con el id.
  • 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.typeCon disable_parallel_tool_use: true
auto (default)Como máximo una tool. Puede responder solo texto.
any o toolExactamente 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.

Lecturas independientes en paralelo; escrituras y computer-use en secuencia

Checklist

  • El handler itera todas las tool_calls / tool_use / function_call.
  • Cada resultado lleva el id original.
  • Lecturas independientes: allSettled. Escrituras: serie, o parallel_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.