🎁 Neu Kostenlos registrieren, 10 Aufrufe gratis. Bis zu 1 $, ohne Karte.
LangChain Prompt-Caching: Setups, die den Cache tatsächlich treffen

LangChain Prompt-Caching: Setups, die den Cache tatsächlich treffen

Inhalt
  1. Zuerst klären: Welches „Caching“ ist gemeint?
  2. Die Lösung: Content-Blöcke statt Strings
  3. Die Position der Template-Variablen bestimmt die Trefferquote
  4. Auch Tool-Definitionen werden gecacht
  5. Multi-Turn: Die Markierung gehört an die letzte Nachricht
  6. Usage-Werte korrekt auslesen
  7. Implizite Caches: Falsche Reihenfolge bleibt unbemerkt und muss besonders genau überwacht werden
  8. Checkliste
  9. Haftungsausschluss
  10. Quellen

Dieser LangChain-System-Prompt sieht völlig korrekt aus, landet aber nie im Cache:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", BIG_STABLE_SYSTEM_PROMPT),   # the syntax every tutorial uses
    ("human", "{question}"),
])

Wir haben ihn zweimal mit claude-sonnet-5 ausgeführt. Der System-Prompt war beide Male identisch und 1.800 Token lang. Die Usage-Felder meldeten bei beiden Aufrufen 0 Cache-Writes und 0 Cache-Reads. Kein partieller Treffer, kein fragmentierter Cache. Nichts. Der Grund: Anthropic cached nur Inhalte, die mit cache_control markiert sind. In einem einfachen String innerhalb eines ("system", ...)-Tupels lässt sich diese Markierung nicht setzen. Ausgerechnet die bequemste LangChain-Syntax verzichtet damit auf den gesamten Rabatt, ohne einen Fehler auszugeben.

TL;DR

  • Das LangChain-Tupel ("system", "string") kann kein cache_control enthalten. Claude cached daher nichts: Ein identischer System-Prompt mit 1.800 Token ergab auf claude-sonnet-5 0 Cache-Writes und 0 Cache-Reads.
  • Die Lösung ist eine SystemMessage mit cache_control im Content-Block. Eine Markierung am System-Block deckt auch Tools ab, die mit bind_tools gebunden wurden.
  • Mit langchain-anthropic 1.4.8 bleibt input_token_details.cache_creation selbst bei echten Writes auf 0. Der tatsächliche Wert steht in ephemeral_5m_input_tokens.
  • Bei einem falsch sortierten RAG-Prompt mit variablem Kontext vor den stabilen Regeln fällt bei jedem Aufruf der Cache-Write-Aufschlag von ungefähr 1.25x an. Das ist teurer, als überhaupt nicht zu cachen.

Serie: Teil 5 von 5 · Zuvor: Teil 1 — Grundlagen des Cachings · Teil 2 — Provider-Vergleich und Bewertung · Teil 3 — Tutorial mit funktionierendem Code · Teil 4 — Das beste LLM nach Anwendungsfall

Dies ist Teil 5 der Caching-Serie. Teil 1 erklärt das Prefix-Caching, Teil 3 zeigt die Umsetzung mit dem nativen SDK, und der vollständige Leitfaden zum Prompt-Caching behandelt alle Provider. Hier geht es darum, was sich ändert, wenn LangChain die Prompts zusammensetzt. Alle folgenden Messungen stammen vom 2026-07-04 und liefen über das Synthorai-Gateway mit langchain-core 1.4.8, langchain-anthropic 1.4.8 und langchain-openai 1.3.3.

Zuerst klären: Welches „Caching“ ist gemeint?

Zwei völlig verschiedene Funktionen tragen denselben Namen. Wer danach sucht, landet in der LangChain-Dokumentation meistens bei der falschen.

Response-Caching (LangChains InMemoryCache)Prompt-Caching (diese Serie)
Was gespeichert wirdDie vollständige Completion in der AnwendungDer KV-Zustand des Prompt-Präfixes beim Provider
Wann es Geld spartWenn exakt dieselbe Anfrage erneut kommtWenn verschiedene Anfragen dasselbe Präfix verwenden
Woset_llm_cache(InMemoryCache()), SQLite, Rediscache_control-Markierungen oder automatischer Präfixabgleich
Agent-Loops, RAG, ChatNahezu nutzlos, weil jede Anfrage anders istDer wichtigste Hebel, weil System-Prompt und Tools bei jedem Turn erneut gesendet werden

