DEV Community

Cover image for Recuperación de errores de agentes IA: Patrones de reintento, tiempo de espera, retroceso y disyuntor
Roobia
Roobia

Posted on • Originally published at apidog.com

Recuperación de errores de agentes IA: Patrones de reintento, tiempo de espera, retroceso y disyuntor

Tu agente llama a una API. La API devuelve un 429. Tu agente reintenta de inmediato, recibe otro 429 y vuelve a reintentar. El resultado es un bucle que golpea un servicio limitado hasta que la ejecución falla o la factura se dispara. Nadie suele escribir este bucle a propósito: aparece al implementar una versión ingenua de “manejar el error” y es una de las dudas más comunes en el foro de discusión del SDK de Anthropic.

Prueba Apidog hoy

La recuperación de errores es lo que separa una demo limpia de un agente que puedes operar en producción. El modelo no es el problema: importa qué hace tu código cuando una herramienta responde lento, devuelve un límite de tasa o falla. Una recuperación correcta convierte una dependencia inestable en una pausa breve; una recuperación deficiente puede convertir un único 500 en un incidente.

Esta guía implementa cuatro patrones:

  1. Reintentos con retroceso exponencial y fluctuación.
  2. Tiempos de espera por llamada y por ejecución.
  3. Disyuntores para dependencias caídas.
  4. Claves de idempotencia para reintentos seguros.

Después, verás cómo probarlos contra un simulacro. Para un contexto más amplio, empieza con por qué los agentes de IA fallan en producción.

No puedes probar la recuperación contra una API sana

En desarrollo, una dependencia suele responder bien. Las llamadas tienen éxito, la demo funciona y la lógica de recuperación nunca se ejecuta. El problema aparece cuando el primer 429, 500 o timeout real ocurre en producción.

La regla práctica es simple: para probar la recuperación, genera fallos intencionalmente.

Configura un simulacro de la API que consume la herramienta de tu agente y programa respuestas como:

  • 429 Too Many Requests con Retry-After.
  • 500 Internal Server Error.
  • Una respuesta lenta que provoque un timeout.
  • Un cuerpo JSON malformado.
  • Una conexión cerrada antes de enviar la respuesta.

Después, apunta el agente al simulacro y verifica su comportamiento. Así, el fallo deja de ser algo que te despierta a las 3 de la mañana y pasa a ser un caso reproducible en CI. Apidog permite configurar simulacros y programar estas respuestas.

Reintentos con retroceso exponencial y fluctuación

El patrón ingenuo es reintentar inmediatamente:

while True:
    try:
        return call_api()
    except Exception:
        continue
Enter fullscreen mode Exit fullscreen mode

Esto puede empeorar una caída. Si muchos clientes fallan al mismo tiempo, todos reintentan al mismo tiempo y mantienen el servicio bajo presión.

La implementación debe incluir:

  • Retroceso exponencial: aumenta la espera entre intentos.
  • Fluctuación (jitter): añade aleatoriedad para evitar reintentos sincronizados.
  • Límite de intentos: evita reintentar indefinidamente.
  • Límite de espera: evita retrasos excesivos.

Ejemplo en Python:

import random
import time

MAX_ATTEMPTS = 4
BASE_DELAY_SECONDS = 1
MAX_DELAY_SECONDS = 8

def retry_with_backoff(operation):
    for attempt in range(MAX_ATTEMPTS):
        try:
            return operation()
        except Exception as error:
            is_last_attempt = attempt == MAX_ATTEMPTS - 1
            if is_last_attempt:
                raise

            exponential_delay = min(
                BASE_DELAY_SECONDS * (2 ** attempt),
                MAX_DELAY_SECONDS,
            )
            jitter = random.uniform(0, exponential_delay)
            time.sleep(jitter)
Enter fullscreen mode Exit fullscreen mode

Con este patrón, los reintentos no ocurren en ráfaga. En lugar de reintentar inmediatamente, el cliente espera un intervalo creciente y aleatorio.

Como regla general, entre tres y cinco intentos suelen cubrir errores transitorios. Más intentos normalmente indican que estás reintentando un fallo permanente.

El SDK de Anthropic aplica reintentos para algunas de sus propias llamadas: reintenta errores de conexión y ciertos códigos de estado con retroceso exponencial. Sin embargo, no cubre las APIs externas llamadas por las herramientas de tu agente. Debes envolver esas llamadas con tu propia política de reintentos.

