~/BITACORA.logbot-whatsapp-n8n-no-olvida-datos.md FrankOS

Cómo evitar que tu bot de WhatsApp en n8n olvide el nombre y el email

· 5 min de lectura

También en: Português · English

Mientras construía un recepcionista con IA para inmobiliarias, que atiende por WhatsApp y Telegram, las primeras pruebas mostraron dos fallos que cualquiera que haya montado un agente en n8n probablemente ha visto:

  1. El bot volvía a pedir datos que ya tenía. El cliente daba su nombre y su email al principio y, unos mensajes después, el bot se los pedía otra vez.
  2. El bot confirmaba una cita sin tener todos los datos. Decía “¡listo, agendado!” aunque faltaba el teléfono o la fecha.

Los dos tienen la misma causa, y la solución no es un prompt mejor.

La causa: el estado vive en el historial, no en datos

La forma rápida de montar un agente en n8n es el nodo AI Agent con su memoria nativa. Esa memoria guarda los últimos mensajes de la conversación y se los pasa al modelo en cada turno.

El problema es que ahí el nombre y el email no son datos: son frases sueltas dentro de un historial. El modelo tiene que volver a encontrarlos en cada turno, y cuando la conversación crece y la ventana de memoria se recorta, los primeros mensajes —justo donde el cliente dio su nombre— desaparecen.

Y para el segundo fallo pasa algo parecido: si la regla “no confirmes sin todos los datos” solo está escrita en el prompt, el modelo la cumple casi siempre. En un sistema que agenda visitas reales, “casi siempre” no sirve.

La solución en cuatro piezas

1. El estado de la conversación en Postgres

Cada conversación es una fila en una tabla sessions, con los datos ya confirmados en una columna slots (JSONB) y el historial corto aparte:

CREATE TABLE sessions (
  id            BIGSERIAL PRIMARY KEY,
  tenant_id     INT  NOT NULL REFERENCES tenants(id),
  canal         TEXT NOT NULL,              -- 'whatsapp' | 'telegram'
  canal_user_id TEXT NOT NULL,              -- número o chat_id
  slots         JSONB DEFAULT '{}',         -- nombre, email, teléfono, propiedad, fecha...
  historial     JSONB DEFAULT '[]',         -- últimos turnos
  estado        TEXT  DEFAULT 'abierta',
  ultima_act    TIMESTAMPTZ DEFAULT now(),
  UNIQUE (tenant_id, canal, canal_user_id)
);

El workflow carga la sesión al recibir el mensaje y la guarda al final con un upsert:

INSERT INTO sessions (tenant_id, canal, canal_user_id, slots, historial, ultima_act)
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, now())
ON CONFLICT (tenant_id, canal, canal_user_id)
DO UPDATE SET slots = $4::jsonb, historial = $5::jsonb, ultima_act = now();

En historial guardo solo los últimos 10–12 turnos: lo importante ya no depende de él.

2. Decirle al modelo, explícitamente, qué ya sabe

Antes de llamar al modelo, un nodo Code convierte los slots en texto y lo pone en el prompt. Así el nombre no está “en algún mensaje de antes”, sino en la instrucción de este turno:

const s = $json.slots ?? {};
const requeridos = $json.reglas?.campos_requeridos
  ?? ['nombre', 'email', 'telefono', 'propiedad', 'fecha_hora'];

const conocidos = requeridos.filter(k => s[k]).map(k => `- ${k}: ${s[k]}`).join('\n') || '- (ninguno todavía)';
const faltantes = requeridos.filter(k => !s[k]);

const contexto = `
DATOS YA CONFIRMADOS DEL CLIENTE (NO los vuelvas a pedir):
${conocidos}

DATOS QUE AÚN FALTAN: ${faltantes.length ? faltantes.join(', ') : 'ninguno'}

Pide UN dato faltante por mensaje, el primero de la lista.
`;

return [{ json: { ...$json, contexto_slots: contexto, faltantes } }];

La lista de campos requeridos sale de la configuración de cada cliente, no del código: cada inmobiliaria puede pedir datos distintos.

3. Respuesta en JSON, no en texto libre

Al final del prompt, el modelo recibe un formato obligatorio. Además del mensaje para el cliente, devuelve los datos nuevos que extrajo y la acción que propone:

Responde SIEMPRE y ÚNICAMENTE con un objeto JSON válido, sin markdown:
{
  "mensaje": "texto a enviar al cliente",
  "slots_actualizados": { "nombre": "...", "email": "..." },
  "accion": "responder" | "verificar_disponibilidad" | "agendar" | "escalar",
  "confianza": 0.0-1.0
}

Con eso, los datos del cliente pasan de “texto que el modelo recuerda” a campos que el workflow guarda en slots.

4. La regla importante, en código

El último paso valida la salida del modelo antes de hacer nada. Si el modelo quiere agendar y falta algún dato, el código lo impide, sin importar lo que diga el prompt:

let r;
try {
  r = JSON.parse($json.output.replace(/```json|```/g, '').trim());
} catch (e) {
  return [{ json: { accion: 'escalar', mensaje: 'Un momento, te paso con un asesor.' } }];
}

const slots = { ...($json.slots ?? {}), ...(r.slots_actualizados ?? {}) };
const faltantes = ($json.reglas?.campos_requeridos
  ?? ['nombre', 'email', 'telefono', 'propiedad', 'fecha_hora']).filter(k => !slots[k]);

// bloqueo duro: no se agenda con datos incompletos
if (r.accion === 'agendar' && faltantes.length > 0) {
  r.accion = 'responder';
  r.mensaje = `Perfecto. Para cerrar el agendamiento me falta un dato: ¿me confirmas ${faltantes[0]}?`;
}
// poca confianza → a un humano
if (r.confianza !== undefined && r.confianza < 0.5) r.accion = 'escalar';

return [{ json: { ...$json, ...r, slots, faltantes } }];

Si el JSON no se puede leer, la conversación pasa a un humano en lugar de improvisar.

Cómo probarlo

La prueba que uso es justo el caso que fallaba:

  1. Dar nombre y email en el primer mensaje.
  2. Hablar de otra cosa varios turnos (precio, ubicación, fotos).
  3. Pedir una visita.

El bot no debe volver a pedir ni el nombre ni el email, y no debe confirmar la cita hasta tener todos los campos requeridos. Si falla, la tabla sessions muestra exactamente qué tenía guardado en ese momento, algo que con la memoria nativa no se puede ver.

Lo que me llevo

  • El estado de una conversación es un dato, no un recuerdo del modelo. Va en una base de datos que puedes consultar.
  • Las reglas de negocio que no se pueden romper van en código. El prompt sirve para el tono y para guiar, no para garantizar.
  • El modelo propone, el workflow decide. Pedirle JSON y validarlo convierte una conversación libre en un proceso controlable.

Este patrón es la base del recepcionista con IA que estoy validando con inmobiliarias en Brasil. Si estás montando algo parecido en n8n y te atascas, escríbeme.

← Bitácora · volver a FrankOS