„Exakt dieselbe Anfrage“ ist dabei wörtlich zu nehmen: Die integrierten Caches verwenden das Paar aus serialisiertem Prompt und Model-Config-String als Schlüssel. Unsere Messung: Eine identische Wiederholung kam ohne API-Aufruf in 0 ms zurück. Ein zusätzliches Leerzeichen im Prompt führte zu einem Miss. Dasselbe galt, wenn sich max_tokens nur um eins unterschied. Auch die Usage-Werte des ursprünglichen Aufrufs werden aus dem Cache zurückgegeben. Eine naive Token-Abrechnung zählt sie deshalb doppelt. Semantische Caches gibt es als Integrationen von Drittanbietern; die integrierten Varianten arbeiten ausschließlich mit exakten Übereinstimmungen.

set_llm_cache eignet sich also, um identische Testaufrufe zu deduplizieren. Für einen System-Prompt mit 2.000 Token, der in jedem Agent-Turn erneut gesendet wird, ist Prompt-Caching zuständig. Dafür muss LangChain den Prompt in der richtigen Form zusammensetzen.

Die Lösung: Content-Blöcke statt Strings

cache_control steckt in einem Content-Block. Die Systemnachricht muss daher eine SystemMessage mit Block-Content sein, kein einfacher String:

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(
    model="claude-sonnet-5",
    base_url="https://synthorai.io",   # any Anthropic-compatible endpoint
)

prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content=[{
        "type": "text",
        "text": BIG_STABLE_SYSTEM_PROMPT,
        "cache_control": {"type": "ephemeral"},   # a bare string has nowhere to put this
    }]),
    ("human", "{question}"),
])
chain = prompt | llm

Derselbe System-Prompt mit 1.800 Token, gemessen über dasselbe Gateway:

AufrufString-Tupel-SyntaxContent-Block-Syntax
1. Aufruf (kalt)Write 0 / Read 0Write 1.875 / Read 0
2. Aufruf, andere FrageWrite 0 / Read 0Write 0 / Read 1.875

Ein warmer Read kostet ungefähr 10 % des normalen Input-Preises. Bei Claude entscheidet diese eine strukturelle Änderung also darüber, ob der stabile Teil jedes Aufrufs dauerhaft zum vollen Preis abgerechnet wird oder einen Rabatt von 90 % erhält. Die wirtschaftlichen Details stehen in Teil 1. Die Funktionsweise der Markierung entspricht der Nutzung des nativen SDK in der Dokumentation zur Anthropic-Integration von LangChain und im Leitfaden zum Prompt-Caching von Anthropic.

Die Position der Template-Variablen bestimmt die Trefferquote

Mit LangChain-Templates lassen sich Variablen problemlos an jeder Stelle einfügen. Genau darin liegt das Risiko. Als Cache-Key dient das bytegenaue Präfix. Wir haben ein Datum in den gecachten Block eingefügt und gemessen:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
AufrufErgebnis
Tag A, Frage 1Write 1.865 (kalt für diesen Wert)
Tag A, Frage 2Read 1.865 (gleicher Wert, Treffer)
Tag B, Frage 1Write 1.865 (neuer Wert, wieder kalt)

Der Cache funktioniert weiterhin. Die Variable ist nun aber Teil des Schlüssels. Bei einem wiederkehrenden Wert wie einem Datum fällt pro Wert ein Cache-Write an; danach gibt es Treffer. Ein bei jedem Aufruf eindeutiger Wert wie ein Zeitstempel oder eine Request-ID verursacht dagegen jedes Mal einen kalten Write. Die Trefferquote liegt dann genau bei null.

In der Praxis ist RAG die teure Variante dieses Fehlers. Viele Chains setzen den abgerufenen Kontext an den Anfang des System-Prompts, noch vor die statischen Anweisungen. Wir haben beide Reihenfolgen mit einem pro Anfrage wechselnden Kontext von 800 Token und einem markierten System-Block gemessen:

Reihenfolge im PromptAufruf 1Aufruf 2 (neue Anfrage, neuer Kontext)
Erst Kontext, dann RegelnWrite 3.133erneut Write 3.133, Read 0
Erst Regeln (markiert), Kontext im Human-TurnWrite 1.852Read 1.852

