Módulo 3: Memoria: el agente que recuerda

4. Memoria persistente: session ID y almacenamiento

Descripción

Al terminar esta lección vas a poder configurar un nodo de memoria persistente —Postgres Chat Memory o Redis Chat Memory— para que la conversación de un cliente real sobreviva un reinicio de n8n y el paso de los días, y vas a poder decidir con qué identidad separar esa memoria para que dos clientes distintos jamás terminen leyendo el historial del otro.

Esto importa por una razón muy concreta: en algún momento, alguien de soporte o de cumplimiento te va a pedir "muéstrame exactamente qué le dijo el bot a este cliente hace tres meses". Con memoria en RAM esa pregunta no tiene respuesta —el historial ya no existe—. Con memoria persistente sí la tiene, y además es una consulta SQL, no una reconstrucción manual. Esa diferencia —poder auditar una conversación pasada— es la que separa un prototipo de un sistema que puedes poner en producción y defender frente a un cliente o un regulador.

Conexión con el módulo: en la lección 3 viste cómo funciona la ventana de turnos recientes dentro de una misma sesión —contextWindowLength, sus límites de tamaño, cuándo basta—. Esta lección no repite eso: los dos nodos que vas a usar aquí comparten exactamente ese mismo parámetro, así que ya sabes leerlo. Lo que sí es nuevo es lo que la lección 1 de este módulo llamó alcance y almacenamiento: de dónde sale la identidad que agrupa el historial, y en qué lugar —fuera del proceso de n8n— queda guardado. Tampoco vas a ver todavía cómo el agente encadena varios turnos activos dentro de una conversación en curso ni cómo resuelve referencias como "el pedido anterior" —eso es el trabajo específico de la lección 5, justo después de esta.

De quién es la memoria, y dónde vive

Piensa en la diferencia entre un gafete de visitante y una credencial de empleado. El gafete de visitante lo imprime la recepción cada vez que entras al edificio: un número nuevo, válido solo por ese día, que no sirve de nada si vuelves la semana siguiente. La credencial de empleado es distinta —es la misma tarjeta hoy, mañana y el mes que viene, y abre siempre la misma carpeta de personal sin importar por qué puerta del edificio entres. La memoria de sesión que viste en la lección 3 funciona como el gafete de visitante: la identidad que agrupa el historial (el sessionId que genera el widget de chat) es nueva cada vez que se abre una pestaña. Lo que esta lección agrega es la credencial de empleado: una identidad que tú eliges, estable, que sigue siendo la misma sin importar cuándo ni desde dónde vuelva ese cliente.

En n8n, esa elección vive literalmente en un selector dentro del nodo de memoria, llamado Session ID, con dos opciones reales:

  • Connected Chat Trigger Node (fromInput, la opción por defecto): el nodo busca un campo sessionId que venga de un Chat Trigger conectado directamente. Es el gafete de visitante — útil, pero efímero.
  • Define below (customKey): tú escribes una expresión o un valor fijo en el campo Key. Ahí es donde pones una identidad real del cliente —un teléfono, un ID de cuenta— para que sea la credencial de empleado.

La parte que suele sorprender: si dejas la opción por defecto (fromInput) en un workflow que no arranca con un Chat Trigger —por ejemplo, un Webhook que recibe mensajes desde el sistema de tickets de tu empresa—, no hay ningún sessionId que leer. n8n no inventa uno ni falla en silencio: el nodo lanza un error de ejecución real, No session ID found, con esta descripción exacta: "Expected to find the session ID in an input field called 'sessionId' (this is what the chat trigger node outputs). To use something else, change the 'Session ID' parameter". Ese mensaje es, básicamente, n8n diciéndote: "elegiste la opción del gafete de visitante, pero no hay recepción que lo imprima aquí — cambia a Define below".

La otra mitad de la decisión es dónde vive el historial una vez agrupado por esa identidad. Postgres Chat Memory y Redis Chat Memory son dos nodos que reemplazan la RAM del proceso de n8n por un almacén externo real: una tabla de Postgres o una base de datos Redis, según cuál conectes. Ambos exponen la misma salida ai_memory que ya usaste con Simple Memory, así que desde la perspectiva del nodo AI Agent nada cambia en cómo se conectan — cambia por completo qué tan bien sobrevive lo que guardan.

Ejemplo trabajado

