Ir al contenido principal
Laravel, thinking fast.
Capitulo 1 · Tu primer endpoint de IA

Reintenta lo que merece reintento

Julian Beaujardin

Los reintentos son donde las buenas intenciones producen malos sistemas. El instinto por defecto —«reintenta tres veces si falla»— convierte calladamente una petición malformada en tres, y un error de facturación en tres.

La política que sobrevive al contacto con un proveedor real distingue entre fallos que podrían resolverse solos y fallos que desde luego no.

private static function shouldRetry(Throwable $e): bool
{
    if ($e instanceof ConnectionException) {
        return true;
    }

    if ($e instanceof RequestException) {
        $status = $e->response->status();

        return $status === 429 || ($status >= 500 && $status < 600);
    }

    return false;
}

Los fallos de conexión y los 5xx del proveedor son transitorios: la misma petición puede perfectamente funcionar un momento después. Un 429 es contrapresión explícita, y es el único caso en el que el proveedor te ha dicho que merece la pena esperar.

Todo lo demás no se reintenta, y el miembro importante de ese conjunto es el 4xx. Una petición malformada seguirá estándolo en el segundo intento. Reintentarla desperdicia latencia, desperdicia límite de uso y —según lo que hayas enviado y cómo cuente el proveedor— puede desperdiciar dinero. Peor aún, esconde el error: un fallo que reintentas tres veces es un fallo que tardas el triple en notar.

La última cláusula importa tanto como las dos primeras. Cualquier cosa que no sea un fallo HTTP —un error al decodificar JSON, un error de tipos, un fallo en tu propio código de mapeo— no debe reintentarse. Esos son problemas tuyos, y volver a ejecutarlos sólo retrasa la traza.

Cómo se usa desde la aplicación

Registra el gestor como singleton y pon una facade delante, para que el código llamante se lea como el resto de Laravel:

final class AiServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(AiManager::class);
    }
}
$content = AiFacade::getSiteContent(
    model: config('services.ai.model'),
    prompt: $prompt,
);

Ahora un llamante no puede ver qué proveedor respondió, no puede construir un cliente sin configurar y no puede saltarse por accidente la política de reintentos. Hay exactamente un camino, y está asfaltado.

Qué has comprado

Haz balance, porque es fácil mirar tres ficheros pequeños y concluir que has escrito un envoltorio por escribirlo.

Ahora tienes un único sitio donde vive la forma de la API del proveedor: un fichero que editar cuando renombren un campo. Un único sitio donde se validan las credenciales, con un error que dice dónde mirar. Un único sitio donde se deciden tiempos de espera y reintentos, aplicados a cada llamada de IA de la aplicación, incluidas las que aún no has escrito. Una interfaz que simular en las pruebas, en una frontera que no se mueve.

Y tienes dónde poner la siguiente lección. Esa es la parte que compone. Todo lo que enseña el resto del libro —techos de gasto, validación de salida estructurada, cancelación, observabilidad— aterriza limpiamente porque hay una frontera donde aterrizar. Sin ella, cada una de esas cosas se convierte en un cambio en cada punto de llamada.

El envoltorio no es el objetivo. El sitio donde poner las decisiones sí lo es.

Qué no resuelve todavía

Ten claro qué falta, porque a estas alturas el endpoint es honesto pero ingenuo.

No tiene ni idea de lo que cuesta una llamada, ni techo sobre cuántas puede hacer un cliente. Confía en que la respuesta tenga la estructura que la función necesita, cosa que todavía no es cierta. Se ejecuta dentro de la petición HTTP, que no sobrevivirá a un turno largo. Y su prompt sigue siendo una cadena que alguien montó en algún sitio, que es el tema de los dos capítulos siguientes.

Cada una de esas cosas es un capítulo, y cada una es un cambio dentro de esta frontera en lugar de un cambio repartido por tu aplicación. Ese es el retorno de los tres ficheros.

Principios

  • Pon al proveedor detrás de un contrato que describa tus necesidades, no las suyas. Si la interfaz usa el vocabulario del proveedor, es un cambio de nombre, no una abstracción.
  • El driver es una capa de traducción, y nada más. Recibe un cliente configurado; no lo construye.
  • Garantiza el transporte, valida el significado en otro sitio. El driver promete JSON decodificable. Si ese JSON es correcto es una pregunta para código que conoce el contexto del llamante.
  • Falla donde se configura, no donde se usa. Una credencial ausente debe producir tu mensaje de error, no el engañoso del proveedor.
  • Resuelve credenciales por cliente desde el primer día. Añadir después atribución y topes por cliente sale mucho más caro que empezar con ellos.
  • Reintenta sólo los fallos transitorios. Errores de conexión, 5xx y 429. Nunca 4xx, nunca tus propias excepciones.
  • Un solo camino de entrada. Un singleton detrás de una facade impide que ningún llamante construya por accidente un cliente sin configurar, sin tiempo de espera y sin reintentos.