Construye aplicaciones con Laravel Agents SDK AI

Laravel AI SDK. Esta herramienta no es para ayudarte a programar, sino para que tu aplicación sea inteligente.

Si quieres que tu usuario pueda:

  • Generar una imagen de una "dona espacial" desde un formulario.
  • Traducir un comentario automáticamente.
  • Interactuar con un chatbot que use modelos locales o externos.

Entonces usas el SDK. Es una librería que te permite lanzar prompts directamente desde tu código PHP. A diferencia de Boost, esta es una dependencia normal de la aplicación, ya que formará parte de las funcionalidades que tus usuarios finales van a disfrutar.

Instalación y Configuración del SDK de Laravel AI

A diferencia de Laravel Boost, el SDK de Laravel AI es el que nos permite ensuciarnos las manos creando nuestros propios prompts y lógicas de inteligencia artificial dentro de la app.

Primeros pasos:

  1. Instalación: Ejecutamos el comando de instalación:
    1. $ composer require laravel/ai
    2. $ php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
    3. $ php artisan migrate
  2. Base de Datos: Verás que se crean migraciones para tablas de conversaciones y agentes. Es fundamental ejecutar php artisan migrate para que el SDK tenga dónde guardar el historial y la configuración.
  3. Clave de API: En tu archivo .env, debes configurar tu clave.
    1. ANTHROPIC_API_KEY=
      COHERE_API_KEY=
      ELEVENLABS_API_KEY=
      GEMINI_API_KEY=
      MISTRAL_API_KEY=
      OLLAMA_API_KEY=
      OPENAI_API_KEY=
      JINA_API_KEY=
      VOYAGEAI_API_KEY=
      XAI_API_KEY=

Si buscas una opción gratuita para empezar, Google AI Studio es de las mejores. Solo tienes que entrar, generar tu API Key de Gemini y pegarla en tu proyecto.

El Corazón del SDK: Los Agentes

La gran diferencia aquí es que todo funciona mediante Agentes.

  • Un Agente es como un "empleado virtual" al que le asignas una tarea específica (ej: "Eres un traductor", "Eres un experto en SEO", "Eres un generador de imágenes").
  • Una vez configurado el agente, tú interactúas con él mediante prompts.
use Laravel\Ai\Enums\Lab;
use function Laravel\Ai\agent;
Route::get('/laravel-ia-text', function () {
    $response = agent(
        instructions: 'Eres un asistente experto en Laravel.',)->prompt(
        'Genera una lista de 3 temas de Laravel 13 en formato JSON',
        provider: Lab::Gemini,
        model: <YOURIDLOCALMODAL>,);
    
    dd($response);
});

Esto que vimos, es un agente anónimo ya que, la estructura recomendada es mediante agentes que veremos más adelante.

Google Antigravity: Potencia sin Complicaciones

Ya superamos la curva de aprendizaje de Laravel Boost y OpenCode, así que Google Antigravity te va a parecer un paseo. Es más sencillo porque es menos abstracto: es un IDE que ya trae la IA "en el ADN".

La gran diferencia: Integración vs. Personalización

Aquí hay que ser claros: no hacen "match".

  • OpenCode (VS Code): Es un entorno abierto. Tú instalas los agentes, configuras los archivos Markdown y tienes un control quirúrgico.
  • Anti Gravity: Es un entorno cerrado. Es más limitado en cuanto a reglas externas (no puedes copiar y pegar directamente las guías de OpenCode aquí), pero a cambio te ofrece modelos de primer nivel (Gemini 3 Pro, Claude, Sonnet) de forma gratuita en su plan actual.

Configuración y Reglas (Custom Instructions)

Aunque es más cerrado, no significa que no puedas darle órdenes. En los tres puntos de la interfaz puedes personalizar las reglas, ya sea de forma global o para un espacio de trabajo específico.

  • Al guardar estas reglas, se genera un archivo gemini.md (muy parecido a lo que vimos antes).
  • Mi consejo: Yo no pierdo mucho tiempo configurando esto aquí. Uso Anti Gravity para desarrollos puntuales donde yo mismo le doy toda la información necesaria en el prompt.

Agentes: Planificación vs. Fast (Fase)

Al igual que en OpenCode, aquí tenemos dos modos de trabajo:

  1. Modo Planificación: Ideal para tareas complejas. La IA analiza todo el proyecto, propone un plan y, si le das el OK, ejecuta. Incluso puede levantar un navegador (browser) para verificar si lo que hizo funciona en tiempo real.
  2. Modo Fast (Fase): Es el que más uso. Es para cambios rápidos y directos. Le dices: "Modifica este archivo para incluir el plugin de CKEditor" y lo hace al instante.

Mi Flujo de Trabajo

Cuando trabajo con AntiGravity, sigo un ciclo muy marcado:

  1. Iteración: Pido el cambio y espero a que termine.
  2. Prueba: Si funciona, sincronizo con Git (git commit).
  3. Corrección: Si no funciona, le pido que corrija.
  4. Reversión: Si veo que la IA se está liando y no da pie con bola, tiro de un git checkout o git reset, limpio la mesa y vuelvo a empezar desde un punto seguro.

Ventajas visuales

Una cosa que me encanta de esta herramienta es la ayuda visual. Te marca en amarillo exactamente qué líneas va a cambiar antes de aplicarlas. Eso te da una seguridad que no siempre tienes en la terminal de VS Code.

¿Cuál elegir? OpenCode vs. Antigravity

Si estás en el plan gratuito, este es el dilema:

  • OpenCode: Tienes una personalización total, puedes usar todos los skills de Laravel Boost y tienes una flexibilidad enorme, pero requiere que conozcas bien la herramienta.
  • Antigravity: Tienes los mejores modelos del mercado (incluso Claude, GPT…) gratis y una interfaz mucho más amable para el desarrollo rápido, pero con menos capacidad de auditoría profunda.

En resumen: uso OpenCode para auditorías, revisiones de base de datos o arquitecturas complejas de Laravel, y Antigravity para crear funcionalidades nuevas de forma rápida y visual.

Práctica de Rediseño con Google Antigravity

Vamos a poner a prueba Google Anti Gravity con un ejercicio real: un rediseño sencillo de la aplicación Laravel que estamos construyendo. Aunque confío en la herramienta, mi primera recomendación es siempre la misma: sincroniza con Git antes de empezar. Así, si el resultado no te gusta, un git reset te devuelve a la vida en segundos.

1. Preparando el Escenario (Contexto)

Para que la IA no alucine, lo mejor es darle contexto. En este caso:

  • Copié la ruta relativa de la carpeta css (donde están los estilos del blog y del dashboard).
  • Busqué una imagen de inspiración en internet para dársela como referencia visual.

2. El Prompt: Claro y Directo

Mi objetivo no es cambiar la estructura HTML (para no complicar el curso), sino solo el estilo. Usé el modo Fast  y el modelo Gemini 3 Flash. El prompt fue algo así:

"Haz un sencillo rediseño de la app, sobreescribiendo en lo posible solamente el CSS que está en la ruta:

resources/css/blog.css

resources/css/dashboard.css

 Quiero que te inspires en la imagen adjunta."

Nota: Si tienes dudas sobre cómo abordar un cambio grande, usa el modo Planificación; pero para algo tan puntual como esto, el modo Fast es perfecto.

Ejecución y Resultados

A diferencia de OpenCode, aquí ves cómo la IA empieza a escanear el proyecto de forma visual. Te marca en amarillo exactamente qué líneas de CSS va a cambiar o añadir. Es una ayuda visual brutal.

Este es un ejemplo perfecto de cómo interactuar con la IA en Anti Gravity:

  1. Pides: "Rediseña esto".
  2. Esperas: Ves cómo aplica los cambios visualmente.
  3. Pruebas: Te registras, te autenticas y navegas por la app.
  4. Decides: ¿Te gusta el resultado? Si la respuesta es "está bastante bien", sincronizas. Si no, reviertes con Git.