Retoma la Configuración C que viste en la introducción del módulo: el agente de soporte de TuTienda, con sessionKey = {{ $json.customerPhone }} y Postgres Chat Memory. Ahí quedó un detalle sin explicar — de dónde sale customerPhone — y ahora ya lo sabes: este workflow no arranca con el Chat Trigger que usaste en el Módulo 1 (ese nodo entrega un sessionId efímero, no el teléfono de nadie). Arranca con un nodo Webhook, al que el sistema de tickets de TuTienda le manda cada mensaje ya con el teléfono del cliente autenticado en el cuerpo de la petición:

# Payload que llega al Webhook cuando un cliente escribe
{
  "customerPhone": "5215512345678",
  "message": "¿Dónde está mi pedido #4521?"
}

Y así queda configurado el nodo Postgres Chat Memory conectado a ai_memory:

# Nodo: Postgres Chat Memory
credential                = mi credencial de Postgres (host, base de datos, usuario, contraseña)
sessionIdType              = "Define below"                  # customKey — no hay Chat Trigger conectado
sessionKey                 = "{{ $json.customerPhone }}"      # Key: estable, es el mismo hoy y en un mes
tableName                  = "n8n_chat_histories"              # valor por defecto — se crea sola si no existe
contextWindowLength        = 10

Turno del lunes. El cliente del teléfono 5215512345678 pregunta por el pedido #4521; el agente responde con la fecha de entrega, como ya viste en la lección 1. Postgres Chat Memory guarda ese intercambio en la tabla n8n_chat_histories, bajo session_id = "5215512345678".

Turno de una semana después. El mismo cliente, mismo teléfono, escribe: "Necesito la factura de ese pedido." Entre un turno y otro pasó una semana, y probablemente al menos un reinicio o un redeploy de n8n. No importa: cuando llega este mensaje, n8n vuelve a evaluar {{ $json.customerPhone }}, obtiene el mismo session_id, y Postgres Chat Memory carga el historial guardado. El agente resuelve que "ese pedido" es el #4521 sin que el cliente tenga que repetirlo.

El mismo día, un cliente distinto. Otro cliente, teléfono 5213398765432, escribe por primera vez preguntando por su pedido #7790. Su mensaje llega con un customerPhone distinto, así que Postgres Chat Memory lo guarda bajo un session_id distinto — una fila nueva, sin ningún rastro del historial del cliente anterior.

Qué esperar. Puedes confirmar los tres turnos con una consulta directa a la tabla que el propio nodo creó:

# Query en Postgres — confirmar persistencia y separación por cliente
SELECT session_id, message->>'type' AS role, message->>'content' AS content
FROM n8n_chat_histories
ORDER BY session_id, id;
      session_id | role  | content
------------------+-------+---------------------------------------------
 5213398765432    | human | ¿Dónde está mi pedido #7790?
 5213398765432    | ai    | Tu pedido #7790 está en tránsito...
 5215512345678    | human | ¿Dónde está mi pedido #4521?
 5215512345678    | ai    | Tu pedido llega el 24 de julio.
 5215512345678    | human | Necesito la factura de ese pedido.
 5215512345678    | ai    | Claro, te genero la factura del pedido #4521...

Interpretación: dos columnas cuentan toda la historia. session_id prueba el alcance —cada teléfono tiene sus propias filas, nunca se mezclan—; que la conversación del turno de la semana siguiente aparezca en la misma tabla, sin que nadie la haya vuelto a escribir a mano, prueba el almacenamiento —sobrevivió al tiempo, y a cualquier reinicio que haya ocurrido entre medio, porque nunca dependió de que el proceso de n8n siguiera vivo.

Postgres o Redis: cuánto tiempo guardas, y quién necesita consultarlo

La mecánica de Session ID es idéntica en ambos nodos —el selector fromInput/customKey y el campo Key son exactamente los mismos—. Lo que cambia es el almacén de abajo, y esa elección sí depende del caso de uso:

  • Postgres Chat Memory guarda cada fila para siempre, salvo que tú la borres. Es la opción correcta cuando necesitas un historial permanente y auditable con SQL —como el caso de TuTienda, donde soporte o cumplimiento pueden necesitar revisar una conversación de hace meses—. La tabla no tiene ningún mecanismo de limpieza automática incluido.
  • Redis Chat Memory agrega un parámetro que Postgres no tiene: Session Time To Live (sessionTTL, en segundos). Con un valor mayor a cero, la sesión completa expira sola después de esa cantidad de segundos de inactividad. El valor por defecto es 0 — "no expira", el mismo comportamiento permanente de Postgres, pero corriendo sobre una base de datos en memoria pensada para lecturas y escrituras rápidas, no para consultarse con SQL después.

