Módulo 3: Memoria: el agente que recuerda
7. Cuándo olvidar: resumir y reiniciar el contexto
Descripción
Al terminar esta lección vas a poder armar, dentro de un workflow de n8n, el mecanismo que compacta el historial de una conversación antes de que se salga de la ventana de contexto —sin perder los datos que importan—, y vas a poder decidir, con un criterio concreto y no por intuición, cuándo conviene resumir esa historia y cuándo conviene reiniciarla por completo en vez de arrastrarla.
Esto importa en cualquier agente que sostenga conversaciones largas de verdad: un ticket de soporte técnico que se extiende varios días, una negociación comercial que va y viene por WhatsApp, un asistente de RR. HH. que acompaña todo un proceso de onboarding. En esos casos no es una posibilidad remota que la conversación crezca más allá de lo que cabe en la ventana — es lo esperable. Y la solución ingenua de subir el contextWindowLength a un número enorme no resuelve nada: ya viste en la lección anterior que más historial crudo acumulado no es gratis, degrada la propia precisión del modelo. La pregunta no es "qué tan grande hago la ventana", es "qué hago con lo que se sale de ella".
Conexión con el módulo: en la lección 6 viste los síntomas del context drift y las estrategias generales para mitigarlo, a grandes rasgos. Esta lección toma una de esas estrategias —resumir— y la construye de punta a punta dentro de n8n: qué nodos usar, en qué orden, con qué prompt, y con qué criterio decidir si conviene resumir o si conviene tirar el historial completo y arrancar de cero. No vuelve a explicar por qué ocurre el drift —eso ya lo viste—; tampoco arma todavía el agente completo con memoria persistente por usuario, eso es el mini-proyecto de la lección 8, que va a usar exactamente lo que construyas acá como una pieza más del agente final.
Compactar la historia: resumir antes de que la ventana la descarte
Piensa en alguien que toma notas de una reunión que ya lleva dos horas y va para largo. Transcribir palabra por palabra es imposible —y tampoco serviría de mucho—, así que cada cierto tiempo esa persona hace una pausa, relee lo último que anotó en detalle y lo condensa en tres o cuatro líneas: quién dijo qué, qué se decidió, qué quedó pendiente. Después sigue anotando el resto de la reunión con el mismo nivel de detalle de siempre. El resultado no es una transcripción completa ni un resumen de una sola línea — es una mezcla: lo viejo, condensado; lo reciente, intacto.
Anthropic nombra esta técnica compaction en su guía de context engineering para agentes, y la define así: "Compaction is the practice of taking a conversation nearing the context window limit, summarizing its contents, and reinitiating a new context window with the summary" — la práctica de tomar una conversación que se acerca al límite de la ventana de contexto, resumir su contenido, y reiniciar una ventana de contexto nueva con ese resumen.
Ya sabes, desde la lección 3, que contextWindowLength no borra nada por sí solo en un backend persistente como Postgres Chat Memory: la tabla sigue guardando cada turno para siempre. Lo que contextWindowLength decide es cuántos de los turnos más recientes se reconstruyen en el arreglo de mensajes que recibe el modelo en cada llamada. Todo turno más viejo que esa ventana sigue, técnicamente, ahí, en la base de datos — pero deja de ser algo que el modelo pueda ver, que para efectos prácticos es igual a que nunca hubiera existido. Compactar es la técnica que evita esa pérdida silenciosa: antes de que un bloque de turnos se salga de la ventana, lo condensas en un solo mensaje —normalmente de tipo System— que sí entra en la ventana, y que lleva adentro los datos que importaban de esos turnos.
Ejemplo trabajado
Vas a montar esto sobre un agente de soporte técnico de HostNimbus (hosting y dominios), con Postgres Chat Memory conectada a ai_memory, sessionKey = {{ $json.ticketId }} y contextWindowLength = 10. El ticket #4821 es una migración de dominio (hostnimbus-demo.com) que ya lleva 16 turnos de troubleshooting cuando decides compactar.
Paso 1 — el disparador. Después de cada respuesta del agente, un nodo Chat Memory Manager (operación Get Many Messages, con Simplify Output activado) mide cuántos turnos hay guardados para ese sessionKey. Un IF compara ese conteo contra un umbral con margen — no el mismo número que contextWindowLength, sino uno más alto, para no compactar en cada turno:
# Chat Memory Manager — Operation: Get Many Messages
# Simplify Output: true
memory.sessionKey = "{{ $json.ticketId }}" # 4821
# IF — dispara la rama de compactación solo con margen de sobra
condition: {{ $json.messages.length }} >= 16 # bien por encima de contextWindowLength = 10
Paso 2 — separar lo viejo de lo reciente. Con un nodo Code partes el arreglo: los primeros 10 turnos van a resumirse; los últimos 6 se conservan intactos, para que el agente no pierda precisión en lo más reciente de la conversación.
// Code node — separa el historial guardado en dos partes:
// lo que se va a resumir y lo que se conserva intacto
const messages = $input.first().json.messages;
const keepRaw = 6; // últimos N turnos que se preservan tal cual
return [
{
json: {
oldMessages: messages.slice(0, messages.length - keepRaw),
recentMessages: messages.slice(messages.length - keepRaw),
},
},
];
Paso 3 — el resumen. Un nodo Basic LLM Chain recibe oldMessages y produce un párrafo compacto, con un prompt que dice explícitamente qué preservar y prohíbe inventar:
# Basic LLM Chain — Prompt
Resume la siguiente conversación de soporte técnico en un solo párrafo.
Preserva explícitamente: 1) cualquier identificador que haya dado el
cliente (dominio, número de ticket, cuenta); 2) los pasos que ya se
probaron y su resultado; 3) el estado actual del problema y qué queda
pendiente. No agregues ningún dato que no esté explícito en el texto
de abajo — si algo no se dijo, no lo inventes.
Conversación:
{{ $json.oldMessages }}
Paso 4 — reemplazar. Dos llamadas al Chat Memory Manager, en orden. Primero, Insert Messages con el modo puesto en Override All Messages (no en Insert Messages), tipo System, y el resumen como contenido — esto borra los 16 turnos guardados y deja un solo mensaje. Después, un segundo Chat Memory Manager en Insert Messages sin override, que vuelve a insertar los 6 turnos de recentMessages tal como estaban, preservando su tipo original (User o AI), para que queden después del resumen.
# Chat Memory Manager #1 — Operation: Insert Messages
# Modo: Override All Messages
type: "System"
message: "{{ $json.summaryText }}"
# Chat Memory Manager #2 — Operation: Insert Messages
# Modo: Insert Messages (agrega, no reemplaza)
# — uno por cada turno de recentMessages, con su type original (User / AI)
Qué esperar. Antes de esta rutina, la tabla de Postgres para el ticket #4821 tenía 16 turnos guardados. Después, tiene 7 mensajes: 1 resumen + los 6 turnos más recientes intactos. La próxima vez que el cliente escriba, el arreglo que arma Postgres Chat Memory para el modelo se ve así:
messages = [
{ role: "system", content: "Eres el asistente de soporte técnico de
HostNimbus..." }, # System Message fijo del agente
{ role: "system", content: "Resumen de la conversación hasta el turno
10: el cliente (ticket #4821) migró el
dominio hostnimbus-demo.com hace 2 días.
Ya cambió los NS a ns1/ns2.hostnimbus.com.
La propagación completó, pero el
subdominio 'app.' sigue devolviendo
NXDOMAIN. Se descartó caché del navegador
y DNS local del cliente. Pendiente:
verificar si existe el registro A del
subdominio en el panel de zona." },
{ role: "user", content: "..." }, # turno 11, preservado tal cual
{ role: "assistant", content: "..." },
# ... turnos 12 a 16, intactos ...
{ role: "user", content: "¿ya revisaste lo que te dije al
principio sobre cuándo cambié los
NS?" } # turno 23, el mensaje nuevo
]
El dato de "hace 2 días" no está en ningún turno crudo de los últimos 10 — vino del turno 3, que ya se compactó. Pero sigue disponible, porque quedó escrito en el resumen. El agente puede responder: "Sí, cambiaste los NS hace 2 días, el 19 de julio. Vamos a revisar si el registro A del subdominio existe en tu panel de zona." Sin la compactación, ese dato simplemente no estaría en ningún mensaje que el modelo pudiera leer — el mismo síntoma de "olvido" que viste en la lección 2, pero causado esta vez por la ventana, no por falta de memoria conectada.
Un detalle que conviene anticipar: esta rutina se vuelve a disparar más adelante en la misma conversación, cuando el conteo de turnos vuelva a cruzar el umbral. En esa segunda pasada, el resumen que ya existe entra como parte de oldMessages —se resume junto con los turnos nuevos que envejecieron—, y sale un resumen actualizado que reemplaza al anterior. Eso funciona bien una o dos veces. Pero cada pasada de resumen es, otra vez, una relectura hecha por un modelo — y ahí es donde entra la segunda mitad de esta lección: hay un punto en el que seguir resumiendo deja de ser la decisión correcta.
Reiniciar el contexto: la hoja nueva
Sigue con la persona que toma notas de la reunión. Si a la hora tres el grupo cambia por completo de tema —de presupuesto a un problema de personal que no tiene nada que ver—, lo que corresponde no es resumir el presupuesto en una línea y seguir en la misma hoja: lo que corresponde es cerrar esa hoja, archivarla, y empezar una hoja nueva para el tema nuevo. Forzar todo en la misma hoja no ahorra espacio — mezcla dos conversaciones que no debían estar mezcladas, y complica separar una cosa de la otra después, en vez de facilitarlo.
Reiniciar el contexto es exactamente eso: dejar de leer el historial acumulado y arrancar la siguiente interacción sin nada previo, en vez de comprimirlo. No siempre es lo mismo que "borrar los datos" — esa distinción importa, y vuelve en los errores comunes de esta lección.
Cuatro señales para decidir
| Señal | ¿Resumir y seguir? | ¿Reiniciar? |
|---|---|---|
| Mismo caso, la conversación solo se alargó | Sí | No |
| El caso se cerró (ticket resuelto, venta concretada) y aparece un tema nuevo | No | Sí |
| Ya hubo dos o más rondas de "resumen del resumen" sobre el mismo hilo | No — cada ronda adicional arriesga más deriva | Sí, o al menos congelar el resumen actual como referencia externa y arrancar liviano |
| El cliente cambia de intención por completo (de un problema técnico a uno de facturación, de una compra a un reclamo distinto) | No | Sí |
Retoma el ejemplo. Dos semanas después, el ticket #4821 se cierra: el registro A se corrigió y el subdominio ya resuelve. Tres días más tarde, el mismo cliente escribe por el mismo canal preguntando por un cobro duplicado en su factura — un tema que no tiene nada que ver con DNS.
Mecánica del reinicio en n8n
Opción A — que el reinicio salga solo, por diseño de alcance. Si, como en este caso, sessionKey = {{ $json.ticketId }} (la decisión de alcance de la lección 4), un ticket nuevo trae automáticamente un ticketId distinto, y con eso, un sessionKey distinto. La consulta de facturación abre el ticket #4901, no el #4821, así que Postgres Chat Memory arranca sin nada guardado bajo esa clave nueva. No compactaste nada ni borraste nada a mano — el reinicio salió gratis, como consecuencia de haber elegido bien el alcance en la lección 4.
Opción B — reinicio explícito dentro de la misma sesión. A veces no puedes rediseñar el alcance —el sessionKey sigue siendo, por ejemplo, el teléfono del cliente para todo lo que hable con la empresa— y necesitas vaciar el historial a mano cuando detectas un cierre de caso o un cambio de tema. Para eso está la otra operación del Chat Memory Manager:
# Chat Memory Manager — Operation: Delete Messages
# Modo: All Messages (no Last N)
memory.sessionKey = "{{ $json.phone }}"
Qué esperar. Después de esta operación, la tabla de Postgres ya no tiene ninguna fila bajo ese sessionKey. El próximo mensaje del cliente arranca sin ningún turno anterior en el arreglo que arma Postgres Chat Memory — exactamente como la Configuración A de la lección 1: ningún historial, ninguna compactación arrastrada.
Errores comunes
Dar por hecho que un resumen preserva todo lo que importaba, sin verificarlo (conceptual). Qué pasa: se conecta la rutina de compactación, se prueba una vez, funciona, y a partir de ahí se confía en el resumen a ciegas — hasta que semanas después el agente responde mal porque el resumen omitió un dato que sí importaba (una fecha límite, una excepción que se acordó verbalmente). Por qué pasa: resumir no es una operación mecánica como truncar texto — es una tarea que hace un modelo, con su propio margen de error. Un LLM puede juzgar mal qué es "importante" en una conversación larga, sobre todo si el prompt de resumen es vago. Cómo detectarlo: revisa el resumen generado contra la conversación original al menos en las primeras corridas, buscando específicamente datos concretos (números, fechas, identificadores) que se hayan perdido o distorsionado. Cómo corregirlo: escribe el prompt de resumen con una lista explícita de qué preservar —como el de esta lección: identificadores, pasos ya probados, estado pendiente— y una instrucción explícita de no inventar; un prompt genérico como "resume esta conversación" deja demasiado criterio suelto en manos del modelo.
Confundir cambiar el sessionKey con borrar el historial (conceptual). Qué pasa: alguien dice "para resetear la memoria del cliente le cambié el sessionKey, ya no existe ese registro" — y da por hecho que los datos anteriores desaparecieron. Por qué pasa: cambiar el sessionKey sí logra el efecto que le importa al agente (que no lea ese historial), y eso se confunde fácilmente con "borrarlo". Pero en Postgres Chat Memory las filas del sessionKey anterior siguen existiendo en la tabla tal cual estaban — siguen siendo consultables si alguien busca con la clave vieja. Cómo detectarlo: pregúntate si la necesidad real es "que el agente deje de ver esto" (alcance) o "que esto deje de existir en la base de datos" (almacenamiento) — son la misma distinción de la lección 1 de este módulo. Cómo corregirlo: si de verdad necesitas que el historial deje de existir —por ejemplo, por una solicitud de borrado de datos—, usa Delete Messages con el modo en All Messages sobre ese sessionKey específico; cambiar de clave no cumple ese requisito.
Disparar la compactación en el momento equivocado — demasiado tarde o en cada turno (práctico). Qué pasa: el umbral de disparo se pone igual a contextWindowLength, o directamente no se pone ningún umbral y la rutina corre en cada turno. En el primer caso, para cuando se detecta que hace falta compactar, ya hubo al menos un turno en el que el modelo respondió sin ver algo que se había salido de la ventana — se reaccionó un paso tarde. En el segundo caso, se gastan llamadas de más al modelo resumiendo constantemente, y cada pasada adicional de "resumen del resumen" acumula más riesgo de perder precisión, sin necesidad. Por qué pasa: no es obvio, la primera vez que armas esta rutina, que el umbral de disparo tiene que dejar margen respecto al límite real de la ventana. Cómo detectarlo: si el agente pierde datos que estaban justo en el borde de los últimos turnos, revisa si el umbral de disparo es igual o menor a contextWindowLength; si el costo de tokens del workflow sube de forma notoria, revisa si la compactación corre más seguido de lo necesario. Cómo corregirlo: pon el umbral bien por encima de contextWindowLength —en el ejemplo de esta lección, 16 contra una ventana de 10— y conserva un bloque de turnos recientes sin tocar después de cada compactación, como hiciste con recentMessages, para que pase un buen tramo de conversación antes de que vuelva a hacer falta.
Ejercicios
Ejercicio 1 — Resumir o reiniciar. Un agente de una concesionaria de autos lleva 40 turnos con el mismo cliente, comparando planes de financiamiento para el mismo auto — el cliente todavía no decide. ¿Resumir y seguir, o reiniciar? Dos meses después, ya con el auto comprado, el mismo cliente (mismo sessionKey, basado en su teléfono) escribe preguntando por un reclamo de garantía. ¿Resumir y seguir, o reiniciar? Justifica cada caso con las señales de esta lección.
Ver solución
Caso 1: resumir y seguir. Sigue siendo el mismo caso —la misma negociación de financiamiento— y solo se alargó; no cambió de tema ni se cerró. Caso 2: reiniciar. El caso anterior (la compra del auto) ya se cerró con la venta, y el reclamo de garantía es un tema nuevo que no tiene relación con la negociación de financiamiento. Arrastrar ese resumen no ayuda a resolver la garantía, y puede confundir al agente anclándolo en datos irrelevantes —por ejemplo, el precio negociado, cuando lo que importa ahora es el problema mecánico.
Por qué funciona: el criterio no es "cuánto tiempo pasó" ni "cuántos turnos hay acumulados" — es si sigue siendo el mismo caso o si el caso anterior se cerró y apareció uno distinto.
Ejercicio 2 — Diseñar el umbral. En el ejemplo de esta lección, contextWindowLength = 10 pero el umbral que dispara la compactación es 16, no 10. ¿Por qué ese margen, y qué problema tendrías si pusieras el umbral exactamente en 10?
Ver solución
Si el umbral fuera exactamente 10, para el momento en que detectas que hace falta compactar, el turno número 11 ya llegó y el modelo ya respondió sin ver el turno 1 —porque contextWindowLength = 10 ya lo había excluido de esa llamada—: reaccionarías un paso tarde, después de que ya se perdió algo. Además, sin margen, la rutina tendería a dispararse en casi cada turno nuevo una vez superado el límite, multiplicando las llamadas al modelo y las rondas de "resumen del resumen". El margen (16 contra 10) da espacio para que la compactación corra con la ventana todavía cómoda, y conservar 6 turnos crudos después de cada pasada asegura que pase un buen tramo de conversación antes de que vuelva a hacer falta.
Por qué funciona: dejar distancia entre el momento en que detectas un problema y el momento en que efectivamente se pierde algo es el mismo principio detrás de cualquier alerta basada en un umbral — llegar antes del límite, no justo en él.
Ejercicio 3 — Corregir a un colega. Un colega te dice: "Para resetear la memoria del cliente X, le cambié el sessionKey a uno nuevo — así quedó limpio el historial de siempre, ya no existe ese registro." ¿Qué le corregirías?
Ver solución
Que cambiar el sessionKey no borra nada — solo hace que la próxima consulta no lea el historial viejo, porque busca bajo una clave distinta. En Postgres Chat Memory, las filas del sessionKey anterior siguen existiendo en la tabla tal cual estaban; siguen siendo consultables (por ejemplo, para auditoría) si alguien busca con la clave vieja. Si de verdad necesita que el historial deje de existir —no solo que el agente deje de leerlo—, la operación correcta es Delete Messages con el modo en All Messages sobre ese sessionKey específico, que sí borra las filas.
Por qué funciona: distinguir "lo que el agente ve" de "lo que existe en la base de datos" es la misma separación entre alcance y almacenamiento que trabajaste en la lección 1 de este módulo — reiniciar el contexto toca el alcance, no necesariamente el almacenamiento.
Resumen y siguiente paso
Ya tienes las dos piezas que le faltaban a este módulo para manejar conversaciones que se alargan de verdad: compactar —condensar los turnos que están a punto de salirse de la ventana en un resumen que sí entra, siguiendo la misma idea que Anthropic documenta como compaction— y reiniciar —dejar de arrastrar el historial por completo cuando el caso cambió o se cerró, ya sea porque el sessionKey cambia solo (si diseñaste bien el alcance en la lección 4) o borrando explícitamente con Delete Messages.
Antes de avanzar deberías poder: armar en n8n la secuencia Get Many Messages → separar en viejo/reciente → resumir con un LLM Chain → Insert Messages con Override, para compactar una conversación larga sin perder los datos que importan; decidir, dado un escenario, si conviene resumir y seguir o reiniciar por completo, usando las señales de esta lección y no la intuición; y explicar por qué cambiar el sessionKey no es lo mismo que borrar el historial guardado.
Con esto cierras el problema que abrió la lección 6: qué hacer cuando hay demasiado historial. El mini-proyecto de la lección 8 toma todas las piezas del módulo —alcance y almacenamiento (lecciones 3 y 4), conversaciones de varios turnos (lección 5), y ahora compactar y reiniciar (lecciones 6 y 7)— y te pide ensamblarlas en un solo agente que sostiene memoria persistente por usuario y no se degrada aunque la conversación se alargue.
Recursos
- Effective context engineering for AI agents — Anthropic — la fuente de "compaction", la técnica exacta que implementaste en esta lección: resumir una conversación cerca del límite de la ventana y reiniciarla con ese resumen.
- Chat Memory Manager node — n8n Docs — referencia completa del nodo que usaste para leer (Get Many Messages), reemplazar (Insert Messages con Override All Messages) y borrar (Delete Messages) el historial guardado.
- Basic LLM Chain node — n8n Docs — el nodo que generó el resumen a partir de los turnos viejos, con un prompt propio.
- Postgres Chat Memory node — n8n Docs — referencia del
sessionKeyy el almacenamiento persistente que hace posible tanto compactar como reiniciar sin perder el resto del historial. - Summarization Chain node — n8n Docs — alternativa a Basic LLM Chain cuando el historial a resumir es tan largo que conviene partirlo en fragmentos (map-reduce) en vez de mandarlo entero en un solo prompt.