Diseñando el Retry: Que las Llamadas a LLM Fallen Como Adultas
Resumen
Un try/except con un loop de tres intentos es la forma incorrecta de manejar fallas de LLM, porque las llamadas a LLM no fallan como las llamadas a API normales. Fallan en al menos cinco formas distintas — output malformado, rechazo, timeout, rate limit y stream parcial — y cada una quiere una respuesta distinta: retry-con-reparación, modelo de fallback, degradar elegantemente, o fallar duro. Reintentar un rechazo desperdicia dinero y hace loop. Reintentar un error de JSON malformado sin cambiar el prompt repite el mismo error. La solución es una taxonomía de fallas, la acción correcta por tipo, idempotencia, backoff con jitter, y un circuit breaker para que un mal minuto no se vuelva una tarde en bancarrota.
El primer incidente de producción que causé con un LLM no fue una respuesta incorrecta. Fue un loop de retry.
Había envuelto una llamada al modelo en el mismo patrón defensivo que usé por una década en servicios HTTP inestables: try, catch, reintenta tres veces con backoff. Razonable. Probado en batalla. Completamente equivocado para LLMs. Una tarde un cambio de prompt empezó a producir JSON que el modelo no podía cerrar del todo, el parser lanzó error, mi loop diligentemente reintentó el request idéntico tres veces, y cada retry produjo el JSON roto idéntico — porque nada del request había cambiado. Pagué por cuatro fallas donde una habría bastado, multiplicado por cada request de esa hora.
Esa es la idea central: las llamadas a LLM no fallan como las llamadas a API, así que no se pueden reintentar como las llamadas a API. Un HTTP 503 es la misma falla cada vez y un retry es una apuesta razonable a que el servidor se recuperó. Una falla de LLM puede ser transitoria (rate limit), puede ser permanente hasta que cambies algo (output malformado), o puede ser el modelo rechazando correctamente (un "no" que seguirás pagando por escuchar).
La Taxonomía de Fallas
Antes de poder reintentar bien, tienes que nombrar qué se rompió. Hay cinco formas para las que planifico.
| Falla | Cómo se ve | Qué significa |
|---|---|---|
| Output malformado | El JSON no parsea, violación de schema | El request llegó, el modelo respondió con forma incorrecta |
| Rechazo | "No puedo ayudar con eso" | El modelo funcionó bien; la respuesta es no |
| Timeout | Sin respuesta en el presupuesto | Puede ser transitorio, puede ser una tarea muy difícil |
| Rate limit | 429 del proveedor | Puramente transitorio, puramente sobre el ritmo |
| Stream parcial | El stream se corta a mitad de token | La conexión murió; tienes media respuesta |
Meter todo esto en un solo bloque except es el pecado original. Son cinco problemas distintos y quieren cuatro respuestas distintas.
La Tabla de Decisión
Esta es la tabla que mantengo junto a cualquier código de retry que escribo. La acción depende enteramente del tipo de falla — nunca de un "reintenta N veces" genérico.
┌────────────────┬──────────────────┬───────────────────────────────┐
│ FALLA │ ¿RETRY? │ ACCIÓN CORRECTA │
├────────────────┼──────────────────┼───────────────────────────────┤
│ Output │ Sí, pero REPARA │ Re-promptea con el error + │
│ malformado │ primero │ el output malo. No reenvíes │
│ │ │ el mismo request. │
├────────────────┼──────────────────┼───────────────────────────────┤
│ Rechazo │ NO │ Falla duro o ruta a humano. │
│ │ │ Reintentar paga el mismo no. │
├────────────────┼──────────────────┼───────────────────────────────┤
│ Timeout │ Una vez, luego │ Reintenta mismo modelo una │
│ │ FALLBACK │ vez; si repite, degrada/cae. │
├────────────────┼──────────────────┼───────────────────────────────┤
│ Rate limit │ Sí, con │ Backoff + jitter. Respeta el │
│ │ BACKOFF │ header Retry-After si lo dan. │
├────────────────┼──────────────────┼───────────────────────────────┤
│ Stream parcial │ Sí, pero con │ Reanuda si puedes; si no, │
│ │ cuidado │ retry limpio. Nunca muestres │
│ │ │ el parcial. │
└────────────────┴──────────────────┴───────────────────────────────┘
Output malformado → retry con reparación
No reenvíes el mismo request. Manda un request nuevo que incluya el output roto y el error de parseo, y pídele al modelo que lo arregle. Esta es la diferencia entre "intenta lo mismo otra vez" y "esto fue lo que salió mal, corrígelo". El primero repite el error; el segundo usualmente lo arregla en el primer intento de reparación.
try:
result = parse(completion)
except ParseError as e:
repair = call_model([
*original_messages,
{"role": "assistant", "content": completion},
{"role": "user", "content": f"Eso falló al parsear: {e}. "
f"Devuelve solo JSON válido según el schema."},
])
result = parse(repair) # un intento de reparación, luego falla duroRechazo → no reintentar
Un rechazo es el modelo funcionando correctamente. Reintentarlo es pagar tres veces para que te digan "no" tres veces — y si tu prompt o contenido cruzó un límite de seguridad, lo cruzará de nuevo. Ruta a un humano, devuelve un mensaje elegante, o falla duro. Solo no hagas loop.
Los rechazos se disfrazan de output malformado
Un rechazo frecuentemente llega como texto donde tu código esperaba JSON, así que tu parser lanza error y tu retry de output-malformado se activa — ahora estás reintentando un rechazo. Detecta los rechazos explícitamente antes del paso de parseo, o quemarás presupuesto reintentando un no firme y final.
Timeout → reintenta una vez, luego cae
Un timeout es ambiguo: tal vez la red tuvo un hipo, tal vez la tarea es genuinamente muy difícil para el modelo y presupuesto actuales. Reintenta el mismo modelo exactamente una vez. Si vuelve a hacer timeout, eso es señal — cae a un camino más rápido/pequeño o degrada elegantemente ("no pude completar eso, esto es lo que tengo").
Rate limit → backoff con jitter
La única falla que se comporta como un error transitorio clásico. Haz backoff exponencial, y siempre agrega jitter — sin él, cada cliente al que le pegó rate limit en el mismo instante reintenta en el mismo instante y estampida al proveedor de nuevo. Respeta el header Retry-After cuando el proveedor lo mande.
delay = min(base * (2 ** attempt), cap)
delay += random.uniform(0, delay * 0.3) # el jitter previene la estampidaStream parcial → nunca muestres la media-respuesta
Un stream que muere a mitad de token te ha dado un fragmento que puede ser lo suficientemente coherente para verse completo y lo suficientemente equivocado para ser peligroso. Descártalo. Reintenta limpio, o reanuda desde un checkpoint si tu proveedor lo soporta — pero nunca renderices el parcial a un usuario.
Las Dos Cosas Que Te Salvan de Ti Mismo
Idempotencia
Si una escritura ocurre a partir de una llamada a LLM, cada retry arriesga hacerla dos veces. Adjunta una clave de idempotencia derivada del request para que una llamada reintentada que ya tuvo éxito del lado del servidor no dispare la acción de nuevo. Esta es la diferencia entre un retry que es seguro y un retry que le cobra doble a un cliente.
Un circuit breaker
Este es el que convierte mi viejo incidente de catástrofe a parpadeo. Después de N fallas en una ventana corta, deja de llamar al modelo por completo y falla rápido por un período de enfriamiento.
fallas < N ┌──────────┐
─────────────────────────► CERRADO │ las llamadas fluyen normal
└────┬─────┘
N fallas rápido │
┌────▼─────┐
│ ABIERTO │ rechaza de inmediato,
└────┬─────┘ sin llamadas, ahorra $$$
enfriamiento OK │
┌────▼─────┐
│ SEMI-ABTO │ deja pasar una llamada test
└──────────┘
Un loop de retry ingenuo es un arma de facturación
Un loop de retry sin circuit breaker, envuelto alrededor de una llamada de streaming o agéntica, puede reemitir el mismo request caro cientos de veces antes de que un humano lo note. Lo he visto pasar. El circuit breaker no es infraestructura opcional — es el cinturón de seguridad que evita que un mal minuto se vuelva una tarde en bancarrota. Combínalo con el pulso de monitoreo de costos para que el pico aparezca en un dashboard el mismo día.
Fallar Como Adulto
El manejo maduro de errores para LLMs no se trata de prevenir la falla — estos sistemas fallan constantemente y eso está bien. Se trata de fallar de una forma que sea barata, acotada y explicable. Nombra la falla. Elige la acción de la tabla. Repara en vez de repetir. Haz backoff con jitter. Mantente idempotente. Y pon un circuit breaker entre tus buenas intenciones y tu factura.
Haz eso, y tus features de LLM fallan como adultas: en silencio, de forma segura, y sin llevarse el resto del sistema — ni el presupuesto — con ellas.
Frequently Asked Questions
Artículos Relacionados
La Auditoría Diaria de Prompts de 5 Minutos: Mantén los Costos de LLM Bajo Control
Un ritual diario ligero que atrapa el inflado de tokens, prompts rotos y regresiones silenciosas antes de que aparezcan en la factura. Qué reviso, en qué orden, y por qué solo toma cinco minutos.
La Guía Completa para Transmitir Respuestas de LLM en Streaming
Todo lo que aprendí (por las malas) sobre streaming de respuestas de LLM al navegador. Historias de guerra sobre SSE vs WebSockets, la vez que un usuario dijo que mi IA 'vomita texto,' y por qué absolutamente necesitas un botón de cancelar.
Qué Logueo en Realidad Cuando una Feature de LLM Llega a Producción
Los logs normales de una app no alcanzan para una feature de LLM. Aquí está el conjunto exacto de señales que capturo para poder reconstruir cualquier respuesta incorrecta — snapshot completo del input, modelo y versión, conteo de tokens, tool calls, desglose de latencia, la completion cruda, el resultado parseado, el resultado de validación, y la acción final del usuario.
No te pierdas nada
Artículos sobre IA, ingeniería y las lecciones que aprendo construyendo cosas. Sin spam, lo prometo.
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.