Tres búsquedas, tres esperas
Un agente necesita el estado de un pedido, el perfil del cliente y el stock del producto. Son tres consultas que no dependen entre sí. Aun así, muchos agentes las lanzan una detrás de otra: llamada, espera, razonamiento, llamada, espera, razonamiento. Si cada herramienta tarda 800 ms, pagas 2,4 s de herramientas más tres vueltas al modelo.
Los modelos actuales pueden pedir varias llamadas en un solo turno. Que tu bucle las ejecute en paralelo es decisión tuya, y es una de las mejoras de latencia más baratas que existen. También es una de las que más sorpresas da.
Qué cambia exactamente
En el modo secuencial, cada llamada es un turno completo: el modelo emite una tool_call, tu código la ejecuta, devuelves el resultado y el modelo decide el siguiente paso. En el paralelo, el modelo emite una lista de llamadas en el mismo turno y espera todos los resultados juntos.
| Secuencial | Paralelo | |
|---|---|---|
| Vueltas al modelo | 3 | 1 |
| Tiempo en herramientas | suma (≈2,4 s) | máximo (≈0,8 s) |
| Tokens de contexto | crece en cada vuelta | un solo bloque de resultados |
| Riesgo | bajo | condiciones de carrera |
Ganas en dos frentes: menos espera de red y menos vueltas al modelo, que son las que más cuestan en latencia y en dinero.
Cuándo es seguro paralelizar
La regla es una pregunta: ¿alguna llamada necesita el resultado de otra? Si la respuesta es no, paraleliza. Si es sí, hay dependencia y debe ir en orden.
Casos que sí admiten paralelismo:
- Lecturas independientes sobre fuentes distintas (pedido, cliente, stock).
- Búsquedas del mismo tipo con consultas diferentes, como en RAG agéntico.
- Comprobaciones de validación que no se influyen.
Casos que no:
- Una escritura y la lectura que debe verla.
- Cualquier cosa que dependa de un identificador devuelto por otra llamada.
- Dos escrituras sobre el mismo recurso.
Hay una trampa fina: el modelo puede pedir en paralelo cosas que sí dependen. Si genera crear_ticket y asignar_ticket en el mismo turno, la segunda no tiene el identificador que necesita. No confíes en que el modelo siempre distinga lo uno de lo otro.
Un ejecutor paralelo con red de seguridad
Con asyncio basta una función pequeña. Lo importante son los tres detalles que evitan los problemas habituales: límite de concurrencia, tiempo máximo por llamada y errores aislados.
import asyncio
MAX_CONCURRENTES = 4 # protege tus APIs de ráfagas
TIMEOUT_S = 10 # una herramienta lenta no bloquea el turno
async def ejecutar_una(llamada, herramientas, sem):
"""Ejecuta una llamada y devuelve siempre un resultado, nunca una excepción."""
async with sem:
try:
fn = herramientas[llamada["nombre"]]
salida = await asyncio.wait_for(fn(**llamada["args"]), TIMEOUT_S)
return {"id": llamada["id"], "ok": True, "salida": salida}
except Exception as e:
# El error vuelve al modelo como dato; no tumba al resto.
return {"id": llamada["id"], "ok": False, "salida": f"{type(e).__name__}: {e}"}
async def ejecutar_turno(llamadas, herramientas):
sem = asyncio.Semaphore(MAX_CONCURRENTES)
tareas = [ejecutar_una(c, herramientas, sem) for c in llamadas]
# gather conserva el orden de entrada: el resultado i responde a la llamada i
return await asyncio.gather(*tareas)
Tres decisiones que conviene defender:
- El semáforo. Sin él, un turno con veinte llamadas son veinte peticiones simultáneas a tu base de datos. Es la misma idea de contrapresión que aplicas con los LLM.
- Los errores como resultado. Si una de tres falla, el modelo debe recibir dos resultados buenos y un error legible, no una excepción que pierde todo el turno. Aquí ayudan los errores de herramienta que el modelo entiende.
- El
idde cada llamada. Los resultados se devuelven asociados a suid, no por posición. Así no dependes del orden de llegada.
Lo que se rompe sin avisar
El paralelismo no falla con un error; falla con un comportamiento extraño que solo aparece a veces. Cuatro sospechosos habituales:
- Idempotencia. Con reintentos y ejecución concurrente, una escritura puede ocurrir dos veces. Si tus herramientas no son idempotentes, el paralelismo agrava el problema.
- Límites de tasa del proveedor. Tres llamadas simultáneas a la misma API externa pueden disparar un 429 que nunca verías en secuencial.
- Orden de efectos. Dos herramientas que escriben en el mismo recurso dejan un estado final que depende de cuál termine antes.
- Contexto inflado. Diez resultados voluminosos en un turno llenan la ventana de golpe. Recorta o resume antes de devolver.
Una política razonable por defecto
No hace falta decidir caso a caso. Marca cada herramienta con un atributo y deja que el ejecutor aplique la regla:
- Solo lectura: se puede paralelizar libremente.
- Escritura: se ejecuta sola, en orden, y no comparte turno con otras escrituras.
- Con dependencia declarada: espera a la herramienta de la que depende.
Con esto, aunque el modelo pida cinco cosas a la vez, tu código decide qué es seguro lanzar junto y qué no.
Por dónde empezar
- Mide cuántos turnos de tu agente contienen más de una llamada independiente. Si casi ninguno, el beneficio será pequeño.
- Etiqueta tus herramientas como lectura o escritura.
- Paraleliza solo las de lectura, con semáforo y timeout.
- Compara latencia p50 y p95 antes y después, y cuenta los 429.
El paralelismo no hace al agente más listo. Le quita esperas que nunca debió tener, y eso, bien acotado, se nota desde el primer día.