Der instinktive Schritt, wenn eine LLM-Rechnung unangenehm wird, ist das Modell gegen ein günstigeres zu tauschen und damit fertig zu sein. Manchmal ist das tatsächlich in Ordnung. Oft ist es das nicht, und die Art, wie es nicht in Ordnung ist, ist heimtückisch: der Agent antwortet immer noch, die Demo sieht immer noch gut aus, und drei Wochen später bemerkt jemand, dass die Klassifizierungsgenauigkeit still um vier Punkte gefallen ist, oder dass der Agent anfing, einen Edge Case zu verpassen, den er früher abdeckte. Niemand verbindet diese Regression mit dem Modellwechsel, weil nichts abgestürzt ist – es wurde einfach schlechter auf eine Weise, die nirgendwo auffällt, wenn man nicht gezielt danach misst.
Das eigentliche Problem mit LLM-Kostenoptimierung ist nicht, Techniken zu finden, die Geld sparen – Model-Routing, Prompt-Caching, Kontext-Trimming und semantisches Caching tun das alle zuverlässig. Das Problem ist, dass jede von ihnen ein Hebel ist, der einen Geldbetrag gegen ein Qualitätsrisiko tauscht, und die meisten Teams ziehen den Hebel, ohne jemals den Test zu bauen, der ihnen sagen würde, wie viel Qualität sie tatsächlich eingebüßt haben. Dieser Beitrag behandelt vier konkrete Techniken und für jede den spezifischen Regressionstest, den sie braucht, bevor sie irgendwo in die Nähe von Produktionstraffic kommt.
Du wirst lernen:
- Warum „einfach ein günstigeres Modell verwenden" still statt laut scheitert, und was stattdessen gemessen werden sollte
- Wie man einen Model-Router baut, der einfache Anfragen ohne Rätselraten an ein günstiges Modell sendet
- Wie Prompt-Caching mechanisch funktioniert und warum Präfix-Stabilität das entscheidende Spiel ist
- Eine Kontext-Trimming-Strategie, die nicht still die Tatsache löscht, die der Agent drei Turns später gebraucht hätte
- Wann semantisches Caching das Staleness-Risiko wert ist und wann es still Antworten vergiftet
- Eine konkrete, reproduzierbare Methodik zum Testen jeder Technik vor dem Deployment
- Wie alle vier zu einem einzigen kostenbewussten Call-Pfad kombiniert werden, ohne ihre Risiken zu stapeln
Inhaltsverzeichnis
- Warum „einfach das Modell degradieren" still scheitert
- Model-Routing
- Prompt-Caching
- Kontext-Trimming
- Semantisches Caching
- Eine reproduzierbare Testmethodik
- Alles zusammensetzen: Ein kostenbewusster Call-Pfad
- Auf einen Blick
- Fazit
Warum „einfach das Modell degradieren" still scheitert
Ein Modellwechsel wirft keine Exception, wenn er deinen Agenten schlechter macht. Deine Latenz könnte sogar besser werden. Der Fehler taucht später auf und meist indirekt – ein Support-Ticket wegen einer falschen Antwort, eine leicht erhöhte Eskalationsrate, die niemand genau genug verfolgt, um sie über Wochen zu bemerken, eine Structured-Extraction-Pipeline, die beginnt, still ein optionales Feld fallen zu lassen, das ein kleineres Modell weniger zuverlässig befüllt. Nichts davon löst einen Alert aus, weil in einem typischen Setup nichts auf „hat sich die Qualität geändert" schaut, nur auf „ist der Dienst oben geblieben."
Das ist das eigentliche Argument für den Bau eines Regressionstests, bevor man an den Kosten anpackt, nicht danach: Kosten sind trivial beobachtbar (es ist eine Zahl auf einer Rechnung) und Qualität ist es nicht, und das Optimieren der Sache, die man sehen kann, während man die ignoriert, die man nicht sehen kann, ist genau der Weg, durch den eine Kostensenkungsmaßnahme still zu einer Produktregression wird. Jede Technik unten spart auf eine Weise, die leicht zu verifizieren ist, und riskiert Qualität auf eine Weise, die es nicht ist – es sei denn, du baust das Harness dafür vorab. Wenn der Agent, den du optimierst, graphbasiert ist mit verzweigter Logik statt einem einzelnen Aufruf, ist der Unterschied zwischen einer einfachen Chain und etwas, das LangGraph braucht, eine Frage, die es wert ist, zuerst zu klären, da sich der Ort, an dem du Routing und Caching einfügst, zwischen den beiden deutlich unterscheidet.
Model-Routing
Nicht jeder Aufruf in einen Agenten braucht dein leistungsfähigstes Modell. Ein Klassifizierungsschritt, der entscheidet „ist das eine Abrechnungsfrage oder eine technische?", und ein Schritt, der eine nuancierte Antwort auf einen verärgerten Kunden verfasst, sind nicht gleich schwierig, und für beide Frontier-Modell-Preise zu zahlen ist Geldausgeben beim einfachen für keinen Grund.
Der Mechanismus, der tatsächlich funktioniert, ist ein leichtgewichtiger Klassifikator – oft selbst ein kleines Modell – der die Schwierigkeit der Aufgabe bewertet und entsprechend weiterleitet, anstatt eine fest codierte Zuordnung pro Endpunkt, die manuell gepflegt werden muss, wenn sich dein Produkt ändert.
import anthropic
client = anthropic.Anthropic()
def classify_difficulty(query: str) -> str:
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=10,
messages=[{
"role": "user",
"content": f"Classify this support query as 'simple' or 'complex'. "
f"Reply with one word only.\n\nQuery: {query}",
}],
)
return response.content[0].text.strip().lower()
def route_and_answer(query: str) -> str:
difficulty = classify_difficulty(query)
model = "claude-sonnet-5" if difficulty == "simple" else "claude-opus-5"
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": query}],
)
return response.content[0].text
Der Klassifikator-Aufruf selbst kostet etwas, also zahlt sich Routing nur dann aus, wenn die volumengewichteten Einsparungen durch das Downgrading eines bedeutenden Anteils des Traffics den Klassifizierungs-Overhead überwiegt – in der Praxis ist das fast immer der Fall, sobald man über eine kleine Anzahl täglicher Aufrufe hinaus ist, da der Klassifikator ein winziger, schneller, günstiger Aufruf im Vergleich zur Generation ist, die er steuert. Zum Zeitpunkt des Schreibens kostet Claude Haiku 4.5 etwa $1 pro Million Input-Tokens im Vergleich zu Claude Sonnet 5 mit etwa $3, sodass ein Klassifizierungs-Prompt von ein paar hundert Tokens ein Rundungsfehler neben dem ist, was du sparst, indem du einfache Anfragen nicht an ein teureres Modell sendest – aber behandle diese Zahlen als illustrativ, nicht als Versprechen, da sich Listenpreise ändern und deine eigenen Token-Zahlen abweichen werden.
Der Fehlermodus ist ein Router, der in die teure Richtung falsch liegt – der etwas genuinlich Komplexes an das günstige Tier weiterleitet und eine schlechtere Antwort bekommt, die niemand bemerkt, weil die Antwort immer noch plausibel aussieht. Genau deshalb braucht Routing das Regressionsharness im Testabschnitt unten, nicht nur eine Stichprobe von ein paar Beispielen, die zufällig korrekt weitergeleitet wurden.
Prompt-Caching
Prompt-Caching ist dem Gratis-Gewinn auf dieser Liste am nächsten, weil es weder Modellwahl noch Trimming berührt – es vermeidet nur das erneute Zahlen für Tokens, die du unverändert über Anfragen hinweg immer wieder sendest. Claudes Prompt-Caching funktioniert auf Basis von Präfix-Matching: Markiere einen Punkt in deiner Anfrage mit einem Cache-Breakpoint, und wenn eine spätere Anfrage exakt dieses Präfix teilt, wird der gecachte Teil mit einem deutlichen Rabatt statt zum vollen Input-Preis abgerechnet.
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[{
"type": "text",
"text": LONG_SYSTEM_PROMPT_WITH_TOOL_DEFINITIONS,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": user_query}],
)
print(response.usage.cache_read_input_tokens) # tokens served from cache, billed at a discount
Das mechanische Detail, das bestimmt, ob das wirklich funktioniert, ist Präfix-Stabilität. Caching ist ein Byte-für-Byte-Präfix-Match – ein einziges geändertes Zeichen irgendwo vor dem Cache-Breakpoint invalidiert alles danach. Der häufigste Weg, auf dem Teams unwissentlich ihr eigenes Caching aushebeln, ist das Platzieren von etwas Flüchtigem vor dem Breakpoint: ein Zeitstempel, der in den System-Prompt interpoliert wird, ein unsortiertes Dict, das zwischen Anfragen unterschiedlich serialisiert wird, eine Request-ID, die in Anweisungen eingebacken ist, die generisch hätten bleiben sollen. response.usage.cache_read_input_tokens ist die Zahl, die man beobachten sollte – wenn sie bei Anfragen, die Cache treffen sollten, konstant null ist, variiert irgendwas upstream still das Präfix.
Das Qualitätsrisiko hier ist im Vergleich zu den anderen drei Techniken genuinlich niedrig, weil Caching nicht ändert, was das Modell sieht – es ändert, wie dieser unveränderte Inhalt abgerechnet wird. Der Test, den du trotzdem willst, ist eine Prüfung, dass Cache-Hit- und Cache-Miss-Antworten für dieselbe Eingabe verhaltensidentisch sind, was hauptsächlich dazu dient, Bugs in deinem eigenen Caching-Setup zu fangen (ein falsch platzierter Breakpoint, Inhaltsreihenfolge, die zwischen „Tools dann System" und „System dann Tools" unterschiedlich ist) statt eines inherenten Risikos in der Technik.
Kontext-Trimming
Der Kontext eines Agenten wächst mit jedem Turn – mehr Nachrichten, mehr Tool-Ergebnisse, mehr Geschichte – und jedes dieser Tokens wird bei jedem folgenden Aufruf erneut gesendet und erneut abgerechnet, es sei denn, etwas beschneidet es aktiv. Kontext-Trimming schneidet dieses Wachstum, aber es ist die Technik, die am ehesten etwas Subtiles bricht, weil das Token, das du trimmst, möglicherweise das eine Stück Information ist, das ein späterer Turn tatsächlich gebraucht hätte.
Eine vertretbare Trimming-Strategie hält neuere Turns wörtlich und fasst ältere zusammen oder lässt sie fallen, statt gleichmäßig oder nur nach roher Token-Anzahl zu trimmen:
def trim_context(messages: list[dict], keep_recent: int = 6) -> list[dict]:
if len(messages) <= keep_recent:
return messages
older, recent = messages[:-keep_recent], messages[-keep_recent:]
summary = summarize_messages(older) # a cheap model call, or a rule-based extractor
return [{"role": "user", "content": f"[Earlier conversation summary]: {summary}"}, *recent]
Der Fehlermodus, gegen den man spezifisch designen sollte, ist der Verlust einer Tatsache, die erst mehrere Turns später wichtig wird – eine Einschränkung, die der Benutzer früh geäußert hat („Ich bin allergisch gegen Meeresfrüchte", „Budget ist maximal 500 €"), die ein naives recency-basiertes Trimming still fallen lässt, sobald es aus dem „recent"-Fenster herausaltert. Ein Zusammenfassungsschritt, der explizit angewiesen ist, Einschränkungen, Entscheidungen und geäußerte Tatsachen zu bewahren (nicht nur narrativen Inhalt), ist sinnvoll sicherer als ein harter Token-Anzahl-Cutoff, und es lohnt sich, diese Anweisung explizit zu schreiben statt einem generischen „Fasse das zusammen"-Prompt zu vertrauen, die richtigen Details zu priorisieren.
Das Trimmen von Tool-Ergebnissen verdient eine separate Behandlung vom Trimmen von Gesprächsturns – eine große JSON-Nutzlast aus einem früheren Tool-Aufruf ist oft sicher zu komprimieren oder fallen zu lassen, sobald das Modell sie bereits verwendet hat zu antworten, da es sich um Rohdaten handelt und nicht um eine geäußerte Tatsache, während die eigene Schlussfolgerung des Modells aus diesen Daten es üblicherweise wert ist, beizubehalten.
Semantisches Caching
Semantisches Caching überspringt den Modell-Aufruf vollständig, wenn eine neue Anfrage als ausreichend ähnlich zu einer bereits beantworteten eingestuft wird – typischerweise durch Embedding-Ähnlichkeit gegen einen Cache früherer Anfrage-/Antwort-Paare, statt des exakten Matchings, das ein einfacher Key-Value-Cache bieten würde.
def semantic_cache_lookup(query: str, threshold: float = 0.92) -> str | None:
query_embedding = embed(query)
best_match, best_score = None, 0.0
for cached_query, cached_response, cached_embedding in cache_store:
score = cosine_similarity(query_embedding, cached_embedding)
if score > best_score:
best_match, best_score = cached_response, score
return best_match if best_score >= threshold else None
def answer_with_semantic_cache(query: str) -> str:
cached = semantic_cache_lookup(query)
if cached is not None:
return cached
response = client.messages.create(
model="claude-sonnet-5", max_tokens=1024,
messages=[{"role": "user", "content": query}],
)
answer = response.content[0].text
cache_store.append((query, answer, embed(query)))
return answer
Das ist die riskanteste Technik auf dieser Liste, und es lohnt sich, direkt zu sagen warum: Die gesamte Prämisse ist, eine neue Frage mit einer Antwort zu beantworten, die für eine andere Frage generiert wurde, die nur auf Embedding-Ähnlichkeit hoch bewertet wurde. Zwei Anfragen können semantisch nah sein und trotzdem eine materiell unterschiedliche korrekte Antwort haben – „Was ist Ihre Rückgaberichtlinie für Elektronik" und „Was ist Ihre Rückgaberichtlinie für Elektronik, die während des Weihnachtsverkaufs gekauft wurde" werden nah zusammen eingebettet und können genuinlich unterschiedliche Antworten haben. Ein Cache mit zu niedriger Ähnlichkeitsschwelle scheitert nicht laut; er liefert selbstbewusst falsche Antworten, die wie vollkommen normale Antworten aussehen.
Semantisches Caching verdient sein Risiko in engen Situationen: hohes Anfragevolumen mit echter Wiederholung (ein FAQ-artiger Support-Bot, kein offener Coding-Agent), Inhalt, bei dem Staleness akzeptabel ist oder sich die zugrundeliegenden Fakten langsam ändern, und – entscheidend – eine Schwelle, die du tatsächlich gegen gelabelte Beispiele eingestellt hast statt einen Wert zu nehmen, den du gewählt hast, weil er sich „streng genug anfühlte." Überall dort, wo die genaue Formulierung die korrekte Antwort ändert, sollte diese Technik nirgendwo in der Nähe des Call-Pfads sein.
Eine reproduzierbare Testmethodik
Jede Technik oben braucht dasselbe Grundding, bevor sie in die Nähe von Produktionstraffic kommt: ein festes Evaluationsset, eine Qualitätsmetrik, die kein Bauchgefühl ist, und einen Vorher-Nachher-Vergleich, der gegen beide läuft. Nichts davon erfordert etwas Exotisches – es erfordert, es tatsächlich zu tun, was der Schritt ist, den die meisten Kostensenkungsänderungen überspringen.
Bau ein festes Testset aus echtem Traffic. Ziehe 100–300 echte Anfragen, die dein System tatsächlich verarbeitet hat (nach Bedarf anonymisiert), die den Schwierigkeitsbereich abdecken, den du in der Produktion siehst – nicht nur die einfachen Fälle, die jede Technik sicher aussehen lassen. Wenn du noch keinen Produktionstraffic hast, schreibe Fälle von Hand, die speziell die oben beschriebenen Fehlermodi prüfen: ein Multi-Constraint-Gespräch für Kontext-Trimming, ein Paar fast-doppelter-aber-unterschiedlich-beantworteter Anfragen für semantisches Caching, eine Mischung aus genuinlich einfachen und genuinlich schwierigen Anfragen für Model-Routing.
Bewerte mit etwas Reproduzierbarem, nicht einem einzelnen menschlichen Durchsehen. Für Aufgaben mit einer überprüfbaren Antwort (Klassifizierung, Extraktion, Routing-Entscheidungen) sind Exact-Match oder Field-Level-Genauigkeit gegen ein gelabeltes Set unkompliziert und günstig zu berechnen. Für offene Generation ist ein LLM-als-Richter, der die Baseline-Antwort gegen die optimierte zu einem festen Rubric vergleicht (Korrektheit, Vollständigkeit, Ton), ein vernünftiger Ersatz für menschliche Überprüfung – aber validiere den Richter selbst zuerst gegen eine kleine menschlich-gelabelte Stichprobe, da ein Richtermodell mit seinen eigenen blinden Flecken eine Regression gummistempeln kann, die es zufällig teilt.
Führe Baseline und Optimiert nebeneinander aus, nicht sequentiell. Vergleiche exakt dasselbe Testset gegen die Vor-Änderungs- und Nach-Änderungs-Pipeline im selben Lauf, nicht „die durchschnittliche Qualität letzten Monat" gegen „diesen Monat" – zwischen diesen zwei Fenstern ändern sich zu viele andere Dinge, um eine Verschiebung mit angemessenem Vertrauen deiner spezifischen Änderung zuzuschreiben.
Setze einen konkreten Schwellenwert, bevor du die Ergebnisse betrachtest, nicht danach. Entscheide im Voraus, welcher Qualitätsabfall für welche Einsparungen akzeptabel ist – nur zur Illustration könnte ein Team entscheiden, dass ein 2-Punkte-Abfall bei der Genauigkeit für eine 40%ige Kostensenkung akzeptabel ist und ein 10-Punkte-Abfall nicht, welche auch immer die tatsächlichen Zahlen für ihr eigenes System sind. Den Schwellenwert nach dem Sehen der Zahl zu entscheiden ist der Weg, wie eine marginale Regression im Nachhinein zu „akzeptabel" rationalisiert wird.
Führe dasselbe Testset nach jeder Prompt-, Modell- oder Schwellenänderung erneut aus. Model-Routing-Schwellenwerte, Cache-Ähnlichkeitsgrenzen und Trimming-Fenstergrößen driften alle, wenn sich ein Produkt entwickelt – der Regressionstest ist kein einmaliges Gate, sondern etwas, das du immer dann erneut ausführst, wenn sich einer dieser Knöpfe dreht.
Alles zusammensetzen: Ein kostenbewusster Call-Pfad
Die vier Techniken lassen sich kombinieren, aber jede, die man stapelt, multipliziert die Anzahl der Stellen, an denen eine stille Regression sich verstecken könnte – genau deshalb muss die Testmethodik oben gegen die kombinierte Pipeline laufen, nicht nur gegen jede Technik isoliert.
def answer_query(query: str, history: list[dict]) -> str:
cached = semantic_cache_lookup(query)
if cached is not None:
return cached
trimmed_history = trim_context(history)
difficulty = classify_difficulty(query)
model = "claude-sonnet-5" if difficulty == "simple" else "claude-opus-5"
response = client.messages.create(
model=model,
max_tokens=1024,
system=[{
"type": "text",
"text": STABLE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}],
messages=[*trimmed_history, {"role": "user", "content": query}],
)
answer = response.content[0].text
cache_store.append((query, answer, embed(query)))
return answer
Die Reihenfolge ist wichtig: Zuerst die semantische Cache-Prüfung (günstigster möglicher Ausgang), dann den Kontext trimmen vor dem Routing, damit der Schwierigkeitsklassifikator nicht gegen eine aufgeblähte Geschichte bewertet, dann Prompt-Caching auf den stabilen System-Prompt unabhängig davon, welches Modelltier ausgewählt wird. Jede Schicht ist eine echte, unabhängige Einsparung – aber jede ist auch ein echter, unabhängiger Ort, an dem ein Bug oder ein zu loser Schwellenwert still Qualität kosten kann, was genau der Grund ist, dein Regressionsset gegen diesen vollständigen Pfad auszuführen, nicht vier separate ungetestete Stücke, die zusammengebaut werden und davon ausgegangen werden, sicher zu komponieren.
Auf einen Blick
| Technik | Wobei gespart wird | Primäres Qualitätsrisiko | Wo es am besten passt |
|---|---|---|---|
| Model-Routing | Ausgegebene/eingehende Tokens zum günstigeren Per-Token-Preis abgerechnet | Schwierige Anfragen an ein schwächeres Modell weiterleitet | Hohes Anrufvolumen mit echter Schwierigkeitsverteilung |
| Prompt-Caching | Erneutes Abrechnen für unveränderte Präfix-Tokens | Niedrig – hauptsächlich Implementierungsbugs (kaputte Cache-Treffer) | Jeder Agent mit einem stabilen System-Prompt oder einer Tool-Liste |
| Kontext-Trimming | Tokens, die bei jedem Turn erneut gesendet werden, wenn die Geschichte wächst | Eine Tatsache verlieren, die früh geäußert wurde und später wichtig ist | Lang laufende Gespräche, Multi-Turn-Agenten |
| Semantisches Caching | Den Modell-Aufruf vollständig überspringen | Eine falsche Antwort für eine ähnliche-aber-andere Anfrage liefern | Hochwiederholungs-, niedrige-Staleness-Toleranz-Workloads |
Die Tabelle von links nach rechts zu lesen verfolgt grob das Einsparpotenzial und das Risiko – semantisches Caching kann die Kosten eines Aufrufs vollständig eliminieren, was auch genau der Grund ist, warum ein schlechter Treffer eine falsche Antwort ist, nicht nur eine schlechtere. Prompt-Caching sitzt am anderen Ende: vergleichsweise kleine garantierte Einsparungen auf stabilem Inhalt, mit fast keiner Möglichkeit, versehentlich eine falsche Antwort zu liefern, weil es nie ändert, was das Modell tatsächlich sieht.
Noch ein Hebel, der eine Erwähnung wert ist, auch wenn es keine der vier Kerntechniken ist: die Batch-API, für alles, was nicht latenzempfindlich ist. Batch-Anfragen werden typischerweise deutlich unter dem Standard-Synchronpreis berechnet im Austausch für asynchrone Verarbeitung – eine gute Wahl für nächtliche Zusammenfassungsjobs, Massenklassifizierung oder jede Pipeline, bei der „die Antwort erscheint in ein paar Stunden" ein akzeptabler Tausch für eine wesentlich niedrigere Rechnung ist. Es kombiniert nicht mit den anderen vier so, wie diese miteinander kombinieren – man sendet eine Anfrage entweder synchron durch den kostenbewussten Pfad oben oder asynchron durch Batch – aber für den Teil deines Workloads, der die Verzögerung tolerieren kann, ist es oft die einzige größte verfügbare Einsparung, bevor man Modellwahl, Caching oder Trimming berührt.
Fazit
Keine dieser vier Techniken ist fragwürdiges Engineering – Model-Routing, Prompt-Caching, Kontext-Trimming und semantisches Caching sind allesamt Standard, gut verstandene Wege, eine LLM-Rechnung zu senken, und die meisten Produktionssysteme sollten mindestens die ersten beiden verwenden. Das optionale Teil ist nicht die Technik, sondern ob man den Regressionstest baut, bevor oder nachdem man herausfindet, dass er wichtig war. Kosten sind die einfache Zahl zum Beobachten, weil sie direkt auf der Rechnung steht; Qualität erscheint nur, wenn man etwas baut, das sie gezielt misst, gegen ein festes Testset, bevor die Änderung shippt und jedes Mal, wenn sich einer der Abstimmungsknöpfe dreht. Das tun, und Kosten senken hört auf, eine Wette gegen die eigene Produktqualität zu sein, und wird zu einer Entwicklungsänderung wie jede andere – gemessen, reversibel und in der besten Weise langweilig.

