EN DIRECT
The Download: the Kids issue arrives, and Bill Gates reveals his AI fears26/08/26|Z.ai confirms Ox Alpha is a new GLM-series model and will release its weights26/08/26|How loveholidays is making everyone a builder with Codex26/08/26 · OpenAI|Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers26/08/26 · Hugging Face|Recursive Experiential-Working Memory Evolution for Long-Horizon Agent Harnesses25/08/26 · OpenAI|Granite 4.2 LLMs: How They're Built25/08/26 · Hugging Face|OpenAI Jalapeño: Better than Nvidia Blackwell25/08/26 · OpenAI|Anthropic tells staff to work from home due to possible security team strike25/08/26 · Anthropic|OpenAI restores 5-hour Codex and Work limits for ChatGPT Plus users25/08/26 · OpenAI|Quantization-Aware Healing: a compressed, 4-bit model that outperforms its full-precision original25/08/26 · Hugging Face|The full stack behind abundant intelligence25/08/26 · OpenAI|Jalapeño’s first results show industry-leading speed and efficiency in AI inference25/08/26 · OpenAI|The Download: the Kids issue arrives, and Bill Gates reveals his AI fears26/08/26|Z.ai confirms Ox Alpha is a new GLM-series model and will release its weights26/08/26|How loveholidays is making everyone a builder with Codex26/08/26 · OpenAI|Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers26/08/26 · Hugging Face|Recursive Experiential-Working Memory Evolution for Long-Horizon Agent Harnesses25/08/26 · OpenAI|Granite 4.2 LLMs: How They're Built25/08/26 · Hugging Face|OpenAI Jalapeño: Better than Nvidia Blackwell25/08/26 · OpenAI|Anthropic tells staff to work from home due to possible security team strike25/08/26 · Anthropic|OpenAI restores 5-hour Codex and Work limits for ChatGPT Plus users25/08/26 · OpenAI|Quantization-Aware Healing: a compressed, 4-bit model that outperforms its full-precision original25/08/26 · Hugging Face|The full stack behind abundant intelligence25/08/26 · OpenAI|Jalapeño’s first results show industry-leading speed and efficiency in AI inference25/08/26 · OpenAI|
IntermédiaireNouveau💸

Optimiser les coûts LLM : les 10 leviers, du modèle mental au code

La facture IA explose parce qu'on paye des tokens que personne ne lit. Ce guide donne le modèle mental, les 10 leviers classés par famille, un ordre de bataille 30-60-90 jours pour la DSI, et le code Python pour chaque levier.

32 min de lecturePublié le 26 août 2026 · aujourd'hui

Ton premier pilote IA coûtait 40 € par mois. Le second, 400 €. Le troisième — celui avec des agents — est passé à 4 000 € en trois semaines, et personne n'est capable de dire quel workflow a brûlé quoi. Ce n'est pas un problème de fournisseur ni de modèle : c'est un problème de tokens envoyés que personne ne lit, d'appels qu'on aurait pu ne pas faire, et de facture que personne n'attribue.

Ce guide fait le tour complet. La première moitié est pour le DSI : le modèle mental, les leviers classés par famille, la matrice impact/effort et l'ordre dans lequel les activer. La seconde moitié est pour ton équipe technique : chaque levier avec le code Python correspondant, les pièges, et un cas chiffré sur un pipeline réel. Tu peux lire l'une sans l'autre.

Prérequis. Avoir au moins un cas d'usage LLM en production ou en pilote, avec accès à la facture fournisseur. Pour la seconde moitié : Python 3.11+, un SDK fournisseur (les exemples utilisent le SDK Anthropic, les principes sont identiques chez OpenAI, Mistral ou Google). Aucune connaissance GPU nécessaire sauf pour la section auto-hébergement.

1. Le modèle mental : d'où vient la facture

Avant de toucher au moindre levier, il faut comprendre ce qu'on paye. Un appel LLM facture trois choses, et la plupart des équipes n'en voient qu'une.

Anatomie d'une facture LLM
Coût d'un appel = Entrée × prix_in + Sortie × prix_out puis × nombre de tours pour un agent Système + outils (stable, répété à chaque appel) Historique Question ENTRÉE · prix_in · 40 à 60 % souvent ignorés par le modèle Sortie SORTIE · prix_out = 4 à 5 × prix_in · verbosité = argent × tours (agent qui boucle, retries JSON invalide, évaluateur-optimiseur) MULTIPLICATEUR · c'est lui qui transforme 40 € en 4 000 €
Le prix affiché sur la page du fournisseur est le prix d'un seul appel. En production, la facture réelle est multipliée par le nombre de tours (agents, retries, boucles) et dominée par le contexte répété à chaque appel.

Trois observations en découlent, et elles structurent tout le reste du guide.

L'entrée est dominée par ce qui ne change pas. Ton prompt système, tes définitions d'outils, tes exemples few-shot, ta documentation injectée : tout ça est renvoyé à l'identique à chaque appel. Sur un agent qui fait 30 tours, tu payes 30 fois le même préfixe. C'est le premier gisement, et c'est celui que le prompt caching attaque.

La sortie coûte 4 à 5 fois plus cher que l'entrée. Un modèle qui répond « Bien sûr ! Voici les informations que vous avez demandées, présentées de manière claire et structurée : » avant de donner le JSON que tu attendais vient de te facturer 25 tokens de politesse au tarif le plus cher. Sur un million d'appels, c'est un salaire.

