🎁 Novità Registrati gratis, 10 chiamate le offriamo noi. Fino a $1, senza carta.
Prompt caching con LangChain: configurazioni che funzionano davvero

Prompt caching con LangChain: configurazioni che funzionano davvero

Indice
  1. Prima di tutto: quale tipo di “caching” stai cercando?
  2. La soluzione: content block, non stringhe
  3. La posizione delle variabili nel template determina l’hit rate
  4. Anche le definizioni dei tool vengono memorizzate
  5. Conversazioni multi-turn: sposta il marcatore sull’ultimo messaggio
  6. Leggi i contatori e usa i campi giusti
  7. Cache implicite: gli errori di ordinamento non lasciano tracce, quindi vanno monitorati ancora meglio
  8. Checklist
  9. Esclusione di responsabilità
  10. Fonti

Questo system prompt di LangChain sembra del tutto corretto, ma non mette nulla in cache:

from langchain_core.prompts import ChatPromptTemplate

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

Lo abbiamo eseguito due volte su claude-sonnet-5 con lo stesso system prompt da 1.800 token, quindi abbiamo controllato i campi di utilizzo. Entrambe le chiamate riportavano 0 scritture e 0 letture dalla cache. Non si trattava di un hit parziale né di una cache frammentata. Non era stato memorizzato nulla. Il motivo è che Anthropic mette in cache solo ciò che viene contrassegnato con cache_control, mentre una semplice stringa nella tupla ("system", ...) non può contenere questo marcatore. La sintassi più comoda di LangChain è anche quella che rinuncia completamente allo sconto, senza generare alcun errore.

TL;DR

  • La tupla ("system", "string") di LangChain non può contenere cache_control, quindi Claude non mette nulla in cache: con lo stesso system prompt da 1.800 token, su claude-sonnet-5 abbiamo misurato 0 scritture e 0 letture.
  • La soluzione è usare un SystemMessage con cache_control nel content block. Un solo marcatore sul blocco di sistema copre anche i tool associati tramite bind_tools.
  • Con langchain-anthropic 1.4.8, input_token_details.cache_creation resta a 0 anche quando la scrittura avviene davvero. Il valore corretto si trova in ephemeral_5m_input_tokens.
  • Un prompt RAG ordinato male, con il contesto variabile prima delle regole stabili, paga a ogni chiamata il sovrapprezzo di scrittura in cache, circa 1,25 volte il costo normale. Costa più che non usare affatto la cache.

Serie: Parte 5 di 5 · In precedenza: Parte 1 — Principi del caching · Parte 2 — Confronto e valutazione dei provider · Parte 3 — Tutorial con codice funzionante · Parte 4 — Miglior LLM per caso d’uso

Questa è la quinta parte della serie sul caching. La parte 1 spiega il funzionamento del prefix caching, la parte 3 contiene il tutorial con gli SDK nativi, mentre la guida completa al prompt caching confronta tutti i provider. Qui vediamo cosa cambia quando è LangChain a comporre i prompt. Tutti i dati riportati di seguito sono stati misurati il 2026-07-04 tramite il gateway Synthorai, usando langchain-core 1.4.8, langchain-anthropic 1.4.8 e langchain-openai 1.3.3.

Prima di tutto: quale tipo di “caching” stai cercando?

Due funzionalità senza alcun legame tra loro usano lo stesso termine. La pagina della documentazione LangChain su cui si arriva da una ricerca, di solito, descrive quella sbagliata.

Cache delle risposte (InMemoryCache di LangChain)Prompt caching (questa serie)
Cosa memorizzaL’intera completion, nell’applicazioneLo stato KV del prefisso del prompt, presso il provider
Quando riduce i costiQuando si ripete esattamente la stessa richiestaQuando richieste diverse condividono un prefisso
Doveset_llm_cache(InMemoryCache()), SQLite, RedisMarcatori cache_control o corrispondenza automatica dei prefissi
Loop di agenti, RAG, chatQuasi inutile, perché ogni richiesta è diversaÈ il meccanismo principale, perché system prompt e tool si ripetono a ogni turno

“Esattamente la stessa richiesta” significa identica in ogni dettaglio: le implementazioni integrate usano come chiave la coppia formata dal prompt serializzato e dalla stringa di configurazione del modello. Nei nostri test, una ripetizione identica è stata restituita in 0 ms senza chiamare l’API. Aggiungere un solo spazio al prompt ha prodotto un miss; è bastato anche cambiare max_tokens di uno. La risposta riprodotta dalla cache contiene inoltre i valori di utilizzo della chiamata originale, quindi un conteggio ingenuo dei token li considera due volte. Esistono cache semantiche tramite integrazioni di terze parti, ma quelle integrate funzionano solo con corrispondenze esatte.

