Ottimizzazione dei costi per i sistemi LLM: dove vanno davvero i soldi

Spendi token dove contano davvero.

Indice

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

L’ottimizzazione dei costi non significa tagliare gli angoli. Si tratta di spendere i token dove contano davvero.

Ogni token che sprechi è un token che avresti potuto spendere per una risposta migliore.

LLM cost optimization strategies

Gestione del budget dei token

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

Strategia 1: Budget per sessione

I budget per sessione sono diretti:

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 compito

I budget per compito sono più utili. Compiti diversi 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 i compiti 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 a quanto sono volatili i tuoi carichi di lavoro.

Inference API vs locale

L’inferenza locale è più economica su larga scala. Il punto di pareggio dipende dal tuo hardware e dalle tariffe 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

Il calcolo hardware:

Hardware Iniziale Elettricità mensile Pareggio vs 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 rovescio della medaglia è il capitale iniziale. Una RTX 5080 costa $1.000. Una bolletta API puoi metterla in pausa. L’hardware no.

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à è “sufficiente”.

Strategia 1: Fallback basato sulla qualità

Il fallback basato sulla qualità prova i modelli finché l’output non soddisfa 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 classificatore piccolo. Altri usano controlli euristici — lunghezza, struttura, presenza di parole chiave. Nessuno di questi è perfetto.

Strategia 2: Fallback basato sulla latenza

Il fallback basato sulla latenza è più semplice. Instrada verso il modello più veloce che soddisfa 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 stile FAQ, chiamate ripetute agli strumenti.

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 i 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 è importante. 0,95 è aggressivo — solo prompt molto simili corrispondono. 0,85 è più permissivo ma rischia di restituire risposte errate. Misura il tuo tasso di mancata corrispondenza e regola.

Il caching delle risposte per le query comuni vale la pena anche così. Se gli utenti chiedono ripetutamente “com’è il tempo” o “che ora è”, memorizza 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 un motivo.

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 fai prototipazione, usi un singolo modello o elabori piccoli volumi. La complessità del budgeting, del fallback e del caching non ne vale la pena per un sistema che fa 100 richieste al giorno.

Fai funzionare prima il flusso base. Aggiungi l’ottimizzazione quando arriva la bolletta.

Tradeoff

Strategia Costo Qualità Complessità
Nessuna ottimizzazione Più alto Costante Più bassa
Budget dei token Moderato Variabile Media
Modelli di fallback Basso-Medio Variabile Media
Caching Più basso Alta (per i hit di cache) Media
Ibrido Ottimizzato Ottimizzato Più alta

I sistemi di produzione di solito girano in modalità ibrida. Budget per sessione, fallback sulla qualità o latenza, caching di ciò che puoi. La complessità è reale, ma anche i risparmi.

Correlati

Iscriviti

Ricevi nuovi articoli su sistemi, infrastruttura e ingegneria AI.