Si trabajas con operaciones sensibles, como pagos, revisa esta guía sobre lógica de reintentos para APIs de alto riesgo.

Establece un tiempo de espera en cada llamada

Un reintento solo ayuda si la solicitud termina fallando. El caso más problemático es una solicitud que nunca regresa: la dependencia acepta la conexión y se queda bloqueada.

Sin timeout:

  • La herramienta queda esperando.
  • La ejecución del agente se detiene.
  • No se activa ninguna recuperación.
  • Se consume tiempo y presupuesto sin producir un resultado.

Cada llamada saliente necesita, como mínimo:

  1. Un timeout de conexión.
  2. Un timeout de lectura.
  3. Un presupuesto total para la ejecución del agente.

Ejemplo con httpx:

import httpx

timeout = httpx.Timeout(
    connect=2.0,
    read=10.0,
    write=5.0,
    pool=2.0,
)

with httpx.Client(timeout=timeout) as client:
    response = client.get("https://api.example.com/data")
    response.raise_for_status()
Enter fullscreen mode Exit fullscreen mode

Cuando ocurre un timeout, trátalo como un error potencialmente reintentable:

import httpx

try:
    response = client.get("https://api.example.com/data")
    response.raise_for_status()
except httpx.TimeoutException:
    # Aplicar retroceso y reintentar hasta el límite configurado.
    pass
Enter fullscreen mode Exit fullscreen mode

No elijas los valores al azar. Usa la latencia real de la dependencia:

  • Configura el timeout por encima de su p99, con margen.
  • Si es demasiado bajo, abortarás solicitudes que habrían tenido éxito.
  • Si es demasiado alto, una dependencia bloqueada retendrá al agente más allá de lo útil.
  • Para respuestas en streaming, usa un presupuesto separado: una respuesta larga puede ser legítima, pero una transmisión detenida debe cortar.

Activa un disyuntor cuando una dependencia está caída

El retroceso exponencial ayuda cuando un servicio está temporalmente ocupado. No es suficiente cuando la dependencia está completamente caída.

Si una API lleva un minuto fallando, la siguiente solicitud probablemente también fallará. Seguir reintentando añade carga, consume timeouts y hace esperar al usuario por un resultado predecible.

Un disyuntor usa tres estados:

Estado Comportamiento
Cerrado Las solicitudes pasan normalmente y se cuentan los fallos.
Abierto Las solicitudes fallan rápido durante un periodo de enfriamiento.
Semiabierto Se permite una solicitud de prueba para comprobar si el servicio se recuperó.

Flujo básico:

cerrado
  └─ demasiados fallos ──> abierto
                              └─ termina enfriamiento ──> semiabierto
                                                               ├─ éxito ──> cerrado
                                                               └─ fallo ──> abierto
Enter fullscreen mode Exit fullscreen mode

Una implementación simplificada podría verse así:

import time

class CircuitBreaker:
    def __init__(self, failure_threshold=5, cooldown_seconds=30):
        self.failure_threshold = failure_threshold
        self.cooldown_seconds = cooldown_seconds
        self.failures = 0
        self.opened_at = None

    def allow_request(self):
        if self.opened_at is None:
            return True

        if time.time() - self.opened_at >= self.cooldown_seconds:
            return True  # Estado semiabierto: permitir una sonda.

        return False

    def record_success(self):
        self.failures = 0
        self.opened_at = None

    def record_failure(self):
        self.failures += 1
        if self.failures >= self.failure_threshold:
            self.opened_at = time.time()
Enter fullscreen mode Exit fullscreen mode

En un agente, el disyuntor convierte “la API de pagos está caída” en un error rápido y explícito. El agente puede decidir informar al usuario, usar una alternativa o terminar la tarea de forma limpia.

Configura el disyuntor por dependencia, no globalmente. Una API de búsqueda caída no debería impedir que el agente use una API de facturación saludable.

Haz los reintentos seguros con claves de idempotencia

Los patrones anteriores asumen que reintentar es seguro. A menudo no lo es.

Imagina este flujo:

  1. El agente envía POST /charge.
  2. El servidor procesa el cobro.
  3. La respuesta se pierde o expira.
  4. El agente interpreta que la operación falló.
  5. El agente reintenta.
  6. El cliente recibe un segundo cobro.

El reintento hizo lo que le pediste. El problema es que la operación no era idempotente.

La solución es enviar una clave de idempotencia estable por acción lógica:

POST /charge
Idempotency-Key: charge_01HVQYBXQ2ZP4S1YJ6B4F8J3R7
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