set_llm_cache va quindi bene per deduplicare chiamate identiche nei test. Per il system prompt da 2.000 token inviato di nuovo a ogni iterazione di un agente serve invece il prompt caching, e il prompt deve essere composto nel modo corretto.

La soluzione: content block, non stringhe

cache_control si trova all’interno di un content block. Il messaggio di sistema deve quindi essere un SystemMessage con contenuto strutturato, non una semplice stringa:

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

Con lo stesso system prompt da 1.800 token e lo stesso gateway abbiamo misurato:

ChiamataSintassi con tupla e stringaSintassi con content block
Prima, a freddoscrittura 0 / lettura 0scrittura 1.875 / lettura 0
Seconda, con domanda diversascrittura 0 / lettura 0scrittura 0 / lettura 1.875

Una lettura a caldo viene fatturata a circa il 10% del prezzo normale dei token di input. Su Claude, questa singola modifica strutturale fa quindi la differenza tra pagare sempre il prezzo pieno e ottenere uno sconto del 90% sulla parte stabile di ogni chiamata. I calcoli economici sono nella parte 1. Il funzionamento dei marcatori corrisponde a quello degli SDK nativi descritto nella documentazione dell’integrazione Anthropic di LangChain e nella guida al prompt caching di Anthropic.

La posizione delle variabili nel template determina l’hit rate

I template di LangChain permettono di interpolare facilmente variabili in qualsiasi punto, ed è proprio questo il rischio. La chiave della cache è il prefisso identico byte per byte. Abbiamo inserito una data nel blocco memorizzato e misurato il risultato:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
ChiamataRisultato
giorno A, domanda 1scrittura 1.865, cache fredda per questo valore
giorno A, domanda 2lettura 1.865, stesso valore, hit
giorno B, domanda 1scrittura 1.865, nuovo valore e nuova cache fredda

La cache non ha smesso di funzionare. La variabile è semplicemente diventata parte della chiave. Un valore che si ripete, come una data, richiede una scrittura per ogni valore e produce hit nelle chiamate successive. Un valore diverso a ogni chiamata, come un timestamp o un request ID, rende fredda ogni scrittura e porta l’hit rate esattamente a zero.

La versione più costosa di questo errore si presenta nei sistemi RAG. Molte chain usano un template che inserisce il contesto recuperato all’inizio del system prompt, prima delle istruzioni statiche. Abbiamo misurato entrambi gli ordini con un contesto recuperato da 800 token, diverso per ogni query, e un blocco di sistema contrassegnato:

Ordine nel promptChiamata 1Chiamata 2, nuova query e nuovo contesto
Prima il contesto, poi le regolescrittura 3.133nuova scrittura 3.133, lettura 0
Prima le regole, contrassegnate; contesto nel turno humanscrittura 1.852lettura 1.852

La prima riga non rappresenta soltanto l’assenza di uno sconto. Ogni chiamata paga il sovrapprezzo di scrittura in cache, circa 1,25 volte il normale prezzo dell’input, su tutti i 3.133 token, senza mai recuperare nulla. Un prompt RAG ordinato male con il caching attivo costa più dello stesso prompt senza caching. Il contenuto fisso si trova dopo quello variabile, quindi dal punto di vista della cache è come se non esistesse.

Dai risultati emerge una regola semplice:

  • Prima il testo statico, all’interno del blocco contrassegnato. Regole di sistema, definizioni dei tool ed esempi few-shot.
  • Tutto ciò che varia va dopo il marcatore, preferibilmente nel turno human: contesto recuperato, date e domande dell’utente.
  • Una variabile nel blocco è accettabile solo se si ripete abbastanza spesso da ammortizzare la propria scrittura in cache.

Anche le definizioni dei tool vengono memorizzate

Un agente invia di nuovo gli schemi dei tool a ogni chiamata. Nel formato delle richieste Anthropic, i tool si trovano prima del system prompt. Poiché un marcatore significa “memorizza tutto dall’inizio della richiesta fino a questo punto”, sorgono due questioni pratiche. Il marcatore sul blocco di sistema copre anche i tool che lo precedono? E bind_tools di LangChain serializza sempre i tool negli stessi byte? Se la serializzazione variasse, il prefisso cambierebbe e ogni chiamata produrrebbe un miss.