En mi caso, aunque hubo pequeños fallos que corregiré "por detrás", el resultado me sirve.

Implementar dialogo de confirmación al eliminar

Para cerrar esta práctica con Google Antigravity, vamos a resolver un detalle pendiente: el diálogo de confirmación para eliminar registros. Actualmente, si presionas "Eliminar", el post se borra directamente sin preguntar, lo cual es un peligro.

El Prompt y el Contexto

Lo bueno de Antigravity es que no hace falta ser excesivamente específico si el código está bien estructurado. Simplemente le pasé el archivo index.blade.php de categorías y le pedí:

Implementa un diálogo de confirmación para el botón de eliminar:

resources/views/dashboard/post/index.blade.php

resources/views/dashboard/category/index.blade.php

No tuve que decirle cuál era el botón; la IA analizó el contexto, encontró la etiqueta que dice "Eliminar" y entendió perfectamente dónde debía intervenir.

La Solución: JavaScript Nativo y Efectivo

Fíjate en lo que hizo la IA (puedes verlo en los globos amarillos de cambios): implementó una solución con JavaScript directamente en el evento onsubmit del formulario.

resources/views/dashboard/post/index.blade.php

resources/views/dashboard/category/index.blade.php

<form action="{{ route('category.destroy', $category) }}" method="POST" onsubmit="return confirm('¿Estás seguro de que deseas eliminar esta categoría?')">

¿Cómo funciona esta lógica?

  • Utiliza la función nativa confirm(), que despliega una ventana emergente en el navegador.
  • Esta función retorna un true (si das a Aceptar) o un false (si das a Cancelar).
  • Al colocarlo dentro del onsubmit, si el usuario cancela, el formulario recibe un false y detiene el envío automáticamente. Es una forma limpia y directa de proteger los datos sin necesidad de instalar librerías pesadas.

Conclusión

Esta segunda práctica demuestra por qué uso tanto Antigravity para lo que llamo "desarrollo espiritual" (esos ajustes rápidos que te dan paz mental).

Si hubiéramos querido algo más complejo, como un módulo separado en dashboard.js, la IA se habría encargado de crear el archivo, exportar el módulo e importarlo correctamente. Lo que a nosotros nos tomaría varios minutos de "carpintería" de código, la IA lo resuelve en 3 o 4 minutos.

Laravel AI SDK

¿Cómo usar un LLM Local con Laravel AI SDK y Jan.ai o LM Studio?

En este apartado te mostraré cómo emplear un modelo de lenguaje (LLM) local utilizando Jan.ai o LM Studio en conjunto con el Laravel AI SDK. Esta es una solución ideal si te encuentras en regiones donde servicios como Gemini o ChatGPT están bloqueados, o si simplemente buscas privacidad y ahorro de costos.

¿Qué es un LLM local?

Un LLM (Large Language Model) es una inteligencia artificial diseñada para comprender y generar lenguaje humano. Al ejecutarlo de forma local, aprovechas la potencia de tu propia máquina sin depender de una conexión a internet o de planes de pago. Las ventajas son claras:

  • Privacidad: Tus datos nunca salen de tu equipo.
  • Sin costos: No hay límites de tokens ni suscripciones mensuales.
  • Sin bloqueos: No dependes de restricciones geográficas.

Herramientas y Requisitos

Para correr una IA local, necesitas un software que gestione el modelo y un equipo con cierta potencia. Las opciones más populares son Ollama, LM Studio y Jan.ai.

Consideraciones de Hardware (RAM vs. VRAM)

La potencia del modelo que elijas dependerá de tu memoria:

  • Windows/Linux: Se apoyan mucho en la VRAM de la tarjeta de video. Si el modelo supera tu capacidad (ej. tienes 12GB de VRAM e intentas correr un modelo pesado), el sistema usará la memoria RAM, y el rendimiento caerá drásticamente.
  • Mac (Chips M): Utilizan memoria unificada. Esto significa que la CPU y la GPU comparten la misma RAM (ej. 24GB), lo que permite ejecutar modelos más grandes (como los de 31B de parámetros) con mayor fluidez.

Configuración del Servidor Local

Tanto Jan.ai como LM Studio siguen el estándar de la API de OpenAI, lo que facilita enormemente la integración con Laravel.

  • Instalación: Descarga e instala Jan.ai desde su sitio oficial.
  • Activar el Servidor: Ve a Settings > Local API Server y actívalo.
  • Endpoint: Por defecto, el servidor correrá en http://localhost:1337/v1.
  • API Key: Puedes configurar una clave sencilla (ej. 12345) en los ajustes del servidor local.

LM Studio al igual que Jan.IA es otra excelente opción que tenemos a nuestra disposición para instalar IAs en local y también cuenta con un servidor local al igual que Jan.IA, por lo tanto, podemos utilizarlo también para nuestro proyecto en Flask SIN PRACTICAMENTE CAMBIOS, pero, a diferencia de Jan, LM Studio NO emplea un token BEARER de protección y por defecto su puerto es el 1234 (que puedes cambiar); en la siguiente tabla, te resumo lo comentado

CaracterísticaJan.iaLM Studio
URL por defectohttp://localhost:1337/v1http://localhost:1234/v1
API KeyCualquier texto (ej. "sk-local")No la requiere / Ignorada
CompatibilidadEstilo OpenAI (v1)100% OpenAI-compatible
HeadersContent-Type + AuthorizationContent-Type (Auth opcional)
Motor Principalnitro (especializado en GGUF)llama.cpp

Puedes descargar LM Studio desde:

https://lmstudio.ai/

Su funcionamiento es muy similar al de Jan IA, así que no te perderás, por lo demás, LM Studio al igual que Jan, también puedes conectarte exactamente igual, porque LM Studio expone una API 100% compatible con OpenAI en los cuales, tenemos algunos endpoints interesantes que podemos emplear:

  • v1/chat/completions
  • /v1/models

LM Studio no usa API KEY

Esto significa que debes quitar el header Authorization, porque si lo mandas, LM Studio lo ignora o te lanza 401 en algunos casos.

Integración con Laravel AI SDK

Para conectar Laravel con tu IA local, debemos realizar un pequeño "truco" en el archivo .env. Como el SDK de Laravel no tiene un driver nativo para Jan.ai, utilizaremos el driver de Deepseek o OpenAI para "puentear" (OpenAI Driver Emulation / Emulación del Driver de OpenAI) la conexión.

Configuración en el .env

# Usamos el driver de Deepseek para aprovechar su compatibilidad de endpoints
AI_DRIVER=deepseek
DEEPSEEK_API_KEY=tu_clave_local_12345
DEEPSEEK_BASE_URL=http://localhost:1337/v1

Publicas el archivo de configuración:

$ php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"

Conecta un LLM Local con Jan IA a una app en Python con Flask

En este apartado, vamos a conectarnos al LLM en Local.

Activar el servidor local

Antes de conectar, debemos activar el servidor Jan.IA:

Settings → Local App Server → Establecer tu clave → Start

En LM Studio: Ve a la pestaña de Local Server (el icono de doble flecha o de desarrollador).

Presiona el botón Start Server. (A diferencia de Jan.ai, aquí no necesitas configurar una API Key; el acceso es directo a través de la URL base que te proporciona el programa.)

Controlador base para conectarse al LLM

En cuanto a lo principal, el controlador que se va a conectar al LLM, tenemos:

  • URL y cabeceras

Definimos la URL del endpoint al que le haremos la petición:

http://localhost:port/v1/chat/completions

Y las cabeceras que Jam necesita:

headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer 12345"
}
  • Modelo → el que tengas seleccionado en Jam (debes escribirlo idéntico, ejemplo: yema-3-12b).
  • Mensaje → array JSON con el rol user y el contenido.

En este ejemplo: “cuéntame una broma”.