Die falsche Variante bedeutet nicht nur „kein Rabatt“. Bei jedem Aufruf fällt für alle 3.133 Token der Cache-Write-Aufschlag von ungefähr 1,25-mal dem normalen Input-Preis an, ohne dass später etwas aus dem Cache gelesen wird. Ein falsch sortierter RAG-Prompt mit aktiviertem Caching ist teurer als gar kein Caching. Der unveränderliche Teil steht hinter dem variablen Teil und ist für den Cache damit praktisch wertlos.

Aus den Messungen ergibt sich folgende Regel:

  • Statischer Text kommt zuerst in den markierten Block. Dazu gehören Systemregeln, Tool-Definitionen und Few-Shot-Beispiele.
  • Alles Variable steht hinter der Markierung, idealerweise im Human-Turn: abgerufener Kontext, Datumswerte und Nutzerfragen.
  • Eine Variable im Block ist nur dann sinnvoll, wenn sie sich häufig genug wiederholt, um ihren eigenen Cache-Write zu amortisieren.

Auch Tool-Definitionen werden gecacht

Ein Agent sendet seine Tool-Schemas bei jedem Aufruf erneut. Im Request-Layout von Anthropic stehen die Tools vor dem System-Prompt. Eine Markierung bedeutet „alles vom Anfang der Anfrage bis hierher cachen“. Daraus ergeben sich zwei praktische Fragen: Deckt eine Markierung am System-Block auch die davor stehenden Tools ab? Und serialisiert LangChains bind_tools die Tools bei jedem Aufruf in exakt dieselben Bytes? Sobald die Serialisierung schwankt, ändert sich das Präfix und jeder Aufruf verfehlt den Cache.

Beides haben wir gemessen. Mit demselben markierten System-Prompt betrug der warme Cache-Read ohne Tools 1.861 Token und 2.389 Token mit zwei gebundenen Tools. Die zusätzlichen 528 Token sind die aus dem Cache gelesenen Tool-Schemas. Der Wert von 2.389 wiederholte sich exakt über drei aufeinanderfolgende Aufrufe. bind_tools serialisiert also jedes Mal gleich; das Framework bringt keine variablen Daten in das Präfix ein. Konkret heißt das: Solange der System-Block die Markierung enthält, brauchen die Tools selbst kein cache_control. Die eine Markierung hinter ihnen reicht aus.

Für eine bestimmte Konstellation ist die umgekehrte Anordnung sinnvoll: Die Tools bilden den größten stabilen Teil der Anfrage, während der System-Prompt klein ist oder fehlt. Auch dann braucht die Anfrage irgendwo eine Markierung, die direkt an einem Tool stehen kann. Das funktioniert nur mit einem Dict im nativen Anthropic-Format, weil eine mit @tool dekorierte Funktion kein passendes Feld hat. bind_tools reicht das Dict unverändert durch:

# variant: NO marked system block anywhere; the tool carries the request's only marker
llm.bind_tools([{
    "name": "get_weather",
    "description": LONG_TOOL_DESCRIPTION,
    "input_schema": {...},
    "cache_control": {"type": "ephemeral"},   # passes through bind_tools verbatim
}])

Gemessen wurden ein kalter Write von 3.002 und ein warmer Read von 3.002, obwohl die Anfrage keine markierte Systemnachricht enthielt.

Multi-Turn: Die Markierung gehört an die letzte Nachricht

Eine Unterhaltung wirkt zunächst wie ein weiteres Sortierungsproblem, ist aber der umgekehrte Fall. Die Reihenfolge passt bereits, weil die History nur erweitert wird. Das vollständige Transkript bildet damit ein stabiles Präfix. Das Problem ist die Abdeckung. Eine Markierung am System-Block cached nur diesen Block und nichts dahinter. Mit wachsender History bleibt der warme Read auf der Größe des System-Prompts stehen, während alle angesammelten Turns zum normalen Input-Preis abgerechnet werden.

Die Lösung entspricht der des nativen SDK: Die Markierung kommt auf die neueste Nachricht. Der Breakpoint wandert dadurch nach vorn, und die gesamte bisherige Unterhaltung wird zum gecachten Präfix:

def marked(text):
    return HumanMessage(content=[{
        "type": "text", "text": text,
        "cache_control": {"type": "ephemeral"},
    }])

