¡Hola a todos! Leo aquí de agntdev.com. Hoy quiero hablar sobre algo que he estado pensando mucho últimamente, especialmente porque he estado lidiando con algunos nuevos proyectos de agentes. Se trata del lado “Dev” del desarrollo de agentes, específicamente, cómo vamos sobre construir agentes confiables a partir de partes poco confiables. Sí, me escuchaste. Porque seamos honestos, esa es la realidad para la mayoría de nosotros, ¿verdad?
No solemos trabajar con APIs y servicios perfectamente curados de nivel empresarial. Más a menudo, estamos ensamblando modelos de código abierto, APIs de terceros con límites de tasa cuestionables, y tal vez incluso algunos microservicios caseros que, digamos, tienen una personalidad. Y sin embargo, la expectativa siempre es que nuestros agentes simplemente… funcionen. De manera consistente. Fiablemente. Incluso cuando los componentes subyacentes están haciendo un berrinche.
He recorrido este camino tantas veces. Recuerdo un proyecto el año pasado en el que estaba construyendo un agente para ayudar a gestionar mis contribuciones de código abierto. Necesitaba interactuar con la API de GitHub, un modelo de análisis de sentimientos alojado en un nivel gratuito, y un servicio de notificaciones personalizadas que diseñé en un fin de semana. Cada uno de estos tenía sus peculiaridades. GitHub a veces me limitaba de manera inesperada, el modelo de sentimientos ocasionalmente se agotaba, y mi servicio de notificaciones… bueno, digamos que tenía la costumbre de olvidar sus modales después de una hora de funcionamiento. Si no hubiera implementado algunas salvaguardias serias, todo se habría colapsado como una casa de naipes.
Así que hoy quiero compartir algunas estrategias prácticas y patrones que he adoptado para hacer que mis agentes sean más resilientes, incluso cuando las piezas de las que están construidos son todo menos eso.
La Verdad Inevitable: Las Cosas Se Romperán
Primero, aceptemos esto como una verdad universal. Ninguna API está 100% disponible. Ningún modelo es 100% preciso. Ninguna red es 100% estable. Una vez que aceptas esto, puedes comenzar a diseñar para el fracaso, lo que, de manera contraria a la intuición, hace que tu agente sea más exitoso.
El problema que veo a menudo, especialmente con los nuevos desarrolladores que se adentran en el trabajo de agentes, es que asumen el éxito en cada llamada externa. Escriben código como este:
response = external_api.call_method(data)
# Suponer que la respuesta es siempre perfecta y proceder
processed_data = process_response(response)
Y luego, cuando external_api.call_method lanza un error de conexión, o devuelve un 500, o simplemente envía de vuelta un JSON malformado, todo el agente se detiene. Podemos hacerlo mejor.
Estrategia 1: Reintentos solidos con Retardo
Esta es probablemente la técnica más fundamental, y, sin embargo, a menudo se implementa mal o no se implementa en absoluto. Simplemente reintentar inmediatamente después de un fallo suele ser una mala idea. Si el servicio externo está caído, solo estás golpeándolo más, haciendo potencialmente que las cosas empeoren o limitándote por la tasa.
La clave es el retardo exponencial. Esto significa esperar períodos progresivamente más largos entre reintentos. Le da al servicio externo la oportunidad de recuperarse y reduce la carga que le estás imponiendo.
Ejemplo: Python con Tenacity
Para Python, mi biblioteca de cabecera para esto es Tenacity. Hace que agregar lógica de reintentos sea increíblemente limpio.
import random
import logging
from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception_type
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ExternalServiceError(Exception):
"""Excepción personalizada para fallas en servicios externos."""
pass
# Simular una llamada a API externa poco confiable
def call_unreliable_api(data):
if random.random() < 0.6: # 60% de probabilidad de falla
logger.warning(f"La llamada a la API falló para los datos: {data}")
raise ExternalServiceError("Fallo simulado de API o tiempo de espera")
logger.info(f"Llamada a la API exitosa para los datos: {data}")
return {"status": "success", "result": f"processed_{data}"}
@retry(wait=wait_exponential(multiplier=1, min=4, max=10),
stop=stop_after_attempt(5),
retry=retry_if_exception_type(ExternalServiceError))
def get_processed_data_with_retries(input_data):
logger.info(f"Intentando llamar a la API para: {input_data}")
return call_unreliable_api(input_data)
if __name__ == "__main__":
try:
result = get_processed_data_with_retries("some_important_item")
print(f"Resultado final: {result}")
except ExternalServiceError as e:
print(f"Falló después de múltiples reintentos: {e}")
except Exception as e:
print(f"Ocurrió un error inesperado: {e}")
En este fragmento:
wait_exponentialhace que las esperas sean más largas con cada reintento (4s, luego ~8s, luego ~16s, etc., hasta un máximo de 10s).stop_after_attempt(5)significa que intentará un máximo de 5 veces.retry_if_exception_type(ExternalServiceError)asegura que solo reintente para errores específicos, no para, digamos, unKeyboardInterrupt.
Este patrón es un salvavidas. Lo uso para conexiones a bases de datos, solicitudes HTTP, e incluso para la comunicación interna entre módulos de agentes cuando sé que uno podría estar sobrecargado temporalmente.
Estrategia 2: Disyuntores para Prevenir Fallos en Cascada
Los reintentos son geniales para errores transitorios. Pero, ¿qué pasa si el servicio está completamente caído? Reintentar repetidamente solo agotará tus recursos y potencialmente empeorará el problema para el servicio externo si está luchando por recuperarse. Aquí es donde entra el patrón de disyuntor.
Piensa en ello como un disyuntor eléctrico en tu casa. Si hay una falla (demasiados fallos), se “dispara”, evitando que más corriente fluya y protegiendo el sistema. Después de un tiempo, puede reiniciarse, pero no seguirá intentando enviar corriente a un cable cortocircuitado.
Para los agentes, un disyuntor monitorea las llamadas a un servicio externo. Si la tasa de fallas supera un cierto umbral dentro de un período de tiempo dado, el circuito “se abre”. Cuando está abierto, todas las llamadas posteriores a ese servicio fallan inmediatamente sin siquiera intentar la llamada. Después de un período de “tiempo de espera” configurable, el circuito se mueve a un estado “semi-abierto”, permitiendo un número limitado de llamadas de prueba para ver si el servicio se ha recuperado. Si tienen éxito, se cierra; si fallan, se abre de nuevo.
Por qué es importante para los agentes:
- Conservación de recursos: Tu agente no está perdiendo tiempo y recursos tratando de llamar a un servicio muerto.
- Fallos más rápidos: En lugar de esperar un tiempo de espera, tu agente recibe una señal de fallo inmediato, lo que le permite manejar la situación (por ejemplo, usar un retorno, registrar el problema, notificar a un operador).
- Protege los servicios externos: Evita que tu agente genere un DDoS a un servicio que está luchando.
Usualmente implemento esto usando bibliotecas. Para Python, Pybreaker es excelente.
import time
import random
import logging
from pybreaker import CircuitBreaker, CircuitBreakerError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ExternalAPIClient:
def __init__(self):
# Configurar el disyuntor:
# 3 fallos consecutivos dentro de 60 segundos abrirán el circuito.
# Se queda abierto durante 5 segundos.
self.breaker = CircuitBreaker(fail_max=3, reset_timeout=5, exclude=[TypeError]) # No romper por TypeErrors
def _unreliable_call(self, data):
if random.random() < 0.7: # 70% de probabilidad de falla
logger.warning(f"Simulando error interno de API para los datos: {data}")
raise ConnectionError("Servicio inaccesible")
logger.info(f"La llamada a la API tuvo éxito para los datos: {data}")
return {"result": f"processed_{data}"}
def process_data(self, data):
try:
return self.breaker.call(self._unreliable_call, data)
except CircuitBreakerError:
logger.error(f"¡El circuito está abierto! No llamando a la API para los datos: {data}")
# Lógica de retorno aquí: devolver datos en caché, valor por defecto, o lanzar un error más específico
return {"result": "fallback_data", "source": "circuit_breaker"}
except Exception as e:
logger.error(f"Error durante la llamada a la API (no relacionado con el disyuntor): {e}")
raise
if __name__ == "__main__":
client = ExternalAPIClient()
for i in range(15):
print(f"\n--- Intento {i+1} ---")
try:
result = client.process_data(f"item_{i}")
print(f"Resultado: {result}")
except Exception as e:
print(f"Error manejado: {e}")
time.sleep(1) # Simular algún retraso entre llamadas
Ejecuta esto, y verás que el circuito se abre después de algunos fallos, luego intenta abrirse semilateralmente, y tal vez incluso se cierre de nuevo si el servicio simulado comienza a comportarse.
Estrategia 3: Idempotencia para Operaciones que Cambian el Estado
Esto es crucial para cualquier agente que modifica el estado externo (por ejemplo, crear un registro, enviar un correo electrónico, iniciar un pago). Si tu agente intenta realizar una acción, y la red falla, o el servicio externo se agota, ¿cómo sabes si la acción realmente ocurrió?
Si solo vuelves a intentar sin considerar la idempotencia, podrías realizar accidentalmente la acción dos veces. Imagina enviar el mismo correo electrónico dos veces, o peor, cobrar a un cliente dos veces. No es bueno.
Una operación es idempotente si realizarla varias veces tiene el mismo efecto que realizarla una vez. Por ejemplo, establecer un valor (SET x = 5) es idempotente. Incrementar un valor (x = x + 1) no lo es.
Cómo lograr la idempotencia:
- Usa IDs de solicitud únicos: Al realizar una llamada a la API que cambia el estado, incluye un ID único generado por el cliente en el encabezado de la solicitud (por ejemplo,
X-Idempotency-Key). El servicio externo puede utilizar esta clave para detectar solicitudes duplicadas y devolver la respuesta original sin re-procesar. - Diseña APIs idempotentes: Si controlas la API, diseña puntos finales que sean naturalmente idempotentes. Por ejemplo, en lugar de un punto final de “crear pedido”, ten un punto final de “actualizar o insertar pedido” que pueda crear o actualizar basado en un ID de pedido único.
- Verifica el estado antes de reintentar: Después de una operación fallida que cambia el estado, si la API lo soporta, consulta el estado del recurso utilizando el ID único antes de intentar un reintento.
Si bien no tengo un fragmento de código directo para esto (se trata más del diseño de la API y de la lógica del lado del cliente), aquí tienes cómo podría verse el proceso de pensamiento de tu agente:
# Pseudo-código del agente para una operación idempotente
transaction_id = generate_unique_id()
payload = {"data": "some_value", "idempotency_key": transaction_id}
try:
response = external_payment_api.process_charge(payload)
# ¡Éxito! Almacenar transaction_id y response.
except (ConnectionError, TimeoutError, APIError) as e:
# Oh no, falló. ¿Se procesó el cargo de todos modos?
logger.warning(f"El pago falló, verificando estado con ID: {transaction_id}")
try:
status_response = external_payment_api.get_transaction_status(transaction_id)
if status_response.get("status") == "completed":
logger.info(f"El pago {transaction_id} fue en realidad exitoso en la verificación de reintento.")
# Tratar como éxito, almacenar información.
else:
logger.info(f"El pago {transaction_id} realmente falló, intentando reintento (con la misma clave de idempotencia).")
# Aquí es donde intentarías de nuevo con el *mismo* transaction_id
# La API de pago debería reconocerlo y no cobrar dos veces.
response = external_payment_api.process_charge(payload)
# ... manejar el éxito/fallo del reintento
except Exception as check_e:
logger.error(f"No se pudo comprobar el estado de la transacción para {transaction_id}: {check_e}")
# Necesita registrarse para revisión manual, o moverse a la cola de mensajes muertos
Esto requiere cooperación del servicio externo, pero es un patrón crítico para construir agentes verdaderamente confiables que manejen operaciones financieras u otras sensibles.
Estrategia 4: Respaldos y Degradación Graciosa
A veces, un servicio externo está completamente indisponible y no hay esperanza de reintentar o esperar. En estos casos, un buen agente no solo se bloquea; encuentra una manera de proporcionar una experiencia degradada pero aún útil.
Esto podría significar:
- Usar datos en caché: Si tu agente necesita datos específicos de un servicio, pero el servicio está caído, ¿puedes usar una versión obsoleta de una caché?
- Proporcionar valores predeterminados: Si un modelo de IA para análisis de sentimientos está caído, ¿puedes simplemente clasificar toda la entrada como “neutral” o “desconocido” por un tiempo, en lugar de hacer que todo el flujo del agente falle?
- Cambiar a un servicio de respaldo: Si tu API de traducción principal está caída, ¿puedes redirigir solicitudes a una secundaria, quizás menos eficiente o más costosa?
- Omitir pasos opcionales: Si un paso de enriquecimiento no crítico falla, ¿puede el agente simplemente continuar sin ese enriquecimiento, tal vez registrando una advertencia?
- Notificar a usuarios/operadores: Al menos, falla de manera elegante y comunica claramente el problema al usuario o al operador del sistema.
¿Mi anécdota sobre la falla del servicio de notificaciones? Mi plan de respaldo fue simple: si mi servicio de notificaciones personalizado fallaba, el agente solo registraba el evento localmente y enviaba un correo electrónico a *mí* diciendo “Hola, tu servicio de notificaciones probablemente está caído de nuevo, verifica los registros.” No es ideal para los usuarios finales, pero evitó que todo el agente se bloqueara y aseguró que supiera que algo estaba mal.
Conclusiones Accionables para Tu Próximo Proyecto de Agente
- Supón fallos: Diseña tu agente desde cero esperando que las dependencias externas fallen.
- Implementa reintentos con retroceso exponencial: Usa bibliotecas como Tenacity (Python) o patrones similares en otros lenguajes para errores transitorios.
- Despliega interruptores de circuito: Previene fallos en cascada y conserva recursos mediante el “tripeo” del circuito cuando un servicio falla de manera consistente. Pybreaker es un buen comienzo.
- Prioriza idempotencia para cambios de estado: Asegúrate de que operaciones como pagos o creación de registros no se dupliquen si se produce un reintento. Usa IDs únicos.
- Planifica para una degradación elegante: Identifica dependencias críticas vs. no críticas y construye respaldos. ¿Cuál es la “menos mala” acción que puede tomar tu agente cuando una dependencia falla?
- Monitorea de manera agresiva: Todas estas estrategias generan registros. Asegúrate de estar recopilando y analizando esos registros para entender *por qué* están fallando las cosas y con qué frecuencia.
Construir agentes confiables no se trata solo de algoritmos ingeniosos o modelos potentes. Se trata fundamentalmente de ingenierizar la solidez en cada capa, especialmente cuando se trata de la complicada realidad de las dependencias externas. Al aplicar estas estrategias, pasarás menos tiempo depurando fallos misteriosos del agente y más tiempo construyendo sistemas autónomos genuinamente útiles y confiables.
¿Cuáles son tus estrategias preferidas para tratar con servicios externos poco confiables? Deja un comentario abajo, ¡me encantaría escuchar tus historias y soluciones!
🕒 Last updated: · Originally published: March 25, 2026