Monitoring LLM in produzione: tracciare qualità, costi e anomalie nelle pipeline AI

Monitoring LLM in produzione: tracciare qualità, costi e anomalie nelle pipeline AI

Il primo cliente a cui ho messo un LLM in produzione - un sistema di generazione automatica di descrizioni prodotto per un catalogo e-commerce con 12.000 SKU - ha avuto una sorpresa sgradita alla fine del primo mese: la fattura dell'API Claude era di 420 euro. Il budget stimato era 150 euro. Nessuno nel team sapeva spiegare dove fossero finiti i 270 euro in più, perché nessuno aveva implementato un sistema di tracking delle chiamate API. Non sapevano quante chiamate venivano fatte al giorno, quale fosse la dimensione media dei prompt, quante richieste venivano rifiutate per content filtering e ritentate, né se ci fossero chiamate duplicate causate da retry su timeout. Il problema non era l'LLM - era l'assenza totale di observability sull'LLM. Lo stesso problema che le PMI italiane hanno risolto vent'anni fa per le applicazioni web (log, metriche, alert) doveva essere risolto per le pipeline AI, con strumenti e metriche specifiche per un paradigma completamente diverso.

In sei mesi ho costruito un layer di monitoring per tre sistemi AI di clienti diversi - il generatore di descrizioni prodotto, un chatbot di customer support basato su RAG, e un sistema di analisi automatica di documenti legali - usando una combinazione di strumenti open source e script custom. Il risultato: visibilità completa su costi, latenza, qualità delle risposte e anomalie, con alerting automatico quando qualcosa si discosta dal comportamento atteso. Il costo dell'API del primo cliente è sceso da 420 a 180 euro al mese dopo le ottimizzazioni rese possibili dal monitoring - il 57% in meno, semplicemente identificando e correggendo gli sprechi che prima erano invisibili.

Quali metriche servono davvero per monitorare un LLM in produzione?

Le metriche per un'applicazione LLM sono strutturalmente diverse da quelle di un'applicazione web tradizionale. In un'applicazione PHP, le metriche fondamentali sono tempo di risposta, throughput (richieste al secondo), tasso di errore e utilizzo risorse. Per un LLM, le metriche fondamentali sono cinque e nessuna di esse è opzionale. La prima è il costo per richiesta, calcolato in euro basandosi sui token di input e output consumati da ogni chiamata API, perché a differenza di un server PHP (costo fisso mensile), un LLM ha un costo variabile per ogni singola operazione che esegue, e senza tracking puoi passare da 150 a 420 euro al mese senza sapere perché. La seconda è la latenza end-to-end, misurata come p50, p95 e p99, perché la latenza degli LLM è molto più variabile di quella di un'applicazione tradizionale - una stessa richiesta può impiegare 800 ms un giorno e 4.200 ms il giorno dopo, a seconda del carico sui server del provider. La terza è la qualità della risposta, che in un'applicazione tradizionale è binaria (la query restituisce i dati corretti o no), ma in un LLM è uno spettro: la risposta può essere corretta, parzialmente corretta, pertinente ma non precisa, completamente fuori tema, o pericolosa. La quarta è il tasso di retry e di errore, perché le API degli LLM hanno rate limit, possono andare in overload, e i retry non gestiti correttamente moltiplicano i costi. La quinta è il drift della qualità nel tempo, perché i provider aggiornano i modelli e il comportamento può cambiare senza preavviso - una risposta che era corretta ieri potrebbe non esserlo più domani con un modello aggiornato.

Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nel monitoring di pipeline AI in produzione - un'area dove la mancanza di standard di settore rende l'esperienza pratica il fattore differenziante più importante.

Il layer di tracking: registrare ogni chiamata API con contesto

Il componente base del monitoring è un wrapper attorno al client API che registra ogni chiamata con il suo contesto completo. In PHP, il pattern è un decorator attorno all'SDK ufficiale di Anthropic:

// Wrapper di monitoring per le chiamate Claude API
class MonitoredClaudeClient
{
    public function __construct(
        private readonly ClaudeClient $client,
        private readonly PDO $metricsDb,
        private readonly LoggerInterface $logger,
    ) {}

