Reduisez vos couts LLM jusqu'a 90% avec le prompt caching : cache explicite Claude, cache automatique OpenAI, structuration des prompts et mesure du taux de cache hit.
Le probleme : payer deux fois la meme chose
Dans la plupart des applications LLM, une grande partie du prompt ne change jamais : le system prompt, les definitions d'outils, un document de reference, des exemples few-shot. A chaque appel, ces milliers de tokens identiques sont retraites et... refactures plein tarif.
Le prompt caching resout exactement ce gaspillage. Le fournisseur memorise la portion stable du prompt et, aux appels suivants, ne la retraite pas : il la lit depuis son cache, jusqu'a 10 fois moins cher et plus rapidement. Pour une app avec un gros contexte fixe, c'est l'optimisation au meilleur rapport effort/economie.
Comment fonctionne le prompt caching
Le principe repose sur le prefixe commun. Le cache fonctionne tant que le debut du prompt est strictement identique d'un appel a l'autre. Des qu'un token differe, tout ce qui suit n'est plus cachable.
| Element du prompt | Position ideale | Cachable ? |
|---|---|---|
| System prompt | Tout debut | Oui (stable) |
| Definitions d'outils | Debut | Oui (stable) |
| Documents de contexte | Debut | Oui (stable) |
| Exemples few-shot | Debut | Oui (stable) |
| Historique recent | Milieu | Partiellement |
| Question utilisateur | Fin | Non (variable) |
Cache explicite avec Claude
Chez Anthropic, le cache est explicite : vous marquez les blocs a memoriser avec cache_control. C'est plus de controle, mais aussi plus de responsabilite.
// claude-cache.js — marquer un gros contexte comme cachable
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const message = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
system: [
{
type: 'text',
text: 'Tu es un assistant juridique. Voici le code complet : ' + grosDocument,
// Marquer ce bloc pour mise en cache (point de rupture)
cache_control: { type: 'ephemeral' },
},
],
messages: [
// La question varie a chaque appel : non cachee, placee en dernier
{ role: 'user', content: 'Que dit l\'article 5 sur les delais ?' },
],
});
// usage indique ce qui a ete ecrit/lu dans le cache
console.log('Cache cree :', message.usage.cache_creation_input_tokens);
console.log('Cache lu :', message.usage.cache_read_input_tokens);
console.log('Tokens neufs :', message.usage.input_tokens);
Le premier appel ecrit le cache (legerement plus cher que le tarif normal). Tous les appels suivants dans la fenetre de vie du cache le lisent a tarif reduit. Le cache expire apres quelques minutes d'inactivite.
// Mettre en cache plusieurs blocs : system + outils + documents
const tools = [/* definitions d'outils volumineuses */];
const res = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
// Les outils stables peuvent aussi etre caches
tools: tools.map((t, i) =>
i === tools.length - 1 ? { ...t, cache_control: { type: 'ephemeral' } } : t
),
system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
messages: conversation,
});
cache_control marque la fin d'un segment cachable. Tout ce qui precede ce marqueur est mis en cache. Placez-le apres votre dernier element stable.
Cache automatique avec OpenAI
OpenAI applique le caching automatiquement pour les prompts dont le prefixe depasse un certain seuil. Vous n'avez rien a marquer, mais vous devez structurer votre prompt pour que le prefixe reste identique.
// openai-cache.js — le cache s'active seul sur les longs prefixes stables
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// IMPORTANT : le contenu stable (system + contexte) doit etre EN PREMIER
// et strictement identique a chaque appel pour declencher le cache.
const res = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPromptVolumineux }, // stable, en premier
{ role: 'user', content: documentDeReference }, // stable
{ role: 'user', content: questionVariable }, // variable, en dernier
],
});
// Les tokens caches apparaissent dans usage
console.log('Tokens caches :', res.usage.prompt_tokens_details?.cached_tokens);
console.log('Tokens totaux :', res.usage.prompt_tokens);
Structurer le prompt pour maximiser les hits
Que le cache soit explicite ou automatique, la regle est la meme : organiser le prompt du plus stable au plus variable.
// MAUVAISE structure : contenu dynamique au debut casse le cache
const mauvais = [
{ role: 'system', content: `Date: ${new Date()}. Tu es un assistant...` }, // KO
{ role: 'user', content: gigaDocument },
{ role: 'user', content: question },
];
// BONNE structure : stable d'abord, dynamique a la fin
const bon = [
{ role: 'system', content: 'Tu es un assistant...' }, // 100% stable
{ role: 'user', content: gigaDocument }, // stable
// L'info dynamique va dans le dernier message variable
{ role: 'user', content: `(Date: ${new Date()}) ${question}` },
];
- System prompt et instructions en tout premier
- Documents de contexte et exemples few-shot ensuite
- Definitions d'outils stables groupees au debut
- Aucun timestamp, UUID ou valeur aleatoire en debut de prompt
- Question utilisateur et variables toujours en dernier
Mesurer le taux de cache hit
Sans mesure, vous ne savez pas si le cache fonctionne. Loggez systematiquement le ratio de tokens lus depuis le cache.
// Calculer et logger le taux de cache hit (Anthropic)
function logCacheStats(usage) {
const cached = usage.cache_read_input_tokens ?? 0;
const created = usage.cache_creation_input_tokens ?? 0;
const fresh = usage.input_tokens;
const total = cached + created + fresh;
const hitRate = total > 0 ? (cached / total) * 100 : 0;
console.log(`Cache hit rate : ${hitRate.toFixed(1)}%`);
// Estimation de l'economie (cache lu a ~10% du prix)
const economie = cached * 0.9; // tokens "gratuits" equivalents
console.log(`Economie estimee : ${Math.round(economie)} tokens-equivalents`);
// Alerter si le cache ne se declenche pas comme attendu
if (hitRate < 30 && total > 2000) {
console.warn('Taux de cache faible — verifier la structure du prompt');
}
}
Cas d'usage a fort impact
- Chatbot avec un long system prompt reutilise a chaque message
- Q&R sur un document volumineux (le doc est cache, les questions varient)
- Agent avec de nombreuses definitions d'outils stables
- Few-shot avec beaucoup d'exemples constants
- Conversation longue : l'historique deja vu est cache
- Traitement batch partageant le meme contexte de base
// Cas type : Q&R sur document — le doc cache, N questions variables
async function askAboutDoc(document, questions) {
const answers = [];
for (const q of questions) {
const res = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 512,
system: [{
type: 'text',
text: `Reponds en te basant sur ce document :\n${document}`,
cache_control: { type: 'ephemeral' }, // document cache une fois
}],
messages: [{ role: 'user', content: q }], // seule la question varie
});
answers.push(res.content[0].text);
}
return answers; // 1ere question ecrit le cache, les suivantes le lisent
}
Calculer le ROI du caching
Avant d'optimiser, chiffrez. Le caching n'est rentable que si la partie stable est grosse et reutilisee souvent. Un petit simulateur permet de decider en connaissance de cause et de justifier l'effort aupres de l'equipe.
// roi.js — estimer l'economie mensuelle du prompt caching
function estimerEconomie({ tokensStables, requetesParJour, prixEntree, ratioCacheLu = 0.1 }) {
const JOURS = 30;
const appels = requetesParJour * JOURS;
// Sans cache : on paie le prefixe stable plein tarif a chaque appel
const coutSansCache = appels * tokensStables * prixEntree;
// Avec cache : 1 ecriture (~1.25x) puis lectures a ~10% du prix
const coutEcriture = tokensStables * prixEntree * 1.25;
const coutLectures = (appels - 1) * tokensStables * prixEntree * ratioCacheLu;
const coutAvecCache = coutEcriture + coutLectures;
return {
coutSansCache: coutSansCache.toFixed(2),
coutAvecCache: coutAvecCache.toFixed(2),
economie: (coutSansCache - coutAvecCache).toFixed(2),
reduction: (((coutSansCache - coutAvecCache) / coutSansCache) * 100).toFixed(0) + '%',
};
}
// Exemple : system prompt de 5000 tokens, 50 000 requetes/jour
console.log(estimerEconomie({
tokensStables: 5000,
requetesParJour: 50000,
prixEntree: 0.000003, // prix par token d'entree ($)
}));
| Profil d'application | Tokens stables | Caching pertinent ? |
|---|---|---|
| Chatbot a gros system prompt | 2 000 - 8 000 | Oui, fort impact |
| Q&R sur document long | 10 000+ | Oui, impact majeur |
| Agent multi-outils | 3 000 - 15 000 | Oui |
| Requetes ponctuelles uniques | < 500 | Non, prefixe trop court |
Une couche service qui garantit le cache
En production, le risque numero un est qu'un developpeur insere par megarde une valeur dynamique en debut de prompt et casse le cache sans s'en rendre compte. La parade : centraliser la construction du prompt dans une couche service qui fige le prefixe stable et n'expose que la partie variable.
// llm-service.ts — encapsuler le prefixe stable pour proteger le cache
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// Le prefixe stable est defini UNE fois, hors du chemin de requete
const SYSTEM_STABLE = [{
type: 'text' as const,
text: chargerSystemPrompt(), // identique a chaque appel
cache_control: { type: 'ephemeral' as const },
}];
// Seule la question utilisateur (variable) entre par parametre
export async function ask(question: string) {
if (question.length > 4000) throw new Error('Question trop longue');
const res = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
system: SYSTEM_STABLE, // jamais modifie -> cache garanti
messages: [{ role: 'user', content: question }],
});
// Observabilite : on remonte le hit rate a chaque appel
reporterCache(res.usage);
return res.content[0].type === 'text' ? res.content[0].text : '';
}
// metrics.js — agreger le hit rate pour un dashboard (Prometheus-like)
let totalCached = 0, totalInput = 0;
export function reporterCache(usage) {
totalCached += usage.cache_read_input_tokens ?? 0;
totalInput += (usage.cache_read_input_tokens ?? 0)
+ (usage.cache_creation_input_tokens ?? 0)
+ usage.input_tokens;
}
// Endpoint expose au monitoring : /metrics
export function cacheHitRate() {
return totalInput > 0 ? totalCached / totalInput : 0;
}
Pieges et limites
- Le cache expire apres quelques minutes d'inactivite (TTL court)
- Ecrire le cache coute un peu plus que le tarif normal (rentable des le 2e hit)
- Une taille minimale de prompt est requise pour activer le cache
- Tout contenu dynamique en debut de prompt annule le benefice
- Le cache n'accelere pas la generation, seulement le traitement de l'entree
Le piege le plus frequent : penser que le cache fonctionne alors qu'un detail (un horodatage, un ID de session injecte trop tot) casse le prefixe a chaque appel. D'ou l'importance de mesurer le hit rate reel plutot que de supposer.
Erreurs frequentes a l'integration
L'erreur silencieuse par excellence : une variable dynamique glissee en debut de prompt. Un horodatage dans le system prompt, un identifiant de session injecte trop tot, ou meme l'ordre non deterministe d'un objet serialise en JSON suffit a casser le prefixe commun. Le code « fonctionne » — il renvoie de bonnes reponses — mais le hit rate reste a zero et la facture ne baisse jamais. D'ou la regle absolue : toujours mesurer le hit rate reel, ne jamais le supposer.
Deuxieme piege cote architecture multi-tenant : concatener les donnees d'un utilisateur au debut du contexte partage. Si chaque client a son propre prefixe, aucun cache n'est mutualise. Placez la portion reellement commune (instructions, schema, documentation produit) en tete, et reportez les donnees specifiques au tenant dans la partie variable, en fin de prompt.
Enfin, attention au TTL plus court que votre frequence d'appel. Le cache expire apres quelques minutes d'inactivite ; sur une application a trafic faible ou en rafales espacees, le cache est ecrit puis expire avant d'etre relu, et vous payez le surcout d'ecriture sans jamais beneficier des lectures. Dans ce cas, un cache applicatif classique (Redis sur les reponses completes) est plus adapte que le prompt caching du fournisseur.
Un garde-fou simple permet de detecter en developpement qu'un prefixe cense etre stable a change entre deux appels — signe qu'une valeur dynamique s'y est glissee :
// prefix-guard.js — alerter si le prefixe stable varie entre deux appels
import { createHash } from 'node:crypto';
let dernierHash = null;
export function verifierPrefixeStable(prefixe) {
const h = createHash('sha256').update(prefixe).digest('hex');
if (dernierHash && h !== dernierHash) {
// En dev : le prefixe a change -> le cache ne se declenchera jamais
console.warn('Prefixe non stable : une valeur dynamique casse le cache');
}
dernierHash = h;
return h;
}
Conclusion
Le prompt caching est l'une des optimisations les plus rentables des applications LLM : peu d'effort, jusqu'a 90% d'economie sur les tokens d'entree repetes, et une latence reduite. Le principe tient en une phrase : tout ce qui est stable au debut du prompt devient quasi gratuit aux appels suivants.
Chez Anthropic, marquez explicitement vos blocs avec cache_control ; chez OpenAI, laissez le cache automatique operer en gardant un prefixe identique. Dans les deux cas, structurez vos prompts du plus stable au plus variable, bannissez le contenu dynamique en debut, et mesurez le hit rate. C'est le premier levier a actionner des qu'une application LLM passe a l'echelle.
- Le cache memorise le prefixe stable du prompt (jusqu'a -90%)
- Anthropic : explicite via
cache_control - OpenAI : automatique sur les longs prefixes identiques
- Stable au debut, variable a la fin — jamais de dynamique en tete
- Toujours mesurer le taux de cache hit reel