data = {
    "model": "gemma-3-12b-it-IQ4_XS",
    "messages": [
        #   {"role": "user", "content": request.form.get("prompt", "Tell me a joke.")}
        {"role": "user", "content": prompt}
    ]
}

Haciendo la petición

Como esto puede fallar, lo envolvemos en un try.

Realizamos un requests.post(...) enviando:

  • URL.
  • headers.
  • json=data (no body, porque es JSON explícitamente).

Luego validamos la respuesta: si response.status_code == 200, todo perfecto.

  • 404 sería ruta no encontrada.
  • 500 = algo explotó en el servidor o mandaste algo inválido.

Ejecución en el código

Para realizar una petición, puedes usar un agente o una llamada directa. Es vital que el nombre del modelo coincida exactamente con el ID que te proporciona Jan.ai (puedes consultarlo en la lista de modelos descargados o vía Postman).

use Illuminate\Support\Facades\AI;
$response = AI::chat('Genera una lista de 3 temas de Laravel 13 en formato JSON')
   ->withDriver('deepseek')
   ->withModel('gemma-2-9b-it') // Reemplaza por tu ID de modelo local
   ->send();
return $response;

Adicionalmente, usamos deepseek como "puente" para conectar Laravel con herramientas locales como Jan.ia o LM Studio:

1. El estándar de la industria (OpenAI API Format)

Casi todas las herramientas locales (Jan.ia, LM Studio, Ollama) no inventan su propia forma de recibir datos. En su lugar, imitan (clonan) la estructura de la API de OpenAI.

Prism-php y el SDK de Laravel AI suelen tener drivers específicos. Si el driver de openai está configurado para apuntar a la nube de OpenAI, a veces se recomienda usar el de deepseek porque:

  • DeepSeek usa exactamente el mismo formato de API que OpenAI.
  • Permite cambiar la base_url de forma sencilla hacia http://127.0.0.1:1337/v1 (tu Jan.ia).
  • Evita conflictos si ya estás usando el driver de openai oficial para otros servicios en la misma app.

Aunque el driver se llame deepseek, eso es solo el "cartero" que lleva el mensaje. El parámetro ->withModel('gemma-2-9b-it') es el que le dice a Jan.ia qué modelo debe usar realmente tu hardware.


¿Qué está pasando realmente?

Cuando ejecutas ese código:

  • Laravel le pasa el paquete al driver deepseek.
  • Prism-php lo envía al puerto 1337 de tu propia máquina.
  • Jan.ia recibe la petición, ve que pides gemma-2-9b-it y despierta a tu RTX 5070 para procesarlo.
  • Gemma genera el JSON y lo devuelve por el mismo camino.

Que modelo local seleccionar

Si no sabes qué modelo descargar, te recomiendo empezar por Gemma (de Google) o Llama. Si tienes dudas sobre cuál soporta tu equipo, puedes tomar una captura de tus especificaciones técnicas y preguntarle a la propia IA; ella te dirá si te conviene un modelo ligero de 7B parámetros o uno más robusto de 31B.

Prompt: Tengo una mac mini m4 pro, cual uso, dame una descripción para que usar cada uno y cual me recomienda

  1. Los "Pesados" (31B) - Para Razonamiento Complejo
    Modelos con 31 billones de parámetros. Son los más inteligentes de la lista.
    1. Gemma-4-31B-It-GGUF:
    2. Para qué: Es ideal para cuando necesitas que la IA entienda arquitectura de software compleja, resuelva bugs difíciles en Laravel 13 o redacte contenido educativo profundo para tu academia.
    3. Rendimiento en M4 Pro: Irá bien, pero consumirá mucha RAM (unos 16-18 GB solo el modelo). Si tienes 24GB o más de RAM, este es tu modelo principal.
  2. El "Equilibrio" (26B) - El todoterreno
    Modelos con 26 billones de parámetros.
    1. Gemma-4-26B-A4B-It-GGUF:
    2. Para qué: Es una arquitectura "Mixture of Experts" (MoE). Es más rápido que el de 31B pero mantiene casi la misma inteligencia. Perfecto para generación de código diaria y testing unitario.
    3. Rendimiento en M4 Pro: Volará. Es el punto dulce entre velocidad y "cerebro".
  3. Los "Ligeros" (E4B / E2B) - Para Automatización y Velocidad
    Modelos de 4 y 2 billones de parámetros.
    1. Gemma-4-E4B / E2B:
    2. Para qué: Son modelos "Edge", diseñados para ser ultra rápidos. Úsalos para tareas simples como autocompletado de código, formatear JSON o resumir textos cortos.
    3. Rendimiento en M4 Pro: Instantáneo. Casi no notarás que está procesando.

Análisis para la RTX 5070

A diferencia del procesador de Apple, la 5070 tiene núcleos Tensor diseñados específicamente para IA. Esto significa que los modelos que elijas responderán casi en tiempo real.

  • Gemma-4-26B-A4B-It-GGUF (Recomendado):
    • Por qué: Al ser una arquitectura MoE (Mixture of Experts) de 26B, solo activa una fracción de sus parámetros para cada palabra (en este caso, 4 billones activos).
    • En la 5070: Cabrá muy bien en la VRAM si usas una cuantización tipo Q4_K_M o Q5. Tendrás una velocidad de generación de texto superior a la de la Mac.
    • Gemma-4-31B-It-GGUF (El límite):
  • Por qué: Este modelo es denso (usa los 31B siempre).
    • En la 5070: Es probable que "desborde" la VRAM de la tarjeta. Cuando un modelo no cabe en la tarjeta de video, se pasa a la memoria RAM normal (el i7 14700K), y la velocidad cae drásticamente (de 50 tokens por segundo a 2 o 3).
    • Uso: Solo úsalo si necesitas la máxima precisión posible en un tutorial complejo y no te importa esperar unos segundos más.
    • Gemma-4-E4B-It-GGUF (Para desarrollo):
  • Por qué: Es pequeño (4B).
    • En la 5070: Es instantáneo. Puedes usarlo como un copiloto que te sugiera código en tiempo real mientras escribes tus libros o cursos de Laravel sin que el ventilador de la GPU se inmute.

¡Con esto ya tienes tu propio entorno de inteligencia artificial totalmente privado y funcional en Laravel!

Ejemplos Agentes Anónimos Prácticos: Chat, Código y Análisis de Sentimiento

Para que tengas más variedad, he preparado tres ejemplos adicionales. Aunque la base es similar a lo que hemos visto, estos casos te ayudarán a visualizar cómo estructurar un proyecto real y funcional en el futuro, más allá de una simple petición estática.

He creado un nuevo controlador con tres métodos específicos:

  • Chat General: Una conversación abierta.
  • Generación de Código: Creación de estructuras como migraciones de Laravel.
  • Análisis de Sentimiento: Determinar si un mensaje es positivo, negativo o neutro.

routes\web.php

// Chat básico
Route::get('/agents/chat', [AgentsTestController::class, 'chat']);
// Generación de código
Route::get('/agents/generar-codigo', [AgentsTestController::class, 'generarCodigo']);
// Análisis de sentimientos
Route::get('/agents/analizar', [AgentsTestController::class, 'analizar']);

app\Http\Controllers\AgentsTestController.php

<?php
namespace App\Http\Controllers;
use App\Ai\Agents\PokemonAgent;
use App\Ai\Agents\QuizAgent;
use App\Ai\Agents\QuizGenerator;
use App\Ai\Agents\SalesCoach;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Ai\Enums\Lab;
use function Laravel\Ai\agent;
/**
 * Controlador para ejemplos de uso de Gemma 3 con IA local (Ollama).
 */
