
Compartir:
Benjamin Aronov es desarrollador de Vonage. Es un constructor de comunidades con experiencia en Ruby on Rails. Benjamin disfruta de las playas de Tel Aviv, a la que llama hogar. Su base en Tel Aviv le permite conocer y aprender de algunos de los mejores fundadores de startups del mundo. Fuera de la tecnología, a Benjamin le encanta viajar por el mundo en busca del perfecto pain au chocolat.
Impulsado: Construir una aplicación Laravel sin saber Laravel
En este tutorial, aprenderás a crear un conserje de viajes de WhatsApp a través de un agente de IA utilizando Laravel, OpenAI y la API Messages API de Vonage.
WhatsApp chat interface showing the Vonage-powered travel concierge ready to receive and respond to user messages.
Introducción
TL;DR: Vaya más allá y encuentre el código de trabajo en GitHub.
Hace un par de meses, Jim Seconde me preguntó si quería escribir algo de contenido PHP. Jim es una celebridad de PHP desde hace años, tocando casi todo PHP bajo el sol. ¿Pero yo? No he tocado PHP en probablemente 12 años.
Jim dijo: "No te preocupes. Hay una cosa nueva y genial llamada Laravel Boost. Es un servidor MCP específico de Laravel que te ayuda a crear aplicaciones Laravel. No se necesitan elefantes. Quizá quieras echarle una mano".
Eso se convirtió en un pequeño experimento con una restricción muy clara: ¿podría construir una aplicación Laravel no trivial en una pila que apenas conocía, utilizando un agente de codificación de IA como herramienta de implementación principal?
Me propuse un reto deliberadamente difícil: crear un conserje de viajes de WhatsApp en Laravel y PHP. La aplicación no podía limitarse a devolver texto. Necesitaba recibir mensajes de WhatsApp a través de la Messages API de Vonageutilizar OpenAI para generar respuestas, elegir el tipo de mensaje de WhatsApp adecuado para la respuesta, admitir elementos interactivos como botones y listas, y mantener suficiente memoria de conversación para sentirse como un asistente y no como un bot sin estado.
Eso lo convirtió en un caso de prueba útil. No se trataba de una aplicación CRUD de juguete, y no era un tutorial de framework en el que pudiera fingir. Implicaba webhooks, trabajos en segundo plano, autenticación de API, restricciones de plataformas externas y formatos de mensajería estructurados.
Este artículo no es un tutorial de Laravel paso a paso. Es un informe sobre lo que realmente fue construir una aplicación real de esta manera: qué avisos ayudaron, qué fases mantuvieron el trabajo bajo control, donde el agente fue útil, donde se cayó, y cómo la documentación respaldada por MCP cambió el resultado cuando las cosas se pusieron difíciles.
Todo el proyecto me llevó unas ocho horas en dos días. Al final, tenía una demo de Laravel WhatsApp que funcionaba. Y lo que es más importante, tenía una idea mucho más clara de para qué sirve realmente el desarrollo ágil.
Requisitos previos
PHP
Compositor PHP
Agente compatible con Laravel Boost (por ejemplo, GitHub CoPilot)
Cuenta API OpenAI u otro LLM
ngrok
Cuenta API de Vonage
Un número virtual de Vonage
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Configuración de la aplicación de Vonage
Para crear una aplicación, vaya a la sección Crear una aplicación en el panel de Vonage y define un nombre para tu aplicación.
Si tiene intención de utilizar una API que utilice Webhooks, necesitará una clave privada. Haga clic en "Generar clave pública y privada"; la descarga debería iniciarse automáticamente. Guárdela de forma segura; esta clave no puede volver a descargarse si se pierde. Seguirá la convención de nomenclatura private_<id de su aplicación>.key. Esta clave puede utilizarse ahora para autenticar llamadas a la API. Nota: La clave no funcionará hasta que se guarde la aplicación.
Elija las funciones que necesite (por ejemplo, Voice, Messages, RTC, etc.) y proporcione los webhooks necesarios (por ejemplo, URL de eventos, URL de respuestas o URL de mensajes entrantes). Estos se describirán en el tutorial.
Para guardar e implementar, haz clic en "Generar nueva aplicación" para finalizar la configuración. Tu aplicación ahora está lista para usar con las API de Vonage.
Tendrá que habilitar las Capacidades de Mensajes. Para Mensajes, necesitará habilitar webhooks. Por ahora, sólo tiene que añadir marcadores de posición. Más tarde, una vez que la aplicación local se está ejecutando a través ngrok, actualizar el webhook de entrada para que apunte al punto final Laravel.
Establezca la URL de entrada en https://placeholder.com/inbound.
Establezca la URL de estado en https://placeholder.com/status.
A continuación, vincule su WhatsApp Business (WABA) haciendo clic en la pestaña "Vincular cuentas externas":
Vonage dashboard showing a WhatsApp Business Account linked to an application in the “Link external accounts” section.Asegúrese de anotar el:
ID de aplicación
Clave privada (descargada como archivo .key clave)
Clave/secreto API
Los necesitaremos más adelante para nuestra aplicación Laravel.
Configuración del Agente
En un tutorial normal, sólo necesitamos configurar nuestra aplicación. Pero en un tutorial agentico como este, también necesitamos configurar nuestro agente.
Utilicé un sencillo archivo de instrucciones plan-first (copiado de Alex Finn):
Lee el código base y piensa en el problema.
Escribir un plan en todo.md.
Espere a recibir la aprobación antes de iniciar la aplicación.
Realice las tareas de una en una.
Explicar los cambios a alto nivel.
Los cambios deben ser sencillos y mínimos.
Además, animé explícitamente al agente a utilizar dos herramientas respaldadas por MCP:
En Laravel Boost MCP para el andamiaje y las convenciones que tienen en cuenta el marco de trabajo
En Documentación de Vonage Servidor MCP, para conocer la corrección de la API y los detalles del formato de los mensajes
Esta es una de las primeras lecciones claras del proyecto: la ingeniería rápida es en realidad ingeniería del flujo de trabajo, y las herramientas MCP son las que convierten ese flujo de trabajo en algo fiable.
En primer lugar, crea el markdown donde vivirán tus reglas:
touch ./github/instructions/workflow.instructions.md
A continuación, añada las instrucciones de acuerdo con la sintaxis de las instrucciones de Copilot:
4 pasos para crear la aplicación
Cuando empecé, no tenía ni idea de cuánto tiempo llevaría ni de lo que implicaría. Pero cada vez que el agente completaba una lista de tareas, yo cambiaba el nombre del archivo y lo guardaba. Entonces podía enviar las tareas completadas, junto con el alcance original del proyecto, al agente, y éste creaba el siguiente conjunto de objetivos.
En lugar de incitar al agente con enormes instrucciones, me centré en pequeñas fases con resultados claros.
Construir la aplicación básica de Laravel y la integración de WhatsApp
Añadir respuestas de WhatsApp desde la aplicación al usuario
Añade memoria de conversación para obtener mejores respuestas de OpenAI
Añadir tipos de mensajes inteligentes (botones, listas)
Resumiré lo sucedido en cada fase y destacaré las indicaciones más interesantes.
Fase 1: Dejar que el agente planifique la aplicación
Ver: initial_todo.md para las tareas del agente.
La primera pregunta fue intencionadamente de alto nivel. Describí la aplicación de demostración, la integración con WhatsApp y el requisito de utilizar OpenAI tanto para el contenido como para las decisiones sobre el tipo de mensaje.
Initial prompt used to define the travel concierge app and guide the agent to generate a structured development plan using Laravel Boost and Vonage docs.
Aquí es donde Laravel Boost MCP hizo una diferencia notable.
En lugar de producir una estructura genérica de PHP, el agente se apoyó en las convenciones de Laravel inmediatamente. Sugirió rutas, controladores, trabajos en cola y clases de servicio de una manera que se alineaba con la forma en que las aplicaciones Laravel se organizan normalmente. También propuso un todo.md por fases sin que se le pidiera explícitamente que lo hiciera.
El agente utilizaba el conocimiento del marco expuesto a través de MCP para dar forma a la arquitectura. Creó una ruta webhook, un controlador para la validación, trabajos en cola para procesar y enviar mensajes, y clases de servicio para OpenAI y Vonage.
Si yo hubiera estado trabajando sin experiencia Laravel, esto habría tomado mucho más tiempo para armar manualmente. Así que la primera conclusión es clara: cuando el agente tiene acceso a herramientas MCP conscientes del marco, es muy fuerte en el andamiaje greenfield.
AI-generated development plan outlining the Laravel WhatsApp app architecture, including webhooks, queue jobs, services, and OpenAI integration.
El primer fracaso: Código que parecía correcto pero no hacía nada
La fase de andamiaje había ido sorprendentemente bien. El agente había construido una aplicación Laravel completa: rutas, controladores, trabajos, servicios, incluso un archivo de prueba. Con Laravel Boost MCP guiando la estructura, parecía una aplicación real mucho antes de lo que esperaba.
Los primeros problemas aparecieron cuando ejecuté las pruebas que había obligado a escribir al agente.
Uno de ellos falló inmediatamente:
The expected [App\Jobs\ProcessIncomingMessage] job was not dispatched.
Esto parecía más fácil de arreglar de lo que realmente era.
El agente no rastreó el fallo hasta el controlador. Se quedó en la superficie, especulando sobre la configuración y la configuración de la prueba, a pesar de que el error era consistente: el webhook devolvió una respuesta, pero el trabajo nunca fue enviado.
Una vez que inspeccionó la prueba y el controlador uno al lado del otro, finalmente encontró el problema. El controlador validaba la solicitud y devolvía una respuesta, pero nunca activaba el resto del sistema. La solución era una sola línea:
ProcessIncomingMessage::dispatch($data['to'], $data['from'], $data['message']);
Después de eso, la prueba pasó. Hasta este punto, el agente había sido muy eficaz en el andamiaje. Producía código que parecía correcto y seguía las convenciones del framework. Pero no era fiable garantizar que el sistema se comportó de extremo a extremo.
Al forzar las pruebas en el flujo de trabajo, acabé con una versión ligera de TDD. La prueba definía el comportamiento, y el agente tenía que iterar hasta que la implementación coincidiera con él. No era un bucle suave, pero me dio una señal estable para dirigir contra.
El agente se encargaba del trabajo repetitivo mientras yo me centraba en alinearlo con el comportamiento esperado. Esta es una forma ideal de dividir el trabajo entre agentes y humanos.
No porque el agente fuera perfecto, sino porque hacía que algo como el TDD fuera más fácil de sostener de lo que suele ser a mano.
En este punto, pude enviar un mensaje a la aplicación y verlo registrado en el terminal:
[2026-03-09 17:15:00] local.INFO: WhatsApp webhook received {
"from":"1***2364506",
"to":"1***3508504",
"message_type":"text",
"text_body":"Let’s test the Vonage WhatsAppp Service"
}
Fase 2: La aplicación empieza a responder
Ver: second_todo.md para las tareas del agente.
La estructura básica estaba en su sitio, pero había un problema. No recibía mensajes de la aplicación. El flujo debería haber sido: el usuario envía un mensaje de WhatsApp, llega al webhook, fluye a través de Laravel, obtiene una respuesta de OpenAI y luego vuelve a salir a través de Vonage.
Parecía como si todo esto existiera. El agente había organizado las tareas, los servicios y las integraciones. Los mensajes llegaban a la aplicación y, obviamente, no había nada roto. Pero seguía sin funcionar de extremo a extremo.
Fallo en tiempo real en el último paso
La primera vez que el sistema se ejecutó por completo, falló en lo más profundo de la tubería con un error de ejecución:
Sending message via Vonage {"to":"unknown","type":"text"}
Undefined array key "to"Se trata de un tipo específico de fallo que sólo aparece una vez que el sistema está en su mayor parte cableado. Debido a que el agente había estado construyendo todas las piezas de forma aislada, todo parecía correcto. Y todo estaba funcionando: el webhook recibió el mensaje, el trabajo fue enviado, OpenAI generó una respuesta.
El fallo sólo aparecía al intentar enviar la respuesta. Mirando los logs, el problema era sutil:
{
"to": unknown,
"type": "text"
}
El sistema intentaba enviar un mensaje sin un destinatario real. En algún momento, ese valor se había perdido y había sido sustituido por "desconocido". Esto se debía a que, de forma aislada, el agente hacía que todo se viera bien, pero perdía de vista el panorama general y cómo estaba todo conectado.
El copiloto seguía arreglando la capa equivocada
Este fue el primer punto en el que el agente empezó a tener problemas. No ignoró el error. Siguió proponiendo soluciones. Pero, una vez más, sus propuestas se quedaron cerca de la superficie: ajustar las indicaciones, modificar la forma en que se analizaba la respuesta de OpenAI, cambiar los formatos de respuesta...
Todas ideas razonables, pero ninguna abordaba el fallo real. El problema no era cómo se generaba la respuesta. Era cómo se movían los datos por el sistema.
Ocurrían tres cosas a la vez: el modelo podía definir campos como alos valores nulos fluían a través de varias capas y nada obligaba a rellenar los campos obligatorios antes de enviarlos. Esta combinación hacía que el sistema solo fallara al final, cuando Vonage intentaba utilizar los datos.
Incluso después de algunas iteraciones, el agente no convergió en eso. Siguió produciendo arreglos plausibles que no cambiaron el resultado.
El cambio de agente cambió el enfoque
En ese momento, cambié de CoPilot con GPT-5 Mini a mi IDE favorito, Windsurf con Claude Sonnety le di los registros completos en lugar de sólo el error. La diferencia no fue que escribiera mejor código. Enfocó el problema de forma diferente. En lugar de centrarse en la línea que fallaba, rastreó todo el camino del mensaje:
webhook → OpenAI → trabajo → Vonage
A continuación, añadió comprobaciones en cada frontera: validación temprana de las entradas, imposición de los campos obligatorios antes del envío y registro del estado intermedio.
El cambio importante era dónde se producía la validación. Hasta ese momento, el sistema asumía que todo era válido y fallaba tarde. Después de este paso, empezó a fallar pronto, en lugares predecibles. Una vez establecidas estas comprobaciones, el problema "a": "desconocido" desapareció.
Claude analyzing the error across the full message pipeline and applying validation at multiple layers instead of patching a single failure point.
El agente intentó reinventar la rueda
Una vez corregido el flujo de datos, el sistema avanzó e inmediatamente se encontró con el siguiente problema. Los mensajes salientes seguían fallando, esta vez con el error 401 No autorizado. La respuesta del agente fue interesante. En lugar de utilizar las herramientas existentes, empezó a reconstruir la capa de autenticación desde cero:
generación manual de JWT
using openssl_sign()
construcción manual de fichas
envío de solicitudes HTTP sin procesar
A primera vista, el código parecía razonable para la IA. Pero nunca funcionó de forma fiable. Este es un modo de fallo diferente. En este caso, estaba adivinando cómo funcionaba la autenticación de Vonage en lugar de usar las convenciones del SDK.
El punto de inflexión: Forzar la documentación a través de MCP
La solución vino de una indicación muy directa:
¿Por qué no has utilizado la herramienta de documentación de Vonage para generar un JWT a través del SDK? Seguimos fallando al intentar crearlos manualmente.
En lugar de seguir improvisando, el agente tiró de la herramienta MCP de documentación de Vonage y cambió de enfoque. Utilizó el Vonage PHP SDKy dejó que el SDK manejara la generación de JWT directamente.
Eso eliminó el problema por completo.
Prompt that forced the agent to stop generating JWTs manually and use the Vonage documentation MCP tool and official SDK.
Fase 3: Añadir memoria de conversación
Ver: tercer_todo.md para las tareas del agente.
Llegados a este punto, tenía un chatbot de WhatsApp básico funcionando. Pero parecía muy tonto. Cada mensaje se gestionaba de forma aislada. El asistente respondía correctamente, pero no recordaba nada de lo anterior.
El objetivo aquí era sencillo: almacenar el historial de conversaciones, incluir los mensajes recientes en el aviso de OpenAI y hacerlo de una forma lo suficientemente sencilla como para no introducir nueva complejidad.
En comparación con la Fase 2, ésta parecía una ampliación sencilla.
Un primer pase sin contratiempos
El agente implementó la memoria de conversación con una base de datos SQLite sin muchos problemas.
El agente creó una Conversación y una tabla, comenzó a almacenar los mensajes entrantes en el trabajo existente, y pasó los mensajes recientes al prompt de OpenAI. Debido a que el resto de la aplicación ya estaba estructurada de una manera bastante estándar en Laravel, esto encajó de forma natural. No hubo muchas idas y venidas ni depuración.
Este es uno de los aspectos en los que la estructura anterior ha merecido la pena. Una vez que la aplicación tiene forma, los agentes tienden a ampliarla de manera predecible.
"Trabajar" no era realmente trabajar
Tras la implementación inicial, la función parecía funcionar. La aplicación respondía a los mensajes. Hacía referencia al contexto anterior. Parecía que la memoria estaba en su sitio.
Pero una rápida prueba en el mundo real puso de manifiesto el problema: el asistente no recordaba la conversación correctamente. El sistema no fallaba del todo. Se comportaba de forma incoherente. Se trataba de un fallo diferente al de fases anteriores.
Example of inconsistent conversation memory: the assistant initially references prior context (user name), but fails to connect follow-up messages about Tel Aviv and May.
El fallo sutil: memoria incompleta
Cuando el agente examinó lo que se estaba almacenando realmente, el problema quedó claro. Sólo se guardaban los mensajes de los usuarios. Las respuestas de los asistentes no.
Así que la historia de la conversación al entrar en OpenAI se parecía a esto:
User: I want to plan a trip to Tel Aviv
User: Can you help me plan this trip in May?Desde la perspectiva de la modelo, no hubo conversación. No hay constancia de cómo respondió el asistente, así que no tiene nada en lo que basarse. ¡Imagina que intentas mantener una conversación pero solo oyes la mitad de lo que se dice!
En lugar de tratarlo todo como un único flujo de mensajes, el sistema necesitaba hacer un seguimiento explícito de ambas partes. El agente actualizó la estructura para almacenar tanto las entradas del usuario como las respuestas del asistente, reconstruir la conversación como turnos alternos y, a continuación, pasar los últimos turnos a OpenAI.
Una vez hecho esto, el comportamiento se ajustó a las expectativas. Las preguntas de seguimiento empezaron a tener sentido porque el modelo podía ver realmente el intercambio anterior.
Un punto de fallo más: Pruebas y migraciones
También fue la primera vez que la persistencia empezó a afectar al conjunto de pruebas. Todo funcionaba localmente, pero las pruebas empezaron a fallar inmediatamente con:
SQLSTATE[HY000]: no such table: conversationsNo era un problema de la aplicación. La base de datos de prueba no tenía la nueva tabla. Las migraciones no se estaban ejecutando como parte de la configuración de prueba. Un desarrollador de Laravel conocería la solución: usar RefreshDatabase para que las migraciones se ejecuten en las pruebas y actualicen los payloads de las pruebas para incluir los campos necesarios, como desde.
Una vez colocado esto, las pruebas volvieron a pasar.
Fase 4: Mensajes estructurados (la MCP se convierte en obligatoria)
Ver: cuarto_todo.md para las tareas del agente.
A estas alturas, la aplicación ya podía enviar y recibir mensajes de forma fiable. Pero sólo mensajes de texto. El paso final fue aprovechar todo el potencial de WhatsApp y hacer algo genial añadiendo:
botones de respuesta
listas interactivas
Parecía una pequeña ampliación, pero se ha convertido en la parte del proyecto más compleja de integrar.
Carga útil plausible rechazada por WhatsApp
El primer intento parecía funcionar. OpenAI devolvía respuestas estructuradas como:
{
"type": "list",
"content": {
"items": [...]
}
}
Desde el lado de Laravel, todo parecía correcto: 1) la respuesta de OpenAI volvió en el formato esperado, 2) el trabajo se ejecutó y 3) se llamó al servicio de Vonage.
Pero el paso final falló con:
Invalid params: The value of one or more parameters is invalid.O a veces, lo que es más confuso, no aparecía nada en WhatsApp. Desde la perspectiva de Laravel, todo funcionaba. La API no estaba de acuerdo.
Laravel logs showing a WhatsApp list message flowing through OpenAI and Vonage correctly, but failing at the final step with an “Invalid params” error due to a malformed interactive payload.El agente respondió como lo había hecho en fases anteriores. Intentó cambiar la forma de la carga útil, ajustar los formatos e incluso volver a texto sin formato para que pasara algo. Técnicamente funcionó, pero evitó el verdadero problema.
Los mensajes interactivos de WhatsApp no son sólo texto estructurado. Deben seguir esquemas estrictos, y las pequeñas desviaciones los invalidan. El agente no tenía un modelo fiable de esas restricciones, así que seguía generando cargas útiles que parecían razonables pero no eran aceptadas.
Desajuste de mensajes: SDK vs Realidad
Tras unas cuantas iteraciones, apareció un error diferente:
Argument #1 ($message) must be of type BaseMessage, array given
El SDK de Vonage espera objetos de mensaje tipificados, mientras que los mensajes interactivos requieren JSON profundamente anidado. El agente pasó matrices que coincidían con la forma de la carga útil, pero el SDK las rechazó.
En ese momento, el problema no era sólo la carga útil, sino también la capa por la que intentábamos enviarla.
Segundo asalto: forzar la documentación a través de MCP
Aquí es donde la herramienta MCP de documentación de Vonage volvió a ser fundamental.
En lugar de seguir remodelando las cargas útiles, empujé al agente a inspeccionar los esquemas reales de los mensajes de WhatsApp y trabajar a partir de ahí.
El enfoque cambió muy rápidamente:
utilizar el SDK donde sea útil (autenticación, cliente base)
enviar mensajes interactivos a través de HTTP sin procesar
crear cargas útiles que coincidan exactamente con la estructura documentada
Una vez que el agente tuvo el esquema delante, el agente tuvo éxito.
Using the Vonage documentation MCP tool to confirm the correct SDK-based approach before refactoring the integration.
Conclusión
Dos días antes de esto, no habría elegido Laravel para nada más allá de un tutorial. Al final, tenía una aplicación de WhatsApp con OpenAI, memoria de conversación y mensajería estructurada. Pero más importante que la aplicación era entender cómo funciona realmente esta forma de construir.
El agente era servicial, pero no autónomo. Manejaba bien el andamiaje y las extensiones, especialmente una vez que la estructura estaba en su sitio. Donde tuvo problemas fue en los límites (flujo de datos, autenticación, API estrictas), donde "casi correcto" no es suficiente.
Lo que marcó la diferencia fue el flujo de trabajo. Dividir el trabajo en fases, forzar las pruebas desde el principio y utilizar herramientas MCP para vincular el agente a la documentación real me permitió hacer que las funciones complicadas funcionaran con rapidez. Sin eso, el agente tendía a adivinar. Con el flujo de trabajo adecuado, me permitió crear una aplicación cuya sintaxis no puedo explicar.
Próximos pasos
A partir de aquí, hay mucho margen para ampliar la aplicación. Podrías añadir nuevos tipos de mensajes de WhatsApp, como mensajes de ubicación para mostrar a los usuarios dónde se encuentran exactamente los lugares de su itinerario, o utilizar mensajes de archivo para generar y enviar una versión en PDF de un plan de viaje. Este tipo de adiciones impulsan aún más la integración y ponen de relieve que las API estructuradas y un flujo de datos claro son realmente importantes.
La conclusión es bastante sencilla: con un agente puedes adentrarte en pilas desconocidas mucho más rápido, pero sólo si mantienes el control de la estructura y los límites. Si quieres explorar el proyecto completo, incluido el flujo de trabajo por fases, el código completo está disponible en el repositorio de repositorio GitHub.
¿Tienes alguna pregunta o algo que compartir? Únete a la conversación en Slack de la comunidad de Vonagey mantente actualizado con el Boletín para desarrolladoressíguenos en X (antes Twitter)suscríbete a nuestro canal de YouTube para ver tutoriales en video, y sigue la página de página para desarrolladores de Vonage en LinkedInun espacio para que los desarrolladores aprendan y se conecten con la comunidad. Mantente conectado, comparte tu progreso y entérate de las últimas noticias, consejos y eventos para desarrolladores.
Compartir:
Benjamin Aronov es desarrollador de Vonage. Es un constructor de comunidades con experiencia en Ruby on Rails. Benjamin disfruta de las playas de Tel Aviv, a la que llama hogar. Su base en Tel Aviv le permite conocer y aprender de algunos de los mejores fundadores de startups del mundo. Fuera de la tecnología, a Benjamin le encanta viajar por el mundo en busca del perfecto pain au chocolat.