El servidor registra esa clave cuando procesa la primera solicitud. Si recibe otra solicitud con la misma clave, devuelve el resultado original en lugar de ejecutar la acción otra vez.

Ejemplo:

import uuid
import httpx

idempotency_key = str(uuid.uuid4())

headers = {
    "Idempotency-Key": idempotency_key,
}

def create_charge(payload):
    with httpx.Client(timeout=10) as client:
        return client.post(
            "https://payments.example.com/charge",
            json=payload,
            headers=headers,
        )
Enter fullscreen mode Exit fullscreen mode

La clave debe generarse una vez antes del bucle de reintentos, nunca dentro:

# Correcto: la misma clave se reutiliza en todos los intentos.
idempotency_key = str(uuid.uuid4())

for attempt in range(3):
    send_charge(idempotency_key=idempotency_key)
Enter fullscreen mode Exit fullscreen mode
# Incorrecto: cada intento usa una clave distinta.
for attempt in range(3):
    send_charge(idempotency_key=str(uuid.uuid4()))
Enter fullscreen mode Exit fullscreen mode

Usa claves de idempotencia en toda llamada que cree o modifique estado:

  • Cobros.
  • Pedidos.
  • Correos enviados.
  • Nuevos registros.
  • Cambios de inventario.
  • Operaciones de escritura en sistemas externos.

Para más detalle, revisa la guía sobre claves de idempotencia.

Sobrevive a los límites de tasa y al bucle RateLimitError

Los límites de tasa requieren un tratamiento específico porque el servidor normalmente incluye instrucciones.

Una respuesta de límite de tasa excedido suele devolver:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Enter fullscreen mode Exit fullscreen mode

El encabezado Retry-After indica cuánto tiempo esperar antes de reintentar. Respétalo.

Si el servidor pide 30 segundos y tu agente reintenta en 2 segundos, recibirá otro 429. Si vuelve a intentarlo pronto, entrará en el bucle RateLimitError: capturar el límite, reintentar demasiado pronto, volver a ser limitado y repetir hasta agotar la ejecución.

También aparece en este hilo del SDK de Anthropic.

Implementa esta prioridad:

  1. Si recibes 429 y existe Retry-After, espera al menos ese tiempo.
  2. Si no existe el encabezado, aplica retroceso exponencial con fluctuación.
  3. Limita los intentos.
  4. Cuando se alcance el límite, devuelve un fallo limpio.

Ejemplo:

import random
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

def parse_retry_after(value: str) -> float:
    try:
        return float(value)
    except ValueError:
        retry_at = parsedate_to_datetime(value)
        now = datetime.now(timezone.utc)
        return max(0, (retry_at - now).total_seconds())

def retry_after_delay(response, attempt: int) -> float:
    retry_after = response.headers.get("Retry-After")

    if retry_after:
        return parse_retry_after(retry_after)

    max_delay = min(2 ** attempt, 8)
    return random.uniform(0, max_delay)
Enter fullscreen mode Exit fullscreen mode

El SDK de Anthropic ya respeta Retry-After para sus propias llamadas. Aplica la misma regla a las APIs externas que usan las herramientas de tu agente.

Además, añade regulación proactiva. Si un proveedor permite un número fijo de solicitudes por minuto, mide tus llamadas con un cubo de tokens u otro limitador local. La recuperación gestiona los límites que alcanzas; la regulación evita alcanzarlos.

Cómo probar la ruta de recuperación

Los patrones anteriores valen lo que valen tus pruebas. Una API sana no ejercita la recuperación, así que debes forzar cada fallo.

Usa esta estructura para cada escenario:

  1. Simula la dependencia. Configura un simulacro de la API llamada por la herramienta del agente. Así controlas códigos de estado, encabezados, cuerpos y retrasos, sin crear cargos ni enviar correos reales.
  2. Programa una secuencia. Por ejemplo: primero un 429 con Retry-After: 2, luego un 500 y finalmente un 200 con un cuerpo válido.
  3. Dirige el agente al simulacro. Configura la URL base de la herramienta para que apunte al simulacro en lugar del servicio real.
  4. Verifica el comportamiento. Comprueba tiempos de espera, número de intentos, encabezados y resultado final.

Un escenario de recuperación podría ser:

Llamada Respuesta del simulacro Comportamiento esperado
1 429 con Retry-After: 2 Espera al menos 2 segundos.
2 500 Aplica retroceso y reintenta.
3 200 Devuelve el resultado correcto.