class AgentsTestController extends Controller
{
    /**
     * Chat básico
     */
    public function chat(Request $request): JsonResponse
    {
        $mensaje = $request->input('mensaje', 'Hola, dime qué sabes sobre Laravel');
        $response = agent(
            instructions: 'Eres un asistente útil y conciso.',
        )->prompt(
                $mensaje,
                model: <YOURIDLOCALMODAL>
            );
        return response()->json([
            'respuesta' => $response->text,
            'modelo' => 'gemma3:12b-it-IQ4_XS (Ollama)',
        ]);
    }
    /**
     * Generación de código
     */
    public function generarCodigo(Request $request): JsonResponse
    {
        $tipo = $request->input('tipo', 'migration de usuarios');
        $response = agent(
            instructions: 'Eres un experto en Laravel. Generas código limpio y sigues las mejores prácticas.',
        )->prompt(
                "Genera {$tipo} en Laravel 13. Solo dame el código, sin explicaciones.",
                model: <YOURIDLOCALMODAL>
            );
        return response()->json([
            'codigo' => $response->text,
            'solicitud' => $tipo,
        ]);
    }
    /**
     * Análisis de texto
     */
    public function analizar(Request $request): JsonResponse
    {
        $texto = $request->input('texto', 'Este producto es increíble, lo recomiendo totalmente');
        $response = agent(
            instructions: 'Eres un analizador de sentimientos. Respondes solo con positivo, negativo o neutro.',
        )->prompt(
                "Clasifica el siguiente texto: {$texto}",
                model: <YOURIDLOCALMODAL>
            );
        return response()->json([
            'sentimiento' => trim($response->text),
            'texto_original' => $texto,
        ]);
    }
}

Análisis de los nuevos ejemplos

1. Dinamismo con request()->input()

Para que esto sea una aplicación web real, necesitamos que el usuario interactúe. He configurado los métodos para que busquen un campo llamado texto en la petición. Si no existe, usamos un valor por defecto:

$input = $request->input('texto', '¿Qué sabes sobre Laravel?');

Esto nos permitirá conectar fácilmente nuestra lógica a un formulario de usuario en el futuro.

2. Agentes Anónimos y Análisis de Sentimientos

En estos ejemplos estamos utilizando Agentes Anónimos. Son agentes que definimos "al vuelo" con instrucciones específicas. Por ejemplo, para el analizador de sentimientos:

Instrucción: "Eres un experto analizando sentimientos. Responde solo: Positivo, Negativo o Neutro".

Entrada: "¡Este producto es increíble, lo recomiendo!"

Resultado esperado: Positivo.

3. Generación de Código Personalizada

Aquí combinamos un prompt fijo con la entrada del usuario. Le indicamos a la IA que es un experto en Laravel 13 y que debe generar código sin explicaciones adicionales. 

$tipo = $request->input('tipo', 'migration de usuarios');
->prompt("Genera {$tipo} en Laravel 13. Solo dame el código, sin explicaciones.",***)

Si el usuario pide "migración de usuarios", el sistema genera el esquema de base de datos exacto.

Ejemplos de respuestas

http://larapackage.test/agents/chat

{"respuesta":"Laravel es un framework PHP de c\u00f3digo abierto dise\u00f1ado para facilitar el desarrollo de aplicaciones web siguiendo el patr\u00f3n MVC (Modelo\u2011Vista\u2011Controlador). Sus principales caracter\u00edsticas son:\n\n- **Eloquent ORM**: modelo activo y sencillo para interactuar con bases de datos.\n- **Rutas declarativas**: definici\u00f3n clara de URLs y controladores asociados.\n- **Middleware**: filtrado de peticiones HTTP antes de llegar a la l\u00f3gica de negocio.\n- **Blade**: motor de plantillas ligero que permite escribir vistas con sintaxis PHP limpia.\n- **Migrations & Seeders**: manejo estructurado del esquema de bases de datos y datos iniciales.\n- **Colas y eventos**: procesamiento as\u00edncrono y arquitectura basada en eventos.\n- **Seguridad**: protecci\u00f3n CSRF, hashing de contrase\u00f1as, autenticaci\u00f3n lista para usar.\n\nLaravel sigue el principio \u201cconvention over configuration\u201d, lo que acelera la productividad sin sacrificar flexibilidad. Es ideal tanto para proyectos peque\u00f1os como para aplicaciones empresariales complejas.","modelo":"gemma3:12b-it-IQ4_XS (Ollama)"}

http://larapackage.test/agents/generar-codigo

{"codigo":"```php\n<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('users', function (Blueprint $table) {\n            $table->id();\n            $table->string('name');\n            $table->string('email')->unique();\n            $table->timestamp('email_verified_at')->nullable();\n            $table->string('password');\n            $table->rememberToken();\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('users');\n    }\n};\n```","solicitud":"migration de usuarios"}

http://larapackage.test/agents/analizar

{"sentimiento":"positivo","texto_original":"Este producto es incre\u00edble, lo recomiendo totalmente"}

Observaciones finales sobre las respuestas

Recuerda que, aunque el prompt sea el mismo, las IAs son estocásticas; es decir, no siempre darán la misma respuesta exacta, lo cual es parte de la magia del procesamiento de lenguaje natural.

Si notas que la conexión tarda o falla ocasionalmente, suele ser por la carga de tu equipo (CPU/GPU) al procesar el modelo localmente y no un error de programación. ¡Ya tenemos los cimientos! En la próxima clase dejaremos de usar agentes anónimos para crear Agentes estructurados, con clases propias y una lógica mucho más robusta.

Hay muchos errores que pueden ocurrir como:

Illuminate\Http\Client\ConnectionException
vendor\laravel\framework\src\Illuminate\Http\Client\PendingRequest.php:1805
cURL error 28: Operation timed out after 60013 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://127.0.0.1:1337/v1/chat/completions
--
Error [400]: Unknown error

Agentes Estructurados: Más allá de los Agentes Anónimos

Hasta ahora hemos trabajado con agentes anónimos, que son la forma más rápida y sencilla de empezar. Sin embargo, en la documentación de Laravel AI SDK encontramos lo que podemos llamar "agentes con nombre" o clases de agentes. Estos nos permiten organizar la lógica de la IA de una manera mucho más profesional y reutilizable.

1. Estructura de un Agente

Al crear un agente (como el ejemplo de SalesCoach), nos encontramos con una estructura muy interesante:

  • Constructor: Permite pasar datos externos al agente.
  • Instrucciones (instructions): Define el comportamiento o rol de la IA.
  • Mensajes (messages): Un método interno para cargar conversaciones previas o contexto histórico. Esto es vital para que la IA no "comience de cero" en cada interacción.
  • Herramientas (tools): Permiten que la IA ejecute funciones programáticas (por ejemplo, generar un número aleatorio o consultar una API externa) para resolver tareas específicas.

Creación de Agentes vía CLI

Para generar estos agentes, el SDK nos ofrece dos comandos principales que definen cómo será la respuesta de la IA:

Agente Estándar:

$ php artisan make:agent SalesCoach

Crea una estructura básica con instrucciones, mensajes y herramientas, pero sin un formato de salida obligatorio.

Agente con Estructura (Schema):

$ php artisan make:agent SalesCoach --structured
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;
class SalesCoach implements Agent, HasTools //Conversational
{
    use Promptable;
    public function __construct(public ?User $user = null) {}
    /**
     * Get the instructions that the agent should follow.
     */
    public function instructions(): Stringable|string
    {
        return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
    }
    /**
     * Get the tools available to the agent.
     *
     * @return Tool[]
     */
    public function tools(): iterable
    {
        ***
    }
    /**
     * Get the agent's structured output schema definition.
     */
     public function schema(JsonSchema $schema): array
     {
         return [
             'feedback' => $schema->string()->required(),
             'score' => $schema->integer()->min(1)->max(10)->required(),
        ];
     }
}

Este es el más interesante para nosotros, ya que implementa un Esquema.