Le misurazioni rispondono affermativamente a entrambe le domande. Con lo stesso system prompt contrassegnato, la lettura a caldo è stata di 1.861 token senza tool e di 2.389 token con due tool associati. I 528 token in più corrispondono agli schemi dei tool recuperati dalla cache. Il valore di 2.389 si è ripetuto esattamente per tre chiamate consecutive: bind_tools produce quindi sempre la stessa serializzazione e il framework non introduce variazioni nel prefisso. In pratica, se il blocco di sistema contiene il marcatore, i tool non hanno bisogno di un proprio cache_control. Il singolo marcatore successivo ai tool copre tutto.

Esiste anche il caso opposto: i tool sono la parte stabile più grande della richiesta, mentre il system prompt è minimo o assente. Serve comunque un marcatore da qualche parte, e in questo caso può essere inserito in un tool. Funziona soltanto con un dizionario nel formato Anthropic, perché una funzione decorata con @tool non ha un campo in cui inserirlo. bind_tools inoltra il dizionario senza modificarlo:

# 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
}])

Risultato misurato: scrittura a freddo di 3.002 token e lettura a caldo di 3.002 token, senza alcun messaggio di sistema contrassegnato nella richiesta.

Conversazioni multi-turn: sposta il marcatore sull’ultimo messaggio

Una conversazione potrebbe sembrare un altro problema di ordinamento, ma qui accade il contrario: l’ordine è già corretto. La cronologia cresce solo in coda, quindi l’intera conversazione precedente resta un prefisso stabile. Il problema è la copertura. Un marcatore sul blocco di sistema memorizza soltanto quel blocco e nulla di ciò che viene dopo. Con l’aumentare della cronologia, la lettura a caldo rimane fissa alla dimensione del system prompt, mentre tutti i turni accumulati vengono fatturati come input normale.

La soluzione è la stessa usata con gli SDK nativi: posizionare il marcatore sul messaggio più recente. Il breakpoint avanza a ogni turno e l’intera conversazione fino a quel momento diventa il prefisso memorizzato:

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)])

Nelle misurazioni su due turni, il primo ha scritto 1.864 token. Il secondo ha letto 1.864 token e scritto soltanto il delta di 15 token, composto dalla risposta precedente e dalla nuova domanda. Il prefisso precedente è stato fatturato alla tariffa di lettura, pari a circa il 10%. È il comportamento ideale per il loop di un agente, e in LangChain basta un normale elenco di messaggi. Anthropic consente fino a quattro marcatori per richiesta, quindi il marcatore mobile può convivere con uno fisso sul blocco di sistema o sui tool.

Leggi i contatori e usa i campi giusti

LangChain normalizza i dati di utilizzo in usage_metadata, ma abbiamo incontrato un problema: con langchain-anthropic 1.4.8, in tutte le nostre esecuzioni il campo standard input_token_details.cache_creation è rimasto a 0 anche quando la scrittura in cache era avvenuta. Il conteggio reale si trova in una chiave non standard:

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)

Il provider riportava correttamente la scrittura: cache_creation_input_tokens: 1875 nella risposta grezza, accessibile tramite r.response_metadata["usage"]. È la mappatura standardizzata a inserirla nella chiave relativa al TTL. Una dashboard dei costi che controlla cache_creation indicherà che il caching non costa nulla, mentre i sovrapprezzi di scrittura continuano ad accumularsi. Bisogna leggere l’oggetto di utilizzo grezzo oppure conoscere le chiavi dei bucket. È lo stesso tipo di problema che si verifica quando i gateway riportano campi di cache errati, analizzato in Il tuo gateway LLM mente sulla cache?.

Cache implicite: gli errori di ordinamento non lasciano tracce, quindi vanno monitorati ancora meglio

La cache di Claude è esplicita. GPT e la maggior parte dei provider open-weight usano automaticamente la cache quando i prefissi coincidono, senza marcatori. Con LangChain, la stessa chain funziona cambiando soltanto il costruttore:

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

Con un system prompt costituito da una semplice stringa e senza marcatori, la seconda chiamata a GLM 5.2 ha letto 1.088 token da un prefisso di circa 1.850 token. Non l’intero prefisso: le cache automatiche confrontano blocchi di dimensione fissa, invece di arrivare byte per byte fino alla fine. OpenAI, per esempio, documenta una granularità di 128 token. Fin qui, è un risparmio ottenuto senza lavoro aggiuntivo. Il rischio di ordinamento mostrato nella tabella RAG rimane però invariato e il fallimento è ancora più difficile da rilevare. Abbiamo ripetuto lo stesso esperimento con il caching automatico e un nuovo contesto recuperato a ogni chiamata:

