← Volver al blog

Cómo construimos el agente de Bailante en 3 semanas

Cómo construimos el agente de Bailante en 3 semanas

Hace tres semanas Bailante — un estudio de danza y academia en Bogotá — manejaba su agenda inteira por WhatsApp. Catorce horas a la semana de mensajes, traducciones de voz a texto a mano, y una hoja de Excel que se rompía cada viernes. Hoy el agente responde solo, agenda solo, y el equipo dedicó ese tiempo a enseñar, no a administrar.

Este es el write-up técnico de cómo lo construimos.

El problema

El flujo original era brutal en su simplicidad y su ineficiencia:

  1. Un alumno enviaba un WhatsApp: “Quiero agendar para el curso de salsa”
  2. Una administradora (o el fundador) le respondía: “¿Qué día te sirve?”
  3. El alumno respondía con tres opciones
  4. Se consultaba la hoja de Excel
  5. Se enviaba una confirmación
  6. Se copiaba el evento a Google Calendar
  7. Si había un no-show, se mandaba recordatorio manual

Sumado a esto: mensajes de voz que nadie transcribía, clases que se cancelaban sin aviso, y un 30% de las consultas que nunca llegaban a convertirse en agenda.

La métrica era clara: 14 horas/semana de trabajo administrativo que no debería existir.

La.stack elegida

No elegimos la stack más elegante. Elegimos la que podíamos operar a las 2am cuando algo se rompiera.

ComponenteTecnologíaPor qué
Motor de agenteLaravel (PHP)El fundador ya conocía PHP; rápida iteración
IA generativaOpenAI GPT-4oLatencia aceptable, precio razonable
MensajeríaWhatsApp Cloud APISin hardware adicional, webhooks nativos
CalendarioGoogle Calendar APIOAuth 2.0 estable, bien documentado
Cola de trabajosLaravel Queue (Redis)Job retries automáticos fuera de la caja
DespliegueDokploy (VPS propio)Control total, costo fijo, no vendor lock

La decisión más debatida fue no usar un framework de agentes como LangChain o Autogen. Teníamos tres razones:

  1. Debugging: un loop de while(true) con logs es más fácil de leer que una cadena de prompts dentro de un framework opaque.
  2. Latencia: cada round-trip a OpenAI suma 800ms-2s. Sin framework overhead, el loop entero corre en menos de 4s.
  3. Costo: el framework añade tokens y llamadas innecesarias para problemas que resolví con un switch.

La arquitectura del agente

El loop principal

El agente opera en un loop de estado muy simple:

Estado: IDLE → INTERPRETING → SCHEDULING → CONFIRMING → DONE

Cada estado tiene transiciones definidas y acciones asociadas. No hay loops infinitos sin salida — cada transición tiene un máximo de reintentos (3) y un fallback a intervención humana.

class AgentLoop
{
    public function run(Message $message): void
    {
        $state = State::IDLE;

        while ($state !== State::DONE) {
            $state = match($state) {
                State::IDLE => $this->interpret($message),
                State::INTERPRETING => $this->classifyIntent($message),
                State::SCHEDULING => $this->proposeSlots($message),
                State::CONFIRMING => $this->confirmBooking($message),
            };

            $this->persistState($message->from, $state);
        }
    }
}

El módulo de interpretación

El primer paso es entender qué quiere el usuario. Usamos un prompt de clasificación con 5 categorías:

  1. SOLICITAR_CITA — quiere agendar una clase
  2. CONSULTAR_HORARIO — quiere saber qué días hay disponibles
  3. CANCELAR — quiere cancelar una clase ya confirmada
  4. REPROGRAMAR — quiere cambiar el día/hora de una clase
  5. OTRO — pasa a un humano
private function classifyIntent(Message $message): State
{
    $response = $this->openai->chat([
        'model' => 'gpt-4o',
        'messages' => [
            ['role' => 'system', 'content' => 'Clasifica el siguiente mensaje en una de estas categorías: SOLICITAR_CITA, CONSULTAR_HORARIO, CANCELAR, REPROGRAMAR, OTRO. Responde solo con la categoría.'],
            ['role' => 'user', 'content' => $message->body],
        ],
    ]);

    return match(trim($response)) {
        'SOLICITAR_CITA' => State::SCHEDULING,
        'CONSULTAR_HORARIO' => State::SCHEDULING,
        'CANCELAR' => State::SCHEDULING,
        'REPROGRAMAR' => State::SCHEDULING,
        default => State::DONE, // pasa a humano
    };
}

El módulo de calendario

Una vez clasificado el intent, el agente consulta Google Calendar para encontrar horarios disponibles. La lógica:

  1. Obtener eventos de los próximos 14 días
  2. Filtrar los que tienen show-as === free
  3. Generar una lista de 3 opciones con día y hora
  4. Presentar al usuario