n Laravel estamos acostumbrados a que todo esté fuertemente estructurado. Si consultas un modelo Post, sabes exactamente qué campos esperar. Si usas un método all(), esperas una colección. Con la IA, si no definimos un esquema, el formato de respuesta puede variar y romper nuestra aplicación.

¿Por qué usar respuestas estructuradas?

Al definir un esquema, obligamos a la IA a que su respuesta encaje exactamente en el formato que necesitamos (generalmente JSON). Esto es excelente porque:

  • Integración: Puedes mapear la respuesta de la IA directamente a un componente de Livewire o a un controlador.
  • Consistencia: Si tienes una aplicación de planetas, puedes exigir que la IA siempre devuelva: nombre, descripción, diámetro y densidad.
  • Seguridad: Te aseguras de que tu código pueda procesar la data sin errores de formato inesperados.

El ejemplo Pokémon

Para este ejemplo, he configurado un agente experto en Pokémon. La instrucción es clara: "Eres un experto en Pokémon, genera datos realistas en formato JSON exacto". Aunque el SDK permite definir la estructura mediante código, siempre es buena práctica reforzar el formato deseado en el prompt.

Esta parte es fundamental porque funciona de forma muy similar a las validaciones de formularios en Laravel, garantizando la integridad de los datos.

routes\web.php

// Structured Output: Lista de Pokemon con PokemonAgent
Route::get('/agents/pokemon-lista', [AgentsTestController::class, 'listaPokemones']);

app\Http\Controllers\AgentsTestController.php

public function listaPokemones(Request $request): JsonResponse
{
    $resultado = (new PokemonAgent)->prompt(
        'Genera una lista de 3 Pokemon diferentes',
        model: <YOURIDLOCALMODAL>
        timeout: 120
    );
    // dd($resultado);
    return response()->json([
        'resultado' => $resultado->toArray(),
        'esquema' => [
            'pokemones' => [
                '*' => [
                    'nombre' => 'string',
                    'tipo' => 'enum',
                    'tamano' => 'integer',
                ],
            ],
        ],
    ]);
}

El agente queda como:

app\Ai\Agents\PokemonAgent.php

<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
 * Agente para generar lista de Pokemon con schema estructurado.
 *
 * Schema devuelto:
 * - pokemones: array de objetos
 *   - nombre: string
 *   - tipo: enum (fuego, agua, electrico, planta, normal, volador, psiquico)
 *   - tamano: integer (10-500 cm)
 */
// php artisan make:agent SimpleAgent --structured
class PokemonAgent implements Agent, HasStructuredOutput
{
    use Promptable;
    public function instructions(): Stringable|string
    {
        return 'Eres un experto en Pokemon. Generas listas de Pokemon realistas en formato JSON exacto.';
    }
    public function schema(JsonSchema $schema): array
    {
        return [
            'pokemones' => $schema->array()->items(
                $schema->object([
                    'nombre' => $schema->string()->required(),
                    'tipo' => $schema->string()->enum([
                        'fuego',
                        'agua',
                        'electrico',
                        'planta',
                        'normal',
                        'volador',
                        'psiquico',
                        'roca',
                        'tierra',
                    ])->required(),
                    'tamano' => $schema->integer()->min(10)->max(500)->required(),
                ])
            )->required(),
        ];
    }
}

Para asegurar que la IA nos devuelva exactamente lo que nuestra aplicación necesita, utilizamos los métodos internos de validación del SDK. La estructura se define así:

  • Nombre: Tipo string y obligatorio (required).
  • Tipos: Una lista de valores permitidos (fuego, agua, eléctrico, etc.).
  • Tamaño: Un valor entero (integer) con un rango mínimo y máximo definido.

Ejemplo de Generador de Cuestionarios (Q&A)

La instrucción principal de este agente es: "Genera preguntas y respuestas basándote en el contenido de los posts; genera preguntas de tipo Verdadero o Falso". Para asegurar que la salida sea procesable, definimos un esquema (Schema) con tres campos obligatorios:

  • Pregunta: El enunciado del cuestionamiento.
  • Respuesta: El valor booleano (Verdadero/Falso).
  • Explicación: El razonamiento detrás de la respuesta para ayudar al estudiante.

routes\web.php

// Quiz Verdadero/Falso basado en Posts (versión simple con prompt)
Route::get('/agents/quiz-simple', [AgentsTestController::class, 'quizPostsSimple']);

Podemos pasarle la cantidad de preguntas que deseamos (por defecto 5) y una lista de identificadores (IDs). Para procesar los IDs, utilizamos la función explode de PHP, que convierte un string separado por comas (ej. 1,2,3) en un array, permitiéndonos usar el método whereIn de Eloquent.

app\Http\Controllers\AgentsTestController.php

public function quizPostsSimple(Request $request): JsonResponse
{
    $ids = $request->input('ids'); // 1,2,3
    $cantidad = $request->input('cantidad', 5);
    $posts = Post::when($ids, fn($query) => $query->whereIn('id', array_filter(explode(',', $ids))), fn($query) => $query->where('posted', 'yes')->limit(1))
        ->get(['title', 'content']);
    if ($posts->isEmpty()) {
        return response()->json(['error' => 'No hay contenido'], 404);
    }
    
    $contenido = $posts->map(fn($post) => "Título: {$post->title}\nContenido: {$post->content}")->join("\n\n---\n\n");
    $prompt = "Basándote en los siguientes posts, genera {$cantidad} preguntas de verdadero o falso:\n\n{$contenido}";
    $resultado = (new QuizAgent)->prompt(
        $prompt,
        model: <YOURIDLOCALMODAL>
    );
    return response()->json([
        'quiz' => $resultado->toArray(),
        'posts_utilizados' => $posts->count(),
    ]);
}

Una vez obtenido el post, extraemos el título y el cuerpo del artículo para alimentar al agente. Es importante distinguir entre la instrucción del agente (su rol general) y la tarea específica (el prompt actual), que en este caso le indica cuántas preguntas generar exactamente basándose en el texto proporcionado.

El agente queda como:

app\Ai\Agents\QuizAgent.php

<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
 * Agente para generar quiz de Verdadero/Falso basado en posts.
 *
 * Schema devuelto:
 * - preguntas: array de objetos
 *   - pregunta: string
 *   - respuesta: boolean (true = verdadero, false = falso)
 *   - explicacion: string
 */
class QuizAgent implements Agent, HasStructuredOutput
{
    use Promptable;
    public function instructions(): Stringable|string
    {
        return 'Eres un generador de preguntas de quiz. Basándote en el contenido de los posts, generas preguntas de verdadero o falso. Las preguntas deben ser claras y la respuesta correcta debe estar basada en el contenido.';
    }
    public function schema(JsonSchema $schema): array
    {
        return [
            'preguntas' => $schema->array()->items(
                $schema->object([
                    'pregunta' => $schema->string()->required(),
                    'respuesta' => $schema->boolean()->required(),
                    'explicacion' => $schema->string()->required(),
                ])
            )->required(),
        ];
    }
}

Ejemplo de salida:

  • Pregunta: "¿Laravel Volt es un entorno de ejecución para PHP?"
  • Respuesta: "Falso".
  • Explicación: "Laravel Volt es una API funcional para Livewire que permite escribir lógica y plantillas en un mismo archivo".

La importancia del Esquema (Schema)

Aquí llegamos a lo que mencionas como el "talón de Aquiles" de la IA en el desarrollo de software: la falta de estructura.

En Laravel estamos acostumbrados a que todo esté fuertemente estructurado. Si consultas un modelo Post, sabes exactamente qué campos esperar. Si usas un método all(), esperas una colección. Con la IA, si no definimos un esquema, el formato de respuesta puede variar y romper nuestra aplicación.

¿Por qué usar respuestas estructuradas?

