Por qué tu agente de AI sigue perdiendo su memoria (y cómo lo solucionamos)
Tu agente de AI acaba de tener una conversación de 30 minutos con un usuario. Discutieron requisitos del proyecto, compartieron preferencias, tomaron decisiones. Luego, el usuario escribe /new para comenzar una sesión nueva.
El agente intenta consolidar esa conversación en la memoria a largo plazo. La llamada al LLM falla. Rate limit. Timeout. O el modelo devuelve texto en lugar de llamar a la tool requerida.
La memoria ha desaparecido. Treinta minutos de contexto, evaporados.
Esto sucede más a menudo de lo que piensas. Lo rastreamos en nuestras instancias de LemonClaw: la consolidación de memoria tenía una tasa de fallo de aproximadamente el 15% en cualquier modelo individual. Para una funcionalidad que se supone que es infraestructura invisible, eso es inaceptable.
Cómo manejan esto otros frameworks (no lo hacen)
La mayoría de los frameworks de agentes de AI tratan la consolidación de memoria como una simple llamada de LLM. Si funciona, genial. Si no, la memoria se pierde.
OpenClaw, el framework de agentes open-source más popular, utiliza el mismo modelo para la consolidación que para la conversación. Una llamada a Claude Sonnet que cuesta $0.003 y tarda más de 8 segundos, solo para resumir un chat que el usuario nunca verá. Cuando esa llamada falla (rate limit, timeout, error del modelo), el framework registra una advertencia y continúa. El contexto del usuario se ha ido.
nanobot, otro framework popular, tiene la misma arquitectura. Un modelo, un intento, sin fallback. La función de consolidación ni siquiera tiene un timeout. Un upstream lento (los errores 524 de Cloudflare son comunes) bloquea toda la sesión hasta que la conexión se cae.
Ninguno de los frameworks separa la consolidación del modelo principal. Ninguno tiene lógica de fallback para operaciones de memoria. Ninguno distingue entre "la llamada a la API falló" y "la llamada a la API tuvo éxito pero el modelo no hizo lo que pedimos".
Estos no son casos aislados. Con tasas de fallo del 15% en cualquier modelo individual, un framework que ejecute 100 consolidaciones por día pierde la memoria en 15 de ellas. En una semana, eso son 105 conversaciones donde el agente lo olvida todo.
El problema es más profundo que la lógica de reintento
La solución obvia es el reintento con exponential backoff. Nosotros teníamos eso. Maneja bien los errores HTTP transitorios:
# Retry loop: 1s → 2s → 4s backoff
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
Esto captura los 429 y los problemas de red momentáneos. Pero dos modos de fallo se escapan:
Modo de fallo 1: El modelo no puede realizar tool calling. Algunos modelos, especialmente los más pequeños que se ejecutan en motores de inferencia rápidos, ocasionalmente no logran generar llamadas a funciones válidas en prompts complejos. La API devuelve un 200 con un ServiceUnavailableError envuelto dentro de un MidStreamFallbackError. Tu lógica de reintento ve una excepción, reintenta el mismo modelo y obtiene el mismo error.
Modo de fallo 2: El modelo "tiene éxito" pero no llama a la tool. El LLM devuelve una respuesta perfectamente válida. HTTP 200. Sin errores. Pero en lugar de llamar a save_memory con datos estructurados, escribe un resumen en texto plano. Tu motor de reintentos considera esto un éxito. La función de consolidación busca llamadas a tools, no encuentra ninguna y se rinde.
El segundo modo de fallo es el insidioso. La capa de transporte cree que todo funcionó. La capa de negocio sabe que no fue así. Ninguna cantidad de reintentos a nivel HTTP arreglará un modelo que no entiende tu esquema de tool.
Arquitectura de Fallback de doble capa
Solucionamos esto con dos bucles de fallback independientes que operan a diferentes niveles:
Usuario envía /new
│
▼
consolidate() ─── Fallback de Capa de Negocio
│ "¿El modelo llamó a save_memory?"
│ No → intentar siguiente modelo en la cadena
│
▼
_chat_with_retry() ─── Fallback de Capa de Transporte
│ Errores HTTP → exponential backoff
│ Reintentos agotados → recorrer cadena de fallback
│
▼
Cadena de fallback MODEL_MAP:
llama-3.3-70b ─$0.59/M─→ qwen3-32b ─$0.29/M─→ llama-4-scout ─$0.11/M─→ gpt-4.1-mini ─→ claude-haiku
(394 TPS) (662 TPS) (594 TPS) (confiable) (último recurso)
La Capa 1 maneja fallos de transporte. La Capa 2 maneja fallos de lógica de negocio. La cadena de fallback se comparte entre ambas capas, definida una sola vez en un catálogo central.
Este es un enfoque fundamentalmente diferente al de reintentar-el-mismo-modelo. Cuando un modelo falla al llamar a una tool, reintentarlo con el mismo prompt rara vez ayuda. Cambiar a un modelo diferente con pesos distintos y un comportamiento de tool calling diferente, sí lo hace.
El Catálogo de Modelos: Una única fuente de verdad
Cada modelo en nuestro catálogo tiene un campo opcional fallback que apunta al siguiente modelo a intentar:
@dataclass(frozen=True)
class ModelEntry:
id: str
label: str
tier: str
description: str
fallback: str | None = None
hidden: bool = False # Oculto de la lista /model para el usuario
MODEL_CATALOG = [
# Modelos visibles para el usuario (16 modelos entre los que los usuarios pueden cambiar)
ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
"Recomendado", fallback="claude-sonnet-4-5"),
ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
"Tool calling estable", fallback="claude-haiku-4-5"),
# Modelos de consolidación ocultos (solo para uso interno)
ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
"394 TPS", fallback="qwen3-32b", hidden=True),
ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
"662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
# ...
]
El flag hidden=True mantiene los modelos internos fuera del comando /model orientado al usuario, mientras siguen participando en las cadenas de fallback. Los usuarios ven 16 modelos entre los que pueden elegir. El sistema utiliza 19. Los tres modelos ocultos existen únicamente para tareas en segundo plano como la consolidación de memoria, donde la velocidad y el costo importan más que la calidad conversacional.
Este catálogo es la única fuente de verdad para todo el enrutamiento de modelos. Añadir un nuevo modelo a la cadena de fallback significa añadir una línea. No hay archivos de configuración que sincronizar, ni variables de entorno que actualizar, ni scripts de despliegue que modificar.
Capa de Transporte: Fallback encadenado con detección de ciclos
El motor de reintentos recorre la cadena de fallback utilizando un conjunto de visitados para evitar bucles infinitos:
async def _chat_with_retry(self, kwargs, original_model):
# Fase 1: Exponential backoff en el modelo primario
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError, APIError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
except AuthenticationError:
return LLMResponse(content="API key invalid.", finish_reason="error")
# Fase 2: Recorrer la cadena de fallback
visited = {original_model}
current = original_model
while True:
entry = MODEL_MAP.get(current)
if not entry or not entry.fallback or entry.fallback in visited:
break
current = entry.fallback
visited.add(current)
# Resolver el gateway correcto para este modelo
gw = self._resolve_gateway_for_model(current)
resolved = self._resolve_model(current, gateway=gw)
fb_kwargs = {**kwargs, "model": resolved}
# Corregir api_base para el protocolo del modelo de destino
if gw and gw.default_api_base:
fb_kwargs["api_base"] = gw.default_api_base
try:
response = await acompletion(**fb_kwargs)
return await self._collect_stream(response)
except Exception:
continue # Intentar el siguiente en la cadena
return LLMResponse(content="Service unavailable.", finish_reason="error")
El conjunto visited es crítico. Sin él, una cadena como A→B→A entraría en un bucle infinito. Con él, el motor intenta cada modelo exactamente una vez.
La resolución del gateway también es importante. Diferentes modelos necesitan diferentes formatos de API. Los modelos de Claude se enrutan a través de un gateway con formato Anthropic (sin sufijo /v1). Los modelos GPT se enrutan a través de un gateway compatible con OpenAI (con /v1). Los modelos de Groq utilizan otro endpoint distinto. El motor de fallback resuelve el gateway correcto para cada modelo en la cadena, evitando desajustes de protocolo como enviar peticiones de Anthropic a un endpoint de OpenAI.
Este es un detalle que la mayoría de los frameworks ignoran por completo. Asumen que todos los modelos hablan el mismo protocolo. En producción, con 19 modelos a través de 4 formatos de API diferentes, esa suposición se rompe inmediatamente.
Capa de Negocio: Verificación de Tool Call
La función de consolidación añade su propio bucle de fallback por encima:
async def consolidate(self, session, provider, model, **kwargs):
visited = set()
current_model = model
while current_model and current_model not in visited and len(visited) <= 3:
visited.add(current_model)
response = await asyncio.wait_for(
provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
timeout=30,
)
if response.has_tool_calls:
# Éxito: extraer y guardar memoria
args = response.tool_calls[0].arguments
self.write_long_term(args["memory_update"])
self.append_history(args["history_entry"])
return True
# El modelo no llamó a la tool — intentar el siguiente en la cadena
entry = MODEL_MAP.get(current_model)
next_model = entry.fallback if entry else None
if next_model and next_model not in visited:
current_model = next_model
continue
return False # No hay más fallbacks
return False
Esto captura el caso en el que _chat_with_retry devuelve una respuesta exitosa (HTTP 200, contenido válido) pero el modelo no utilizó la tool. La función de consolidación comprueba has_tool_calls y, si falta, pasa al siguiente modelo en la cadena.
El envoltorio de timeout (asyncio.wait_for) también activa el fallback. Si un modelo tarda más de 30 segundos (común con errores 524 de Cloudflare en upstreams lentos), la función captura el TimeoutError e intenta el siguiente modelo en lugar de bloquear la sesión del usuario indefinidamente.
Por qué Groq para la consolidación
La consolidación de memoria es una tarea en segundo plano. El usuario no ve el resultado. Solo necesita que funcione. Esto lo convierte en un candidato perfecto para modelos rápidos y baratos.
La mayoría de los frameworks utilizan el mismo modelo costoso para todo. Si estás usando Claude Sonnet para la conversación, también estás usando Claude Sonnet para la consolidación de memoria. Eso son $3/M de tokens de entrada y más de 8 segundos por consolidación, para una tarea que produce un resultado que ningún humano lee jamás.
Desacoplamos completamente la consolidación del modelo de conversación. La conversación utiliza cualquier modelo que el usuario haya seleccionado. La consolidación utiliza una cadena dedicada de modelos alojados en Groq:
| Modelo | Velocidad | Costo de Entrada | Costo de Salida |
|---|---|---|---|
| llama-3.3-70b-versatile | 394 TPS | $0.59/M | $0.79/M |
| qwen3-32b | 662 TPS | $0.29/M | $0.59/M |
| llama-4-scout-17b-16e | 594 TPS | $0.11/M | $0.34/M |
| gpt-4.1-mini (anterior) | ~150 TPS | $0.40/M | $1.60/M |
El modelo primario (llama-3.3-70b) consolida una sesión de 60 mensajes en ~5 segundos. El valor predeterminado anterior (gpt-4.1-mini) tardaba más de 8 segundos. El costo por consolidación bajó de ~$0.003 a ~$0.001.
La contrapartida: los modelos de Groq tienen un tool calling menos confiable en prompts complejos. Por eso precisamente existe el fallback de doble capa. Cuando llama-3.3-70b falla al llamar a la tool, qwen3-32b toma el relevo. Si eso también falla, lo intenta llama-4-scout. Si los tres modelos de Groq fallan, gpt-4.1-mini lo maneja con una confiabilidad de tool calling cercana al 100%.
En producción, vemos que el modelo primario tiene éxito el ~85% de las veces. La cadena llega a gpt-4.1-mini en menos del 2% de las consolidaciones. Tasa de fallo total: efectivamente cero.
Resultados en Producción
Desplegamos esto en dos instancias de LemonClaw y lo probamos con conversaciones reales de Telegram.
Primer despliegue (solo fallback de una capa):
Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."
La capa de transporte capturó el primer fallo y activó el fallback. Pero qwen3-32b devolvió texto sin llamar a la tool. El fallback de una sola capa no pudo manejar esto. Este es exactamente el escenario en el que cualquier otro framework perdería la memoria silenciosamente.
Segundo despliegue (fallback de doble capa):
Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining
Mismo modelo, mismo volumen de mensajes. Esta vez funcionó al primer intento. La naturaleza intermitente del fallo de tool calling es exactamente la razón por la que se necesita una cadena de fallback en lugar de un único modelo de respaldo.
Cuando el modelo primario falla, la cadena lo captura:
llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done
Cuatro modelos intentados, memoria guardada. El usuario ve "Nueva sesión iniciada." y no tiene idea de que nada de esto ocurrió.
La Brecha Arquitectónica
El sistema de memoria de LemonClaw frente a las alternativas, característica por característica:
| Capacidad | Framework de Agente de AI Típico | LemonClaw |
|---|---|---|
| Modelo de consolidación | El mismo que el de conversación (caro, lento) | Cadena de modelos independiente, acelerada por Groq |
| Manejo de fallos | Registra advertencia, pierde memoria | Fallback de doble capa, 5 modelos de profundidad |
| Fallback de transporte | Reintenta el mismo modelo 3 veces | Fallback encadenado a través de diferentes modelos |
| Fallback de lógica de negocio | Ninguno | Verificación de tool call + cambio de modelo |
| Protección de timeout | Ninguna (Cloudflare 524 bloquea la sesión) | asyncio.wait_for(timeout=30) + fallback |
| Truncado de sesión | Ninguno (el contexto crece infinitamente) | Trunca mensajes antiguos tras la consolidación |
| Búsqueda en el historial | Ninguna | Ventana rodante en HISTORY.md, buscable con grep |
| Modelos internos | No soportado | hidden=True para modelos solo del sistema |
| Prevención de ciclos | No necesaria (no hay cadenas) | Conjunto visited evita bucles A→B→A |
| Resolución de gateway | Se asume un único formato de API | Gateway por modelo con detección de protocolo |
Cada fila de esta tabla representa un fallo en producción que experimentamos nosotros mismos o que observamos en los rastreadores de problemas de otros frameworks. El fallback de doble capa, el catálogo de modelos ocultos, la resolución de gateway por modelo, el fallback activado por timeout: nada de esto existe en OpenClaw, nanobot ni en ningún otro framework de agentes open-source que hayamos examinado.
Lo que aprendimos
"La solicitud tuvo éxito" no es "la tarea tuvo éxito". Los motores de reintento genéricos operan a nivel HTTP. No pueden saber que una respuesta 200 con JSON válido es en realidad un fallo porque el modelo no utilizó la tool que pediste. Las operaciones críticas para el negocio necesitan sus propios criterios de éxito y su propia lógica de fallback.
Los modelos pequeños fallan de forma diferente a los modelos grandes. Los modelos grandes (GPT-4.1, Claude Sonnet) casi siempre llaman a las tools cuando se les pide. Los modelos pequeños en motores de inferencia rápidos a veces generan respuestas con apariencia válida que ignoran por completo el esquema de la tool. Esto no es un bug que se pueda arreglar con prompt engineering. Es una brecha de capacidad que requiere mitigación arquitectónica.
Prueba con datos de producción, no con datos sintéticos. Nuestra prueba inicial con 6 mensajes sintéticos pasó en todos los modelos. La sesión real de 60 mensajes con historial de llamadas a tools, marcas de tiempo e idiomas mixtos falló en dos de los tres modelos de Groq. La complejidad de los datos reales expone modos de fallo que los datos de prueba limpios nunca mostrarán.
LemonClaw es un framework de agentes de AI open-source con enrutamiento multimodelo integrado, memoria persistente e integraciones con más de 10 plataformas de chat. Todo el sistema de fallback de doble capa descrito aquí se incluye en la versión open-source. Ejecútalo en tu propio servidor: github.com/hedging8563/lemonclaw
¿Necesitas más de 300 modelos de AI a través de una sola API key? lemondata.cc proporciona acceso unificado a OpenAI, Anthropic, Google, DeepSeek, Groq y más.
