Tus agentes se pisan el contexto: memoria determinista en la capa de datos
Meter el historial en el prompt del redactor no es memoria de sistema. Si clasificador y fetch solo ven la frase actual, el follow-up muere aunque el LLM “recuerde”.
En este artículo
Llevas meses montando un pipeline de agentes “con memoria”. El historial entra al redactor. El system prompt dice que recuerde la conversación. En la demo funciona. En producción, el usuario dice “crúzalo con el coste” o “ábreme el primero” y el sistema se hace el loco.
No es que el modelo sea tonto. Es que un agente le pasó el contexto a otro y este lo reescribió con lo suyo, o que la capa que trae los datos solo vio la frase actual. El LLM redactor “recordaba”. El sistema, no.
Esta guía no va de venderte una ventana de un millón de tokens ni de un framework mágico. Va de rediseñar la capa de datos para que el estado sea compartido, determinista y barato (sin que el LLM reescriba lo que el sistema ya sabía).
Qué problema resuelve de verdad
El síntoma que oyes del usuario es siempre el mismo: “el bot no se acuerda”. Si te lanzas al prompt del redactor, estás mirando el sitio equivocado.
En un pipeline con varias capas (enrutar, clasificar intención, traer datos, redactar), las decisiones que importan se toman antes de escribir la respuesta. ¿Esto es un follow-up? ¿De qué entidad hablamos? ¿Hay que cruzar fuentes o abrir un ítem de una lista previa? Si esas capas solo ven el mensaje suelto, da igual que el redactor reciba cincuenta turnos de historial: no hay datos que redactar, o hay datos de la entidad equivocada.
La promesa concreta de hacer esto bien:
- Los follow-ups con referencia (“ese”, “el primero”, “crúzalo”) resuelven entidad y llegan a datos sin reescribir la frase con otro LLM.
- El estado del turno anterior no lo pisa el agente de abajo con un resumen libre.
- Cuando sospeches que la capa determinista te está jodiendo la naturalidad, lo demuestras con un A/B ciego sobre trazas reales (no con fe).
Memoria cosmética vs memoria de sistema
Hay dos sitios donde la gente “pone memoria”. Solo uno cuenta.
Memoria cosmética: el historial de mensajes (o un resumen) entra al LLM que escribe la respuesta final. El modelo suena coherente. Puede aludir a lo dicho. Pero las capas de arriba ya decidieron sin ese contexto. Si el clasificador marcó el turno como charla general, el fetch ni se ejecutó. El redactor recuerda en el vacío.
Memoria de sistema: el contexto conversacional llega a cada capa que decide con él. No como prosa para que el modelo la reinterprete, sino como estado estructurado: entidad activa, lista del turno anterior, señales de follow-up, lo-que-pidió-el-cliente separado de lo-que-consta-en-el-sistema.
La lección fea: en un pipeline con LLM al final, darle memoria solo al redactor es maquillaje. El modelo recuerda; el sistema no. El contexto tiene que poder viajar de forma determinista y barata (memoria de turno más señales fuertes) sin una reescritura por LLM en el camino común.
Cuando alguien diga que el bot no se acuerda, el truco de diagnóstico es simple: traza el follow-up capa a capa y busca la primera que solo ve la frase actual. Ahí está el bug.
Dónde se rompe el follow-up (el caso del chat de informes)
Un sistema multiagente de informes “tenía memoria”: cincuenta mensajes de historial entraban al redactor. Aun así fallaban frases naturales del tipo “crúzalo con el coste” o “ábreme el primero”.
La auditoría no encontró un modelo malo. Encontró una capa de datos ciega:
- Clasificador de intención + fetch veían únicamente la frase del turno. Sin entidad del turno anterior. Sin lista previa. Sin saber que el asistente acababa de devolver una tabla.
- El clasificador podía etiquetar el follow-up como charla general. El turno nunca llegaba a traer datos. El redactor podía recitar el historial entero: no había nada que cruzar.
- El cruce de fuentes exigía la entidad nombrada en la misma frase. “Crúzalo” no nombra nada. Match fallido. Silencio o alucinación educada.
Eso es el mismo patrón que cuando un agente le pasa contexto a otro en prosa libre: el receptor no “continúa” el estado; lo regenera según su propio prompt y se carga lo anterior. Si el estado vive como texto maleable dentro del LLM, cualquier eslabón puede pisarlo.
El rediseño: estado compartido en la capa de datos
La corrección no fue “un prompt mejor” ni “más historial”. Fueron dos piezas deterministas en la capa de datos.
1. Memoria del turno anterior entra al fetch
El cruce y la resolución de referencias reciben un entity_hint (y el resto de memoria de turno) derivado del estado anterior (no de una reescritura creativa del usuario).
Reglas que importan:
- La resolución por frase siempre gana. Si el usuario nombra otra entidad, esa manda. El hint no compite ni se mezcla.
- El hint solo se usa si cero fuentes resolvieron con la frase sola. No inventas un cruce “por si acaso”.
- Referencias del tipo “el primero”, “ese”, “el de antes” se resuelven dentro del fetch, contra la lista o el resultado que el turno anterior dejó en memoria estructurada (no contra un párrafo que el redactor “cree recordar”).
En la práctica es un contrato de datos, no un monólogo del modelo:
turno_n-1.memoria = {
entidades: [...],
lista_visible: [...],
ultimo_resultado_ids: [...],
tipo_salida: "tabla_informes"
}
turno_n.fetch_input = {
frase: "crúzalo con el coste",
entity_hint: turno_n-1.memoria.entidades, # solo si frase no resuelve
refs: resolver_referencias(frase, turno_n-1.memoria.lista_visible)
}El LLM redactor sigue pudiendo ver historial si quieres tono conversacional. Pero la verdad operativa (qué entidad, qué fila, qué cruce) no depende de que él la reconstruya.
2. Pre-route determinista para follow-ups fuertes
Si el turno inmediatamente anterior del asistente dejó memoria de datos y la frase actual trae señal fuerte de follow-up (referencia, petición de cruce, apertura de ítem), se llama a la capa de datos directo, sin pasar por el clasificador.
Qué ganas:
- Te ahorras una llamada LLM de routing en el camino caliente.
- Blindas el follow-up contra un “general” mal clasificado que mataba el fetch.
Qué acota los falsos positivos:
- Adyacencia: solo el turno inmediatamente anterior, no “algo dicho hace ocho mensajes”.
- Si el pre-route no produce contexto útil, el flujo normal (clasificador + resto) sigue íntegro. No es un atajo suicida; es un fast-path con puerta de atrás.
Patrón general: memoria determinista y con gating en el camino común; el LLM generaliza donde hace falta, no reescribe el estado en cada eslabón.
Lo que se descartó (y por qué)
El arreglo “obvio” que sale en todos los hilos es query rewriting: un LLM reescribe la frase del usuario con el historial (“crúzalo con el coste” → “cruza el informe de ventas Q3 con la tabla de costes”) y esa frase expandida alimenta clasificador y fetch.
Se descartó por dos motivos duros:
- +1 llamada siempre, también cuando la frase ya venía completa.
- No-determinismo aguas arriba de los matchers. Los detectores y resolutores dejan de ver lo que el usuario escribió; ven una paráfrasis. Cuando falle (y fallará), depuras un fantasma.
Seamos justos: el query rewriting tiene su sitio cuando el dominio es tan abierto que no puedes predefinir señales fuertes ni mantener un schema de memoria de turno fiable. Si tu producto es un chat genérico donde el usuario salta de tema sin avisar y no hay entidades estructuradas que persistan, una reescritura con LLM puede ser la única forma de no perder el hilo. El coste extra y la opacidad en depuración se asumen porque la alternativa determinista directamente no aplica. Pero si tu dominio sí tiene entidades, listas y un flujo de trabajo acotado, meter un LLM a reescribir antes de cada decisión es comprar complejidad que no necesitas.
Hubo un fix colateral de la misma auditoría que parece tonto hasta que te quema una tarde: las pistas casaban con casefold, que no quita tildes. “Crúzalo” no disparaba el detector de cruce. La memoria de sistema también es normalización aburrida y tests; no solo embeddings bonitos.
Ventana de contexto, context rot y compaction: qué cubren y qué no
Aquí es donde hay que contrastar lo que dicen las fuentes con el fallo de pisarse el estado (sin mezclar churras con merinas).
Según la documentación de Anthropic sobre context windows, la estrategia principal para gestionar contexto en conversaciones largas y flujos agenticos es la server-side compaction. Sin ella, el contexto crece sin techo, aparece degradación y baja la fiabilidad del agente en producción. El fenómeno tiene nombre: context rot (la precisión y el recall se degradan conforme sube el recuento de tokens). Curar qué hay en contexto importa tanto como la capacidad bruta.
Esa misma documentación deja claro qué cuenta dentro de la ventana: system prompt, mensajes, resultados de herramientas, imágenes, documentos, definiciones de tools… y la propia salida del modelo, incluido el extended thinking. En modelos Claude recientes (Opus 4.8/4.7/4.6, Sonnet 5/4.6) hay ventanas de 1M tokens; otros como Sonnet 4.5 se quedan en 200k. Con extended thinking, los bloques de pensamiento previos se conservan por defecto en los modelos nuevos y suman tokens; se pueden strippear para ahorrar, pero en ciclos de tool use hay que devolverlos con los resultados de herramientas.
Por el lado de LangChain, los Deep Agents anuncian automatic context compression como feature batteries-included para agentes frente a los límites de ventana. La documentación pública no detalla el mecanismo interno ni la configuración fina: coincide con Anthropic en el problema (el contexto se hincha y hay que gestionarlo); no te entrega el mismo nivel de detalle operativo sobre cómo comprime ni cuándo.
Dónde resuelven tensión y dónde no. Compaction, ventanas grandes y compresión automática atacan el presupuesto de tokens y el context rot: menos basura acumulada, menos degradación por longitud, menos hostias contra el techo de la ventana. Eso es necesario en producción y está documentado como tal.
Lo que no puedes afirmar con ese material es que compaction o compression, solas, impidan que un agente pise el estado que otro dejó, ni que eviten el follow-up ciego del clasificador/fetch. Son capas distintas:
| Problema | Lo que lo ataca | Lo que no lo arregla |
|---|---|---|
| Contexto infinito / rot por longitud | Compaction server-side, curar qué entra, elegir ventana (Anthropic); compression en agentes (LangChain, sin detalle interno) | Un entity_hint bonito |
| Follow-up sin entidad / fetch ciego | Memoria de turno en la capa de datos + pre-route | Meter más historial solo al redactor |
| Un agente reescribe el estado del anterior | Estado compartido determinista fuera del prosa-LLM | Confiar en que el modelo “respete” el contexto en lenguaje natural |
| Sospecha de que las reglas te están matando | A/B ciego sobre trazas | Rearquitecturar por corazonada |
Si tu dolor es tokens y degradación por longitud, lee en serio la guía de ventanas de contexto y deja de empujar basura al prompt. Si tu dolor es agentes que se pisan o follow-ups muertos, la compaction no es el rediseño que buscas (aunque conviva con él). En la misma línea de por qué el contexto se degrada en producción y qué implica “olvidar” bien, encaja lo que ya cubrimos sobre por qué tu LLM en producción necesita dormir.
Antes de rearquitecturar: A/B a ciegas con trazas reales
Rearquitecturar por sospecha es apostar el trimestre a una intuición. En un agente de email de producción pasó exactamente eso: conforme crecía el pipeline determinista (~14k líneas de reglas que “corregían” al LLM), el sistema se volvió tonto y menos natural. Una auditoría de 28 correos reales sin cribar dio un 50% de fallos.
La sospecha: la capa determinista re-derivaba y pisaba datos que el LLM ya extraía bien. En lugar de reescribir producción a ciegas, se montó un A/B offline sobre trazas reales:
- A = la respuesta real del pipeline vigente (sacada de las trazas).
- B = prototipo fino: modelo capaz + dossier grounded que separa lo-que-pide-el-cliente de lo-que-consta-en-el-sistema + 5 invariantes duros + post-check determinista.
- Juez LLM ciego al origen, con orden alternado para matar sesgo de posición, puntuando ejes separados (fidelidad, naturalidad, seguridad; 0-2 cada uno) en vez de un “¿cuál es mejor?” global.
Resultado v1: B ganó 24/28 (media 5.57 vs 3.50 sobre 6). Las 4 derrotas de B no venían del manejo de datos: venían de falta de conocimiento de negocio. Se inyectó esa capa desde las fuentes reales del repo (no inventada), se afinó un invariante, y la v3 dio 6.00/6 en los tres ejes, 25/28 y 0 violaciones (sin un solo correo donde B fuera peor). Con ese dato se aprobó la rearquitectura y se retiró el chorizo de reglas.
Dos hallazgos que se quedan en la pared:
- La re-derivación determinista pierde contra un modelo capaz con contexto grounded (dossier, no prosa libre que el siguiente agente reinterpreta).
- Sobre-ajustar reglas a N casos de feedback genera conflictos que crecen mal: cada regla nueva pelea con las anteriores.
Simetría o te estás mintiendo
Un A/B de arquitecturas tiene que diferir solo en la arquitectura, no en el cariño del prompt. Si auditas la rama que falla, le metes un arreglo al system prompt y la otra no lo recibe, ya no comparas arquitecturas: comparas mimos.
La simetría no se “vigila con buena voluntad”. Se impone en código: texto compartido en una sola constante y un test que rompe si divergen. Y hay que trazar la frontera: lo inherente a una arquitectura (por ejemplo, que el híbrido retome una conversación ya empezada) es variable experimental; un consejo extra en el prompt, no.
Si montas evaluación en serio con datos tuyos en lugar de leaderboards de postureo, el enfoque encaja con cómo montar un pipeline de evaluación de LLMs con datos propios.
Cómo montarlo sin convertirlo en religión
Guía operativa, no dogma de PowerPoint.
Requisitos mínimos
- Un pipeline con más de una capa que decide (router/clasificador, retrieval/fetch, tools, redactor). Si solo tienes un chat mono-turno, esto es overkill.
- Trazas reales con entrada, salida por capa y decisión de routing. Sin trazas no hay auditoría ni A/B.
- Sitio donde persistir memoria de turno estructurada (Redis, tabla, store del orquestador: lo que sea, pero schema fijo, no un párrafo).
- Criterio de qué cuenta como señal fuerte de follow-up en tu dominio (referencias, verbos de cruce, “el primero”, IDs cortos, etc.).
Paso a paso del camino feliz
Define el schema de memoria de turno
Entidades activas, IDs de resultados mostrados, tipo de salida, flags de “hay datos vivos”. Nada de “resumen libre del agente”. Campos tipados. Versiona el schema.Haz que el fetch acepte frase + memoria
Resolución por frase primero. Hint solo si cero hits. Referencias se resuelven contralista_visible/ultimo_resultado_ids, no contra el monólogo del modelo.Añade pre-route con gating
Condición: memoria de datos del turno anterior + señal fuerte en la frase. Acción: fetch directo. Fallback: pipeline normal intacto si no hay contexto útil.Separa dossier de redacción
En el camino hacia el LLM final, distingue lo-que-pide-el-cliente de lo-que-consta-en-el-sistema. El redactor inventa tono, no hechos ni entidad.Instrumenta
Loguea por turno: ¿entró por pre-route? ¿se usó entity_hint? ¿la frase resolvió sola? ¿el clasificador habría dicho “general”? Sin esto vuelves a mirar el prompt a ciegas a los tres meses.Cuando la naturalidad se caiga, A/B offline
Mismo set de trazas, juez ciego, ejes separados, simetría de prompts impuesta por test. No rearquitectures por una reunión donde “se siente peor”.
Invariantes que merecen un post-check
No hace falta un motor de 14k líneas. Hace falta un puñado de invariantes que fallen ruidoso:
- No mezclar entidades del hint con entidades nombradas en la frase.
- No llamar fetch “de follow-up” si no hay memoria adyacente.
- No dejar que un agente de abajo emita un nuevo “estado canónico” en prosa que sustituya el store.
- Si hay tool results y extended thinking en el stack, contabilizar tokens con la misma paranoia que la doc de Anthropic: todo suma, incluido lo que el modelo “pensó” antes.
Errores comunes (donde la caga todo el mundo)
- Historial al redactor y a dormir. Es el default cómodo. Es memoria cosmética. El follow-up muere arriba.
- Query rewrite con LLM como primer recurso. Más coste, más no-determinismo, peor depuración. Úsalo solo si el camino determinista se queda corto y lo mides.
- Hint que pisa la frase. Si el usuario cambió de entidad y tu hint “ayuda”, has inventado un bug de confusión de identidad.
- Memoria eterna sin adyacencia. Todo el hilo como señal de follow-up = falsos positivos y cruces alucinados con lo de hace veinte turnos.
- Reglas que re-derivan lo que el modelo ya extrajo bien. El caso del email: 14k líneas pelearon entre sí. El A/B demostró que un modelo capaz con dossier grounded + pocos invariantes ganaba de calle.
- A/B contaminado. Arreglas solo la rama B mientras miras los fallos y te autocongratulas. Simetría o fraude inconsciente.
- Confundir compaction con arquitectura de estado. Compactar contexto (Anthropic) o “automatic context compression” (LangChain Deep Agents, sin detalle público del cómo) no diseña por ti un store compartido ni un pre-route. Son piezas del presupuesto de tokens, no del contrato entre agentes.
- Normalización cutre.
casefoldsin quitar tildes, IDs con espacios rotos, “el primero” cuando la lista ya no está en memoria. Los bugs de producción son esto, no el paper de turnos.
Tips de quien ya se quemó
- Cuando el usuario diga que no se acuerda, no abras el prompt del redactor primero. Abre la traza y busca la primera capa ciega.
- Prefiere señales fuertes + adyacencia a “inteligencia” de reescritura en el 90% de follow-ups. El LLM al final para redactar; el estado, fuera.
- Si vas a tener varios agentes, el bus de estado es un schema versionado, no un mensaje más en el chat interno. El chat interno es un canal de coordinación; no es tu base de verdad.
- Mide tokens y mira context rot en conversaciones largas (compaction, qué strippeas del thinking, qué tools devuelven novelas). Pero no uses esa métrica para autoengañarte: un pipeline con 1M de ventana y fetch ciego sigue siendo un pipeline con fetch ciego.
- Retira reglas cuando el A/B demuestre que sobran. El coraje no es añadir guardrails; es borrar los que pelean entre sí.
Cierre
El día que un agente te pise el contexto no es un misterio de “alineamiento”. Es un fallo de capa de datos: estado maleable donde hacía falta estado compartido, y un LLM reescribiendo lo que el sistema ya sabía.
Pon la memoria donde se decide. Hazla determinista, barata y con gating. Valida con trazas y un A/B que no haga trampas. Y deja que la compaction se ocupe de los tokens (no de fingir que tienes arquitectura).
Preguntas frecuentes
¿Por qué el bot “no se acuerda” si le paso todo el historial al LLM?
Porque el historial en el redactor es memoria cosmética: el modelo puede sonar coherente, pero clasificador y fetch suelen decidir solo con la frase actual. Si el follow-up no nombra la entidad o se clasifica como charla general, no hay datos que redactar. La memoria tiene que llegar a cada capa que decide, no solo al que escribe.
¿Qué es el context rot y cómo se relaciona con agentes en producción?
Es la degradación de precisión y recall cuando sube el número de tokens en contexto. Anthropic lo documenta junto a la compaction server-side como estrategia principal en conversaciones largas y flujos agenticos: sin gestionar qué hay en ventana, el contexto crece sin techo y baja la fiabilidad. No sustituye un estado compartido entre agentes; ataca longitud y ruido, no el pisado de entidad.
¿Debo usar query rewriting con LLM para los follow-ups?
No como primer recurso en el camino común: suma una llamada siempre e introduce no-determinismo antes de matchers y fetch. Prioriza memoria de turno estructurada, resolución de referencias en el fetch y pre-route determinista con señales fuertes. Reserva la reescritura para casos medidos donde el camino determinista se quede corto.
¿Compaction o ventanas de 1M tokens evitan que los agentes se pisen el contexto?
No con lo que documentan las fuentes generales: compaction y ventanas grandes (p. ej. 1M en ciertos Claude frente a 200k en otros) gestionan presupuesto de tokens y context rot, y LangChain menciona compression automática en Deep Agents sin detallar el mecanismo. Evitar que un agente sobreescriba el estado de otro exige memoria compartida determinista en la capa de datos, no solo más contexto o más compresión.