~/DIARIO.logbot-whatsapp-n8n-nao-esquece-dados.md FrankOS

Como evitar que seu bot de WhatsApp no n8n esqueça o nome e o email

· 5 min de leitura

Também em: Español · English

Enquanto construía uma recepcionista com IA para imobiliárias, que atende por WhatsApp e Telegram, os primeiros testes mostraram duas falhas que quem já montou um agente no n8n provavelmente conhece:

  1. O bot pedia de novo dados que já tinha. O cliente informava nome e email no começo e, algumas mensagens depois, o bot pedia tudo outra vez.
  2. O bot confirmava um agendamento sem ter todos os dados. Dizia “pronto, agendado!” mesmo faltando o telefone ou a data.

As duas têm a mesma causa, e a solução não é um prompt melhor.

A causa: o estado vive no histórico, não em dados

O jeito rápido de montar um agente no n8n é o nó AI Agent com a memória nativa. Essa memória guarda as últimas mensagens da conversa e as passa ao modelo a cada turno.

O problema é que ali o nome e o email não são dados: são frases soltas dentro de um histórico. O modelo precisa encontrá-los de novo a cada turno, e quando a conversa cresce e a janela de memória é cortada, as primeiras mensagens — justamente onde o cliente disse o nome — somem.

Com a segunda falha acontece algo parecido: se a regra “não confirme sem todos os dados” está só no prompt, o modelo a cumpre quase sempre. Num sistema que agenda visitas reais, “quase sempre” não serve.

A solução em quatro peças

1. O estado da conversa no Postgres

Cada conversa é uma linha numa tabela sessions, com os dados já confirmados numa coluna slots (JSONB) e o histórico curto à parte:

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)
);

O workflow carrega a sessão ao receber a mensagem e a salva no final com um 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();

Em historial guardo só os últimos 10–12 turnos: o que importa já não depende dele.

2. Dizer ao modelo, explicitamente, o que ele já sabe

Antes de chamar o modelo, um nó Code transforma os slots em texto e o coloca no prompt. Assim o nome não está “em alguma mensagem anterior”, e sim na instrução deste 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 } }];

A lista de campos obrigatórios vem da configuração de cada cliente, não do código: cada imobiliária pode pedir dados diferentes.

3. Resposta em JSON, não em texto livre

No final do prompt, o modelo recebe um formato obrigatório. Além da mensagem para o cliente, ele devolve os dados novos que extraiu e a ação que propõe:

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
}

Com isso, os dados do cliente deixam de ser “texto que o modelo lembra” e viram campos que o workflow salva em slots.

4. A regra importante, em código

O último passo valida a saída do modelo antes de fazer qualquer coisa. Se o modelo quer agendar e falta algum dado, o código impede, não importa o que diga o 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 } }];

Se o JSON não puder ser lido, a conversa passa para um humano em vez de improvisar.

Como testar

O teste que uso é exatamente o caso que falhava:

  1. Informar nome e email na primeira mensagem.
  2. Falar de outra coisa por vários turnos (preço, localização, fotos).
  3. Pedir uma visita.

O bot não deve pedir de novo nem o nome nem o email, e não deve confirmar o agendamento até ter todos os campos obrigatórios. Se falhar, a tabela sessions mostra exatamente o que estava salvo naquele momento, algo que a memória nativa não deixa ver.

O que eu levo disso

  • O estado de uma conversa é um dado, não uma lembrança do modelo. Vai num banco de dados que você pode consultar.
  • Regras de negócio que não podem ser quebradas vão em código. O prompt serve para o tom e para orientar, não para garantir.
  • O modelo propõe, o workflow decide. Pedir JSON e validá-lo transforma uma conversa livre num processo controlável.

Esse padrão é a base da recepcionista com IA que estou validando com imobiliárias no Brasil. Se você está montando algo parecido no n8n e travou, fale comigo.

← Diário · voltar ao FrankOS