Nella 1.0 del Laravel AI SDK la classificazione diventa una capacità a sé, accanto a testo, immagini, audio ed embedding. Non è un dettaglio di catalogo: è il framework che dice ad alta voce una cosa che chi ha un prodotto in produzione ha già imparato pagando, e cioè che «rispondere a una domanda chiusa» e «generare qualcosa» sono due lavori diversi, con due costi diversi e due profili di rischio diversi. In Miraviso quella separazione non è un'ottimizzazione arrivata dopo: è la ragione per cui il prodotto può dire che il colore non lascia mai il dispositivo.
Come si presenta nel SDK
L'API è deliberatamente stretta. Fai una serie di domande tipizzate e ricevi risposte tipizzate:
use Laravel\Ai\Classification;
use Laravel\Ai\Classification\Boolean;
use Laravel\Ai\Classification\Choice;
$response = Classification::of($ticket->body)->questions([
'is_urgent' => new Boolean('Does this message convey urgency?'),
'department' => new Choice('Which team should handle this?', [
'billing' => 'Payments, invoicing, refunds',
'technical' => 'Bugs, outages, integrations',
'sales' => 'Pricing, plans, upgrades',
]),
])->classify();
C'è anche un tipo Score che restituisce un valore fra 0.0 e 1.0, e una macro Str::decide per la singola domanda sì/no con una soglia di certezza. Gira su modelli Jev di TypeSafe e su OpenRouter; l'annuncio parla di risposte «in millisecondi a una frazione del prezzo» rispetto a un LLM tradizionale. Quella frase la prendo per quello che è — un claim del fornitore, da misurare sul proprio carico — ma la forma dell'API mi interessa a prescindere dal benchmark.
Il punto è che il tipo di ritorno è chiuso. $response['department']->choice è uno dei tre valori che hai dichiarato, oppure non è niente. Non c'è un JSON da riparare, non c'è un prompt che chiede gentilmente di rispondere solo con una parola, non c'è il retry quando il modello ha voglia di spiegarsi. Chiunque abbia messo in produzione un LLM per fare da smistatore sa quanto codice difensivo sparisce quando il contratto è questo.
Le tre fasce in cui divido il lavoro
In Miraviso — lo specchio virtuale per i saloni di parrucchieri — ogni pezzo di lavoro finisce in una di tre fasce, e la fascia si decide prima di scrivere codice.
Sul dispositivo. Tutto ciò che è geometria e segmentazione. La prova colore gira interamente sul tablet con MediaPipe on-device: il modello individua i capelli nel frame, il colore viene composto in locale, nessun fotogramma lascia l'apparecchio. Questa non è una scelta di costo, è una scelta di prodotto: è una frase che posso dire a un titolare di salone senza asterischi, ed è verificabile guardando il traffico di rete.
Decisioni chiuse. Sono le domande con un insieme finito di risposte, quelle per cui esiste — ora — una capacità apposta nel SDK di Laravel. Nel mio caso stanno quasi tutte sul lato gestionale e non toccano l'immagine: smistare una richiesta in arrivo, capire se una nota del salone contiene qualcosa da trattare come sensibile, decidere se un testo va mostrato a un operatore o no. Sono domande a cui un modello generativo sa rispondere benissimo, e per cui è lo strumento più caro, più lento e più difficile da testare che potessi scegliere.
Generazione vera. Una sola cosa: l'anteprima del taglio, che richiede di produrre pixel nuovi e plausibili. Gira su Gemini in Vertex AI in regione UE, previo consenso esplicito, e non viene mai scritta su disco. È l'unico pezzo di pipeline che ha bisogno di un modello grande, ed è anche l'unico che porta con sé un problema di privacy da spiegare per intero all'utente finale — e per cui, come ho scritto a proposito delle approvazioni degli strumenti nel SDK, il confine che conta sta prima del modello, non dentro l'agente.
Perché la fascia va decisa prima, non dopo
La tentazione, quando hai già l'integrazione con un modello generativo che funziona, è di usarla per tutto: c'è già il client, c'è già la gestione degli errori, aggiungere una domanda in più costa cinque minuti. È esattamente così che un prodotto accumula chiamate generative che nessuno aveva progettato.
Le tre conseguenze che ho visto sul mio codice, in ordine di quanto mi hanno dato fastidio:
- Il perimetro dei dati si allarga di soppiatto. Ogni domanda affidata a un modello remoto è un dato in più che esce. Se la risposta poteva arrivare da un classificatore o da una regola, hai allargato il perimetro senza guadagnarci niente — e il perimetro è la cosa che poi devi spiegare in un'informativa.
- I test smettono di essere test. Una domanda chiusa ha un insieme di risposte accettabili e si asserisce. Una risposta in linguaggio naturale si controlla con un altro modello, o a occhio, e a quel punto la suite non ti dice più se hai rotto qualcosa.
- Le deprecazioni fanno più male. Ne ho già scritto: quando un endpoint viene ritirato, la migrazione fa male in proporzione a quante decisioni hai appoggiato su quel modello. Le decisioni chiuse si rimisurano in mezza giornata, la generazione no.
Il mestiere è lo stesso di sempre
Detta senza riverenza per il momento storico: questa è la stessa domanda che ci si fa da vent'anni davanti a una query lenta. Serve davvero chiamare il servizio, o la risposta la posso calcolare qui? La differenza è che questa volta la chiamata non costa solo latenza: costa denaro a ogni richiesta e sposta un dato fuori da un confine che hai promesso di tenere.
È anche la ragione per cui continuo a scrivere i giochi di logica del sito a mano, senza librerie: quando il risolutore del campo minato deve garantire che una griglia non costringa mai a indovinare, quella garanzia arriva da una dimostrazione, non da una stima. Non ogni problema difficile è un problema da modello — e riconoscere quali non lo sono è diventato, negli ultimi due anni, una delle poche competenze che fanno differenza sul costo di un prodotto.
Il lato buono della notizia è tutto qui: avere la classificazione come capacità separata rende quella scelta esplicita nel codice, invece di lasciarla implicita in un prompt. Un framework che ti costringe a dichiarare che tipo di lavoro stai chiedendo è un framework che ti aiuta a non chiedere quello sbagliato.