Pasar al contenido principal
osky
Cómo construir un bot de WhatsApp con contexto unificado para evitar respuestas contradictorias en retail
Tema WhatsApp Bot + Contexto Unificado Retail

El problema real: tu bot miente sin querer

Sinch lo documentó bien: un cliente recibe un SMS que dice que su pedido se retrasó, abre el chatbot del mismo retailer y el bot le responde, con toda la confianza del mundo, que el paquete salió ayer. El cliente cuelga frustrado, llama a soporte, y el agente le vuelve a pedir su número de orden desde cero. Tres canales, tres versiones distintas de la verdad. Resultado: confianza destruida.

Este no es un problema de inteligencia artificial. Es un problema de arquitectura. El bot no sabe lo que sabe el SMS porque nadie los conectó. Y en 2026, con los clientes acostumbrados a que Alexa, ChatGPT y hasta el cajero automático sepan quiénes son, ese tipo de inconsistencia ya no se perdona.

Este tutorial te muestra paso a paso cómo construir una capa de contexto unificado en tu bot de WhatsApp usando la WhatsApp Cloud API, un estado centralizado en Redis, y un modelo de lenguaje con acceso en tiempo real al estado del pedido. Sin magia, sin humo: arquitectura concreta que un dev puede implementar hoy.


Requisitos previos

  • Cuenta en Meta for Developers con una app de WhatsApp Cloud API activa
  • Node.js 20+ o Python 3.11+
  • Redis 7+ (puede ser Redis Cloud free tier para empezar)
  • Acceso a tu sistema de órdenes (REST API o base de datos directa)
  • Una clave de API de OpenAI, Anthropic o el LLM que uses
  • ngrok o un servidor con dominio público para el webhook

Arquitectura general

WhatsApp Cloud API
        │
        ▼
   Webhook Handler
        │
        ▼
  Context Resolver  ◄──── Redis (estado del pedido + historial)
        │
        ▼
  LLM con contexto ◄──── Orders API (estado real y actualizado)
        │
        ▼
  Respuesta enviada vía Cloud API

La clave está en el Context Resolver: antes de que el LLM genere una sola palabra, este módulo consulta Redis para ver si ya hay estado reciente del pedido. Si el estado tiene más de 2 minutos, lo refresca desde tu Orders API. El LLM nunca adivina; siempre habla con datos frescos.


Paso 1: Configura tu webhook de WhatsApp Cloud API

// webhook.js
const express = require('express');
const app = express();
app.use(express.json());

const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN;

// Verificación inicial de Meta
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];
  if (mode === 'subscribe' && token === VERIFY_TOKEN) {
    return res.status(200).send(challenge);
  }
  res.sendStatus(403);
});

// Recepción de mensajes
app.post('/webhook', async (req, res) => {
  const body = req.body;
  if (body.object === 'whatsapp_business_account') {
    const entry = body.entry?.[0]?.changes?.[0]?.value;
    const message = entry?.messages?.[0];
    if (message?.type === 'text') {
      await handleIncomingMessage(message, entry.metadata.phone_number_id);
    }
  }
  res.sendStatus(200);
});

app.listen(3000);

Advertencia de producción: responde siempre con 200 OK antes de procesar el mensaje. Si tardas más de 20 segundos en responder al webhook, Meta reintentará y recibirás el mensaje duplicado. Procesa de forma asíncrona.


Paso 2: El Context Resolver — corazón del sistema

// contextResolver.js
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
await client.connect();

const CONTEXT_TTL = 120; // 2 minutos en segundos

async function resolveContext(phoneNumber, userMessage) {
  const cacheKey = `order_ctx:${phoneNumber}`;
  let context = await client.get(cacheKey);

  if (context) {
    context = JSON.parse(context);
    // Verifica si el contexto sigue fresco
    const ageSeconds = (Date.now() - context.fetchedAt) / 1000;
    if (ageSeconds > CONTEXT_TTL) {
      context = await fetchFreshContext(phoneNumber);
    }
  } else {
    context = await fetchFreshContext(phoneNumber);
  }

  return context;
}

async function fetchFreshContext(phoneNumber) {
  // Llama a tu Orders API con el número de teléfono como identificador
  const response = await fetch(
    `${process.env.ORDERS_API_URL}/customer/by-phone/${phoneNumber}/latest-order`,
    { headers: { Authorization: `Bearer ${process.env.ORDERS_API_KEY}` } }
  );
  const order = await response.json();

  const context = {
    orderId: order.id,
    status: order.status,           // 'delayed', 'shipped', 'delivered', etc.
    estimatedDelivery: order.eta,
    lastUpdated: order.updatedAt,
    fetchedAt: Date.now()
  };

  await client.setEx(`order_ctx:${phoneNumber}`, 600, JSON.stringify(context));
  return context;
}