Al definir un esquema, obligamos a la IA a que su respuesta encaje exactamente en el formato que necesitamos (generalmente JSON). Esto es excelente porque:

  • Integración: Puedes mapear la respuesta de la IA directamente a un componente de Livewire o a un controlador.
  • Consistencia: Si tienes una aplicación de planetas, puedes exigir que la IA siempre devuelva: nombre, descripción, diámetro y densidad.
  • Seguridad: Te aseguras de que tu código pueda procesar la data sin errores de formato inesperados.

Ya sabes cómo generar estos agentes y por qué la estructura es la clave para integrar la inteligencia artificial en el flujo de trabajo de un desarrollador. No siempre querremos un simple texto por pantalla; la mayoría de las veces necesitaremos datos que nuestra aplicación pueda entender y procesar.

El Agente de Quiz: Del ejemplo a la utilidad real

Quiero profundizar en el agente de "Quiz" que implementamos anteriormente. Más allá de los ejemplos teóricos que ya vimos, quiero que lo analicemos desde un punto de vista funcional y realista. Mi propósito es que entiendas cómo adaptar un esquema similar para cualquier otra tarea que necesites en tus propios proyectos.

Para recapitular: este agente tiene una tarea fija, que es generar preguntas y respuestas en un formato específico. En esta ocasión, no estoy usando el proyecto del curso, sino uno propio donde estoy migrando mi aplicación actual a un entorno limpio. He decidido integrar este generador de quizzes porque, como saben, mis libros y cursos están íntimamente relacionados (relación uno a uno); por lo tanto, todo el contenido escrito es material perfecto para evaluar al alumno.

El problema de la creación manual

Crear laboratorios o cuestionarios manualmente (por ejemplo, para Udemy) es un verdadero fastidio. Requiere mucho tiempo pensar qué preguntas encajan mejor en cada sección, redactar las opciones correctas e incorrectas y darles variedad. Por falta de tiempo, muchas veces no los incluyo, y es precisamente aquí donde la IA se convierte en una herramienta invaluable para resolver un problema real de mi flujo de trabajo.

Implementación técnica con Livewire y PHP

En esta implementación utilizo Livewire. Para llamar al método que genera el quiz, utilizo un wire:click desde la vista. Si no dominas Livewire aún, no te preocupes (lo veremos más adelante); por ahora, piensa en esto simplemente como un formulario que, al hacer submit, ejecuta una función en el servidor escrita en PHP:

public function generarQuiz()
    {
        // $sections = BookSection::where('book_id', $this->model->id)->where('posted', 'yes')->where('orden', '>', 0)->get();
        $sections = BookSection::where('book_id', $this->model->id)->where('posted', 'yes')->where('orden', 4)->get();
        foreach ($sections as $s) {
            $resultado = (new QuizAgent)
***
<flux:button size="xs" variant="danger" wire:click='generarQuiz'>
     <flux:icon name="trash" />
</flux:button>            

La ventaja principal aquí es la integración total. A diferencia de otros enfoques donde podrías usar una herramienta externa o un asistente aislado (como los de GPT personalizados o herramientas CLI), aquí todo ocurre dentro de la misma aplicación. No necesitamos conectar bases de datos externas ni exportar archivos; el código PHP lee el capítulo del libro directamente de la base de datos y se lo pasa al agente.

Análisis del contenido y contexto

El contenido es extenso ya que, son capítulos ENTEROS de mis libros. Hacer 30 o 40 preguntas de este contenido manualmente podría tomar una hora por capítulo. Con el agente, simplemente le paso el texto completo y él se encarga del trabajo pesado.

Ventajas sobre el modelo de asistentes externos

  • Antes de llegar a esta solución, yo utilizaba un esquema mucho más rudimentario:
  • Extraía la información de la base de datos.
  • La pegaba en un asistente local (usando Jan.ai o LM Studio).
  • Pedía la salida en JSON.
  • Convertía ese JSON a SQL para insertarlo en mi base de datos.

Este flujo era horrible y fallaba constantemente por límites de contexto o respuestas incompletas. La integración directa en Laravel elimina estas ambigüedades. Al tener el agente encapsulado, puedo pasarle "super-prompts" que incluyen el contenido íntegro del capítulo sin salir de mi entorno de desarrollo.

Pruebas en el mundo real y rendimiento local

Actualmente estoy probando este sistema con un LLM local. Es importante mencionar que, aunque tengo una Mac potente, intentar ejecutar modelos muy pesados (como uno de 31B parámetros) puede provocar errores 500 sin descripción clara, simplemente porque el sistema se queda sin memoria o excede los tiempos de espera:

DeepSeek Error [500]: server_error - Compute error.
LARAVEL
13.4.0
PHP
8.4.19

Para estas prácticas, he configurado Gemma 3 12B y estoy evaluando los resultados. Fíjate que el proceso puede tardar (por eso es ideal usar sistemas de Colas y Jobs en Laravel), pero el resultado es coherente:

Agente de IA + Colas de Trabajo

Vamos a llevar nuestro agente al siguiente nivel mediante el uso de colas de trabajo (Queues). El motivo es sencillo: las IA suelen ser lentas en responder, especialmente cuando dependemos de servicios de terceros o procesos locales pesados.

Laravel + IA

Lo bonito de tener Laravel como un framework extremadamente robusto, que nos permita hacer de todo, junto con la IA, es que en este caso no vamos a usar Laravel como el bus para desarrollar, sino como una herramienta para llamar y hacer algo. En este caso, tal cual lo estamos haciendo desde la clase anterior, recuerda que lo estoy ligando; es el mismo agente, solo que aquí le llamé de otra manera, que es “trozos”. Ya te voy a explicar por qué.

Laravel nos ofrece un framework robusto para manejar estas tareas en segundo plano, permitiendo que la IA trabaje como una herramienta externa sin bloquear el hilo principal de nuestra aplicación.

El Desafío Técnico: Procesamiento Masivo de Datos con IA

En un entorno de desarrollo real, el problema principal no es solo conectar con la IA, sino cómo gestionar el flujo de información de manera efectiva. Mi caso de uso actual es un claro ejemplo: necesito generar una gran cantidad de preguntas y respuestas basadas en el contenido teórico de mis libros.

Aunque este flujo puede adaptarse a mil escenarios distintos, la constante es la misma: tienes una gran cantidad de data que debe ser procesada sin comprometer la estabilidad de la aplicación.

1. El Riesgo de Bloqueo en la Interfaz

No es viable procesar estas peticiones directamente en el hilo principal. Si intentamos llamar a la IA de forma síncrona, la aplicación se "colgará" inevitablemente durante segundos o incluso minutos. Esto genera una experiencia de usuario deficiente y puede provocar errores de timeout en el navegador o en el servidor.

2. Eficiencia y Escalabilidad

Para que el procesamiento sea verdaderamente efectivo, debemos implementar una arquitectura que permita:

  • Desacoplar la petición del procesamiento: El usuario solicita la generación y la aplicación responde de inmediato, mientras el trabajo pesado ocurre en segundo plano.
  • Gestión de colas de trabajo: Utilizar herramientas como las colas de Laravel para administrar las tareas, permitiendo que se ejecuten una tras otra de forma organizada.
  • Segmentación (Chunking): Dividir la información en trozos manejables para evitar saturar la memoria del servidor o los límites de contexto de la IA.

El Problema: El Hilo Principal Bloqueado

Anteriormente, llamábamos al agente directamente desde una clase de Livewire. Al hacerlo mediante un wire:click, el navegador y la aplicación se quedaban "colgados" esperando la respuesta de la IA.

En un entorno real, si necesitas procesar grandes volúmenes de datos (como generar cientos de preguntas basadas en libros), no puedes permitir que la interfaz se congele. Para solucionar esto, migramos la lógica a un Job de Laravel.

Gestión de Carga y Uso de IAs Locales

Para que nuestro sistema sea estable, no basta con enviar tareas a la cola; debemos ser estratégicos con el consumo de recursos. Si saturas tu máquina, el sistema operativo (ya sea Windows o macOS) podría cerrar los procesos por falta de memoria o exceso de carga, lo que interrumpiría la generación de tus contenidos.

1. La Estrategia del "Goteo" de Tareas

La recomendación principal es evitar el envío masivo de trabajos de golpe. Es mucho más efectivo pasar un grupo pequeño de tareas y, a medida que los Workers de Laravel se desocupen, enviar el siguiente lote.

En mi caso, estoy procesando fragmentos de mis libros. En lugar de intentar que la IA "lea" todo el libro a la vez, le paso fragmentos específicos directamente en el mensaje.

2. Formato de Entrada: El Contexto en el Prompt

Actualmente estamos usando el método más tradicional: incluir todo el contenido teórico directamente en la introducción del prompt.

  • Ventaja: Es directo y fácil de implementar al inicio.
  • Limitación: El tamaño del prompt tiene un límite (ventana de contexto). Si el fragmento es muy grande, la IA puede perder precisión o simplemente fallar.

3. Implementación con IAs Locales

Una de las grandes ventajas de este flujo es la posibilidad de usar modelos locales. Esto es ideal para trabajar con datos delicados o simplemente para ahorrar costes de API.

  • Ollama: Es la opción más robusta y "oficial" para integrar modelos locales en aplicaciones actualmente.
  • Alternativas: Aunque existen herramientas excelentes como Jan.ai o LM Studio, la integración directa mediante librerías suele estar más optimizada para Ollama.

4. ¿Por qué usar IAs Locales?

El uso de modelos locales (como Llama 3 o Mistral) nos permite realizar pruebas ilimitadas sin coste adicional. Solo debemos asegurarnos de que nuestra PC tenga la potencia necesaria (especialmente en GPU) para responder en tiempos aceptables.

Además de manejar TODOS los datos de manera privada

Estructura de la Cola de Trabajo (Job)

El Job se encarga de recibir fragmentos de contenido (segmentados en chunks) para que la IA no colapse. Estos son los puntos clave de la configuración:

  • Tiempo de Ejecución: He configurado un timeout alto (por ejemplo, 900 segundos) y el método set_time_limit(0) para evitar que PHP o el sistema de colas maten el proceso prematuramente.
  • Intentos (Tries): Es vital manejar excepciones. Si la IA falla por carga de trabajo, el sistema reintentará la tarea automáticamente.
    • public $timeout = 900;
      public $maxExceptions = 3;
      public $memory = 512;
      public $tries = 3;
    • Además de otros parámetros como la cantidad de memoria asignada.
  • Segmentación del Contenido: No le pasamos el libro entero. Dividimos el texto en fragmentos de unos 1,500 caracteres. Esto asegura que modelos locales (como Gemma 3 12B vía Jan.ai u Ollama) puedan procesar la información eficazmente.
    • foreach ($sections as $s) {
         $chunks = mb_str_split($s->content, 15000, 'UTF-8');
         foreach ($chunks as $content) {
            GenerateChunkQuizJob::dispatch(

Definición del Esquema JSON y el Modelo de Datos

Para que la respuesta de la IA sea útil, debe ser estructurada. He actualizado el esquema para que devuelva un formato tipo "Trivia" (estilo Duolingo):

  • Pregunta: El texto del desafío.
  • Opciones: Un array estrictamente de 4 opciones.
  • Respuesta Correcta: El índice de la opción válida y una explicación de por qué es correcta o incorrecta.
$resultado = (new QuizAgent)
    ->prompt(
        "
        Actúa como un profesor de programación. 
        Lee este fragmento de mi libro: '{$content}'
        Genera un examen de {$this->numQuestions} preguntas basado únicamente en ese texto.
        Responde SOLO con JSON válido, sin texto adicional ni markdown.
        Estructura requerida:
        {
            \"preguntas\": [
            {
                \"text\": \"pregunta\",
                \"options\": [\"opción1\", \"opción2\", \"opción3\", \"opción4\"],
                \"options_message\": [\"mensaje1\", \"mensaje2\", \"mensaje3\", \"mensaje4\"],
                \"correct_answer_index\": 0,
                \"language\": \"spanish\"
            }
            ]
        }
        Me lo das en {$this->language}
    ",
        model: $this->modelAi
    );
public function schema(JsonSchema $schema): array
{
    return [
        'preguntas' => $schema->array()->items(
            $schema->object([
                'text' => $schema->string()->required(),
                'options' => $schema->array()->items(
                    $schema->string()
                )->min(4)->max(4)->required(),
                'options_message' => $schema->array()->items(
                    $schema->string()
                )->min(4)->max(4)->required(),
                'correct_answer_index' => $schema->integer()->required(),
                'language' => $schema->string()->enum(['english', 'spanish'])->required(),
            ])
        )->required(),
    ];
}

La migración queda como:

Schema::create('learning_questions', function (Blueprint $table) {
    $table->id();
    $table->text('text');
    // $table->enum('question_type', ['multiple_choice', 'code'])->default('multiple_choice');
    $table->json('options'); // Almacena las opciones como un array JSON
    $table->json('options_message'); // Almacena las opciones como un array JSON
    $table->unsignedInteger('correct_answer_index');
    $table->enum('language', ['english', 'spanish'])->default('spanish');
    // Asumiendo que book_sections y tutorial_sections son otras tablas
    $table->foreignId('book_section_id')->nullable()->constrained('book_sections')->onDelete('cascade');
    $table->foreignId('tutorial_section_id')->nullable()->constrained('tutorial_sections')->onDelete('cascade');
    $table->timestamps();
});

Lo interesante de este sistema es la sinergia que logramos entre un modelo de Eloquent y las respuestas generadas por la IA. Al utilizar campos de tipo JSON en nuestras migraciones de base de datos, eliminamos la fricción de tener que mapear cada campo individualmente, permitiendo que la data fluya directamente desde la respuesta del modelo de lenguaje hacia nuestra aplicación.

1. El Uso de Campos JSON en las Migraciones

  • En el modelo Learning, hemos definido la estructura de la tabla con columnas JSON. Esto nos ofrece una flexibilidad enorme:
  • Persistencia Directa: Guardamos el objeto tal cual lo devuelve la IA (tras una validación previa).
  • Facilidad de Procesamiento: Laravel convierte automáticamente estos campos en arrays o colecciones de PHP, facilitando su uso en el frontend o en la lógica de negocio.

2. Vinculación entre Laravel y la IA

La clave aquí es la relación directa. No estamos simplemente recibiendo texto plano; estamos integrando la capacidad de razonamiento de la IA con la robustez de Laravel.

  • El flujo es limpio: enviamos un capítulo de un libro como contexto.
  • La IA genera las preguntas y opciones bajo un esquema predefinido.
  • Laravel recibe, valida e inserta el resultado en la base de datos.

3. Automatización del Flujo de Datos

El objetivo final es la automatización total. A diferencia de un proceso manual donde tendrías que copiar y pegar cada pregunta, aquí la IA actúa como un procesador de datos inteligente.

  • Verificación de Calidad: Aunque la IA hace el trabajo pesado, el sistema incluye una capa de validación para asegurar que la respuesta sea un JSON válido y que cumpla con los requisitos antes de la inserción.
  • Consistencia: Al atar el prompt a un capítulo específico del libro, garantizamos que las respuestas no sean "inventadas" al azar, sino que estén estrictamente fundamentadas en el contenido teórico proporcionado.

Este enfoque transforma a Laravel en algo más que un framework de desarrollo web; lo convierte en un motor de gestión de conocimiento. Al delegar la creación de contenido estructurado a la IA y la gestión de datos a Eloquent, podemos generar plataformas educativas masivas con un esfuerzo manual mínimo.

Validación y "Limpieza" de la Respuesta

No podemos confiar ciegamente en que la IA devuelva un JSON perfecto. Por ello, implementamos una lógica de validación:

  • Extract JSON: Un método para limpiar la respuesta de la IA y extraer solo el bloque de código JSON, eliminando texto adicional.
  • Verificación de Integridad: Comprobamos que el JSON sea decodificable y que contenga todos los campos requeridos antes de insertarlo en la base de datos.
  • Logs de Error: Si la decodificación falla, el error se registra en storage/logs/laravel.log para depuración.
if (! $jsonText) {
    \Log::error('GenerateQuizJob: No JSON found in response', [
        'book_section_id' => $this->bookSection->id,
        'response' => mb_substr($responseText, 0, 1000),
    ]);
    return;
}
$data = json_decode($jsonText, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    \Log::error('GenerateQuizJob: Invalid JSON after extract', [
        'book_section_id' => $this->bookSection->id,
        'json_error' => json_last_error_msg(),
    ]);
    return;
}
if (! isset($data['preguntas'])) {
    \Log::warning('GenerateQuizJob: No preguntas key in response', [
        'book_section_id' => $this->bookSection->id,
        'keys' => array_keys($data ?? []),
    ]);
    return;
}
foreach ($data['preguntas'] as $pregunta) {
    LearningQuestion::create([
        'text' => $pregunta['text'],
        'options' => $pregunta['options'],
        'options_message' => $pregunta['options_message'] ?? [],
        'correct_answer_index' => $pregunta['correct_answer_index'],
        'language' => $pregunta['language'],
        'book_section_id' => $this->bookSection->id,
    ]);
}

Ejecución en Tiempo Real

Para poner todo en marcha, ejecutamos el comando:

$ php artisan queue:work

Los códigos completos quedan como:

app\Jobs\GenerateChunkQuizJob.php

<?php
namespace App\Jobs;
use App\Ai\Agents\QuizAgent;
use App\Models\BookSection;
use App\Models\LearningQuestion;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\SerializesModels;
class GenerateChunkQuizJob implements ShouldQueue
{
    use Queueable, SerializesModels;
    public $timeout = 900;
    public $maxExceptions = 3;
    public $memory = 512;
    public function __construct(
        public BookSection $bookSection,
        public string $contentChunk,
        public int $numQuestions = 3,
        public string $modelAi = 'gemma-3-12b-it-IQ4_XS',
        public string $language = 'español',
    ) {}
    public function handle(): void
    {
        $resultado = (new QuizAgent)
            ->prompt(
                "
                Actúa como un profesor de programación. 
                Lee este fragmento de mi libro: '{$this->contentChunk}'
                Genera un examen de {$this->numQuestions} preguntas basado únicamente en ese texto.
                Responde SOLO con JSON válido, sin texto adicional ni markdown.
                Estructura requerida:
                {
                  \"preguntas\": [
                    {
                      \"text\": \"pregunta\",
                      \"options\": [\"opción1\", \"opción2\", \"opción3\", \"opción4\"],
                      \"options_message\": [\"mensaje1\", \"mensaje2\", \"mensaje3\", \"mensaje4\"],
                      \"correct_answer_index\": 0,
                      \"language\": \"spanish\"
                    }
                  ]
                }
                Me lo das en {$this->language}
            ",
                model: $this->modelAi
            );
        $responseText = trim((string) $resultado);
        $jsonText = $this->extractJson($responseText);
        if (!$jsonText) {
            \Log::error('GenerateChunkQuizJob: No JSON found in response', [
                'book_section_id' => $this->bookSection->id,
                'response' => mb_substr($responseText, 0, 1000),
            ]);
            return;
        }
        $data = json_decode($jsonText, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            \Log::error('GenerateChunkQuizJob: Invalid JSON after extract', [
                'book_section_id' => $this->bookSection->id,
                'json_error' => json_last_error_msg(),
            ]);
            return;
        }
        if (!isset($data['preguntas'])) {
            \Log::warning('GenerateChunkQuizJob: No preguntas key in response', [
                'book_section_id' => $this->bookSection->id,
                'keys' => array_keys($data ?? []),
            ]);
            return;
        }
        foreach ($data['preguntas'] as $pregunta) {
            LearningQuestion::create([
                'text' => $pregunta['text'],
                'options' => $pregunta['options'],
                'options_message' => $pregunta['options_message'] ?? [],
                'correct_answer_index' => $pregunta['correct_answer_index'],
                'language' => $pregunta['language'],
                'book_section_id' => $this->bookSection->id,
            ]);
        }
    }
    private function extractJson(string $text): ?string
    {
        $text = trim($text);
        if (preg_match('/```json\s*([\s\S]*?)```/', $text, $matches)) {
            return trim($matches[1]);
        }
        if (preg_match('/\{[\s\S]*\}/', $text, $matches)) {
            return trim($matches[0]);
        }
        return null;
    }
}

app\Ai\Agents\QuizAgent.php

<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
 * Agente para generar preguntas de quiz con opciones múltiples.
 *
 * Schema devuelto:
 * - preguntas: array de objetos
 *   - text: string
 *   - options: array de 4 strings
 *   - options_message: array de 4 strings
 *   - correct_answer_index: integer (0-3)
 *   - language: enum ('english', 'spanish')
 */
class QuizAgent implements Agent, HasStructuredOutput
{
    use Promptable;
    public function instructions(): Stringable|string
    {
        return 'Eres un generador de preguntas de quiz con opciones múltiples. Genera preguntas con 4 opciones teórica. Las opciones válidas se guardan en "options" como array JSON. Los mensajes de verdadero/falso o éxito/error se guardan en "options_message" como array JSON. La opción correcta se indica en "correct_answer_index" (0-3) y el idioma en "language".';
    }
    public function schema(JsonSchema $schema): array
    {
        return [
            'preguntas' => $schema->array()->items(
                $schema->object([
                    'text' => $schema->string()->required(),
                    'options' => $schema->array()->items(
                        $schema->string()
                    )->min(4)->max(4)->required(),
                    'options_message' => $schema->array()->items(
                        $schema->string()
                    )->min(4)->max(4)->required(),
                    'correct_answer_index' => $schema->integer()->required(),
                    'language' => $schema->string()->enum(['english', 'spanish'])->required(),
                ])
            )->required(),
        ];
    }
}
public function generateQuiz()
{
    $sections = BookSection::where('book_id', $this->model->id)
        ->where('posted', 'yes')
        ->where('orden', '>', 0)
        ->get();
    foreach ($sections as $s) {
        $chunks = mb_str_split($s->content, 15000, 'UTF-8');
        foreach ($chunks as $content) {
            GenerateChunkQuizJob::dispatch(
                $s, 
                $content, 
                3, 
                'gemma-3-12b-it-IQ4_XS',
                'español'
            );
        }
    }
    Session::flash('status', 'Tareas almacenadas en la cola para generar las preguntas del libro');
}

Otras consideraciones

Posiblemente tengas que modificar otros parámetros en el servidor en tu php.ini como:

  • max_execution_time: Probablemente esté en 60. Cámbialo a 0 (infinito) o 900.
  • memory_limit: Si el modelo de IA o el procesamiento del texto consume mucha RAM, el proceso muere. Ponlo al menos en 512M.

Todos estos cambios son para evitar errores como:

NFO  Processing jobs from the [default] queue.  
App\Jobs\GenerateQuizJob ..... RUNNING
App\Jobs\GenerateQuizJob ..... 1m FAIL
zsh: killed     php artisan queue:work

Que puedes notar que el trabajo se detiene automáticamente pasado un minuto o un tiempo fijo; recordemos que las IAs tardan mucho tiempo en responder, y si ha esto le sumamos el prompt y la tarea que es pesada, hay que hacer un equilibrio entre los recursos de nuestro computador con la tarea que vamos a realizar.

Aprende a conectar Gemini, ChatGTP o un LLM en local desde tu app en Laravel, presentamos, instalación, uso, crear esquemas, casos de uso y configuración para proyectos reales.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz