Ir al contenido principal
Laravel, thinking fast.

Capitulo 2

Obtener una respuesta que puedas usar

Julian Beaujardin

El modelo respondió. Eso no es lo mismo que una respuesta que puedas usar.

Entre «la petición tuvo éxito» y «la función funcionó» hay tres preguntas distintas que los principiantes juntan en una. ¿Es JSON válido? ¿Tiene las claves que lee mi código? ¿Son ciertas esas claves? Fallan de forma independiente, fallan a ritmos distintos y cada una necesita una defensa distinta.

Este capítulo trata de construir las tres, en los sitios correctos, para que cuando el contenido generado llegue a tu base de datos tenga la misma forma siempre —o no haya llegado nunca.

Pide JSON, y hazlo en serio

Todo proveedor serio tiene un modo que restringe la salida a JSON sintácticamente válido. Actívalo. No existe escenario alguno en el que analizar prosa que pediste con buenos modales sea mejor.

$data = $this->send('POST', '/chat/completions', [
    'model' => $model,
    'messages' => [
        ['role' => 'user', 'content' => $prompt],
    ],
    'response_format' => ['type' => 'json_object'],
]);

Esa sola opción elimina una clase entera de fallos: nada de preámbulos, nada de «¡Claro! Aquí tienes el JSON que pediste:», nada de explicaciones al final sobre lo que acaba de producir.

Lo que no hace —y esta es la distinción sobre la que gira el resto del capítulo— es hacer que el JSON sea correcto. Garantiza que la salida se podrá analizar. No dice nada sobre si el objeto tiene un hero, si ese hero tiene un title, ni si ese título habla del negocio adecuado.

Sintaxis y significado son problemas distintos. Resuélvelos por separado, en sitios distintos.

El bloque de código que añade igualmente

No todas las llamadas pueden usar el modo de salida estructurada. A veces estás en un endpoint que no lo soporta, a veces pides una forma que el modo no sabe expresar, y a veces heredas una llamada escrita antes de que llegaras.

En esos casos el modelo, con gran confianza, envolverá tu JSON en un bloque de código markdown. Ha leído muchísima documentación, y en la documentación el JSON viene en bloques.

$raw = trim((string) $result->json('choices.0.message.content', ''));
$raw = (string) preg_replace('/^```(?:json)?\s*/i', '', $raw);
$raw = trim((string) preg_replace('/\s*```$/', '', $raw));

$decoded = json_decode($raw, true);

Tres líneas, sin astucia, y se dispararán más a menudo de lo que esperas. No intentes ser listo aquí: nada de emparejar llaves, nada de extracción recursiva. Quita el bloque, decodifica y, si el resultado no es lo que querías, trátalo como un fallo en lugar de intentar rescatarlo. Un análisis parcial de una respuesta malformada es como acaba un null en una base de datos.

Hay una segunda normalización que merece la pena robar. Cuando pides una lista de cosas, el modelo a veces devuelve un único objeto en lugar de un array de un elemento, porque tu ejemplo mostraba uno.

if (! is_array($decoded) || $decoded === []) {
    return [['action' => null, 'message' => 'Could not parse AI response.']];
}

if (! array_is_list($decoded)) {
    return [$decoded];
}

return $decoded;

Fíjate en lo que devuelve la rama de fallo: la forma que el llamante espera, con un mensaje dentro, no null ni un array vacío. El llamante tiene un solo camino de código. Muestra un mensaje en cualquier caso. Esa es la diferencia entre una función que se degrada y una que explota —y cuesta una línea.