La semana pasada uno de mis agentes de research se quedó atascado. Misma búsqueda, 47 veces. Costo: $0.83 en tokens de DeepSeek Pro quemados para nada. El problema no es que fallen — es que fallen en silencio y yo me entere cuando veo la factura.
Después de observar cientos de ejecuciones de mis 58 crons, identifiqué 3 señales que delatan a un agente atrapado. Son más simples de lo que pensás, y detectarlas no requiere infraestructura compleja ni LangGraph.
Señal 1: tres tool calls idénticos seguidos
Si tu agente llama a la misma herramienta con los mismos argumentos 3 veces, está atrapado. No necesita ser 47. Tres ya es suficiente.
En mi caso, el agente hacía web_search("kimi k3 open weight release") una y otra vez. El modelo no encontraba lo que esperaba y en vez de cambiar de estrategia, insistía. Como un perro persiguiéndose la cola.
La detección es trivial. En cada iteración comparo el tool call actual contra los dos anteriores. Si los tres son iguales, mato el loop y logueo el error.
if len(recent_calls) >= 3 and len(set(recent_calls[-3:])) == 1:
raise StuckLoopError(f"Loop detectado: {recent_calls[-1]}")
Implementé esto con un array circular de tamaño 3 en el harness de Hermes. 4 líneas de Python. Cero dependencias.
Señal 2: incertidumbre creciente en el razonamiento
Los modelos que usan reasoning traces —DeepSeek V4 Pro, Claude Opus— dejan ver sus dudas. Cuando un agente está atrapado, su razonamiento se vuelve progresivamente más errático. Frases como "quizás debería intentar...", "no estoy seguro...", "tal vez si..." aparecen con más frecuencia cada iteración.
No necesitás un detector sofisticado. Un contador simple de palabras de incertidumbre por iteración te da una señal clarísima:
uncertainty_words = ["quizás", "tal vez", "no estoy seguro", "podría", "intentaré", "no sé"]
uncertainty_count = sum(1 for w in uncertainty_words if w in reasoning_trace)
if uncertainty_count > previous_count for 3 consecutive iterations:
trigger_context_compression()
Esto viene de ACON (Adaptive Context Optimization Network), pero la implementación es mucho más simple: solo resumir contexto cuando se detecta degradación, no por token count. En mi setup atrapó un agente que intentaba parsear un JSON malformado. En vez de admitir que el formato era inválido, el modelo seguía generando hipótesis cada vez más rebuscadas.
Señal 3: progreso estancado
La más obvia, y la que más se ignora. Si definiste métricas de progreso —archivos creados, tests pasados, datos extraídos— y no avanzan durante N ciclos, el agente está muerto en vida.
Yo uso un contador de "acciones productivas": tool calls que producen salida nueva. Si en 5 iteraciones el contador no sube, fuerzo un resumen del contexto y un reinicio de estrategia.
if productive_actions == 0 for 5 consecutive iterations:
summarize_context()
restart_with_alternative_approach()
El truco está en definir qué cuenta como "productivo". Para mí: cualquier tool call cuyo resultado difiere en más de 50 caracteres del anterior. No es perfecto, pero atrapa el 90% de los casos.
Lo que no funciona
No uses timeouts globales. Un agente puede estar 10 minutos generando código perfectamente válido, o 30 segundos atrapado en un loop. El tiempo no es la métrica correcta. Las señales de comportamiento sí.
Tampoco uses límites de iteraciones fijos tipo "máximo 20 tool calls". Un agente de research puede necesitar 40 búsquedas para un tema complejo. Matarlo arbitrariamente a las 20 es perder trabajo válido.
Cómo lo integré en Hermes
Las tres señales corren como validadores en cada iteración del loop. Si cualquiera dispara, el agente recibe el error + el goal original y genera un enfoque alternativo. Si falla dos veces más, muere definitivamente y el cron me notifica por Telegram.
Costo de implementación: ~40 líneas de Python. No necesitás LangGraph, ni Temporal, ni infraestructura compleja. El harness de Mitchell Hashimoto —todo lo que atrapa, corrige y recompensa al agente— puede ser brutalmente simple si sabés qué señales buscar.
Desde que implementé esto hace dos semanas, los loops infinitos pasaron de 3-4 por semana a cero. La factura de DeepSeek bajó otro 12%. Pero lo que más valoro no es el dinero — es no tener que despertarme y encontrar 15 correos de error.
Una advertencia
El threshold de 3 tool calls idénticos no es infalible. A veces un agente necesita reintentar genuinamente: APIs que devuelven 429 (rate limit), timeouts de red, servicios externos intermitentes. En esos casos, 3 reintentos no es un bug, es comportamiento correcto.
Mi solución actual es rudimentaria: si el tool call incluye patrones de error conocidos (429, 503, timeout), permito hasta 5 reintentos. No es elegante, pero funciona.
El refinamiento por tipo de tarea —thresholds distintos para research vs. generación de código vs. web scraping— está en mi lista. Por ahora, el approach simple cubre el 90% de los casos, y con eso me alcanza.