Le multiplicateur est invisible sur la grille tarifaire. Le fournisseur affiche un prix par million de tokens. Il n'affiche pas que ton agent va refaire l'appel 12 fois parce que le JSON était invalide au premier tour, ni que ton pattern évaluateur-optimiseur double mécaniquement chaque requête. Ce multiplicateur est là où les pilotes dérapent.

🧮
La règle de multiplication
Les leviers de coût sont multiplicatifs, pas additifs. Un prompt caching qui divise l'entrée par 2, un routing qui divise le prix moyen par 3, et une compaction qui enlève 30 % du contexte donnent 0,5 × 0,33 × 0,7 ≈ 0,12 — soit -88 %. Mais ça veut aussi dire que le levier le plus rentable est toujours celui qui s'applique à la plus grosse part de la facture, et tu ne le connais pas tant que tu n'as pas mesuré.

2. Les 10 leviers, classés par famille

L'infographie qui circule sur LinkedIn liste douze techniques à plat. C'est un inventaire, pas une stratégie. Voici la même matière organisée par ce qu'elle fait à la formule ci-dessus.

Quatre familles, dix leviers
Payer moins par token agit sur prix_in / prix_out 1 · Prompt caching -45 à -80 % sur l'entrée répétée 2 · Batch API -50 % si latence indifférente 3 · Routing par complexité -60 à -90 % sur les tâches simples 4 · Quantization / distillation auto-hébergé : mémoire ÷ 2 à 4 5 · Décodage spéculatif auto-hébergé : débit × 1,5 à 3 Envoyer moins de tokens agit sur Entrée / Sortie 6 · Compaction de contexte -40 à -60 % de tokens inutiles 7 · Sortie structurée moins de sortie + moins de retries 8 · Retrieval affûté chunks, top-k, reranking Ne pas appeler du tout agit sur le nombre d'appels 9 · Cache sémantique hit rate 20 à 40 % réaliste, 60 %+ sur support répétitif Attribuer transverse · prérequis 10 · Observabilité des coûts par chaîne, par agent, par segment utilisateur Sans ça, tout le reste est du pari
Chaque levier agit sur un terme précis de la formule. La famille 'Attribuer' n'économise rien directement, mais sans elle tu optimises à l'aveugle.