module.exports = { resolveContext };

Paso 3: Construye el prompt con contexto real

// llmHandler.js
const OpenAI = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function generateResponse(userMessage, context, conversationHistory) {
  const systemPrompt = `
Eres el asistente de WhatsApp de una tienda de retail. 
SIEMPRE usa el contexto del pedido que se te proporciona. 
NUNCA inventes información sobre el estado del pedido.
Si el contexto indica un retraso, reconócelo directamente y ofrece disculpas.

CONTEXTO ACTUAL DEL PEDIDO (datos en tiempo real):
- ID de orden: ${context.orderId}
- Estado: ${context.status}
- Entrega estimada: ${context.estimatedDelivery}
- Última actualización: ${context.lastUpdated}
`;

  const messages = [
    { role: 'system', content: systemPrompt },
    ...conversationHistory,
    { role: 'user', content: userMessage }
  ];

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages,
    max_tokens: 300,
    temperature: 0.3  // Bajo para respuestas consistentes y factuales
  });

  return completion.choices[0].message.content;
}

module.exports = { generateResponse };

Nota importante: usa temperature: 0.3 o menos para respuestas sobre estados de pedidos. Temperatura alta = creatividad = el bot inventa que tu paquete llegó a Marte.


Paso 4: Integra todo en el handler principal

// handleIncomingMessage.js
const { resolveContext } = require('./contextResolver');
const { generateResponse } = require('./llmHandler');
const { sendWhatsAppMessage } = require('./whatsappSender');

const conversationStore = new Map(); // En prod: usa Redis también

async function handleIncomingMessage(message, phoneNumberId) {
  const userPhone = message.from;
  const userText = message.text.body;

  // Recupera o inicializa historial
  if (!conversationStore.has(userPhone)) {
    conversationStore.set(userPhone, []);
  }
  const history = conversationStore.get(userPhone);

  // Resuelve contexto con datos frescos
  const context = await resolveContext(userPhone);

  // Genera respuesta con contexto real
  const botResponse = await generateResponse(userText, context, history);

  // Guarda en historial (máximo últimos 10 turnos)
  history.push({ role: 'user', content: userText });
  history.push({ role: 'assistant', content: botResponse });
  if (history.length > 20) history.splice(0, 2);

  // Envía respuesta
  await sendWhatsAppMessage(phoneNumberId, userPhone, botResponse);
}

Paso 5: Envío de mensajes de vuelta al usuario

// whatsappSender.js
async function sendWhatsAppMessage(phoneNumberId, to, text) {
  await fetch(
    `https://graph.facebook.com/v20.0/${phoneNumberId}/messages`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        messaging_product: 'whatsapp',
        to,
        type: 'text',
        text: { body: text }
      })
    }
  );
}
module.exports = { sendWhatsAppMessage };

Advertencias reales de producción

1. El número de teléfono no siempre mapea al cliente. En México especialmente, los números cambian. Complementa con un flujo de verificación por código OTP al primer contacto y guarda el mapeo phone → customerId en Redis con TTL largo (30 días).

2. Redis puede caer. Implementa fallback directo a tu Orders API sin caché. Es más lento, pero mejor que responder con datos rancios o no responder.

3. Rate limits de WhatsApp Cloud API. En tier gratuito son 1,000 conversaciones/mes. En producción real de retail, vas a necesitar tier de pago desde el día uno si tienes más de 500 clientes activos.

4. Nunca cachees estado de pedido más de 5 minutos en situaciones de crisis. Si hay un problema logístico masivo, los estados cambian cada minuto. Ajusta el TTL dinámicamente según el estado: delivered puede cachearse 24 horas; delayed o in_transit, máximo 2 minutos.

5. Logs de auditoría. Guarda qué contexto tenía el bot cuando respondió. Si un cliente escala una queja, necesitas saber exactamente qué datos le mostró el bot y cuándo.


Conclusión

El problema que documentó Sinch no es un bug de IA: es una deuda de arquitectura que se paga con la confianza del cliente. Un bot que dice "tu pedido salió ayer" cuando el SMS ya avisó el retraso no es un bot inteligente: es un bot que miente sin saberlo. Con esta arquitectura de contexto unificado, el LLM siempre habla desde la misma verdad que el resto de tus canales. Eso es lo mínimo que un cliente en 2026 espera de una tienda que dice usar IA.