Además, crea estos casos:

Escenario de abandono

Programa el simulacro para que falle siempre.

Verifica que:

  • El agente no excede el número máximo de intentos.
  • El error final es claro.
  • La ejecución no entra en un bucle infinito.

Escenario de disyuntor

Provoca suficientes fallos consecutivos para abrir el disyuntor.

Verifica que:

  • Las llamadas posteriores fallen rápido.
  • No se pague un timeout por cada intento.
  • Tras el enfriamiento, solo se permita una solicitud de prueba.

Escenario de idempotencia

Este es el caso que evita dobles cobros y envíos duplicados:

  1. El simulacro acepta una solicitud mutante.
  2. Procesa la operación.
  3. No devuelve la respuesta, para que el agente crea que falló.
  4. El agente reintenta.
  5. El simulacro recibe la segunda solicitud.

Verifica que:

  • Ambas solicitudes llevan la misma Idempotency-Key.
  • El simulacro registra una sola acción lógica.
  • No existe una segunda operación creada.

Una clave nueva en el reintento o una operación duplicada significa que detectaste un doble envío antes de que lo detectara un cliente.

Para el flujo de prueba más amplio, revisa cómo probar agentes que llaman a tus APIs.

Lista de verificación de recuperación de errores

Antes de desplegar un agente, comprueba lo siguiente:

  • [ ] Cada llamada saliente tiene timeout de conexión, timeout de lectura y presupuesto total de ejecución.
  • [ ] Los reintentos usan retroceso exponencial con fluctuación.
  • [ ] El retraso y el número de intentos tienen límites definidos.
  • [ ] Las respuestas 429 respetan Retry-After.
  • [ ] Si falta Retry-After, se usa retroceso exponencial como reserva.
  • [ ] Existe un disyuntor por dependencia.
  • [ ] Cada llamada que cambia estado usa una clave de idempotencia estable.
  • [ ] La ruta de abandono devuelve un error limpio.
  • [ ] Cada comportamiento se prueba contra un simulacro que fuerza el fallo.

Si completas estos puntos, tu agente se recuperará de forma deliberada, no por suerte.

Dónde encaja Apidog y dónde no

Conviene mantener clara la responsabilidad de cada herramienta. Apidog no es un framework de agentes, un host de modelos ni un entorno de ejecución. No construye, ejecuta u orquesta tu agente, y no evalúa la salida del modelo.

Su papel está en la capa de API que consume el agente, donde la recuperación gana o pierde valor.

En este flujo, Apidog puede ayudarte a:

  1. Simular las dependencias que usa el agente.
  2. Programar respuestas de fallo, como 429 con Retry-After, 500, timeouts o cuerpos malformados.
  3. Validar las solicitudes recibidas por el simulacro.
  4. Verificar que la clave de idempotencia existe y permanece estable.
  5. Comprobar que el número de llamadas coincide con el esperado.

El objetivo es detectar un encabezado omitido, un doble envío o un reintento defectuoso en una prueba, no en producción.

Preguntas frecuentes

¿El SDK de Anthropic no maneja los reintentos por mí?

Para sus propias llamadas, sí. El SDK reintenta ciertos errores con retroceso exponencial y respeta Retry-After; tú configuras el límite mediante una opción de reintentos máximos. Sin embargo, no cubre las APIs externas llamadas por las herramientas de tu agente. Debes aplicar estos patrones también a esas dependencias.

¿Cuándo necesito una clave de idempotencia?

En cualquier llamada que cree o cambie estado: cargos, pedidos, mensajes enviados o nuevos registros. Las llamadas de solo lectura normalmente se pueden reintentar sin una clave. Genera la clave una vez por acción lógica y reutilízala en todos los reintentos.

Ensaya un fallo esta semana

No necesitas implementar los cuatro patrones a la vez. Empieza por el que podría causar más daño: normalmente un bucle de límite de tasa o un reintento no idempotente.

Programa un 429, elimina una respuesta o devuelve un 500. Después observa:

  • Cuánto espera el agente antes de reintentar.
  • Cuántos intentos realiza.
  • Si respeta Retry-After.
  • Si conserva la misma clave de idempotencia.
  • Si termina con un error limpio cuando debe abandonar.

La primera vez que veas un retroceso correcto y una única clave de idempotencia donde antes podía existir un doble cargo, tendrás una razón operativa para confiar en tu agente, no solo una demo sin errores.

Top comments (0)