Ordine, senza marcatori e con cache automaticaChiamata 1Chiamata 2, nuova query e nuovo contesto
Prima il contesto, poi le regolelettura 0lettura 0
Prima le regole, contesto nel turno humanlettura 0lettura 1.088

Con l’ordine sbagliato il risultato è sempre zero. Il contesto variabile è all’inizio, quindi nessuna coppia di chiamate condivide un prefisso e lo sconto non viene mai applicato. Nel percorso esplicito, lo stesso errore è almeno visibile in fattura sotto forma di sovrapprezzo di scrittura a ogni chiamata. Nel percorso implicito non ci sono sovrapprezzi, errori o altri segnali. Il prompt continua semplicemente a non soddisfare i requisiti, mentre si presume che “automatico” significhi “funzionante”. Non essendoci marcatori da posizionare, l’ordine del prompt è l’unico parametro disponibile con il caching implicito.

Bisogna quindi controllare i contatori in produzione, non una sola volta durante un test: input_token_details.cache_read in LangChain oppure prompt_tokens_details.cached_tokens nei dati grezzi. La documentazione sul caching automatico di OpenAI indica inoltre una lunghezza minima del prefisso di 1.024 token. TTL e criteri di idoneità cambiano da un provider all’altro; ne parliamo nella parte 2.

Checklist

  • Su Claude, una tupla di stringhe ("system", "...") non può contenere cache_control: non viene memorizzato nulla e non compare alcun avviso. I system prompt da memorizzare devono essere inseriti in un SystemMessage con content block e marcatore.
  • La chiave della cache è il prefisso identico byte per byte: prima il contenuto statico, poi le variabili, dopo il marcatore o nel turno human. Mettere il contesto RAG prima delle regole non produce soltanto un miss: fa pagare il sovrapprezzo di scrittura a ogni chiamata.
  • Una variabile nel blocco memorizzato crea una voce di cache per ogni valore. I valori ripetuti ammortizzano il costo; quelli univoci per chiamata, come timestamp e request ID, non producono mai hit.
  • I tool precedono il system prompt nel prefisso, quindi il marcatore di sistema memorizza anche i tool associati. bind_tools li serializza in modo deterministico. Se i tool sono il blocco stabile più grande, il marcatore può essere inserito in un dizionario tool nel formato Anthropic.
  • Nelle conversazioni, un marcatore fisso sul blocco di sistema lascia tutta la cronologia crescente al prezzo pieno. Va spostato sul messaggio più recente, in modo che ogni turno legga il prefisso precedente e scriva soltanto il delta.
  • Non monitorare input_token_details.cache_creation: resta a 0 anche durante le scritture, quindi una dashboard segnala un caching gratuito mentre si accumulano i sovrapprezzi. Il valore reale è in ephemeral_5m_input_tokens; in alternativa, leggi response_metadata["usage"].
  • Nei modelli con cache automatica, come GPT, GLM e DeepSeek, l’ordine del prompt è l’unico parametro disponibile e un ordine errato non produce segnali: nessun sovrapprezzo, nessun errore, solo uno sconto che non arriva mai. Verifica gli hit nei campi di utilizzo.
  • set_llm_cache memorizza intere risposte usando come chiave il prompt esatto e la configurazione del modello. Conviene soltanto quando si ripetono richieste identiche, mai nel loop di un agente.

Le accortezze necessarie sono minime: un content block al posto di una stringa, la parte statica prima di quella variabile, un marcatore che avanza con la conversazione e la lettura del campo di utilizzo corretto. Nei test, la differenza è stata uno sconto del 90% su ogni token stabile invece di nessuno sconto; nel caso RAG ordinato male, invece, si è arrivati a pagare di più. LangChain non ostacola il prompt caching: rende soltanto altrettanto facile scrivere un prompt con la struttura sbagliata o con quella corretta.


Esclusione di responsabilità

Misurazioni effettuate il 2026-07-04 su https://synthorai.io/ con langchain-core 1.4.8, langchain-anthropic 1.4.8, langchain-openai 1.3.3, i modelli claude-sonnet-5 e glm-5.2, un prefisso di sistema in inglese da circa 1.800 token, campioni ridotti e un intervallo di 1–2 secondi tra chiamate consecutive, per dare alla cache il tempo di completare le scritture. Ogni esperimento ha usato un nuovo prefisso casuale per garantire una cache fredda. Per questo i conteggi di riferimento variano leggermente tra le tabelle, da 1.852 a 1.875. Le mappature dei campi nelle librerie e il comportamento della cache dei provider possono cambiare tra le versioni. Ripeti le misurazioni sul tuo stack prima di basarti su questi valori.

Fonti

← Torna al blog