Chaque appel d’inférence à un LLM recalcule l’attention sur l’intégralité de la séquence d’entrée, à moins que le fournisseur du modèle ne mette en cache les tenseurs clé-valeur intermédiaires d’un appel précédent partageant un préfixe commun. Le prompt caching, proposé comme un niveau de facturation distinct par tous les grands fournisseurs, permet de réutiliser ces tenseurs KV précalculés pour la portion d’une nouvelle requête qui correspond exactement à un préfixe déjà mis en cache, en évitant complètement le calcul pour ce segment et en n’exécutant la passe avant que sur les nouveaux tokens ajoutés ensuite.
La structure de coût qui en résulte est asymétrique par conception : les tokens d’entrée en cache sont généralement facturés à environ un dixième du coût des tokens d’entrée non mis en cache chez les principaux fournisseurs, tandis que le prix des tokens de sortie n’est pas affecté par la mise en cache, la génération étant intrinsèquement séquentielle et donc impossible à précalculer. Pour des charges de travail avec un large contexte partagé et stable, un long prompt système, un ensemble de documents récupérés, ou un extrait de code source, sollicité de façon répétée sur de nombreuses requêtes, ce facteur dix sur le coût du côté entrée se cumule pour produire les économies globales importantes pour lesquelles cette technique est connue.
Le mécanisme apporte aussi un bénéfice de latence souvent aussi précieux que le bénéfice de coût : sauter la passe avant sur les tokens en cache réduit directement le temps jusqu’au premier token, puisque le modèle n’a pas besoin d’exécuter de calcul d’attention sur un contenu déjà traité, ce qui compte de façon disproportionnée pour les applications sensibles à la latence comme les agents conversationnels, où les utilisateurs remarquent davantage le délai avant le premier token que la durée totale de génération.
2. Durée de vie du cache, invalidation et différences entre fournisseurs
Un préfixe mis en cache n’est pas permanent : les fournisseurs évincent les entrées de cache après une période d’inactivité, allant typiquement de quelques minutes à environ une heure selon le fournisseur et le niveau de cache, et un cache miss retombe silencieusement sur un traitement à plein tarif et pleine latence plutôt que de générer une erreur, ce qui signifie qu’une stratégie de mise en cache qui paraît correcte en test peut cesser discrètement de générer des économies en production si les schémas de requêtes deviennent trop peu fréquents ou trop espacés dans le temps pour maintenir le cache chaud.
Les fournisseurs diffèrent sensiblement dans la façon dont la mise en cache est déclenchée et facturée : certains exigent un marqueur explicite de contrôle de cache dans la requête identifiant précisément quel segment de préfixe doit être mis en cache, tandis que d’autres le font automatiquement dès qu’une correspondance de préfixe dépasse un seuil minimal de tokens, sans opt-in explicite, et les coûts d’écriture en cache (mettre un nouveau préfixe en cache pour la première fois) sont parfois facturés avec une surprime par rapport au tarif d’entrée standard, ce qu’il faut prendre en compte pour estimer les économies dès la toute première requête d’une session, pas seulement en régime stationnaire.
Comme les détails d’éviction et de facturation sont propres à chaque fournisseur et évoluent dans le temps à mesure que ceux-ci font évoluer leurs produits de cache, la pratique d’ingénierie durable consiste à structurer vos prompts pour favoriser le cache de façon agnostique vis-à-vis du fournisseur, et à traiter le taux de succès du cache comme une métrique à surveiller par fournisseur, plutôt que de coder en dur des hypothèses sur des durées de vie ou des multiplicateurs de prix exacts dans votre modèle de coût.
3. Structurer les prompts pour maximiser les cache hits
Le prompt caching repose sur la correspondance de préfixe, ce qui signifie que tout le bénéfice dépend de l’ordre dans lequel vous placez le contenu du prompt : le contenu le plus volumineux et le plus stable en premier, et le contenu le plus petit et le plus variable en dernier, car tout changement n’importe où dans le prompt invalide le cache pour tout ce qui suit ce point, mais laisse intact et toujours exploitable tout ce qui le précède.
La structure standard qui maximise les hits est : les instructions système et la définition du rôle en premier (ne changent quasiment jamais entre les requêtes), puis le matériel de référence statique comme les définitions d’outils, un extrait de base de connaissances, ou un extrait de code source (change rarement, souvent uniquement lors d’un déploiement), puis l’historique de conversation jusqu’au tour actuel (grandit progressivement mais ne change pas rétroactivement au sein d’une session), et enfin le tour utilisateur actuel et tout paramètre spécifique à la requête en dernier, puisque c’est le seul élément garanti d’être différent à chaque appel.
L’anti-patron le plus courant qui détruit silencieusement les taux de cache hit consiste à insérer un horodatage, un identifiant de requête, ou toute autre valeur unique par requête n’importe où avant la fin du prompt, souvent dans ce qui ressemble à un en-tête de prompt système anodin. Même un seul caractère différent invalide le cache pour tout ce qui le suit, donc toute valeur dynamique doit strictement se trouver en fin de structure de prompt, jamais mêlée à un contenu par ailleurs statique.
4. Conversations multi-tours et fenêtres de cache glissantes
Les agents conversationnels multi-tours sont la charge de travail où la mise en cache se cumule le plus visiblement, car chaque nouveau tour vient s’ajouter à un historique de conversation qui était, par définition, exactement le préfixe mis en cache au tour précédent. Si la liste complète de messages du tour N était en cache, le tour N+1 n’a besoin de calculer l’attention que sur le nouveau message utilisateur ajouté et la nouvelle réponse de l’assistant, en réutilisant tout ce qui précède, ce qui fait que le coût et la latence par tour restent à peu près stables au lieu de croître linéairement avec la longueur de la conversation, comme ce serait le cas sans mise en cache.
Cela crée une forte incitation à conserver l’historique de conversation tel quel et en ajout seul, plutôt que de résumer ou de réécrire les tours précédents pour économiser des tokens, car toute modification du contenu antérieur, même un résumé destiné à réduire le nombre de tokens, invalide le préfixe mis en cache pour toute la conversation et force un recalcul à plein tarif dès le tour suivant, ce qui est souvent un moins bon compromis que d’assumer simplement la croissance linéaire modeste du nombre brut de tokens.
Pour les applications qui doivent réellement borner la longueur du contexte dans de très longues conversations, le meilleur patron consiste à rompre le cache à une frontière naturelle (par exemple, garder les N derniers tours tels quels et en cache, et ne résumer que la partie plus ancienne au-delà de cette fenêtre) plutôt que de réécrire tout l’historique, ce qui préserve le bénéfice du cache pour la portion récente et la plus consultée de la conversation tout en contrôlant la taille totale du contexte pour le contenu le plus ancien.
5. Analyse de documents et traitement par lots
Les charges de travail d’analyse de documents, exécutant de nombreuses questions ou tâches d’extraction différentes sur le même document volumineux, sont proches du cas d’école pour le prompt caching : le document lui-même est le préfixe large et stable, et chaque question est le suffixe petit et variable, ce qui correspond exactement à la forme pour laquelle la mise en cache est optimisée. Structurer ces charges de travail pour mettre le document en cache une seule fois et émettre de nombreuses requêtes de suivi peu coûteuses peut réduire le coût effectif par requête d’un ordre de grandeur par rapport au renvoi du document complet à chaque question.
Le patron opérationnel qui capture ce bénéfice consiste à délibérément sérialiser les requêtes liées portant sur le même document au sein de la fenêtre active du cache, plutôt que de les paralléliser sur des documents non liés, car des requêtes parallèles sur différents documents paient chacune indépendamment le coût plein d’écriture en cache, tandis que des requêtes séquentielles sur le même document, après la première, bénéficient du cache chaud. Cela signifie parfois échanger délibérément du parallélisme au niveau des requêtes contre de l’efficacité de coût, un vrai compromis qu’il vaut mieux rendre explicite dans la logique de planification de vos jobs par lots plutôt que de partir par défaut sur un parallélisme maximal.
Pour de très gros traitements par lots (traiter des milliers de documents face à un ensemble fixe de questions d’extraction), regrouper le lot par document et exécuter toutes les questions pour un document donné consécutivement avant de passer au document suivant, plutôt que de regrouper par question et de parcourir tous les documents pour la question un avant la question deux, est l’ordre qui réalise réellement le bénéfice de mise en cache ; un planificateur de lots naïvement parallélisé sur les documents peut finir avec un coût effectif pire qu’une exécution séquentielle non optimisée.
6. Suivi des métriques de cache en production
Les réponses des API de fournisseurs incluent des champs d’usage de cache (comptage de tokens en cache séparé du total de tokens d’entrée) dans chaque réponse, et ceux-ci doivent être capturés et agrégés comme une métrique de premier ordre, pas simplement consultés en passant pendant le développement, car le taux de succès du cache est le chiffre unique qui indique si votre structuration de prompt fonctionne réellement sous des schémas de trafic de production réels, pas seulement dans un test local propre.
La métrique à suivre par point d’entrée ou par flux de travail est le taux de succès du cache : tokens d’entrée en cache divisés par le total de tokens d’entrée, agrégés sur une fenêtre glissante, aux côtés de la tendance de coût par requête qui en découle, car une baisse du taux de succès du cache se traduit directement et de façon prévisible par une hausse du coût par requête qu’il est autrement facile de manquer dans un tableau de bord de dépenses agrégées qui ne distingue pas le volume de tokens en cache de celui hors cache.
Un taux de succès du cache qui se dégrade au fil du temps, plutôt que de rester stable, est un signal précis et diagnosticable : il signifie généralement soit que le volume de requêtes pour un préfixe donné est tombé en dessous de ce qui est nécessaire pour maintenir le cache chaud dans sa durée de vie, soit qu’un changement de code récent a introduit une valeur dynamique plus tôt que prévu dans la structure du prompt, et les deux méritent un seuil d’alerte explicite (par exemple, alerter si le taux de succès d’un point d’entrée à fort volume descend sous 70 %) plutôt que de découvrir la régression seulement à la lecture d’un rapport de coût mensuel élevé.
7. Synthèse : ingénierie IA haute performance
Le prompt caching est l’une des rares optimisations de coût pour les LLM qui n’exige absolument aucun compromis de précision : contrairement à une réduction de taille de modèle ou à un élagage agressif du contexte, la mise en cache consiste purement à restructurer la façon dont vous envoyez la même information, et correctement réalisée, elle réduit simultanément le coût et la latence, ce qui en fait presque un gain gratuit une fois le mécanisme suffisamment compris pour structurer les prompts correctement.
Les trois pratiques qui capturent l’essentiel du bénéfice atteignable sont : ordonner strictement les prompts du contenu stable vers le contenu volatile sans jamais entremêler de valeurs dynamiques dans des segments par ailleurs statiques, conserver l’historique de conversation multi-tours en ajout seul plutôt que de réécrire les tours précédents pour économiser des tokens, et regrouper les traitements par lots d’analyse de documents par document plutôt que par question afin que les requêtes séquentielles tombent réellement sur un cache chaud.
Traitez le taux de succès du cache comme une métrique de production de premier ordre dès le premier jour, et non comme une réflexion après coup découverte lors d’une revue de dépenses, car c’est l’indicateur avancé qui vous dit si votre discipline d’ingénierie de prompt tient bon sous un trafic réel, et une régression silencieuse du taux de succès est l’une des façons les plus courantes et les plus évitables dont les coûts d’infrastructure LLM dérivent vers le haut sans aucune augmentation correspondante d’usage réel ou de valeur livrée.
src/llm/promptBuilder.tstypescript
interface PromptSegments { systemInstructions: string // statique : le cache touche presque toujours toolDefinitions: string // statique : change seulement au deploiement knowledgeBaseExcerpt: string // semi-statique : change a la mise a jour du contenu conversationHistory: Message[] // croit de facon monotone dans une session currentUserTurn: string // toujours unique : doit etre en dernier}function buildCacheFriendlyPrompt(segments: PromptSegments): Message[] { // L'ordre est toute la strategie : du stable au volatile, jamais entremele. return [ { role: 'system', content: segments.systemInstructions, cacheControl: { type: 'ephemeral' } }, { role: 'system', content: segments.toolDefinitions, cacheControl: { type: 'ephemeral' } }, { role: 'system', content: segments.knowledgeBaseExcerpt, cacheControl: { type: 'ephemeral' } }, ...segments.conversationHistory, { role: 'user', content: segments.currentUserTurn }, // pas de marqueur de cache : toujours nouveau ]}
monitoring/cache_metrics.pypython
from dataclasses import dataclassfrom collections import deque@dataclassclass CacheSample: cached_input_tokens: int total_input_tokens: intclass CacheHitTracker: def __init__(self, window_size: int = 500, alert_threshold: float = 0.70): self.window: deque[CacheSample] = deque(maxlen=window_size) self.alert_threshold = alert_threshold def record(self, cached_tokens: int, total_tokens: int) -> None: self.window.append(CacheSample(cached_tokens, total_tokens)) def hit_ratio(self) -> float: if not self.window: return 0.0 cached = sum(s.cached_input_tokens for s in self.window) total = sum(s.total_input_tokens for s in self.window) return cached / total if total else 0.0 def should_alert(self) -> bool: return len(self.window) >= 50 and self.hit_ratio() < self.alert_threshold