Tu agente funcionó perfecto en local. Los tests pasaron verde. Pero a las 3 AM del domingo, un deploy cambió una clase de Tailwind de bg-blue-500 a bg-blue-600 y tu automatización web murió silenciosamente, perdiendo datos críticos. La recuperación automática en agentes de IA no es un lujo: es una necesidad cuando operas scraping o RPA a escala. Vamos a construir un sistema que no solo detecte fallos, sino que los corrija sin despertarte.
El problema real: Clases dinámicas, no data-testid
El error común es culpar a los equipos de frontend por no usar data-testid. La realidad: incluso con buenas prácticas, enfrentas:
- Frameworks CSS-in-JS: Styled Components, Emotion o Tailwind generan clases hash como
sc-bdfBQBobg-[#1da1f2]que cambian entre builds. - Rehydration de React/Vue: El DOM inicial difiere del renderizado final, causando «element detached».
- A/B testing en producción: El mismo flujo puede mostrar variantes UI diferentes según el bucket del usuario.
- Detección granular: Capturar excepciones específicas (
TimeoutError,ElementHandleError,StrictModeViolation) en lugar de hacer parsing de strings de error. - Diagnóstico contextual: Tomar snapshot del DOM visible y el selector que falló (no genéricos).
- Curación asistida por LLM: Generar selectores alternativos basados en atributos semánticos (texto contenido, role ARIA) en lugar de clases CSS.
- Validación segura: Verificar que el nuevo selector no contenga payloads maliciosos antes de ejecutar.
No se trata de «selectores frágiles», sino de entornos web inherentemente dinámicos.
Arquitectura de recuperación: Más que un try-catch
Un sistema de self-healing robusto sigue cuatro fases:
Evita usar MD5 del HTML completo para detectar «estabilidad DOM»: es computacionalmente caro y falla con relojes, animaciones CSS o contadores de notificaciones. Mejor esperar a que el innerText de un contenedor padre sea consistente por 300ms.
Implementación práctica con Playwright
Este código corrige los errores comunes de tutoriales básicos: maneja excepciones específicas, valida seguridad y cachea respuestas LLM para no quebrarte financieramente.
import time
import hashlib
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
from openai import OpenAI
class ResilientAgent:
def __init__(self):
self.client = OpenAI()
self.healing_cache = {} # Reduce costos LLM en 80%
def is_safe_selector(self, selector):
"""Bloquea inyección de código en selectores generados"""
dangerous = ['javascript:', 'onerror=', 'onload=', '<script', 'eval(']
return not any(d in selector.lower() for d in dangerous)
def wait_for_stability(self, page, checks=3):
"""Espera estabilidad sin MD5: compara innerText ligero"""
last = ""
stable = 0
for _ in range(50): # Max 5 segundos
current = page.evaluate("document.body?.innerText?.substring(0,500)")
if current == last:
stable += 1
if stable >= checks:
return True
else:
stable = 0
last = current
time.sleep(0.1)
return False
def heal_selector(self, failed_selector, error_type, page):
"""Invoca LLM solo con contexto relevante (primeros 2KB del body)"""
cache_key = hashlib.md5(f"{failed_selector}:{error_type}".encode()).hexdigest()
if cache_key in self.healing_cache:
return self.healing_cache[cache_key]
context = page.evaluate("""
() => document.body.innerHTML.substring(0,2000)
.replace(/<scriptb[^<]*(?:(?!</script>)<[^<]*)*</script>/gi, '')
""")
prompt = f"""Selector fallido: '{failed_selector}'
Error: {error_type}
DOM (limpio): ...{context}...
Genera UN selector CSS alternativo robusto basado en atributos estables (id, name, aria-label, texto exacto).
Solo el selector, sin comillas ni explicaciones."""
# Usar modelo pequeño para latencia <800ms
resp = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=60
)
new_selector = resp.choices[0].message.content.strip()
if not self.is_safe_selector(new_selector):
raise SecurityError(f"Selector rechazado por seguridad: {new_selector}")
self.healing_cache[cache_key] = new_selector
return new_selector
def rebuild_action(self, page, selector, action, value=None):
"""Reconstruye la acción con reintentos explícitos"""
loc = page.locator(selector).first # .first evita errores strict mode
if action == "click":
loc.click(timeout=8000)
elif action == "fill":
loc.fill(value, timeout=8000)
elif action == "select":
loc.select_option(value, timeout=8000)
def execute(self, page, selector, action="click", value=None):
"""Entry point con manejo de excepciones específicas de Playwright"""
current_selector = selector
try:
self.rebuild_action(page, current_selector, action, value)
return {"status": "success", "selector": current_selector, "healed": False}
except PlaywrightTimeout:
# DOM lento o selector obsoleto
self.wait_for_stability(page)
current_selector = self.heal_selector(selector, "TimeoutError", page)
self.rebuild_action(page, current_selector, action, value)
return {"status": "success", "selector": current_selector, "healed": True}
except Exception as e:
# Elemento desconectado del DOM (rehydration)
error_str = str(e).lower()
if "detached" in error_str or "strict" in error_str:
current_selector = self.heal_selector(selector, "DetachedError", page)
self.rebuild_action(page, current_selector, action, value)
return {"status": "success", "selector": current_selector, "healed": True}
raise
Costos, latencia y seguridad: Lo que duele
Invocar GPT-4 en cada fallo es económicamente suicida. Un agente medio puede fallar 200 veces al día. A $0.03 por invocación, hablamos de $180/mes solo en «curación». Estrategias de mitigación:
Riesgo de inyección: Un atacante que controle el HTML podría inyectar javascript:alert(1) como «selector sugerido». Siempre sanitiza con whitelist (^[a-zA-Z0-9#[]._-='"s]+$) antes de pasar el selector a Playwright.
Alternativas maduras: No reinventes la rueda
Antes de montar tu propio sistema, evalúa:
Checklist para producción
PlaywrightTimeout, ElementHandleError) antes de invocar al LLM.javascript:, onerror).Conclusión
La recuperación automática en agentes de IA no es magia: es ingeniería defensiva. Un sistema bien diseñado no solo te ahorra pagos de guardia a las 3 AM, sino que hace tus automatizaciones web resilientes ante el caos natural del frontend moderno. Pero recuerda: cada invocación a un LLM es un dólar que sale de tu bolsillo. Diseña para fallar barato, curar rápido y nunca confiar ciegamente en lo que un modelo genera sin validación.
¿Ya implementaste self-healing en tus agentes? Comparte tu approach en los comentarios o únete al newsletter donde semanalmente deconstruimos arquitecturas de IA para producción real.
Geek de la tecnología, en busca de la mejora y aprendizaje continuo.
Ingeniero en ciencias de la computación, Postgrado en Análisis y predicción de datos