Tu remarqueras que deux leviers de l'infographie d'origine ont été fusionnés (quantization et distillation, qui répondent à la même question : « ai-je besoin d'un modèle aussi gros ? ») et que le décodage spéculatif est relégué en fin de liste. Ce n'est pas qu'il est inefficace — c'est qu'il ne concerne que les organisations qui hébergent leurs propres GPU, soit une minorité des lecteurs de ce guide. On y revient en section 5.

Ce que chaque famille demande comme effort

La question qu'un DSI doit se poser n'est pas « lequel de ces leviers marche ? » (ils marchent tous) mais « lequel je peux activer cette semaine avec les gens que j'ai ? ».

Matrice impact / effort
Effort de mise en œuvre → Impact sur la facture → FAIRE CETTE SEMAINE PLANIFIER CE TRIMESTRE QUICK WINS SECONDAIRES SEULEMENT SI AUTO-HÉBERGÉ Prompt caching Sortie structurée Observabilité (prérequis) Batch API Routing par complexité Compaction de contexte Cache sémantique Retrieval affûté Quantization / distillation Décodage spéculatif
Les leviers en haut à gauche sont ceux à activer en premier : gros gain, changement de code mineur. Ceux en bas à droite demandent de l'infrastructure ou une refonte de pipeline.
Le piège du cache sémantique. Il est vendu comme un levier facile parce que « c'est juste un cache ». C'est faux : un cache classique compare des clés exactes, un cache sémantique compare des embeddings avec un seuil, et ce seuil décide entre servir une réponse périmée à une question légèrement différente ou ne jamais rien servir. Sur un chatbot de support avec 200 questions récurrentes, le hit rate peut atteindre 60 %. Sur un assistant métier interne où chaque question est contextuelle, il tombe à 5 % et tu payes des embeddings pour rien. Mesure avant.

3. L'ordre de bataille : 30, 60, 90 jours

Voici la séquence que je recommande pour une DSI qui découvre une facture LLM en dérive. Elle part du principe que tu n'as pas d'équipe dédiée et que chaque levier doit prouver son gain avant le suivant.

Roadmap 30-60-90 jours
J+30 · Voir -30 à -50 % attendus • Observabilité par chaîne • Prompt caching activé • Sortie structurée partout • Plafond de dépense par agent Effort : 3 à 5 jours dev J+60 · Trier -20 à -40 % supplémentaires • Routing via gateway • Batch API sur le non-urgent • Budget tokens par étape • Trim de l'historique Effort : 1 à 2 semaines dev J+90 · Structurer -10 à -30 % selon le cas • Cache sémantique (si FAQ) • Reranking du retrieval • Petit modèle sur tâches étroites • Étude auto-hébergement Effort : projet à part entière Gain cumulé typique : -70 à -85 % à iso-qualité
Chaque palier finance le suivant : les gains des 30 premiers jours justifient le temps d'ingénierie des 60 suivants.

J+30 — Voir. Tu instrumentes chaque appel (quel workflow, quel agent, quel client interne, combien de tokens, combien d'euros), tu actives le prompt caching sur tout ce qui a un préfixe stable, tu forces la sortie structurée, et tu poses un plafond de dépense dur par agent. Ce dernier point n'est pas un levier de coût : c'est une assurance. Un agent qui boucle sans plafond peut dépenser en une nuit ce que tu voulais économiser en un trimestre.

J+60 — Trier. Avec 30 jours de données, tu sais quels workflows coûtent cher. Tu mets en place une gateway (LiteLLM, Portkey, OpenRouter, ou un simple module Python maison) qui route chaque requête vers le modèle le moins cher qui satisfait la qualité. Tu bascules tout ce qui n'est pas temps réel (enrichissement, classification nocturne, rapports) vers la Batch API. Tu fixes un budget de tokens par étape de pipeline et tu coupes l'historique.

J+90 — Structurer. C'est là que tu décides si un cache sémantique a du sens (seulement si ton trafic est répétitif), si ton RAG mérite un reranker, si une tâche étroite à fort volume justifie un petit modèle dédié, et si ton volume est tel que l'auto-hébergement devient rentable. Cette dernière question a une réponse chiffrée : en dessous de quelques dizaines de millions de tokens par jour, presque jamais.

🎯
Le seul KPI qui compte
Pas « coût total » mais coût par unité de valeur : par ticket résolu, par document traité, par lead qualifié. Un pipeline qui passe de 400 € à 800 € par mois en traitant 4 fois plus de documents est un succès. C'est ce ratio que ton observabilité doit produire, et c'est lui que tu présentes à la direction.

4. Les leviers en code

La suite s'adresse à l'équipe qui va implémenter. Chaque levier suit le même format : ce qu'il fait, le code minimal, le piège qui fait échouer la mise en œuvre. Les exemples utilisent le SDK Anthropic ; les concepts sont transposables sans surprise.

4.1 Observabilité : instrumenter avant d'optimiser

Le principe : chaque appel LLM porte des métadonnées (workflow, agent, utilisateur ou segment) et son coût calculé. Tu les envoies vers Langfuse, Helicone, ou un simple OpenTelemetry vers ta stack existante. Le plus simple pour démarrer est un décorateur maison.

import time, functools
from anthropic import Anthropic

client = Anthropic()

# Prix par million de tokens — à externaliser dans une config, vérifier régulièrement
PRICES = {
    "claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_read": 0.10, "cache_write": 1.25},
    "claude-sonnet-4-5": {"in": 3.00, "out": 15.00, "cache_read": 0.30, "cache_write": 3.75},
}

def cost_eur(model: str, usage) -> float:
    p = PRICES[model]
    cached = getattr(usage, "cache_read_input_tokens", 0) or 0
    written = getattr(usage, "cache_creation_input_tokens", 0) or 0
    fresh = usage.input_tokens - cached - written
    usd = (fresh * p["in"] + cached * p["cache_read"] + written * p["cache_write"]
           + usage.output_tokens * p["out"]) / 1_000_000
    return usd * 0.92  # taux de change à sortir en config

def traced(workflow: str, agent: str):
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            t0 = time.perf_counter()
            resp = fn(*args, **kwargs)
            record = {
                "workflow": workflow, "agent": agent,
                "model": resp.model, "latency_ms": round((time.perf_counter() - t0) * 1000),
                "in": resp.usage.input_tokens, "out": resp.usage.output_tokens,
                "cache_read": getattr(resp.usage, "cache_read_input_tokens", 0),
                "cost_eur": cost_eur(resp.model, resp.usage),
                "segment": kwargs.get("segment", "unknown"),
            }
            emit(record)  # → Langfuse / OTel / ta table PostgreSQL
            return resp
        return wrapper
    return deco

@traced(workflow="support-triage", agent="classifier")
def classify(ticket: str, segment: str = "b2b"):
    return client.messages.create(
        model="claude-haiku-4-5", max_tokens=50,
        messages=[{"role": "user", "content": ticket}],
    )

Le piège : instrumenter le coût par appel et s'arrêter là. Ce qui te fait prendre des décisions, c'est l'agrégation par workflow et par tour d'agent. Une requête qui coûte 0,002 € est invisible ; un workflow qui en fait 800 par jour avec 15 tours chacun ne l'est plus.

Le plafond dur. Au-delà du tracing, pose un compteur par session d'agent et coupe au-delà d'un seuil (par exemple 0,50 € ou 25 tours). C'est trois lignes de code et c'est ce qui empêche la nuit à 4 000 €. Le SDK Agent d'Anthropic l'expose nativement via max_budget_usd ; en custom, tu l'écris toi-même.

4.2 Prompt caching : payer le préfixe une fois

Le principe : le fournisseur garde en mémoire le préfixe stable de ton prompt (outils, système, exemples, documents) et te facture sa relecture à une fraction du prix — chez Anthropic, 10 % du prix d'entrée pour une lecture, 125 % pour l'écriture initiale. Le cache est rentable dès la deuxième lecture dans la fenêtre (5 minutes par défaut, extensible à une heure).

Prompt caching : où placer le point de rupture
Outils définitions JSON Système rôle, règles, exemples Documents référentiel, contrat cache_control Historique varie Question varie Lu depuis le cache · 10 % du prix Plein tarif Minimum cachable : 1 024 tokens (2 048 sur les petits modèles) · jusqu'à 4 points de rupture
Tout ce qui précède le marqueur cache_control est mis en cache. Le contenu doit être strictement identique, octet pour octet, d'un appel à l'autre — un timestamp dans le prompt système casse tout.
SYSTEM = open("prompts/support_system.md").read()        # 3 000 tokens, stable
KNOWLEDGE = open("kb/politique_retours.md").read()       # 8 000 tokens, stable

def answer(history: list[dict], question: str):
    return client.messages.create(
        model="claude-sonnet-4-5", max_tokens=600,
        system=[
            {"type": "text", "text": SYSTEM},
            {"type": "text", "text": KNOWLEDGE,
             "cache_control": {"type": "ephemeral"}},   # ← tout ce qui précède est caché
        ],
        messages=history + [{"role": "user", "content": question}],
    )

resp = answer([], "Je peux retourner un article acheté en promo ?")
u = resp.usage
print(u.input_tokens, u.cache_creation_input_tokens, u.cache_read_input_tokens)
# 1er appel :  11 060  |  11 000 écrits  |  0 lus
# 2e appel  :  11 075  |  0              |  11 000 lus  → l'entrée coûte ~12 % du prix

Les pièges, par ordre de fréquence :

  • Un élément variable dans le préfixe. La date du jour, un identifiant de session, le nom de l'utilisateur dans le prompt système : chacun invalide le cache. Tout ce qui varie va dans messages, après le point de rupture.
  • L'ordre des blocs. Le cache s'applique à un préfixe. Si tu changes l'ordre des outils entre deux appels, le préfixe diffère et rien n'est réutilisé.
  • Un préfixe trop court. Sous le seuil minimum, le marqueur est ignoré silencieusement. Tu crois cacher, tu ne caches rien. Vérifie cache_read_input_tokens dans les usages, pas ton intuition.
  • Une fenêtre qui expire. Sur un workflow qui tourne une fois par heure, le cache de 5 minutes ne sert à rien. Soit tu passes en TTL d'une heure (écriture à 200 %), soit tu regroupes les appels.

Chez OpenAI, le caching est automatique sur les préfixes identiques au-delà d'un seuil, avec une remise de 50 à 90 % selon les modèles — pas de marqueur à placer, mais la même discipline sur la stabilité du préfixe. Chez Mistral et Google, le mécanisme est explicite et proche de celui d'Anthropic.

4.3 Sortie structurée : arrêter de payer la politesse

Le principe : forcer le modèle à répondre dans un schéma (JSON typé) plutôt qu'en prose. Double effet : moins de tokens de sortie (le poste le plus cher), et zéro retry pour « le JSON était invalide ». La façon la plus fiable de l'obtenir chez tous les fournisseurs est de définir un outil avec un schéma strict et d'imposer son usage.

EXTRACT_TOOL = {
    "name": "extract_ticket",
    "description": "Extrait les champs structurés d'un ticket support.",
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["billing", "bug", "feature", "other"]},
            "urgency": {"type": "integer", "minimum": 1, "maximum": 5},
            "product": {"type": "string"},
            "summary": {"type": "string", "maxLength": 200},
        },
        "required": ["category", "urgency", "summary"],
        "additionalProperties": False,
    },
}