Un ejemplo donde Redis gana: un agente que responde dudas sobre una promoción de temporada que dura seis semanas. No necesitas guardar esas conversaciones para siempre —de hecho, probablemente prefieres que no queden ahí indefinidamente por razones de retención de datos—. Configuras sessionTTL = 5184000 (sesenta días en segundos) y Redis borra solo el historial de cualquier cliente que no haya vuelto a escribir en esos dos meses.

Errores comunes

Dejar "Session ID" en su valor por defecto cuando el disparador del workflow no es un Chat Trigger, o cuando sí lo es pero necesitas identidad de cliente real (conceptual). Qué pasa: conectas Postgres Chat Memory o Redis Chat Memory, no tocas el selector Session ID —queda en Connected Chat Trigger Node— y el workflow falla en la primera ejecución con el error No session ID found, o (si el disparador sí es un Chat Trigger) funciona, pero la memoria sigue siendo tan efímera como Simple Memory, porque el sessionId que trae el Chat Trigger es el mismo gafete de visitante de la lección 3. Por qué pasa: cambiar de Simple Memory a un nodo persistente resuelve el almacenamiento automáticamente, pero el alcance sigue siendo una decisión manual — el selector no cambia solo porque el nodo sí. Cómo detectarlo: revisa el valor de Session ID en el nodo; si dice Connected Chat Trigger Node y tu trigger es un Webhook, vas a ver el error en la próxima ejecución. Cómo corregirlo: cambia a Define below y escribe en Key una expresión que apunte a una identidad estable del cliente real, no al sessionId de la sesión de chat.

Cambiar el nodo de memoria a uno persistente sin cambiar también la Key (conceptual). Qué pasa: alguien migra de Simple Memory a Postgres Chat Memory para "arreglar" que la memoria se pierde en cada reinicio, pero deja Session ID en Define below con la misma expresión de antes —{{ $json.chatSessionId }}, el id efímero del widget—. El historial ahora sí sobrevive un reinicio, pero sigue reseteándose cada vez que el cliente abre una pestaña nueva, porque el problema nunca fue solo el almacenamiento. Por qué pasa: "memoria persistente" suena a una sola mejora, y es fácil dar por resuelto el alcance solo porque cambiaste el nodo. Son las mismas dos decisiones independientes de la lección 1 del módulo — cambiar una no arregla la otra. Cómo detectarlo: si el cliente vuelve al día siguiente y el agente no reconoce la conversación de ayer, a pesar de tener Postgres o Redis conectado, revisa qué expresión tiene la Key — probablemente sigue apuntando a algo que cambia en cada sesión, no al cliente. Cómo corregirlo: la Key tiene que resolver siempre al mismo valor para el mismo cliente real, sin importar cuándo ni desde qué dispositivo escriba — un teléfono, un ID de cuenta, un correo verificado.

Asumir que Redis limpia solo el historial viejo (práctico). Qué pasa: alguien configura Redis Chat Memory esperando que las conversaciones inactivas desaparezcan automáticamente después de un tiempo razonable, y meses después la base de datos Redis sigue creciendo sin límite, con miles de sesiones de clientes que nunca volvieron a escribir. Por qué pasa: Session Time To Live existe exactamente para esto, pero su valor por defecto es 0 — "no expira nunca" — el mismo comportamiento permanente de Postgres. Nada se limpia solo a menos que tú lo configures. Cómo detectarlo: revisa el tamaño de la base de datos Redis y cuántas claves de sesión tiene sin actividad reciente; si sessionTTL nunca se tocó, sigue en 0. Cómo corregirlo: define explícitamente sessionTTL en segundos según cuánto tiempo de inactividad tiene sentido para tu caso —y si necesitas retención permanente y auditable, esa es justamente la señal de que Postgres, no Redis, es el nodo correcto.

Ejercicios

Ejercicio 1 — Predecir el error. Un workflow arranca con un nodo Webhook (no un Chat Trigger) que recibe mensajes de un formulario de contacto. Alguien conecta Postgres Chat Memory al agente y deja Session ID en Connected Chat Trigger Node, sin tocar nada más. ¿Qué pasa en la primera ejecución, y por qué exactamente ese error y no otro?

Ver solución