# each turn: history stays plain, only the newest human message carries the marker
llm.invoke([system, *history, marked(new_question)])

Messung über zwei Turns: Turn 1 schrieb 1.864 Token. Turn 2 las 1.864 und schrieb nur das Delta von 15 Token aus der vorherigen Antwort und der neuen Frage. Das vorherige Präfix wurde zum Read-Preis von ungefähr 10 % abgerechnet. Genau dieses Muster braucht ein Agent-Loop, und in LangChain reicht dafür eine gewöhnliche Nachrichtenliste. Anthropic erlaubt bis zu vier Markierungen pro Anfrage. Die mitwandernde Markierung lässt sich daher mit einer festen Markierung am System-Block oder an den Tools kombinieren.

Usage-Werte korrekt auslesen

LangChain vereinheitlicht die Usage-Daten unter usage_metadata. Dabei sind wir auf eine Falle gestoßen: Mit langchain-anthropic 1.4.8 blieb das standardisierte Feld input_token_details.cache_creation bei allen Antworten unserer Messungen selbst bei einem tatsächlichen Cache-Write auf 0. Der echte Write-Wert landet in einem nicht standardisierten Schlüssel:

r = chain.invoke({"question": "..."})
det = r.usage_metadata["input_token_details"]
det["cache_read"]                  # correct on hits (1875 above)
det["cache_creation"]              # 0 even on a cold write; do not alert on this
det["ephemeral_5m_input_tokens"]   # the actual write count (1875)

Der Provider meldete den Write korrekt. In der Raw-Response stand cache_creation_input_tokens: 1875, abrufbar über r.response_metadata["usage"]. Nur das standardisierte Mapping ordnet den Wert dem TTL-Bucket-Key zu. Ein Kosten-Dashboard, das cache_creation überwacht, meldet fälschlich kostenloses Caching, während sich die Write-Aufschläge unbemerkt summieren. Verwendet das rohe Usage-Objekt oder berücksichtigt die Bucket-Keys. Dass Gateways Cache-Felder falsch melden können, untersuchen wir in Meldet dein LLM-Gateway falsche Cache-Werte?.

Implizite Caches: Falsche Reihenfolge bleibt unbemerkt und muss besonders genau überwacht werden

Claudes Cache ist explizit. GPT und die meisten Open-Weight-Provider cachen automatisch, sobald das Präfix übereinstimmt. Markierungen sind nicht nötig. In LangChain reicht es, den Konstruktor auszutauschen:

llm = ChatOpenAI(model="glm-5.2", base_url="https://synthorai.io/v1")

Mit einem einfachen String als System-Prompt und ohne Markierungen las der zweite Aufruf von GLM 5.2 insgesamt 1.088 Token des ungefähr 1.850 Token langen Präfixes aus dem Cache. Nicht das gesamte Präfix: Automatische Caches gleichen grobe Blöcke ab, statt bytegenau bis zum Ende zu cachen. OpenAI dokumentiert beispielsweise eine Granularität von 128 Token. Das spart ohne weiteren Aufwand Geld. Das Sortierungsproblem aus der RAG-Tabelle gilt hier jedoch genauso und fällt noch schwerer auf. Wir haben denselben Reihenfolgetest über den automatischen Pfad wiederholt, jeweils mit neuem abgerufenem Kontext:

Reihenfolge (keine Markierungen, automatischer Cache)Aufruf 1Aufruf 2 (neue Anfrage, neuer Kontext)
Erst Kontext, dann RegelnRead 0Read 0
Erst Regeln, Kontext im Human-TurnRead 0Read 1.088

Bei falscher Reihenfolge bleibt der Wert immer null: Der wechselnde Kontext steht am Anfang, daher teilen keine zwei Aufrufe dasselbe Präfix und der Rabatt greift nie. Auf dem expliziten Pfad lässt sich derselbe Fehler zumindest an den Cache-Write-Aufschlägen bei jedem Aufruf erkennen. Beim impliziten Pfad gibt es weder Aufschlag noch Fehler noch ein anderes Signal. Der Prompt erfüllt einfach nie die Voraussetzungen, obwohl „automatisch“ nach „funktioniert bereits“ klingt. Da keine Markierung gesetzt werden kann, ist die Reihenfolge des Prompts der einzige Stellhebel des impliziten Pfads.

