Intelligence Artificielle angularforall.com

- Prompt caching : reduire les couts LLM de 90%

Prompt-Caching Cout-Llm Optimisation-Llm Anthropic Openai Claude Llm Cache-Control Ia-Generative Node-Js Prompt-Engineering Performance-Ia Tokens Production-Ia
Prompt caching : reduire les couts LLM de 90%

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.

Ordre de grandeur : un assistant avec un system prompt de 5000 tokens appele 100 000 fois par jour retraite 500 millions de tokens identiques quotidiennement. Avec le cache, ces tokens sont factures a environ 10% : l'economie se chiffre en centaines d'euros par jour.

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 promptPosition idealeCachable ?
System promptTout debutOui (stable)
Definitions d'outilsDebutOui (stable)
Documents de contexteDebutOui (stable)
Exemples few-shotDebutOui (stable)
Historique recentMilieuPartiellement
Question utilisateurFinNon (variable)
Regle d'or : stable au debut, variable a la fin. Tout element place avant la premiere variation est cachable ; tout ce qui vient apres ne l'est pas. La structure du prompt determine l'economie.

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,
});
Point de rupture : 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);
Avec OpenAI, le cache est transparent mais fragile : la moindre variation en debut de prompt (un timestamp, un ID dynamique) casse le prefixe commun et annule le benefice. Bannissez tout contenu dynamique du debut.

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}` },
];
Checklist de structuration :
  • 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');
  }
}
Objectif : sur une application avec gros contexte fixe, viser un hit rate superieur a 70%. Un taux bas signale presque toujours une variation parasite en debut de prompt qui casse le prefixe commun.

Cas d'usage a fort impact

Le caching rapporte le plus quand :
  • 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'applicationTokens stablesCaching pertinent ?
Chatbot a gros system prompt2 000 - 8 000Oui, fort impact
Q&R sur document long10 000+Oui, impact majeur
Agent multi-outils3 000 - 15 000Oui
Requetes ponctuelles uniques< 500Non, prefixe trop court
La regle de seuil : l'ecriture du cache coute un peu plus cher qu'un appel normal. Il devient donc rentable des le deuxieme hit dans la fenetre de vie du cache. Si vos requetes sont espacees de plus que le TTL, le cache expire avant d'etre reutilise — et l'optimisation ne sert a rien.

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;
}
Benefice architectural : en interdisant aux appelants de toucher au prefixe, vous transformez « le cache marche si tout le monde fait attention » en « le cache marche par construction ». C'est aussi le bon endroit pour brancher rate limiting, logs et fallback multi-provider.

Pieges et limites

A surveiller :
  • 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.

A retenir :
  • 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

Partager