La mayoría de los tutoriales de chatbots muestran un bot que responde un mensaje y listo. La realidad de producción es muy diferente: conversaciones que deben persistir entre múltiples mensajes, flujos condicionales que dependen del estado del usuario, integraciones con APIs externas que fallan, y todo esto ejecutándose en un entorno serverless donde cada instancia Lambda es efímera.
En este artículo describo los desafíos técnicos que resolvimos al construir laravel-v12-api-converse-admin: una API REST en Laravel 12 para gestionar conversaciones de WhatsApp y Telegram con chatbots inteligentes, desplegada en AWS Lambda.
1. El Motor: Máquinas de Estado para Conversaciones
El problema central de un chatbot es que HTTP es stateless pero una conversación no lo es. El usuario envía un mensaje, el sistema responde, y luego el usuario envía otro mensaje que solo tiene sentido dentro del contexto de lo que se habló antes.
Decidimos modelar cada flujo de conversación como una Máquina de Estado Finito (FSM). Cada conversación tiene un estado actual, y cada mensaje entrante dispara una transición a un nuevo estado o permanece en el mismo si el input es inválido.
// Estado simple: pregunta y valida
'ask_name' => StateDefinition::make()
->ask('¿Cómo prefieres que te llame?')
->validateMinLength(2, 'Ingresa al menos 2 caracteres.')
->saveAs('custom_name')
->then('register'),
// Estado dinámico: accede a datos previos
'start' => function (array $data) {
$name = $data['platform_name'] ?? '';
return StateDefinition::make()
->say("Hola, {$name}!")
->askWithButtons('¿Continuamos con ese nombre?', [
'yes' => 'Sí, ese es mi nombre',
'no' => 'Prefiero usar otro',
])
->validateIn(['yes', 'no'])
->saveAs('name_confirmed')
->when('yes', 'register')
->when('no', 'ask_name');
},
// Estado de proceso: lógica de negocio, sin input
'register' => StateDefinition::make()
->process(function ($_, array $data) {
$identity = Identities::create(['display' => $data['custom_name']]);
return ['data' => ['identity_id' => $identity->id], 'next' => 'welcome'];
}), El builder pattern de StateDefinition hace que el código del flujo sea declarativo y fácil de razonar. Los estados pueden ser estáticos o dinámicos (closures que reciben $data). Los métodos saveAs, then, when y process controlan el almacenamiento de respuestas y las transiciones entre estados.
Tipos de input soportados
Comportamientos del motor
2. El Problema de Serialización: Redis y Closures en PHP
Usamos BotMan como framework base para el manejo de conversaciones. BotMan persiste el estado de cada conversación en Redis para poder reanudarla cuando llega el siguiente mensaje del usuario.
El problema: PHP no puede serializar Closures. Y nuestro StateDefinition usa closures para las callbacks de validación y transformación. Cuando BotMan intentaba serializar la conversación al guardarla en Redis, obteníamos un TypeError silencioso que corrompía el estado.
Sin solución personalizada, el flujo era: usuario envía mensaje → BotMan intenta serializar la conversación con sus Closures → serialize() lanza excepción → conversación se pierde → usuario recibe un error genérico o la conversación reinicia desde cero.
La solución fue implementar __serialize() y __unserialize() personalizados en StateMachineConversation. En lugar de serializar los closures, persistimos solo el estado mínimo necesario: el nombre del estado actual, los datos del usuario y el número de reintentos. Al deserializar, reconstruimos el objeto StateDefinition completo en runtime llamando al método que define los flujos.
public function __serialize(): array
{
return [
'currentState' => $this->currentState,
'retryCount' => $this->retryCount,
'conversationId'=> $this->conversationId,
// Los StateDefinitions NO se serializan
// Se reconstruyen en __unserialize()
];
}
public function __unserialize(array $data): void
{
$this->currentState = $data['currentState'];
$this->retryCount = $data['retryCount'];
$this->conversationId = $data['conversationId'];
// Reconstruir definiciones desde el método del flujo
$this->states = $this->defineStates();
}3. Drivers Customizados: WhatsApp Cloud API y Telegram
BotMan tiene drivers para múltiples plataformas, pero el driver oficial de WhatsApp no soporta los tipos de mensajes interactivos de WhatsApp Cloud API (botones, listas, templates). Tampoco implementa la verificación HMAC de webhooks requerida por Meta.
Construimos CustomWhatsappDriver (600+ líneas) que cubre todo el contrato con la Cloud API de Meta:
Un detalle que requirió trabajo especial fue el challenge-response handshake de Meta. Cuando registras un webhook, Meta hace un GET con un token de verificación y espera que tu servidor lo devuelva en un formato específico. Si el endpoint no responde correctamente en los primeros segundos, Meta rechaza el webhook definitivamente.
Implementamos también el evento OutgoingMessageSent que se dispara cada vez que el bot envía un mensaje, permitiendo al resto del sistema (logging, analytics, billing) reaccionar sin acoplar esa lógica al driver.
4. Detección de Intención con IA: Gemini, Groq y Circuit Breakers
No todos los mensajes que recibe el bot son respuestas a una pregunta dentro de un flujo. Los usuarios escriben cosas como "cancelar", "ayuda", "quiero hablar con un agente", y el sistema necesita detectar estas intenciones antes de procesarlos como input del estado actual.
Usamos LLMs para clasificar intenciones, con configuración deliberadamente conservadora:
// Temperatura baja: queremos clasificación, no creatividad
'temperature' => 0.2,
// Output mínimo: un intent es máximo 2-3 palabras
'maxTokens' => 64,
// Modelo: gemini-2.5-flash-lite (rápido y económico para clasificación)
'model' => 'gemini-2.5-flash-lite',
// El prompt está en un archivo Markdown externo
// → facilita versionar y probar variaciones del prompt sin tocar el código
'promptFile' => resource_path('prompts/intent-detection.md'),El problema de depender de una sola API externa es la disponibilidad. Gemini puede estar caído, puede tener rate limits, o simplemente latencia alta. Implementamos dos capas de resiliencia:
Si Gemini falla (timeout, error 5xx, rate limit), automáticamente reintenta con Groq. El cliente de fallback es transparente: el IntentDetectionService no sabe qué modelo usó realmente.
Usamos ackintosh/ganesha con el patrón bulkhead. Si Gemini acumula suficientes fallas, el circuit breaker se abre y las solicitudes van directo a Groq sin intentar Gemini, evitando latencia acumulada.
5. Arquitectura Event-Driven con AWS EventBridge
El chatbot no vive aislado. Cuando un usuario completa un flujo de onboarding, otros servicios necesitan saberlo: el sistema de CRM debe crear el perfil, el servicio de notificaciones debe enviar una confirmación, el servicio de analytics debe registrar la conversión.
El acoplamiento directo entre servicios es frágil: si el CRM está caído, ¿el onboarding falla? ¿Se reintenta? ¿Cuántas veces? En su lugar, publicamos eventos de negocio en AWS EventBridge y cada servicio se suscribe a lo que le interesa.
La Lambda también actúa como consumidor de EventBridge. Cuando otro servicio publica un evento (por ejemplo, que se creó una cuenta espejo para el usuario), la Lambda lo recibe y puede continuar o iniciar una conversación con el usuario en WhatsApp, todo sin que el usuario haya enviado un mensaje.
6. Timeout de Conversaciones: El Problema del Contexto Sintético
Los usuarios abandonan conversaciones a medias. Si alguien deja una conversación en el paso 3 de un onboarding y vuelve 30 minutos después, ¿qué hace el bot?
Implementamos ConversationTimeoutJob: un job que se encola con un delay configurable por estado. Si el usuario no responde antes de que expire, el job se ejecuta y puede cancelar la conversación, enviar un recordatorio, o hacer una transición de estado automática.
El desafío técnico aquí es que BotMan fue diseñado para responder a mensajes entrantes. Para reanudar una conversación sin que el usuario haya enviado nada, necesitamos simular el contexto del driver. Desarrollamos TimeoutBridge: una clase que construye un objeto BotMan con un driver sintético que apunta al usuario correcto, permitiendo al FSM ejecutarse como si hubiera recibido un mensaje real.
Si el usuario responde justo cuando el job de timeout se está ejecutando, podemos tener dos transiciones de estado simultáneas. El job verifica el estado esperado antes de ejecutar: si el estado cambió desde que se encoló, el job cancela su ejecución silenciosamente.
7. Logging y Observabilidad
En un sistema event-driven con múltiples plataformas, saber qué pasó cuando algo falla es tan importante como que las cosas funcionen. Implementamos trazabilidad end-to-end desde el webhook hasta el evento de negocio.
Excepciones no manejadas, fallos de integración con APIs externas y errores de serialización.
Eventos de alto valor: onboarding completado, cuenta creada, alertas de ubicación activadas.
Inicio de conversaciones, cambios de estado del FSM y cancelaciones.
Logs estructurados de Lambda. Correlación de request IDs entre el webhook y los eventos de negocio.
Todos los mensajes se persisten en la tabla Messages con dirección (incoming/outgoing), permitiendo reconstruir cualquier conversación completa. Los webhooks crudos se guardan en WebhookLogs antes de procesarse, lo que permite depurar problemas con el payload original de Meta o Telegram.
8. Serverless con Bref + AWS Lambda
Desplegamos con Bref, que permite ejecutar PHP en AWS Lambda. No hay servidores que mantener, el escalado es automático, y solo se paga por ejecución.
Los desafíos específicos de serverless con Laravel:
9. Lo que Aprendimos
En un sistema de chatbot, ningún mensaje se procesa en tiempo real desde la perspectiva del usuario. Asumimos latencia, reintentos y fallas desde el diseño inicial. Los sistemas que asumen happy path primero tienen deuda técnica imposible de pagar después.
Circuit breakers, retries con backoff, fallbacks y timeouts no son features adicionales: son parte del diseño de cualquier integración con APIs externas. Gemini, Meta y Telegram fallan. No con frecuencia, pero cuando fallan en producción importa.
El FSM de StateMachineConversation no sabe si está hablando con WhatsApp o Telegram. Esa separación nos permitió agregar Telegram después sin tocar la lógica de negocio, y nos prepara para agregar futuras plataformas.
La trazabilidad end-to-end (webhook → estado → evento de negocio) no fue un afterthought. Sin ella, depurar por qué una conversación se rompió en producción es prácticamente imposible en un sistema event-driven.
10. Retos por Resolver
El sistema funciona en producción, pero hay componentes de optimización en diseño cuya implementación está pendiente. Documentamos estos retos para no perder el razonamiento detrás de las decisiones tomadas.
El pipeline llama al LLM en cada mensaje para detectar intent y extraer parámetros. El diseño plantea un sistema en capas: L1 con Redis para exact match por hash xxh3 (O(1), sin cómputo), y L2 con pgvector para búsqueda por similitud semántica. Solo ante un doble miss se llega al LLM. Las tablas ai_intent_cache y el pipeline de capas están diseñadas pero pendientes de implementar.
Cuando el LLM extrae "FORD CAJA" de un mensaje, actualmente siempre consulta la API externa. El reto es cachear estas resoluciones por identity_id + entity_type + raw_name via ai_entity_resolutions, con ILIKE matching sobre ai_entity_catalog. El scope por identity es intencionado: cada cliente tiene su propio vocabulario de entidades y un cache global contaminaría datos entre clientes.
El catálogo local de entidades (unidades, conductores, rutas desde mirror accounts) necesita mantenerse fresco. El diseño plantea sync on-demand ante un cache miss, y sync periódico via job programado (~cada 6 horas). Al re-sincronizar, las resoluciones de esa identity deben invalidarse porque los nombres canónicos pueden haber cambiado en el sistema externo.
Laravel 12 incluye un SDK oficial de AI con soporte nativo para embeddings (Gemini, OpenAI, Cohere, etc.) y vector queries en Eloquent via pgvector. Esto cubriría la búsqueda L2 del ai_intent_cache sin SQL raw. La decisión pendiente es si migrar también los clientes custom (GeminiAIClient / GroqAIClient / FallbackAIClient) al SDK, o adoptarlo solo para embeddings y vector queries mientras se mantiene el pipeline de cache como código propio.
Gran parte de este proyecto fue desarrollado con la asistencia de Claude Code como herramienta principal. También experimentamos con Gemini CLI, Qwen y Kimi AI en distintas etapas del desarrollo. Fue un ejercicio real de cómo los agentes de IA pueden acelerar la construcción de sistemas complejos sin reemplazar el criterio de ingeniería detrás de cada decisión.
Caso de Uso: Cuentas Espejo GPS
La mejor forma de entender el rol de API Converse es ver un producto real que la usa. Cuentas Espejo GPS es una PWA que permite a empresas de transporte compartir el seguimiento de su flota desde WhatsApp o Telegram —sin revelar credenciales ni cambiar de proveedor GPS. En este sistema, API Converse administra la conversación con el operador; la lógica de negocio (crear la cuenta espejo, validar el vehículo, emitir el token) vive en una API separada: API Mirror Accounts.
El flujo completo ocurre dentro del chat: el dueño de transporte indica el vehículo, configura el tiempo de acceso, y en segundos recibe un enlace temporal con branding propio que puede mandar a su cliente. La cuenta espejo expira automáticamente. Sin apps, sin paneles de administración, sin fricción.
PWA para compartir seguimiento GPS en tiempo real por WhatsApp o Telegram. Compatible con Atlantida, Geotrucks y Zeekgps. Branding personalizado, acceso temporal con expiración automática, alertas periódicas de ubicación al destinatario.
¿Construyendo algo similar?
En Mango Binario diseñamos e implementamos sistemas de chatbot para empresas que necesitan automatizar conversaciones a escala.
Si estás evaluando arquitecturas para tu proyecto o buscas un equipo con experiencia en integraciones de WhatsApp, IA aplicada y arquitecturas serverless, conversemos.