private function proposeSlots(Message $message): State
{
    $freeSlots = $this->calendar->getFreeSlots(
        start: now(),
        end: now()->addDays(14),
        duration: 60 // minutos
    );

    $options = collect($freeSlots)->take(3)->map(fn($slot) =>
        $slot->format('l j F, H:i')
    )->join("\n");

    $this->whatsapp->send(
        to: $message->from,
        text: "Estos son los horarios disponibles:\n\n{$options}\n\nResponde con el número de la opción o propón otro día."
    );

    return State::CONFIRMING;
}

Lo que se rompió

Problema 1: Webhook idempotencia

WhatsApp Cloud API reenvía webhooks. Un mensaje puede llegar 3-4 veces si Facebook cree que no recibió el 200. Sin manejo de idempotencia, cada mensaje generaba 3-4 agentes ejecutándose en paralelo.

Solución: hash del message.id + Redis SETNX con TTL de 5 minutos. Si el hash ya existía, descartamos el mensaje.

public function handleWebhook(array $payload): Response
{
    $messageId = $payload['entry'][0]['changes'][0]['value']['messages'][0]['id'] ?? null;

    if (!$messageId || !$this->idempotency->claim($messageId)) {
        return response('', 200); // confirmamos igualmente
    }

    // procesar mensaje...
}

Problema 2: Rate limits de WhatsApp API

WhatsApp Cloud API tiene un límite de ~80 mensajes/segundo por número de teléfono. En horas pico (9am y 6pm), Bailante recibía ráfagas de 20-30 mensajes en 2 segundos. Sin cola, algunos fallaban silenciosamente.

Solución: Laravel Queue con Redis. Cada mensaje saliente va a la cola con backoff exponencial (5s, 10s, 20s). Si falla 3 veces, el job se marca como fallido y se envía alerta a Telegram al fundador.

Problema 3: GPT-4o y la zona horaria

GPT-4o no sabe en qué zona horaria está el usuario. Cuando el agente proponía “martes 3pm”, para un usuario en Colombia eso era UTC-5. Pero si el mensaje venía de un alumno temporal de Miami, la interpretación era UTC-4.

Solución: todos los timestamps se manejan en UTC dentro del sistema. Solo se convierten a hora local en el momento de enviar el mensaje What`sApp, usando la zona horaria del contacto (guardada en la tabla de contactos).

Los resultados

Después de 3 semanas en producción:

MétricaAntesDespués
Tiempo administrativo semanal14 horas2 horas
Rate de conversión de consulta a cita52%78%
No-shows18%3%
Tiempo de respuesta a consulta4-8 horas< 2 minutos
Satisfacción del equipo+40% NPS interno

El número más surprising fue el rate de conversión: pasó de 52% a 78% no porque el agente fuera más persuasivo, sino porque eliminó la fricción. Antes, un lead se perdía si la administradora no respondía en 2 horas (momento de interés perdido). Ahora responde instantáneamente, 24/7.

Lecciones para otros agentes

Después de tres semanas con este agente en producción, estas son las cosas que haría diferente:

1. El estado inicial del agente importa más que el prompt

Pasamos las primeras 48 horas ajustando el prompt. No fue hasta la semana 2, cuando reescribimos la máquina de estados, que la calidad subió dramáticamente. Un buen prompt sobre una máquina de estados rota produce respuestas elegantes que no llevan a ningún lado.

2. Idempotencia desde el día 1

No lo hicimos. Pagamos el precio con mensajes duplicados en la primera semana. Si estás conectando cualquier API que pueda reenviar webhooks, haz idempotencia antes de escribir la primera línea del negocio.

3. El fallback a humano es una feature

Al principio lo tratamos como un último recurso. Hoy es una de las features más valoradas: si el agente no está seguro, pasa a un humano con todo el contexto ya transcrito. El fundador no recibe un “qué necesito hacer?” sino un “este lead quiere salsa para el 15, ¿confirmas?”

4. Métricas antes de deployed

Instrumentamos métricas antes de запуска. No lo hicimos. Pusimos Datadog el día 5, cuando ya habíamos tenido 3 incidentes que no supimos debuggear bien. La regla: si no está métricas, no está en producción.

¿Querés algo similar?

Si manejás un negocio de servicios en Latam yEstás pagando una o más personas para responder WhatsApp, agenda un Mini-Diagnóstico. En 45 minutos mapamos el flujo, cuantificamos el tiempo administrativo, y te decimos si un agente tiene sentido para tu caso — y si lo tiene, te damos un estimado realista de inversión y tiempo.

El agente de Bailante tomó 3 semanas de build y 1 de ajuste fino. Eso es el camino feliz. El camino correcto empieza con entender tu flujo.


¿Te resultó útil este write-up? Suscribite al newsletter para más contenido sobre arquitectura de agentes en producción.