El 90% de los AI Agents de GitHub Ejecutan Tool Calls Como si Fuera 1999
Uno tras otro. Esperando a que el anterior termine aunque no tengan ninguna dependencia entre sí.
Eso desperdicia entre un 47% y un 62% de la latencia — y no se arregla con un mejor modelo. Se arregla con un scheduler.
Los modelos ya emiten múltiples tool calls en una sola pasada de generación. GPT‑5.6, Claude y todos los frontier models de 2026 despachan lotes completos de llamadas a herramientas cada iteración del loop. El problema es que los frameworks de orquestación más populares las ejecutan en el orden en que llegan, una a una, como quien recorre un array con un `for` y espera a cada elemento.
*El error de diseño raíz no es el modelo. Es tratar el agent loop como una lista de tareas cuando es un grafo de dependencias. *
La sabiduría convencional asume que la calidad de un agente depende del modelo o del prompt. El dato del 47-62% demuestra que el cuello de botella está en la capa de orquestación: cómo se programan y se despachan las llamadas a herramientas, no en qué modelo las genera.
El Problema: Creéis que un Agent Loop es una Lista de Tareas
Tomad un agente de research típico. El modelo emite en una sola pasada tres tool calls:
1. web_search("precios SaaS 2026 en España")
2. query_database("SELECT id, mrr FROM clientes WHERE churned = true")
3. read_file("docs/comparativa_2025.md")
Ninguna depende de la otra. Son tres ramas independientes.
¿Qué hace un orquestador secuencial ingenuo? Espera a que `web_search` termine. Espera a que `query_database` termine. Espera a que `read_file` termine.
En secuencial tardas la suma de los tres. En paralelo tardas el máximo de los tres.
Para un agente con 5 ramas independientes que cada una tarda 2 segundos: 10 segundos en secuencial, 2 en paralelo. El ahorro crece con el número de ramas independientes — y casi ningún flujo real es 100% secuencial.
❌ El anti-patrón (lo que generan el 90% de los frameworks por defecto):
```python
ORQUESTADOR SECUENCIAL INGENUO — EL ANTI-PATRÓN
async def run_naive_loop(tool_calls):
results = []
for call in tool_calls: # FIFO: uno detrás de otro
result = await execute(call) # Espera completa por llamada
results.append(result) # Aunque no comparta nada con la anterior
return results
```
Ese `for` serializa incluso llamadas completamente independientes. Es la fuente del 47-62% de latencia extra.
✅ El despacho paralelo con primitivas nativas:
```python
FAN-OUT: lanza ramas independientes en paralelo
import asyncio
async def run_parallel_batch(independent_calls):
results = await asyncio.gather(
*[execute(call, timeout=10) for call in independent_calls]
)
return results
```
`asyncio.gather` en Python, `Promise.all` en TypeScript. La misma idea. El modelo ya emitió el lote; el scheduler decide quién espera y quién no.
La Evidencia: Lo que Pasa Cuando No Orquestas
No me creáis a mí. Medidlo vosotros.
Antes de tocar nada, instrumentad vuestro loop actual con un wrapper de timing que registre el wall-clock por tool_call y por iteración completa:
```python
import time
async def timed_execute(call):
start = time.perf_counter()
result = await call.execute()
elapsed = round(time.perf_counter() - start, 3)
log_call(call.name, elapsed, start_time=start)
return result
Al final del loop:
wall_clock_total vs. max(individual_calls) → ahí vive tu delta
```
Responded a esta pregunta: ¿cuántas de vuestras tool calls por iteración son realmente dependientes? En casi cualquier flujo real — research, extracción, e-commerce, atención al cliente — el ratio de ramas independientes está por encima del 50%.
Una nota de honestidad: las cifras del 90% y del 47-62% provienen de un análisis observacional del ecosistema de GitHub, no de un benchmark externo publicado. Tratadlas como hipótesis medibles. El harness de medición de arriba es vuestra herramienta de validación — replicad la medición en vuestro propio stack y comprobad el delta vosotros mismos.
El Framework: El Orquestador de 3 Fases
La corrección no vive en el modelo ni en el prompt. Vive en una capa de scheduling entre la salida del LLM y la ejecución. Lo llamo El Orquestador de 3 Fases, y en el fondo es un DAG disfrazado.
Fase 1: Secuenciación Obligada
Inventariad vuestras tools y clasificad sus dependencias. Para cada par de herramientas, responded a una pregunta: ¿la salida de una es entrada de la otra?
Si `web_search` alimenta `parse_results`, son secuencia obligada. Se ejecutan en orden, sin discusión. Modelar esto explícitamente ya aporta valor aunque no haya ni una sola llamada paralela: fuerza a hacer visible qué depende de qué en vez de asumirlo.
Fase 2: Fan-Out de Ramas Independientes
Los nodos sin aristas entre sí se lanzan en paralelo con `asyncio.gather` o `Promise.all`. Con timeouts por rama, para que una herramienta lenta no bloquee todo el bucle.
El paralelismo no es ilimitado. Es fan-out controlado: límites de concurrencia configurables y capas de espera si alguna rama toca el mismo rate limit.
Fase 3: Agregación y Orden Canónico
Aquí es donde el 90% de las implementaciones naive de paralelismo se rompen. Paralelizar cambia la semántica de error.
En secuencial, un fallo aborta el flujo limpiamente. En paralelo tienes fallos parciales, retries por rama y el problema crítico: inyectar resultados desordenados en el contexto del LLM.
La solución es un orden canónico determinista basado en el orden de aparición de las tool calls en el lote original. Ese es el orden que el modelo espera cuando le devolvéis el contexto — romperlo degrada la coherencia aunque todos los resultados sean correctos.
```python
import asyncio
class ThreePhaseOrchestrator:
def classify(self, tool_calls):
phase1 = [] # Secuencia obligada: input depende de output previo
phase2 = [] # Fan-out: sin dependencias entre sí
phase3 = [] # Agregación ordenada
for call in tool_calls:
if any(call.depends_on(prev) for prev in phase1):
phase1.append(call) # Fase 1: secuenciar
else:
phase2.append(call) # Fase 2: paralelizar
return phase1, phase2, phase3
async def run(self, tool_calls):
phase1, phase2, _ = self.classify(tool_calls)
Fase 1: estrictamente secuencial por dependencia
phase1_results = []
for call in phase1:
phase1_results.append(await execute(call))
Fase 2: fan-out con semántica de fallo tolerante
phase2_results = await asyncio.wait_for(
asyncio.gather(
*[execute(c, timeout=15) for c in phase2],
return_exceptions=True
),
timeout=30
)
Fase 3: orden canónico = orden original del lote
ordered = self.reorder(phase1_results, phase2_results, tool_calls)
return ordered # Devuélvelo al LLM en el mismo orden que emitió
```
El manejo de fallos en ramas paralelas
Con `return_exceptions=True` en Python o `Promise.allSettled` en TypeScript, una rama que reviente no aborta las demás:
Retry por rama, no por lote completo.
Resultados parciales tolerados: si tres de cinco ramas responden, se devuelven las tres con una nota de fallo.
Orden canónico de inyección para no romper la coherencia que el LLM espera.
La Analogía que lo Explica Todo
En scraping aprendí una lección que aplica a agents: el código es commodity; la capa operativa es el producto.
Cualquiera escribe una función con un decorador y llama a `MCP.add_tool()` — las definiciones de tools son commodity. Pero el bucle que decide qué se ejecuta en paralelo, qué se secuencia y cómo se agregan los resultados es la capa operativa que distingue un demo de un sistema en producción.
Los modelos no planifican ejecución eficiente. Emiten llamadas. Sin una capa de scheduling que clasifique dependencias, el modelo no tiene forma de forzar paralelismo — y el dato del 47-62% sugiere que tampoco los frameworks actuales lo hacen por defecto. El scheduling es responsabilidad del orquestador, no del modelo.
Esto conecta con lo que OpenAI ya empuja en 2026 con su Programmatic Tool Calling: escribir JavaScript para orquestar tools, ejecutar llamadas independientes en paralelo y procesar los outputs fuera del context window. El modelo se queda con lo que requiere inteligencia — aplicar juicio — y la capa operativa hace el scheduling.
Cómo Implementarlo Hoy en tu Stack
Paso 1 — Inventaría tus tools. Lista cada una y escribe sus dependencias explícitas. No las dejes implícitas en el prompt.
Paso 2 — Divide tu flujo en las 3 fases. Fase 1 con dependencias reales, Fase 2 con el fan-out, Fase 3 con la agregación determinista.
Paso 3 — Implementa el despacho paralelo con `asyncio.gather` o `Promise.all`. Timeouts por rama.
Paso 4 — Mide antes y después. Wall-clock por tool_call y por iteración completa. El delta frente a tu medición base es tu ahorro real — no el 47-62% teórico, sino el tuyo.
Paso 5 — Añade semántica de fallos. `allSettled`, retry por rama, resultados parciales y orden canónico de inyección.
Si trabajáis con SDKs como Claude Agent SDK, LangGraph o CrewAI, el patrón se integra igual: interceptad el lote de tool calls que emite el modelo y sustituid el dispatch por defecto por vuestro orquestador de 3 fases. No necesitáis cambiar de framework — necesitáis una capa de scheduling encima del que ya usáis.
La Conclusión Directa
El 90% de los agents de GitHub pierden entre 47% y 62% de su latencia en un error de diseño que no necesita un modelo mejor: necesita un scheduler.
Tratad el agent loop como lo que es: un grafo de dependencias, no un array. Orquestad en fases. Medid el delta en vuestro stack.
El modelo emite. El orquestador decide. Y esa capa de decisión es el producto real que os separa de un demo — la capa operativa que el resto del ecosistema sigue tratando como commodity.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

