Kostenoptimalisatie voor LLM-systemen: Waar het geld echt naartoe gaat
Besteed tokens waar het echt toe doet.
De kosten van LLM’s schalen lineair mee met het gebruik. Een systeem dat 10.000 verzoeken per dag verwerkt tegen $0,01 per verzoek, kost $100 per dag — dat is $365 per jaar. Op enterprise-schaal loopt dat op tot meer dan $10.000.
Kostenoptimalisatie gaat niet over hoekjes afsnijden. Het gaat erom tokens te besteden waar het echt toe doet.
Elke token die je verspilt, is een token die je had kunnen besteden aan een beter antwoord.

Tokenbudgettering
De eenvoudigste manier om kosten te beheersen, is door limieten in te stellen. Per sessie, per taak of per dag.
Strategie 1: Budgetten per sessie
Budgetten per sessie zijn eenvoudig:
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
Strategie 2: Budgetten per taak
Budgetten per taak zijn nuttiger. Verschillende taken vereisen verschillende hoeveelheden context:
task_budgets:
classify:
max_tokens: 100
model: qwen2.5-1.5b
summarize:
max_tokens: 500
model: qwen2.5-7b
code_review:
max_tokens: 2000
model: qwen2.5-coder-7b
reason:
max_tokens: 4000
model: qwen2.5-32b
Strategie 3: Adaptieve budgetten
Adaptieve budgetten passen zich aan op basis van wat er daadwerkelijk gebeurt. Als classificatietaken consistent 80 tokens gebruiken, stop dan met het toewijzen van 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
)
De exponentiële glanzende gemiddelde (met een gewicht van 0,9) betekent dat recent gebruik zwaarder weegt dan geschiedenis. Pas het gewicht aan op basis van hoe volatiel je werklasten zijn.
API versus lokale inferentie
Lokale inferentie is op schaal goedkoper. Het break-even punt hangt af van je hardware en API-tarieven.
| Model | API ($/M tokens) | Lokale kosten/uur | Break-even |
|---|---|---|---|
| GPT-4o | $2,50 / $10,00 | — | N.v.t. |
| Claude Sonnet 4 | $3,00 / $15,00 | — | N.v.t. |
| Qwen2.5-72B | $0,50 / $2,00 | ~$0,50 | ~4 uur/dag |
| Qwen2.5-32B | $0,30 / $1,20 | ~$0,20 | ~2 uur/dag |
| Qwen2.5-7B | $0,10 / $0,40 | ~$0,05 | ~1 uur/dag |
De hardware-rekening:
| Hardware | Investering | Maandelijkse stroomkosten | Break-even vs API |
|---|---|---|---|
| RTX 3090 (tweedehands) | $600 | $15 | ~4 maanden |
| RTX 4090 | $1.500 | $20 | ~6 maanden |
| RTX 5080 | $1.000 | $18 | ~5 maanden |
| DGX Spark | $2.000 | $30 | ~8 maanden |
Bij matig gebruik — een uur of meer per dag — betaalt lokale inferentie zich terug. Bij hoog gebruik zijn de besparingen aanzienlijk. Het nadeel is de initiële kapitaalinvestering. Een RTX 5080 kost $1.000. Een API-rekening kun je pauzeren. Hardware kun je niet.
Terugvalstrategieën
Wanneer je voorkeurmodel te duur of te traag is, val terug op iets goedkopers. De sleutel is weten wanneer de kwaliteit “goed genoeg” is.
Strategie 1: Kwaliteitsgebaseerde terugval
Kwaliteitsgebaseerde terugval probeert modellen totdat de output een bepaalde drempel bereikt:
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": "qwen2.5-32b", "cost": 0.001},
{"model": "qwen2.5-7b", "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)
Het probleem is de evaluatie zelf. Hoe meet je kwaliteit zonder nog een model aan te roepen? Sommige systemen gebruiken een kleine classifier. Anderen gebruiken heuristische controles — lengte, structuur, aanwezigheid van trefwoorden. Geen van deze methoden is perfect.
Strategie 2: Latentie-gebaseerde terugval
Latentie-gebaseerde terugval is eenvoudiger. Routeer naar het snelste model dat binnen je tijdslimiet past:
class LatencyFallback:
def __init__(self, max_latency: float = 5.0):
self.max_latency = max_latency
self.models = [
{"model": "qwen2.5-1.5b", "latency": 0.5},
{"model": "qwen2.5-7b", "latency": 2.0},
{"model": "qwen2.5-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
Caching is de meest onderschatte kostenoptimalisatie. Identieke prompts komen vaker voor dan je denkt — classificatieverzoeken, FAQ-achtige queries, herhaalde tool-aanroepen.
Strategie 1: Promptcaching
Exacte promptcaching is eenvoudig:
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
Strategie 2: Semantisch caching
Semantisch caching is nuttiger. Het vangt prompts die verschillend zijn, maar hetzelfde betekenen:
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
De drempelwaarde is belangrijk. 0,95 is agressief — alleen zeer vergelijkbare prompts komen overeen. 0,85 is vergevingsgezinder, maar loopt het risico verkeerde antwoorden terug te geven. Meet je miss-ratio en pas hem aan.
Reactie-caching voor veelvoorkomende queries is ook de moeite waard. Als gebruikers herhaaldelijk vragen naar “wat is het weer” of “hoe laat is het”, cacheer dan het patroon, niet alleen de exacte prompt:
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
Dit is niet geavanceerd, maar het werkt. Veelvoorkomende queries zijn veelvoorkomend om een reden.
Wanneer optimalisatie helpt
Optimalisatie is belangrijk wanneer je hoge volumes verwerkt, gemengde werklasten uitvoert of API-kosten betaalt die oplopen.
Het doet er niet toe wanneer je prototype, één model gebruikt of lage volumes verwerkt. De complexiteit van budgettering, terugval en caching is niet de moeite waard voor een systeem dat slechts 100 verzoeken per dag doet.
Krijg eerst de basisflow werkend. Voeg optimalisatie toe wanneer de rekening binnenkomt.
Afwegingen
| Strategie | Kosten | Kwaliteit | Complexiteit |
|---|---|---|---|
| Geen optimalisatie | Hoogst | Consistent | Laagst |
| Tokenbudgettering | Matig | Variabel | Middel |
| Terugvalmodellen | Laag-Matig | Variabel | Middel |
| Caching | Laagst | Hoog (voor cache-hits) | Middel |
| Hybride | Geoptimaliseerd | Geoptimaliseerd | Hoogst |
Productiesystemen draaien meestal hybride. Budgetteer per sessie, val terug op kwaliteit of latentie, en cacheer wat je kunt. De complexiteit is reëel, maar de besparingen ook.
Gerelateerd
- Model Routing Strategies — routing op basis van capaciteit, kosten en latentie
- LLM Guardrails in Practice — inputvalidatie, outputfiltering, veiligheid
- Multi-Model System Design — architectuur voor meerdere modellen
- LLM Architecture — systeemontwerp pijler: routing, kosten, guardrails en orkestratie