Ottimizzazione dei costi per i sistemi LLM: dove si spendono realmente i soldi

Spendi i token dove contano davvero.

Indice

I costi degli LLM scalano linearmente con l’utilizzo. Un sistema che elabora 10.000 richieste al giorno a 0,01 $ per richiesta costa 100 $ al giorno, ovvero 365 $ all’anno. Su scala enterprise, si supera i 10.000 $.

L’ottimizzazione dei costi non si tratta di tagliare corners. Si tratta di spendere i token dove contano.

Ogni token sprecato è un token che potresti aver speso per una risposta migliore.

Strategie di ottimizzazione dei costi LLM

Budget dei token

Il modo più semplice per controllare i costi è impostare dei limiti. Per sessione, per attività o per giorno.

Strategia 1: Budget per sessione

I budget per sessione sono semplici:

class SessionBudget:
    def __init__(self, budget_tokens: int = 10000):
        self.budget = budget_tokens
        self.used = 0

    def allocate(self, tokens: int) -> bool:
        if self.used + tokens <= self.budget:
            self.used += tokens
            return True
        return False

    def remaining(self) -> int:
        return self.budget - self.used

Strategia 2: Budget per attività

I budget per attività sono più utili. Diverse attività richiedono quantità diverse di contesto:

task_budgets:
  classify:
    max_tokens: 100
    model: qwen3-1.7b
  summarize:
    max_tokens: 500
    model: qwen3-8b
  code_review:
    max_tokens: 2000
    model: qwen2.5-coder-7b
  reason:
    max_tokens: 4000
    model: qwen3-32b

Strategia 3: Budget adattivi

I budget adattivi si regolano in base a ciò che accade realmente. Se le attività di classificazione utilizzano costantemente 80 token, smetti di allocarne 100:

class AdaptiveBudget:
    def __init__(self):
        self.task_history = {}

    def allocate(self, task_type: str) -> int:
        if task_type in self.task_history:
            return int(self.task_history[task_type] * 1.5)
        return 1000

    def record(self, task_type: str, tokens_used: int):
        if task_type not in self.task_history:
            self.task_history[task_type] = tokens_used
        else:
            self.task_history[task_type] = (
                0.9 * self.task_history[task_type] + 0.1 * tokens_used
            )

La media mobile esponenziale (peso 0.9) significa che l’utilizzo recente conta più della storia. Regola il peso in base alla volatilità dei tuoi carichi di lavoro.

API vs inferenza locale

L’inferenza locale è più economica su larga scala. Il punto di pareggio dipende dall’hardware e dai tassi API.

Modello API ($/M token) Costo locale/ora Pareggio
GPT-4o 2,50 $ / 10,00 $ — N/D
Claude Sonnet 4 3,00 $ / 15,00 $ — N/D
Qwen2.5-72B 0,50 $ / 2,00 $ ~0,50 $ ~4 ore/giorno
qwen3-32b 0,30 $ / 1,20 $ ~0,20 $ ~2 ore/giorno
qwen3-8b 0,10 $ / 0,40 $ ~0,05 $ ~1 ora/giorno

I calcoli sull’hardware:

Hardware Costo iniziale Elettricità mensile Pareggio rispetto all’API
RTX 3090 (usato) 600 $ 15 $ ~4 mesi
RTX 4090 1.500 $ 20 $ ~6 mesi
RTX 5080 1.000 $ 18 $ ~5 mesi
DGX Spark 2.000 $ 30 $ ~8 mesi

Con un utilizzo moderato — un’ora o più al giorno — l’inferenza locale si ripaga. Con un utilizzo elevato, i risparmi sono drastici. Il problema è il capitale iniziale. Una RTX 5080 costa 1.000 $. Una fattura API si può sospendere. L’hardware no.

Questo calcolo del pareggio presuppone che tu sia libero di cambiare, il che spesso è l’incognita maggiore rispetto al costo dell’hardware. Se i tuoi dati di fine-tuning, i tuoi strumenti di valutazione e gli schemi dei tuoi tool sono già costruiti attorno all’API di un provider, il vero costo della migrazione è il tempo di ingegneria necessario per svincolarsi — non il prezzo di una GPU. Data Gravity: Il vero costo dell’IA API-First copre questo meccanismo di lock-in e un’elenco di controllo per valutare quanto sei già dentro, prima di fare questi calcoli.

Strategie di fallback

Quando il tuo modello preferito è troppo costoso o troppo lento, passa a qualcosa di più economico. La chiave è sapere quando la qualità è “abbastanza buona”.

Strategia 1: Fallback basati sulla qualità

Il fallback basato sulla qualità prova i modelli finché l’output non raggiunge una soglia:

class QualityFallback:
    def __init__(self, quality_threshold: float = 0.8):
        self.threshold = quality_threshold
        self.models = [
            {"model": "claude-sonnet-4", "cost": 0.015},
            {"model": "qwen2.5-72b", "cost": 0.002},
            {"model": "qwen3-32b", "cost": 0.001},
            {"model": "qwen3-8b", "cost": 0.0004},
        ]

    def route(self, prompt: str) -> str:
        for model_config in self.models:
            result = self.call_model(model_config["model"], prompt)
            if self.evaluate_quality(result) >= self.threshold:
                return result
        return self.call_model(self.models[0]["model"], prompt)

Il problema è la valutazione stessa. Come misuri la qualità senza chiamare un altro modello? Alcuni sistemi usano un piccolo classificatore. Altri usano controlli euristici — lunghezza, struttura, presenza di parole chiave. Nessuno di questi è perfetto.

Strategia 2: Fallback basati sulla latenza

Il fallback basato sulla latenza è più semplice. Instrada verso il modello più veloce che soddisfi il tuo budget di tempo:

class LatencyFallback:
    def __init__(self, max_latency: float = 5.0):
        self.max_latency = max_latency
        self.models = [
            {"model": "qwen3-1.7b", "latency": 0.5},
            {"model": "qwen3-8b", "latency": 2.0},
            {"model": "qwen3-32b", "latency": 10.0},
            {"model": "claude-sonnet-4", "latency": 5.0},
        ]

    def route(self, prompt: str) -> str:
        for model_config in sorted(self.models, key=lambda x: x["latency"]):
            if model_config["latency"] <= self.max_latency:
                return self.call_model(model_config["model"], prompt)
        return self.call_model(self.models[0]["model"], prompt)

Caching

Il caching è l’ottimizzazione dei costi più sottovalutata. I prompt identici accadono più spesso di quanto pensi — richieste di classificazione, query di tipo FAQ, chiamate di tool ripetute.

Strategia 1: Caching dei prompt

Il caching esatto dei prompt è semplice:

import hashlib

class PromptCache:
    def __init__(self, max_size: int = 1000):
        self.cache = {}
        self.max_size = max_size

    def get(self, prompt: str) -> str | None:
        key = hashlib.sha256(prompt.encode()).hexdigest()
        return self.cache.get(key)

    def set(self, prompt: str, response: str):
        key = hashlib.sha256(prompt.encode()).hexdigest()
        if len(self.cache) >= self.max_size:
            self.cache.pop(next(iter(self.cache)))
        self.cache[key] = response

Strategia 2: Caching semantico

Il caching semantico è più utile. Cattura prompt che sono diversi ma significano la stessa cosa:

from sentence_transformers import SentenceTransformer

class SemanticCache:
    def __init__(self, similarity_threshold: float = 0.95):
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.cache = {}
        self.threshold = similarity_threshold

    def get(self, prompt: str) -> str | None:
        prompt_embedding = self.model.encode([prompt])[0]
        for cached_prompt, cached_response in self.cache.items():
            cached_embedding = self.model.encode([cached_prompt])[0]
            similarity = self.cosine_similarity(
                prompt_embedding, cached_embedding
            )
            if similarity >= self.threshold:
                return cached_response
        return None

    def set(self, prompt: str, response: str):
        self.cache[prompt] = response

La soglia conta. 0.95 è aggressivo — solo prompt molto simili corrispondono. 0.85 è più permissivo ma rischia di restituire risposte errate. Misura il tuo tasso di mancato successo e regola.

La cache delle risposte per le query comuni ne vale la pena. Se gli utenti chiedono “com’è il meteo” o “che ore sono” ripetutamente, metti in cache il pattern, non solo il prompt esatto:

class ResponseCache:
    def __init__(self):
        self.common_queries = {
            "what is the weather": "Check weather API",
            "what is the time": "Check system time",
            "who is the president": "Check current president",
        }

    def get(self, query: str) -> str | None:
        query_lower = query.lower()
        for common_query, response in self.common_queries.items():
            if common_query in query_lower:
                return response
        return None

Non è sofisticato, ma funziona. Le query comuni sono comuni per una ragione.

Quando l’ottimizzazione aiuta

L’ottimizzazione conta quando elabori grandi volumi, esegui carichi di lavoro misti o paghi costi API che si accumulano.

Non conta quando stai facendo prototipazione, usi un singolo modello o elabori bassi volumi. La complessità di budgeting, fallback e caching non vale la pena per un sistema che fa 100 richieste al giorno.

Fai prima funzionare il flusso di base. Aggiungi ottimizzazioni quando arriva la fattura.

Compromessi

Strategia Costo Qualità Complessità
Nessuna ottimizzazione Massima Costante Minima
Budget dei token Moderata Variabile Media
Modelli di fallback Bassa-Media Variabile Media
Caching Minima Alta (per hit di cache) Media
Ibrida Ottimizzata Ottimizzata Massima

I sistemi di produzione di solito usano un approccio ibrido. Budget per sessione, fallback su qualità o latenza, caching di ciò che si può. La complessità è reale, ma così sono i risparmi.

Correlati

Iscriviti

Ricevi nuovi articoli su sistemi, infrastruttura e ingegneria AI.