Dos tool calls pueden llegar en el mismo turno del agente y aun así formar un lote inseguro. Si una llamada depende de la otra, si ambas cambian el mismo registro o si la segunda debe observar el efecto de la primera, Promise.all convierte una regla de negocio en una carrera.

La regla práctica es sencilla: paraleliza solo llamadas independientes que no compartan estado mutable. Pon una barrera en el runtime delante de dependencias, efectos y recursos que necesitan orden. Una configuración del proveedor puede permitir varias llamadas, pero tu aplicación sigue siendo responsable de decidir cómo ejecutarlas.

Esta pregunta es más concreta que la orquestación de agentes de código en TypeScript. Aquí tratamos un turno con varias tools, no la división del trabajo entre agentes.

Diagrama de un agente de IA que paraleliza lecturas independientes y serializa llamadas con estado compartido.

La decisión: ¿independiente o dependiente?

Antes de ejecutar un lote, clasifica cada tool por lo que lee, lo que cambia y el resultado que necesita. El modelo puede sugerir la lista, pero no debe ser la única fuente de esa clasificación.

Situación Política inicial Motivo
get_customer y get_inventory leen recursos distintos Paralelizar Ninguna necesita el resultado ni el efecto de la otra.
reserve_stock y create_order cambian el mismo pedido Serializar o usar una transacción El orden y la atomicidad forman parte del efecto.
search_customer debe entregar un ID a get_balance Serializar La segunda llamada todavía no tiene todos sus datos.
Dos tools escriben en el mismo archivo o documento Serializar por recurso Argumentos distintos aún pueden observar estado compartido.
send_email y charge_card son independientes, pero irreversibles No paralelizar por defecto Un fallo parcial puede dejar efectos difíciles de compensar.

La guía de Anthropic sobre uso paralelo de tools recomienda llamadas paralelas para operaciones independientes y muestra cómo desactivarlas cuando el orden importa. La guía de agentes de AutoGen marca la misma diferencia: el paralelismo ayuda con tools simultáneas, pero debe evitarse cuando interfieren los efectos secundarios o el estado compartido.

Una flag del proveedor no es la política del sistema

En la referencia de creación de Responses API de OpenAI, parallel_tool_calls controla si el modelo puede ejecutar llamadas en paralelo. En la API de Anthropic, disable_parallel_tool_use dentro de tool_choice limita la emisión a una llamada en los modos compatibles. Ambos controles son útiles, pero viven en la frontera del proveedor.

No saben que dos tools internas escriben el mismo pedido, que una aprobación cubre solo el primer cambio o que un worker retomó un paso después de un timeout. Aunque la flag esté activa, tu scheduler debe validar el lote recibido. Aunque esté desactivada, dos workers o dos entregas de una cola pueden crear concurrencia después.

Pon la decisión en un registro de tools

Mantén los metadatos de ejecución junto al schema para que cada handler no invente su propia regla:

type ToolMeta = {
  mode: "read" | "write";
  resource: string;
  dependsOn?: string[];
};

const tools: Record<string, ToolMeta> = {
  get_customer: { mode: "read", resource: "customer" },
  get_inventory: { mode: "read", resource: "inventory" },
  reserve_stock: { mode: "write", resource: "order" },
  create_order: { mode: "write", resource: "order", dependsOn: ["reserve_stock"] },
};

function canShareBatch(aName: string, a: ToolMeta, bName: string, b: ToolMeta) {
  return a.mode === "read" &&
    b.mode === "read" &&
    a.resource !== b.resource &&
    !a.dependsOn?.includes(bName) &&
    !b.dependsOn?.includes(aName);
}

Es ilustrativo. El código de producción todavía necesita validar argumentos, autorización y un grafo completo de dependencias. Lo importante es dónde vive la decisión. El runtime conoce la tool real, el recurso protegido y las aristas de dependencia; el prompt solo conoce la descripción que recibió.

En la práctica, crea lotes independientes y ejecuta cada lote con Promise.all. Cuando una llamada tenga una dependencia, espera el lote anterior, actualiza el contexto y prepara la siguiente. Para las escrituras, usa una transacción, una cola por recurso o una clave de idempotencia. El paralelismo no sustituye el control de concurrencia de la base de datos.

Relaciona los resultados por ID, nunca por posición

Una respuesta paralela puede terminar en un orden distinto al de emisión. Cada llamada necesita un ID estable y cada resultado debe quedar unido a ese ID. La documentación de Anthropic describe devolver los resultados en un turno y usar tool_use_id para relacionar cada resultado con su llamada.

