23 545 tokens par requête : mesurer le coût du re-prefill dans les agents de code
- 6 juil.
- 8 min de lecture
Ce que j'ai mesuré. Ce que j'ai appris. Ce que ça implique pour le design du KV cache.
En 2026, les équipes d'ingénierie se réveillent avec des factures LLM qui rivalisent avec leur infrastructure cloud. Une part significative de ce coût ne va pas dans du travail nouveau — il va dans le retraitement du même contexte, tour après tour, conversation après conversation.
J'ai voulu mesurer exactement combien. J'ai donc configuré Claude Code pour pointer vers mon propre runtime d'inférence, avec Qwen2.5-32B comme modèle de backend, et j'ai ouvert une session. J'ai travaillé normalement. J'ai regardé les logs.
L'intérêt du test n'était pas de remplacer Claude par un plus petit modèle local. C'était de mesurer quelque chose de beaucoup plus précis : combien de tokens un agent d'assistance au code re-prefillait à chaque requête, et combien il pourrait économiser avec un cache KV correctement conçu.
Le problème
Un agent d'assistance au code — Claude Code, Cursor, Zed, Continue, ou n'importe quel autre — envoie à chaque requête un contexte énorme et largement redondant. Le system prompt : plusieurs milliers de tokens qui décrivent ses 28 ou 29 outils. L'historique de la conversation depuis le début de la session. Les fichiers du projet qu'il a lus au fur et à mesure. Les résultats des commandes shell qu'il a exécutées. Les diffs qu'il a produits. Entre deux requêtes consécutives, 90 à 99% de ce contexte n'a pas changé.
Les grands providers le savent. Anthropic a une feature de prompt caching dans son API. OpenAI aussi. Mais c'est un cache côté provider : opaque, TTL court, monolithique, locked dans l'infrastructure du provider. C'est une optimisation qu'ils ont mise en place parce qu'ils voient les mêmes requêtes revenir mille fois — pas une capacité qu'on peut composer, gérer, ou préserver entre sessions.
Le prefix caching aide, mais il ne résout pas le problème. Il optimise un préfixe contigu : tout ce qui est identique depuis le début de la requête est réutilisé, tout ce qui diverge est re-calculé. Le system prompt — le plus gros segment stable — est bien cached. Mais le contexte d'un agent de code n'est pas un préfixe en croissance continue. C'est une composition de segments indépendants : le system prompt ne change jamais, les fichiers lus changent rarement, les résultats de tool calls ne changent jamais après exécution, l'historique de conversation grossit à chaque tour. Chacun de ces segments a son propre cycle de vie. Le prefix caching les noie dans un flux contigu et ne peut pas les adresser séparément. Il ne compose pas les segments — il ne voit qu'un bloc en croissance.
C'est précisément cette limite que KV-first adresse : non pas un cache plus large ou plus long, mais une unité de cache différente — le segment, pas le préfixe.
Le standard qui définit cet artefact — KV-first — est publié sur GitHub sous Apache 2.0 + CC-BY-4.0. Mon runtime l'implémente.
Les chiffres
Mon runtime est KV-first : il gère un cache de session, compose les segments de contexte, persiste les activations entre les requêtes. Mais il dispose aussi d'un mode passthrough — un mode où il relaie les requêtes vers le modèle local sans activer le cache. C'est ce mode que j'ai utilisé pour le test, et c'est ce mode qui simule précisément ce qui se passe quand on parle à un provider qui n'est pas KV-first : chaque requête est stateless, tout est re-prefillé. Claude Code se connecte, et je commence à travailler normalement.
À la troisième requête, je regarde les logs.
23 545 tokens préfillés — cumulés sur un seul cycle de requête.
Une requête de Claude Code, ce n'est pas un seul appel au modèle. C'est une séquence de tool calls — lire un fichier, exécuter une commande, lire un autre fichier — et chacun de ces tool calls renvoie du contexte au modèle. En mode passthrough, le runtime re-prefillait tout à chaque tool call. Le system prompt de Claude Code — 5 936 tokens, les 29 outils décrits un par un — était re-préfillé à chaque fois. L'historique de conversation et les fichiers déjà lus s'accumulaient et étaient re-préfillés à chaque fois. Les 23 545 tokens, c'est la somme de ces re-prefills successifs au sein d'une seule requête utilisateur.
Le setup : Qwen2.5-32B en quantification Q4 (GGUF), via llama.cpp, sur un M4 Max 128 Go. Pas de CUDA, pas de datacenter — une station de travail Apple Silicon avec mémoire unifiée. En débit réel sur cette configuration — autour de 24 tokens par seconde en decode — la génération était lente, mais le vrai coût était le prefill accumulé. Le cycle complet d'une requête, 3 ou 4 tool calls avec chacun leur re-prefill, plus la génération des réponses, se comptait en minutes pour une seule interaction utilisateur. Le débit n'est pas le sujet. Ce qui compte ici, c'est le volume : 23 545 tokens préfillés alors que la quasi-totalité existait déjà sous forme calculée au tour précédent.
À la requête suivante, même structure. Le contexte avait grossi, donc le cumul montait. À la requête d'après, encore plus parce qu'un fichier supplémentaire avait été lu. Le runtime re-prefillait tout, à chaque tool call, à chaque tour, parce que le mode passthrough ne commitait rien dans le cache de session.
En analysant les logs, le diagnostic est clair : le mode passthrough désactivait le commit du cache session, donc l'historique était re-prefillé à chaque tour alors que l'infrastructure pour le cacher existait déjà. La projection du gain potentiel avec un cache de session actif donne :
Turn 1 : prefill system (5 936 tokens) + user (200) = 6 136 tokens
Turn 2 : restore session KV + delta prefill ≈ 500 tokens
Turn 3 : restore session KV + delta prefill ≈ 500 tokens
Turn 5 : restore session KV + delta + fichier caché ≈ 500 tokensEnviron 97% de réduction sur les turns après le premier, si le cache de session était activé et si les fichiers lus étaient commit séparément. Attention : ce tableau est une projection sur ce que le cache de session permettrait — pas une mesure. Il est cohérent avec les chiffres mesurés (5 936 tokens pour le system prompt, croissance de l'historique), mais le gain réel n'a pas été validé par un test avec cache complet actif. La projection porte sur le gain entre les tours. Le test a montré un gaspillage supplémentaire au sein d'un même tour : chaque tool call re-prefille le contexte. Le gain réel, tool calls inclus, serait supérieur à ces chiffres.
Sur un autre test, où le cache KV du system prompt était actif, j'ai mesuré directement le gain : une requête qui trouvait un cache hit sur le system prompt (5 936 tokens) prenait 1 024 millisecondes pour restaurer les activations, contre 5 à 9 secondes pour les recalculer à froid. Un speedup de 5 à 9×. Sur une session où le même cache était réutilisé cinq fois, le temps cumulé économisé était d'environ 190 secondes sur une session totale d'environ 500 secondes — soit 38% de temps gagné sur l'ensemble de la session, sans aucune optimisation côté modèle, juste en évitant de recalculer ce qui l'avait déjà été.
Les mesures directes — 23 545 tokens cumulés, 5-9× speedup, 38% de temps gagné — viennent des logs d'un test réel, sur un vrai agent d'assistance au code, face à un vrai codebase. La simulation de réduction entre les tours est une projection, pas une mesure.
Ce que le test a vraiment montré
Entre deux requêtes d'un agent de code, 90 à 99% du contexte est identique, et rien dans l'API d'un provider ne permet de le préserver.
Le gain est réel et massif. Un ordre de grandeur 5 à 9× sur le speedup de restauration, 30 à 40% de temps gagné sur les sessions avec cache actif, potentiellement 90%+ de réduction de tokens re-prefillés sur les turns après le premier. Ce ne sont pas des chiffres marketing — ce sont des mesures faites sur mes logs en temps réel, sur un workload que je n'avais pas préparé pour être favorable.
Les APIs des providers — OpenAI, Anthropic — ne sont pas KV-first. Elles exposent un modèle requête/réponse où chaque requête est stateless du point de vue du cache KV. Aucune notion de session partagée, aucune composition de segments, aucun contrôle côté client sur ce qui est caché et pendant combien de temps. Tout doit être reconstruit côté runtime, à partir du flux de requêtes qui arrive.
Identifier que deux requêtes consécutives partagent 95% de leur contexte : runtime. Décider ce qui est commit dans le cache de session et ce qui ne l'est pas : runtime. Gérer le TTL, la purge des orphelins, l'invalidation sur modification de fichier, la composition des artefacts : runtime. C'est du travail qui ressemble davantage à construire un système de stockage qu'à ajouter une optimisation. Pendant le test lui-même, on a vu des contentions sur la sérialisation des tool-calls parallèles, des orphelins KV qui n'étaient pas purgés à temps, un sémaphore de gating qui ne disposait que d'un seul permit — le genre de problèmes qu'on rencontre dans un système de stockage, pas dans une feature.
Et il y a une contrainte plus profonde : les modèles on-premise compétents nécessitent de la vraie ressource matérielle. Le test utilisait Qwen2.5-32B en Q4 sur un M4 Max 128 Go — pas un setup léger, mais pas un datacenter non plus. L'économie de la persistance du KV cache ne fait sens que quand le coût du prefill est suffisamment élevé pour justifier l'infrastructure. Pour des interactions légères et stateless, le gaspillage est négligeable. Pour des agents de code lourds sur des codebases massifs, c'est des dizaines de milliers de tokens par tour — et c'est là que l'artefact compte.
Pourquoi le transfert KV ne viendra pas du provider
Les providers connaissent le KV caching. Ils l'utilisent intensivement sur leur infrastructure — gestion par pages de la mémoire KV, prefix caching, décomposition prefill/decode, transfert KV entre nœuds de leur cluster. Ce qu'ils exposent dans leur API n'est pas le début d'un mouvement vers le transfert KV client. C'est un sous-ensemble limité de ce qu'ils font déjà en interne, extrait vers l'API pour rester compétitifs. Il y a trois raisons pour lesquelles ils n'iront pas plus loin.
La première est économique. Les providers facturent au token consommé. Le re-prefill est du revenu direct. Réduire le re-prefill, c'est réduire le revenu. Le prefix caching qu'ils proposent n'est pas un pas vers la solution — c'est un compromis : baisser le coût juste assez pour ne pas perdre de clients face aux autres providers, sans jamais aller jusqu'à éliminer le re-prefill, qui cannibaliserait leur modèle de monétisation. Pour le provider, un contexte re-prefillé n'est pas un gaspillage. C'est un produit facturé.
La deuxième est architecturale. La forme la plus précieuse de transfert KV est le split inference : prefill côté serveur sur l'infra GPU du provider, decode côté client sur la machine de l'utilisateur. C'est le modèle qui élimine le re-prefill tout en décentralisant le calcul. Mais il exige le même modèle des deux côtés — le KV généré par le prefill doit être compatible avec le decode local. Les modèles de Claude, de GPT, de Gemini sont propriétaires. Ils ne seront pas ouverts. Sans accès aux poids, le client ne peut pas decoder. Sans decode local, le split inference est impossible. Le seul KV caching qu'un provider propriétaire peut offrir est le prefix caching serveur — opaque, non composable, non portable. C'est exactement ce qu'ils proposent déjà.
La troisième est pratique. Même en mettant de côté le split inference et l'économie, stocker le KV côté client a un coût. Un KV cache pour un contexte de plusieurs milliers de tokens représente des centaines de mégaoctets à plusieurs gigaoctets par session. Ça consomme du disque, de la bande passante, et ça nécessite une gestion de lifecycle — TTL, purge, invalidation. Un développeur sur un MacBook pourrait ne pas vouloir que son disque se remplisse de blobs KV. Le provider n'a aucun intérêt à introduire cette friction quand le statu quo — re-prefill facturé — est plus simple et plus rentable.
Les trois convergent : le transfert KV ne viendra jamais du provider. Pas parce que c'est techniquement impossible — ils le font déjà en interne. Pas seulement parce que ce serait moins rentable — ils facturent au token. Parce que la forme la plus utile de transfert KV, le split inference, est structurellement incompatible avec un modèle propriétaire. La seule couche KV caching qu'un provider peut offrir est un cache serveur opaque. Tout le reste doit vivre en dehors du provider, dans le runtime, avec des modèles ouverts — Qwen, Llama, Mistral — là où le split inference est possible et où le contrôle appartient à l'utilisateur.
Cette combinaison — gain réel, ingénierie difficile, pas de raccourci provider — c'est exactement ce qui fait du KV cache l'artefact manquant dans la stack. Pas une optimisation qu'on ajoute. Une couche qu'on construit, entre l'agent et le modèle, parce que personne d'autre ne la construira à la place de l'utilisateur.
Commentaires