Ottimizzazione dei costi per i sistemi LLM: dove vanno davvero i soldi
Spendi token dove contano davvero.
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.

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
- Model Routing Strategies — routing basato su capacità, costi e latenza
- LLM Guardrails in Practice — validazione degli input, filtraggio degli output, sicurezza
- Multi-Model System Design — architettura per modelli multipli
- LLM Architecture — pilastro del design del sistema: routing, costi, guardrails e orchestrazione