No dependas de las posiciones del array:

for (const [index, result] of results.entries()) {
  run.toolResults[calls[index].name] = result;
}

Si una llamada falla, se cancela o termina más tarde, el índice deja de ser una identidad fiable. Usa el ID de llamada:

for (const result of results) {
  run.toolResults[result.callId] = {
    status: result.ok ? "ok" : "error",
    value: result.value,
  };
}

Cuando una dependencia no pueda ejecutarse, devuelve un error explícito al agente o detén el turno. No inventes un resultado vacío para rellenar un hueco. El error debe identificar la llamada bloqueada, la dependencia que falta y si ya se confirmó algún efecto.

Prueba la política sin depender de tiempos accidentales

Crea una fixture con dos lecturas que esperen una barrera manual. Libera ambas a la vez y verifica que el scheduler inició las dos. Después usa dos escrituras para el mismo recurso, libera la primera antes de la segunda y verifica que la segunda no comenzó antes. Por último, haz que falle una llamada dependiente y confirma que el siguiente efecto no se ejecutó.

Los casos mínimos son:

  • dos lecturas independientes comparten un lote;
  • dos escrituras del mismo recurso se serializan;
  • una llamada que necesita un ID espera el resultado anterior;
  • los resultados fuera de orden permanecen unidos al callId correcto;
  • una dependencia desconocida o ausente falla antes del efecto externo.

Ejecuté una fixture local determinista con Node.js v25.5.0 el 30 de octubre de 2026. Usó promesas controladas y ningún proveedor de modelos, base de datos o red. El resultado confirmó los cinco comportamientos anteriores. Eso solo prueba la lógica de la fixture, no la latencia ni la seguridad de un SDK. Para carreras propias del dominio, combínala con pruebas deterministas de condiciones de carrera en JavaScript y pruebas contra la base de datos o la cola que protege el efecto real.

Fallos frecuentes

Usar Promise.all para toda lista. La API del modelo no conoce las invariantes de tu dominio. Separa lecturas independientes de mutaciones antes de crear las promesas.

Confiar solo en la instrucción “no llames tools en paralelo”. Eso reduce una conducta del modelo, pero no protege dos workers, reintentos, callbacks o redeliveries de una cola.

Serializar todo y darlo por resuelto. Es útil como diagnóstico, pero puede añadir latencia y ocultar qué lecturas son independientes. Clasifica las tools y reabre el paralelismo solo con pruebas.

Relacionar por nombre. Dos llamadas a la misma tool en un turno necesitan IDs distintos. El nombre identifica una capacidad, no una ocurrencia.

Ignorar un fallo parcial. Si una lectura terminó y una escritura falló, registra el estado conocido. No repitas automáticamente un efecto solo porque el lote no terminó.

Checklist antes de habilitar llamadas paralelas

  • ¿Cada tool declara si lee o cambia estado?
  • ¿El runtime conoce el recurso compartido y las dependencias?
  • ¿El scheduler crea el lote en lugar de aceptar un Promise.all sin límites?
  • ¿Cada resultado lleva el ID original de la llamada?
  • ¿Los efectos externos tienen idempotencia, transacción o cola por recurso?
  • ¿Existe una prueba negativa para una dependencia ausente y un fallo parcial?
  • ¿La traza registra lote, orden, bloqueos y efectos confirmados?
  • ¿La configuración del proveedor se trata como optimización de emisión y no como autorización de negocio?

Conclusión

Las parallel tool calls son útiles cuando puedes demostrar la independencia. No son un atajo para ejecutar cualquier lista más rápido. Mantén la política en el runtime: clasifica las tools, paraleliza lecturas independientes, serializa dependencias y estado compartido, y relaciona cada respuesta con su ID.

Si el lote forma parte de un flujo mayor, combina esta política con validar tool calls antes del handler y con verificar el efecto de una tool antes de reintentar. Así la concurrencia queda conectada con validación y recuperación, en lugar de convertirse en una flag aislada.

Cómo se hizo este artículo

Samuel Fajreldines escribió este artículo a partir del inventario de posts y enlaces del repositorio, la inspección de resultados actuales y la documentación pública de OpenAI, Anthropic y AutoGen. La matriz de independencia, dependencia y estado compartido es una síntesis original. La fixture es ilustrativa y se ejecutó localmente con Node.js, sin llamar a un proveedor de modelos ni a una tool de producción.

Usé RemoteCode como herramienta del autor para organizar la investigación y revisar la consistencia. No ejecutó un agente de producción y no sustituye la validación en el entorno del lector.

Fuentes consultadas