def extract(ticket: str) -> dict:
    resp = client.messages.create(
        model="claude-haiku-4-5", max_tokens=200,
        tools=[EXTRACT_TOOL],
        tool_choice={"type": "tool", "name": "extract_ticket"},   # ← pas de prose possible
        messages=[{"role": "user", "content": ticket}],
    )
    return next(b.input for b in resp.content if b.type == "tool_use")

Sur un ticket moyen, la version prose produisait 180 tokens de sortie (« Voici l'analyse de ce ticket… ») ; la version outil en produit 45. À 5 € le million de tokens de sortie et 20 000 tickets par mois, c'est 13 € au lieu de 54 €. Ce n'est pas énorme en soi — mais c'est à multiplier par chaque étape de chaque pipeline, et c'est surtout la disparition des retries qui compte : un tour en moins sur un agent, c'est tout le contexte en moins.

max_tokens est un levier, pas une formalité. Une valeur trop haute ne coûte rien tant que le modèle s'arrête tôt, mais elle ne te protège pas d'un modèle qui part en digression. Une valeur calibrée sur ta sortie attendue (+30 % de marge) coupe les dérives et te signale les cas anormaux via stop_reason == "max_tokens".

4.4 Batch API : la remise de 50 % que personne n'utilise

Le principe : tout ce qui n'a pas besoin d'une réponse dans la seconde (enrichissement de données, classification nocturne, génération de rapports, évaluation de qualité) part dans un lot traité sous 24 heures, à moitié prix. Chez Anthropic, OpenAI et Google, la remise est de 50 % sur l'entrée et la sortie, cumulable avec le prompt caching.

from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

def enrich_batch(articles: list[dict]):
    requests = [
        Request(
            custom_id=a["id"],
            params=MessageCreateParamsNonStreaming(
                model="claude-haiku-4-5", max_tokens=300,
                system=[{"type": "text", "text": ENRICH_SYSTEM,
                         "cache_control": {"type": "ephemeral"}}],   # cache + batch se cumulent
                tools=[ENRICH_TOOL],
                tool_choice={"type": "tool", "name": "enrich_article"},
                messages=[{"role": "user", "content": a["text"]}],
            ),
        )
        for a in articles
    ]
    batch = client.messages.batches.create(requests=requests)
    return batch.id

# Plus tard (polling ou cron) :
def collect(batch_id: str):
    b = client.messages.batches.retrieve(batch_id)
    if b.processing_status != "ended":
        return None
    return {r.custom_id: r.result for r in client.messages.batches.results(batch_id)}

Le piège est organisationnel, pas technique : le batch impose de découpler l'ingestion de l'inférence. Ton pipeline doit accepter qu'un article ingéré à 8h soit enrichi à 14h. Sur un pipeline de veille comme celui de nAIvigate (11 sources, 4 passages par jour), c'est parfaitement acceptable pour la traduction et le scoring de pertinence ; ça ne l'est pas pour la réponse à un utilisateur qui attend.

4.5 Routing par complexité : le bon modèle pour chaque requête

Le principe : la majorité des requêtes d'un système en production sont des tâches simples (classification, extraction, reformulation) qu'un modèle de la classe Haiku traite aussi bien que le modèle frontière, pour 10 à 20 fois moins cher. Le routing consiste à classifier chaque requête, puis à l'envoyer vers le modèle le moins cher qui satisfait la qualité. Une gateway transforme ce choix en configuration plutôt qu'en code.

Routing par complexité via gateway
Requête Classifieur règles + petit modèle simple / moyen / complexe Gateway LiteLLM · Portkey fallback · quotas · logs Haiku / local 8B 70 % du trafic · ×1 Sonnet 25 % du trafic · ×3 Opus / frontière 5 % du trafic · ×15
Le classifieur est lui-même un petit modèle (ou de simples règles). La gateway centralise clés, quotas, fallback et observabilité — changer de modèle devient un changement de config.
import re

TIERS = {
    "simple":  "claude-haiku-4-5",
    "medium":  "claude-sonnet-4-5",
    "complex": "claude-opus-4-1",   # à réserver au raisonnement multi-étapes
}

def route(task: str, text: str) -> str:
    """Règles d'abord (gratuit), petit modèle seulement en cas de doute."""
    if task in {"classify", "extract", "translate", "summarize_short"}:
        return TIERS["simple"]
    if task in {"plan", "audit", "multi_step"} or len(text) > 12_000:
        return TIERS["complex"]
    if re.search(r"`{3}|def |SELECT |import ", text):        # présence de code
        return TIERS["medium"]
    # Cas ambigu : demander à Haiku de trancher (coût négligeable)
    verdict = client.messages.create(
        model=TIERS["simple"], max_tokens=5,
        system="Réponds uniquement par: simple, medium ou complex.",
        messages=[{"role": "user", "content": text[:2000]}],
    ).content[0].text.strip().lower()
    return TIERS.get(verdict, TIERS["medium"])

Le piège : router sans évaluer. Le routing suppose que tu sais mesurer la qualité par tier. Avant de basculer 70 % du trafic vers un petit modèle, tu prends 200 requêtes représentatives, tu les fais traiter par les deux tiers, et tu compares (juge LLM ou vérité terrain). Si le petit modèle est à 97 % de la qualité du gros sur ta tâche, tu bascules. S'il est à 80 %, tu ne bascules pas — ou tu affines la tâche jusqu'à ce qu'il y arrive.

Où placer la logique de routing ?

 Module Python maisonGateway (LiteLLM, Portkey…)
Mise en place1 jour2-3 jours (déploiement + config)
Changement de modèleCommit + redéploiementChangement de config
Multi-fournisseursÀ coderNatif
Fallback / retry / quotasÀ coderNatif
ObservabilitéTon décorateurIntégrée (Langfuse, OTel)
Surface d'attaqueMinimaleUn service de plus à sécuriser
Pertinent si1 équipe, 1 fournisseurPlusieurs équipes ou fournisseurs

4.6 Compaction de contexte : 40 à 60 % de tokens que le modèle ignore

Le principe : à chaque appel, le contexte contient de l'historique périmé, du boilerplate, des résultats d'outils verbeux dont seule une ligne compte. Le modèle les lit (tu payes), puis les ignore. La compaction fixe un budget de tokens par étape de pipeline et l'applique avant chaque appel : on garde le système, on résume l'historique ancien, on ne conserve que les N derniers tours en clair, on tronque les sorties d'outils.

def compact(history: list[dict], budget_tokens: int, keep_last: int = 6) -> list[dict]:
    """Applique un budget au contexte : résumé de l'ancien, derniers tours en clair."""
    count = lambda msgs: client.messages.count_tokens(
        model="claude-haiku-4-5", messages=msgs).input_tokens

    if count(history) <= budget_tokens:
        return history

    old, recent = history[:-keep_last], history[-keep_last:]
    summary = client.messages.create(
        model="claude-haiku-4-5", max_tokens=400,
        system="Résume cet échange en faits et décisions, sans commentaire. Max 300 mots.",
        messages=old + [{"role": "user", "content": "Résume."}],
    ).content[0].text

    compacted = [{"role": "user", "content": f"[Résumé des échanges précédents]\n{summary}"},
                 {"role": "assistant", "content": "Compris, je reprends à partir de là."}] + recent
    return compacted

def truncate_tool_result(text: str, max_chars: int = 2000) -> str:
    """Un résultat d'outil de 40 Ko n'a jamais 40 Ko d'information utile."""
    return text if len(text) <= max_chars else text[:max_chars] + f"\n…[tronqué, {len(text)} car. au total]"

Le piège : compacter à l'aveugle et perdre l'information qui comptait. Le résumé doit être orienté « faits et décisions », pas « ambiance ». Et sur un agent, la meilleure compaction n'est pas un résumé mais une mémoire externe : l'agent écrit son plan et ses conclusions dans un fichier ou une base, et le contexte ne contient que le pointeur. C'est le pattern décrit dans notre formation sur les systèmes agentiques.

4.7 Retrieval affûté : le RAG est un générateur de tokens

Le principe : dans un RAG, le coût est directement piloté par taille des chunks × nombre de chunks retournés. Retourner 10 chunks de 1 000 tokens à chaque question, c'est 10 000 tokens d'entrée dont 8 000 sont du bruit. Trois réglages : des chunks plus petits et sémantiquement cohérents (300 à 800 tokens), un top-k généreux à la recherche puis un reranker qui ne garde que les 3 à 5 vraiment pertinents, et une recherche hybride (BM25 + vecteurs) qui réduit le bruit à la source.

def retrieve(query: str, top_k_search: int = 20, top_k_final: int = 4) -> list[str]:
    # 1. Hybride : union des candidats lexicaux et vectoriels
    lexical = bm25_index.search(query, k=top_k_search)
    vector = vector_index.search(embed(query), k=top_k_search)
    candidates = dedupe(lexical + vector)

    # 2. Reranking : un cross-encoder (bge-reranker, Cohere Rerank, etc.)
    scored = reranker.score(query, [c.text for c in candidates])
    best = sorted(zip(scored, candidates), reverse=True)[:top_k_final]

    # 3. Seuil : si rien n'est pertinent, n'envoie rien (et dis-le au modèle)
    return [c.text for s, c in best if s > 0.35]

Avec ce pipeline, un contexte RAG passe typiquement de 8 à 10 000 tokens à 2 à 3 000, pour une qualité de réponse égale ou meilleure — le modèle n'a plus à trier lui-même le bruit. Le piège classique : un reranker mal calibré qui coupe trop et laisse le modèle halluciner faute de contexte. Le seuil se règle sur un jeu de questions de test, pas au doigt mouillé.

4.8 Cache sémantique : ne pas appeler du tout

Le principe : avant d'appeler le LLM, tu calcules l'embedding de la question, tu cherches dans un index vectoriel une question passée suffisamment proche (similarité cosinus au-dessus d'un seuil), et si tu la trouves tu renvoies la réponse stockée. Coût : un embedding (quelques centièmes de centime) au lieu d'une génération.

Cache sémantique : le seuil décide de tout
Question Embedding ~0,0001 € Index vectoriel Redis · pgvector similarité ≥ seuil ? oui non Réponse cachée coût ≈ 0 · latence ≈ 0 Appel LLM puis stockage Q + R Scope par tenant · TTL obligatoire · jamais sur des réponses personnalisées
Trop bas, tu sers des réponses fausses à des questions voisines. Trop haut, tu ne sers jamais rien et tu payes les embeddings pour rien. 0,92 à 0,95 est un point de départ ; la bonne valeur dépend du modèle d'embedding et du domaine.
import numpy as np

class SemanticCache:
    def __init__(self, store, threshold: float = 0.93, ttl_s: int = 86_400):
        self.store, self.threshold, self.ttl = store, threshold, ttl_s

    def get(self, tenant: str, question: str):
        q = embed(question)
        hit = self.store.nearest(tenant, q, k=1)          # (score, answer, created_at)
        if hit and hit.score >= self.threshold and not expired(hit.created_at, self.ttl):
            return hit.answer
        return None

    def put(self, tenant: str, question: str, answer: str):
        self.store.add(tenant, embed(question), answer)

cache = SemanticCache(store=RedisVectorStore("faq"))

def ask(tenant: str, question: str) -> str:
    if (cached := cache.get(tenant, question)):
        return cached
    answer = llm_answer(question)
    cache.put(tenant, question, answer)
    return answer

Trois règles non négociables : scope par tenant (le cache d'un client ne doit jamais servir à un autre — c'est une fuite de données, pas une optimisation), TTL (une politique de retours change, la réponse cachée aussi), et exclusion des réponses personnalisées (« où en est ma commande ? » ne se cache pas, quelle que soit la similarité). Mesure le hit rate pendant deux semaines avant de conclure : sur un support répétitif il monte à 40 à 60 %, sur un assistant métier il peut rester sous 10 %, et dans ce cas retire-le.

5. Les leviers d'infrastructure (auto-hébergé uniquement)

Si tu consommes des API, cette section ne te concerne pas : le fournisseur fait déjà tout ça de son côté, et c'est inclus dans le prix. Si tu héberges tes propres modèles (souveraineté, volume, données sensibles), voici les trois réglages qui changent la facture GPU.

Quantization. Un modèle en poids INT8 ou INT4 occupe 2 à 4 fois moins de mémoire que sa version 16 bits, avec une perte de qualité négligeable sur la plupart des tâches (mesurable sur le raisonnement long). Concrètement, un 70B qui demandait deux GPU de 80 Go tient sur un seul. Avec Ollama, c'est le suffixe du tag (llama3.3:70b-instruct-q4_K_M) ; avec vLLM, un flag au lancement. La quantization du KV cache (la mémoire de travail pendant la génération) est un second levier, plus récent, qui permet de servir plus de requêtes simultanées par GPU.

# vLLM : poids quantizés AWQ + KV cache en FP8
vllm serve Qwen/Qwen3-32B-AWQ \
  --quantization awq \
  --kv-cache-dtype fp8 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.92

# Ollama : le tag choisit la quantization
ollama pull qwen3:32b-q4_K_M

Distillation / petit modèle dédié. Pour une tâche étroite à fort volume (classifier 50 000 tickets par jour en 12 catégories), un modèle de 3 à 8 milliards de paramètres fine-tuné sur les sorties du modèle frontière fait aussi bien pour une fraction du coût, et tourne sur un GPU modeste ou même sur CPU. C'est un projet (jeu de données, entraînement, évaluation, maintenance), pas un réglage — à réserver aux tâches dont le volume le justifie. Notre comparateur de modèles locaux te donne les candidats par palier de matériel.

Décodage spéculatif et désagrégation. Le décodage spéculatif fait proposer plusieurs tokens par un petit modèle « brouillon », que le grand modèle vérifie en un seul passage : 1,5 à 3 fois plus de tokens par seconde, sans changer la sortie. La désagrégation prefill/decode sépare sur des GPU différents la lecture du prompt (gourmande en calcul) de la génération (gourmande en mémoire), pour une meilleure utilisation du matériel. Les deux sont des réglages de serveur d'inférence (vLLM, SGLang, TensorRT-LLM), pertinents à partir de plusieurs GPU en production. En dessous, l'effort ne se justifie pas.

Quand l'auto-hébergement devient-il rentable ? Un GPU H100 loué coûte de l'ordre de 2 à 3 € de l'heure, soit 1 500 à 2 200 € par mois en continu, hors ingénierie. Il sert un 70B quantizé à quelques centaines de tokens par seconde. À prix API équivalent, ça correspond à plusieurs dizaines de millions de tokens par jour. En dessous de ce volume — ou si la contrainte est la souveraineté plutôt que le prix — le calcul est différent et il faut le faire honnêtement, ingénierie et astreinte comprises.

6. Cas chiffré : un pipeline de veille

Prenons un cas réel de structure : un pipeline qui ingère 11 sources d'actualité 4 fois par jour, filtre, puis enrichit chaque article retenu (traduction du titre, score de pertinence, extraction de 3 points clés). Environ 200 articles enrichis par jour. Avant optimisation, chaque enrichissement est un appel direct à un modèle de classe Sonnet, avec un prompt système de 1 200 tokens, l'article (500 tokens en moyenne) et une réponse en prose de 350 tokens.

Effet cumulé des leviers sur le pipeline
Départ (Sonnet, prose) 100 + Sortie structurée 73 + Prompt caching 52 + Routing → Haiku 17 + Batch API 9 Trim de l'article (500 → 300 tok.) 7 Base 100 = coût quotidien avant optimisation
Chaque barre représente le coût quotidien relatif après activation du levier. Les quatre premiers leviers sont des changements de code de quelques heures ; le gain est de -90 % à qualité égale sur ce type de tâche.

Le détail du calcul, avec des prix relatifs (entrée = 1, sortie = 5 pour Sonnet ; Haiku à un tiers) :

  • Départ : 200 × (1 700 tokens d'entrée × 1 + 350 de sortie × 5) = 200 × 3 450 = 690 000 unités. Base 100.
  • Sortie structurée : la réponse passe de 350 à 90 tokens (JSON typé avec 3 champs). 200 × (1 700 + 450) = 430 000. Indice 62 — mais on garde 73 dans le graphe pour tenir compte de quelques retries résiduels sur les articles mal formés qui disparaissent seulement à l'étape suivante. Sois conservateur dans tes propres projections.
  • Prompt caching : les 1 200 tokens de système sont lus à 10 % du prix (les 4 passages quotidiens sont regroupés dans la fenêtre). L'entrée passe à 120 + 500 = 620. Indice 52.
  • Routing vers Haiku : la tâche (traduire un titre, scorer, extraire trois points) est évaluée à 96 % de la qualité de Sonnet sur 200 articles de test. Division par 3 du prix moyen. Indice 17.
  • Batch API : l'enrichissement n'a pas besoin d'être instantané. -50 %. Indice 9.
  • Trim : on n'envoie que le chapeau et les deux premiers paragraphes (300 tokens au lieu de 500) — le reste n'améliore pas le score. Indice 7.

Aucun de ces leviers n'a demandé plus d'une demi-journée de travail. Le gain est de 93 %, sur une tâche qui — c'est important — s'y prête particulièrement bien : répétitive, non urgente, à sortie structurée. Un assistant conversationnel temps réel avec des réponses longues ne verra pas 93 %. Il verra 40 à 60 %, ce qui reste considérable.

📐
Ce que ce cas enseigne
Le gros du gain vient de leviers de code (structure, cache, routing, batch), pas d'infrastructure. La quantization et le décodage spéculatif n'apparaissent même pas. Pour l'immense majorité des organisations qui consomment des API, la réponse à « comment réduire ma facture LLM » tient en une semaine de travail d'un développeur qui a lu ce guide — à condition d'avoir d'abord mesuré où va l'argent.

7. Teste ta compréhension

🧠 Quiz
Question 1 sur 6

Ton agent coûte 4 000 € par mois alors que chaque appel unitaire coûte 0,01 €. Quel terme de la formule explique l'écart ?

📚Lexique technique (déroulez)

Token — Unité de facturation d'un LLM ; environ 0,75 mot en anglais, un peu moins en français. L'entrée et la sortie sont facturées à des tarifs différents.

Prompt caching (prefix caching) — Mécanisme fournisseur qui mémorise un préfixe stable du prompt et facture sa relecture à une fraction du prix (10 % chez Anthropic).

cache_control — Marqueur dans l'API Anthropic qui délimite la fin du préfixe à mettre en cache. Jusqu'à 4 points de rupture.

TTL (time to live) — Durée de validité d'une entrée en cache. 5 minutes par défaut pour le prompt caching Anthropic, extensible à 1 heure.

Batch API — Endpoint asynchrone qui traite un lot de requêtes sous 24 heures pour 50 % du prix. Cumulable avec le prompt caching.

Routing par complexité — Classification de chaque requête pour l'envoyer au modèle le moins cher qui satisfait la qualité requise.

Gateway LLM — Proxy centralisant clés, quotas, fallback, routing et observabilité (LiteLLM, Portkey, OpenRouter…). Rend le changement de modèle configurable.

Sortie structurée — Réponse contrainte à un schéma (JSON typé), obtenue de façon fiable via un outil à schéma strict et tool_choice forcé.

Compaction de contexte — Réduction du contexte avant chaque appel : résumé de l'historique ancien, conservation des derniers tours, troncature des sorties d'outils.

Budget de tokens — Plafond de tokens alloué à une étape de pipeline, appliqué avant l'appel.

Reranker — Modèle (cross-encoder) qui rescore les candidats d'une recherche pour ne garder que les plus pertinents. Réduit le nombre de chunks envoyés au LLM.

Recherche hybride — Combinaison d'une recherche lexicale (BM25) et vectorielle pour réduire le bruit à la source.

Cache sémantique — Cache qui compare des embeddings de questions avec un seuil de similarité, pour renvoyer une réponse déjà générée sans appeler le LLM.

Similarité cosinus — Mesure de proximité entre deux embeddings, entre -1 et 1. Le seuil du cache sémantique se règle typiquement entre 0,92 et 0,95.

Quantization — Réduction de la précision des poids (16 bits → 8 ou 4 bits) pour diviser la mémoire par 2 à 4, avec une perte de qualité faible.

KV cache — Mémoire de travail du modèle pendant la génération. Sa quantization (FP8, INT4) permet de servir plus de requêtes simultanées par GPU.

Distillation — Entraînement d'un petit modèle à reproduire les sorties d'un grand sur une tâche étroite.

Décodage spéculatif — Un petit modèle brouillon propose plusieurs tokens, le grand les vérifie en un passage : 1,5 à 3× le débit sans changer la sortie.

Désagrégation prefill/decode — Séparation sur des GPU distincts de la lecture du prompt et de la génération, pour mieux utiliser le matériel.

Coût par unité de valeur — Le KPI à suivre : coût par ticket résolu, document traité, lead qualifié — pas le coût total.

Questions fréquentes

Le prompt caching et la Batch API se cumulent-ils ? Oui, chez Anthropic les deux remises s'appliquent : un préfixe lu depuis le cache dans un batch coûte 10 % × 50 % = 5 % du prix d'entrée standard.

Le routing dégrade-t-il la qualité ? Seulement si tu routes sans évaluer. Le protocole : 200 requêtes représentatives, traitées par chaque tier, comparées. Tu bascules quand le petit modèle est à plus de 95 % de la qualité du gros sur ta tâche.

Par quoi commencer si je n'ai qu'une journée ? Observabilité minimale (le décorateur de la section 4.1) et prompt caching sur ton plus gros préfixe. Tu verras le gain le lendemain dans les usages.

Faut-il une gateway dès le départ ? Non. Un module Python de 30 lignes suffit pour une équipe et un fournisseur. La gateway devient pertinente quand plusieurs équipes ou plusieurs fournisseurs entrent en jeu.

L'auto-hébergement est-il un levier de coût ? Rarement en dessous de plusieurs dizaines de millions de tokens par jour. C'est un levier de souveraineté ou de confidentialité, et il doit être chiffré honnêtement, ingénierie et astreinte comprises.

Pour aller plus loin

La compaction de contexte et la mémoire externe sont traitées en profondeur dans notre formation sur l'architecture des systèmes agentiques, et le pattern orchestrateur/workers — avec son plafond de dépense — dans le guide pratique multi-agents. Pour choisir un modèle local par palier de matériel et comparer les prix API entre fournisseurs, le comparateur nAIvigate intègre un calculateur de coût mensuel par volume de tokens.

Si ta facture LLM est déjà en dérive et que tu veux un diagnostic chiffré de tes workflows — où va l'argent, quels leviers activer, dans quel ordre — c'est précisément ce que couvre un Radar IA.

Tags
coutfinopsllmoptimisationprompt-cachingroutingbatchautomationagentspythonapi
⚡ FICHE #004Les 50 termes IA à maîtriser pour décider en 202612 MIN

À lire ensuite