Ir al contenido principal
Laravel, thinking fast.

Capitulo 6

La lentitud es lo normal

Julian Beaujardin

En desarrollo tu función de IA responde en tres segundos y concluyes que es rápida.

No es rápida. Era pequeña. Un prompt corto, sin historial, sin herramientas, una sola ida y vuelta. En producción la misma función lleva una conversación entera, llama a tres herramientas y tarda noventa segundos —y noventa segundos no es un caso extremo, es un martes por la tarde.

Todo este capítulo se deriva de aceptar ese número en lugar de discutirlo.

La petición web es el sitio equivocado

Ejecuta ese turno dentro de la petición HTTP y pasan cuatro cosas, en este orden.

Un worker de PHP queda retenido los noventa segundos enteros. Tu pool no es grande; un puñado de conversaciones simultáneas y el sitio deja de responder a cualquier petición, incluidas las que no tienen nada que ver con la IA.

Algo más arriba se rinde antes. Un balanceador, un CDN, un proxy —cada uno con su propio tiempo de espera, ninguno fijado por ti, todos más cortos de lo que te gustaría.

El usuario no ve nada. Ni un error, ni una respuesta parcial. Un indicador de carga, y luego un fallo en blanco.

Y lo pagaste todo. El modelo terminó su trabajo, los tokens están facturados y el resultado no fue a ninguna parte porque la conexión que debía llevarlo ya se había cerrado.

Eso último es lo que diferencia esto de un endpoint lento cualquiera. Una consulta lenta a base de datos que agota su tiempo te cuesta una consulta. Un turno de modelo que agota su tiempo te cuesta el turno entero, y no tienes nada que enseñar.

Mueve el turno a una cola

La forma es: la petición despacha un job y vuelve de inmediato; el job hace el trabajo y publica su estado en un sitio que el frontend pueda leer; el frontend consulta hasta que el estado dice que ha terminado.

class ProcessAiMessage implements ShouldQueue
{
    use Queueable;

    public int $timeout = 120;
    public int $tries = 1;

    public function __construct(
        public readonly string $cacheKey,
        public readonly array $conversationHistory,
        public readonly array $messages,
        // ...
        public readonly string $systemPrompt,
    ) {}
}

Dos propiedades de esa clase soportan la mayor parte del diseño.

$timeout = 120 es el límite exterior de todo el turno. No de una llamada: de todo.

$tries = 1. Sin reintento automático, nunca. Un turno que ya se ha ejecutado ha gastado dinero y puede haber llamado a herramientas con efectos reales. El valor por defecto de Laravel —reintentar un job fallido— es exactamente lo contrario de lo que hace falta aquí: volvería a ejecutar un turno de agente a medio completar, lo pagaría otra vez y posiblemente repetiría la misma acción irreversible. Los reintentos en este mundo son una decisión por herramienta, con idempotencia detrás —que es el capítulo 10, y por eso este número es 1.