Prüft die Usage-Werte deshalb kontinuierlich in Produktion und nicht nur einmal im Test: input_token_details.cache_read bei LangChain beziehungsweise prompt_tokens_details.cached_tokens in den Raw-Daten. OpenAIs automatisches Caching setzt laut Dokumentation außerdem ein Präfix von mindestens 1.024 Token voraus. TTL und Voraussetzungen unterscheiden sich je nach Provider; darum geht es in Teil 2.

Checkliste

  • Bei Claude kann ein ("system", "...")-String-Tupel kein cache_control aufnehmen. Nichts wird gecacht, und es gibt keine Warnung. Cachefähige System-Prompts gehören in eine SystemMessage mit Content-Blöcken und Markierung.
  • Der Cache-Key ist das bytegenaue Präfix: zuerst statische Inhalte, danach Variablen hinter der Markierung oder im Human-Turn. RAG-Kontext vor den Regeln führt nicht nur zu einem Miss, sondern verursacht bei jedem Aufruf den Write-Aufschlag.
  • Eine Variable im gecachten Block erzeugt pro Wert einen Cache-Eintrag. Wiederkehrende Werte amortisieren sich; pro Aufruf eindeutige Werte wie Zeitstempel und Request-IDs treffen den Cache nie.
  • Tools stehen im Präfix vor dem System-Prompt. Die System-Markierung cached daher auch gebundene Tools; bind_tools serialisiert deterministisch. Sind die Tools der größte stabile Block, kann die Markierung stattdessen an einem Tool-Dict im Anthropic-Format stehen.
  • In Unterhaltungen wird die wachsende History zum vollen Preis abgerechnet, wenn die Markierung fest auf dem System-Block bleibt. Setzt sie auf die neueste Nachricht, damit jeder Turn das bisherige Präfix liest und nur das Delta schreibt.
  • Überwacht nicht input_token_details.cache_creation: Das Feld bleibt selbst bei Writes auf 0. Ein Dashboard würde deshalb kostenloses Caching melden, während Write-Aufschläge anfallen. Der tatsächliche Wert steht in ephemeral_5m_input_tokens; alternativ lässt er sich aus response_metadata["usage"] auslesen.
  • Bei Modellen mit automatischem Cache wie GPT, GLM und DeepSeek ist die Prompt-Reihenfolge der einzige Stellhebel. Eine falsche Reihenfolge scheitert unbemerkt: kein Aufschlag, kein Fehler, nur ein Rabatt, der nie greift. Prüft Treffer anhand der Usage-Felder.
  • set_llm_cache speichert vollständige Antworten anhand des exakten Prompts und der Model-Konfiguration. Es lohnt sich nur bei identischen wiederholten Anfragen, nie in einem Agent-Loop.

Die nötigen Änderungen sind klein: ein Content-Block statt eines Strings, statische Inhalte vor variablen, eine mit der Unterhaltung wandernde Markierung und das richtige Usage-Feld. In unseren Messungen bedeutete das 90 % Rabatt auf jedes stabile Token statt gar keiner Ersparnis. Beim falsch sortierten RAG-Prompt verhinderte es sogar Mehrkosten. LangChain steht dem Prompt-Caching nicht im Weg; das Framework macht nur die falsche Prompt-Struktur genauso einfach wie die richtige.


Haftungsausschluss

Gemessen am 2026-07-04 über https://synthorai.io/ mit langchain-core 1.4.8, langchain-anthropic 1.4.8, langchain-openai 1.3.3, den Modellen claude-sonnet-5 und glm-5.2, einem englischen System-Präfix von ungefähr 1.800 Token, kleinen Stichproben und 1–2 Sekunden Abstand zwischen aufeinanderfolgenden Aufrufen, damit Cache-Writes wirksam werden konnten. Für jedes Experiment wurde ein neues zufälliges Präfix verwendet, um einen garantiert kalten Cache zu erhalten. Deshalb unterscheiden sich die Token-Werte der Ausgangsmessungen zwischen den Tabellen leicht und liegen zwischen 1.852 und 1.875. Das Mapping der Library-Felder und das Cache-Verhalten der Provider können sich zwischen Versionen ändern. Messt mit eurem eigenen Stack nach, bevor ihr euch auf diese Werte verlasst.

Quellen

← Zurück zum Blog