    public function message(array $params): ClaudeResponse
    {
        $startTime = microtime(true);
        $inputTokensEstimate = $this->estimateTokens($params['messages']);

        try {
            $response = $this->client->message($params);

            $this->recordMetrics([
                'timestamp' => date('Y-m-d H:i:s'),
                'model' => $params['model'],
                'input_tokens' => $response->usage->input_tokens,
                'output_tokens' => $response->usage->output_tokens,
                'cache_read_tokens' => $response->usage->cache_read_input_tokens ?? 0,
                'latency_ms' => (microtime(true) - $startTime) * 1000,
                'cost_eur' => $this->calculateCost($response->usage, $params['model']),
                'status' => 'success',
                'stop_reason' => $response->stop_reason,
                'context' => $params['metadata']['context'] ?? 'unknown',
            ]);

            return $response;

        } catch (\Throwable $e) {
            $this->recordMetrics([
                'timestamp' => date('Y-m-d H:i:s'),
                'model' => $params['model'],
                'input_tokens' => $inputTokensEstimate,
                'output_tokens' => 0,
                'latency_ms' => (microtime(true) - $startTime) * 1000,
                'cost_eur' => 0,
                'status' => 'error',
                'error_type' => get_class($e),
                'error_message' => $e->getMessage(),
                'context' => $params['metadata']['context'] ?? 'unknown',
            ]);

            throw $e;
        }
    }

    private function calculateCost(object $usage, string $model): float
    {
        // Prezzi aggiornati per i modelli Claude (aprile 2026)
        $pricing = match ($model) {
            'claude-sonnet-4-6' => ['input' => 3.0, 'output' => 15.0],
            'claude-haiku-4-5' => ['input' => 0.80, 'output' => 4.0],
            'claude-opus-4-6' => ['input' => 15.0, 'output' => 75.0],
            default => ['input' => 3.0, 'output' => 15.0],
        };

        // Costo in euro (prezzo per milione di token, conversione USD->EUR)
        $usdToEur = 0.92;
        return (
            ($usage->input_tokens / 1_000_000 * $pricing['input']) +
            ($usage->output_tokens / 1_000_000 * $pricing['output'])
        ) * $usdToEur;
    }
}

Il campo context è fondamentale per l'analisi successiva: identifica quale funzionalità dell'applicazione ha generato la chiamata (generazione descrizione, chatbot, analisi documento). Senza questo contesto, sai che hai speso 420 euro ma non sai se il 70% è andato nel chatbot o nelle descrizioni prodotto - e non puoi ottimizzare ciò che non puoi misurare.

Dashboard e alert: dalla metrica grezza alla decisione operativa

I dati grezzi raccolti dal wrapper vengono aggregati in una dashboard che costruisco con Grafana connesso al database delle metriche. Le query SQL di aggregazione sono semplici ma producono insight potenti:

-- Costo giornaliero per contesto applicativo (ultimi 30 giorni)
SELECT
    DATE(timestamp) AS giorno,
    context,
    SUM(cost_eur) AS costo_totale,
    COUNT(*) AS num_chiamate,
    AVG(cost_eur) AS costo_medio_per_chiamata,
    AVG(latency_ms) AS latenza_media_ms,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) AS latenza_p95
FROM llm_metrics
WHERE timestamp > NOW() - INTERVAL 30 DAY
GROUP BY giorno, context
ORDER BY giorno DESC, costo_totale DESC;

Sul primo cliente (generatore di descrizioni), questa query ha rivelato immediatamente il problema dei 270 euro in più: il 62% delle chiamate API proveniva da un job di retry che rieseguiva le generazioni fallite per content filtering senza backoff esponenziale - ogni retry era una chiamata a costo pieno, e il tasso di filtering su certe categorie di prodotto (integratori alimentari, prodotti per adulti) era del 40%. La correzione è stata implementare un meccanismo di fallback che, dopo due retry falliti per content filtering, genera una descrizione generica da un template statico invece di continuare a bombardare l'API con prompt che verranno comunque rifiutati.

L'alerting è configurato su quattro soglie: costo giornaliero sopra il budget (alert immediato), latenza p95 sopra i 5 secondi per più di 10 minuti (degrado del servizio), tasso di errore sopra il 5% nell'ultima ora (possibile outage del provider), e drift di qualità superiore al 10% rispetto alla settimana precedente (possibile cambio nel modello). Gli alert vanno su Telegram per il team tecnico e su email per il product owner - perché un costo che raddoppia è un problema di business, non solo tecnico.

Valutazione automatica della qualità: il problema più difficile