La ejecución falla con el error No session ID found. El nodo, en modo Connected Chat Trigger Node, busca un campo sessionId en el input o intenta leerlo de un Chat Trigger conectado — pero acá no hay ningún Chat Trigger en el workflow, y el payload del formulario tampoco trae un campo llamado sessionId. Como no encuentra ninguna de las dos fuentes, no tiene ninguna identidad con la cual guardar o buscar el historial, así que se detiene con ese error en vez de adivinar un valor.

Por qué funciona: el selector Session ID no es decorativo — determina de dónde sale el valor que indexa toda la memoria, y si esa fuente no existe en el workflow, no hay ningún valor por defecto razonable que n8n pueda usar en su lugar.

Ejercicio 2 — Elegir nodo y configuración. Un agente de soporte de facturación para una app de suscripciones recibe mensajes vía Webhook, con {{ $json.body.accountId }} disponible en cada payload. El equipo de cumplimiento pide que cualquier conversación se pueda auditar con SQL hasta un año después. Escribe la configuración completa de Session ID y Key, y decide entre Postgres Chat Memory y Redis Chat Memory, justificando con lo que aprendiste en esta lección.

Ver solución

Session ID = "Define below", Key = "{{ $json.body.accountId }}" — es la identidad estable del cliente real, no depende de la sesión de chat. Para el almacén, Postgres Chat Memory: el requisito explícito de auditar con SQL hasta un año después descarta Redis, que no está pensado para consultarse con SQL y cuyo sessionTTL serviría exactamente para lo contrario de lo que pide cumplimiento —borrar el historial, no conservarlo—. Postgres Chat Memory no borra nada por su cuenta, así que un año de historial sigue disponible salvo que alguien lo elimine explícitamente.

Por qué funciona: la pregunta que decide entre los dos nodos no es cuál es "mejor" en abstracto, sino qué necesita el caso de uso — retención permanente y consultable (Postgres) frente a expiración automática y velocidad (Redis).

Ejercicio 3 — Diagnosticar una fuga de memoria entre clientes. Un agente de soporte para una cadena de tiendas usa Postgres Chat Memory con Session ID = "Define below" y Key = "{{ $json.body.storeId }}" — el ID de la tienda a la que el cliente le escribió, no un dato del cliente mismo. Dos compradores distintos de la misma tienda reportan que el bot les mezcla los pedidos. ¿Cuál es la causa, y cómo la corregirías?

Ver solución

La causa es que storeId no identifica a un cliente — identifica a la tienda. Todos los compradores que le escriben a la misma tienda comparten el mismo session_id en la tabla, así que Postgres Chat Memory los trata a todos como si fueran una sola conversación continua: el historial de un comprador se mezcla con el del siguiente que le escriba a esa tienda. La corrección es cambiar Key a algo que identifique al cliente individual —su teléfono o su ID de cuenta—, no al canal o la tienda por la que entró.

Por qué funciona: una Key estable resuelve el problema de persistencia en el tiempo, pero solo si además es única por cliente. Estable y compartida entre varias personas sigue siendo el mismo error de fondo que usar un valor fijo — todos caen en el mismo balde.

Resumen y siguiente paso

Ya sabes resolver las dos decisiones completas de memoria persistente: el selector Session ID decide de dónde sale la identidad que agrupa el historial —Connected Chat Trigger Node para el sessionId efímero de un Chat Trigger, Define below con una Key estable cuando necesitas reconocer al mismo cliente real entre sesiones—, y el nodo que elijas —Postgres Chat Memory o Redis Chat Memory— decide dónde vive ese historial y por cuánto tiempo, con sessionTTL como la única diferencia real de comportamiento entre los dos.

Antes de avanzar deberías poder: explicar qué error lanza n8n si dejas Session ID en su valor por defecto sin un Chat Trigger conectado y por qué; escribir una expresión de Key que identifique de forma única y estable a un cliente real, no a una sesión ni a un canal compartido; y decidir entre Postgres y Redis dado un requisito de retención o auditoría.

Lo que todavía no resolviste es qué pasa dentro de una conversación activa con varios turnos seguidos: cómo el agente encadena una pregunta con la anterior, resuelve referencias como "el pedido anterior" sin que el cliente repita el número, y mantiene el hilo cuando la conversación cambia de tema y vuelve. Con la memoria ya persistiendo y separada por cliente, eso es exactamente lo que ves en la próxima lección.

Recursos