Il monitoring dei costi e della latenza è relativamente semplice - sono numeri che si misurano oggettivamente. La qualità della risposta di un LLM è un problema completamente diverso, perché la "correttezza" è soggettiva e dipende dal contesto. Per il generatore di descrizioni prodotto, ho implementato un sistema di valutazione automatica a tre livelli: primo, verifica strutturale - la risposta contiene tutti i campi richiesti (titolo, descrizione breve, descrizione lunga, bullet point), rispetta i limiti di lunghezza, e non contiene placeholder o istruzioni del prompt leaked nell'output. Secondo, verifica linguistica - la risposta è in italiano corretto, non contiene frasi in inglese mischiate, e ha un punteggio di leggibilità Gulpease superiore a 50. Terzo, verifica semantica - la risposta menziona il brand del prodotto, include almeno due specifiche tecniche dal catalogo, e non contiene claim medici o promesse non verificabili che violerebbero la normativa sulla pubblicità. I primi due livelli sono automatici e catturano circa il 15% delle risposte problematiche. Il terzo livello richiede supervisione umana a campione - un operatore review 50 descrizioni a settimana scelte casualmente e segna quelle inadeguate, alimentando un dataset di feedback che uso per affinare il prompt.

Ottimizzazione dei costi basata sui dati di monitoring

Il monitoring non è solo difensivo - è lo strumento che abilita ottimizzazioni concrete sui costi operativi delle pipeline AI. Dopo il primo mese di raccolta dati sul generatore di descrizioni, ho identificato quattro interventi di ottimizzazione che hanno ridotto il costo mensile del 57% senza impattare la qualità delle risposte.

Il primo intervento è stato il prompt caching. Analizzando i log delle chiamate, ho scoperto che il 73% delle richieste condivideva lo stesso prompt di sistema (le istruzioni generali per la generazione delle descrizioni), che pesava 2.400 token. Con il prompt caching attivo sull'API Claude, quei 2.400 token vengono inviati una volta e riutilizzati nelle chiamate successive, riducendo il costo dei token di input del 68% per le chiamate dopo la prima della sessione. Il risparmio: circa 95 euro al mese.

Il secondo intervento è stato il model routing. Non tutte le descrizioni richiedono lo stesso livello di qualità. Le descrizioni per i prodotti di punta (i 200 SKU con il 60% del fatturato) vengono generate con Claude Sonnet per la massima qualità. Le descrizioni per i prodotti di catalogo standard (i restanti 11.800 SKU) vengono generate con Claude Haiku, che costa un quarto di Sonnet e produce risultati più che adeguati per descrizioni brevi e standardizzate. La logica di routing è una semplice regola nel wrapper: se il prodotto ha il flag premium nel catalogo, usa Sonnet; altrimenti usa Haiku. Il risparmio: circa 70 euro al mese.

Il terzo intervento è stato l'eliminazione dei retry su content filtering con un fallback a template statico, come descritto prima. Il quarto è stato la deduplicazione delle richieste: il job di generazione rieseguiva la chiamata API anche per i prodotti che avevano già una descrizione valida nel database, perché il check di esistenza era un SELECT sulla colonna sbagliata (cercava nella tabella vecchia delle descrizioni invece che nella nuova). Un bug di una riga che costava 45 euro al mese in chiamate API duplicate.

Nessuna di queste ottimizzazioni sarebbe stata possibile senza il monitoring. Senza i dati disaggregati per contesto, modello e tasso di retry, il team avrebbe continuato a pagare 420 euro al mese pensando che fosse "il costo dell'AI" - quando in realtà il 64% di quel costo era spreco recuperabile.

Ho descritto un approccio simile al monitoring applicativo tradizionale nel mio articolo su Laravel Pulse come strumento di osservabilità nativa - il principio è identico: non puoi migliorare ciò che non misuri, e la misurazione deve essere proporzionata alla criticità del sistema. Per una pipeline AI in produzione che gestisce contenuti rivolti a clienti finali, la qualità del monitoring è ciò che separa un sistema professionale da un esperimento che prima o poi esploderà in modo imbarazzante. Se hai pipeline AI in produzione senza monitoring strutturato - senza sapere quanto spendi per richiesta, quale sia la qualità media delle risposte, e quali anomalie il sistema sta nascondendo - contattami per implementare un layer di observability: in due giornate di lavoro configuriamo il tracking delle chiamate, la dashboard di costi e latenza, e il sistema di alerting che ti avvisa prima che i numeri diventino un problema.

Ultima modifica: