Cost Optimization — Réduire la facture LLM de 60-80% sans dégrader la qualité
TL;DR En 2026, un agent LLM qui coûte 80 k€/an mal architecturé peut tomber à 15 k€/an sans baisse de qualité avec 5 leviers : (1) prompt caching (cache read à ~0.1× le prix input sur prompts répétés), (2) batch API (-50% sur jobs non-temps-réel), (3) model tiering (Haiku 4.5 pour tâches simples, Opus 4.8 pour le hard), (4) semantic cache (cache des réponses sémantiquement proches), (5) prompt compression (LLMLingua, factorisation des outils MCP). Le pattern qui paie : un router multi-tier qui classe d'abord la difficulté, route Haiku pour 70% des cas, Sonnet pour 25%, Opus pour 5%, avec semantic cache devant. Côté freelance, vendre un "FinOps LLM audit" est typiquement 12-25 k€ de mission avec ROI client < 4 mois. Toujours présenter avec un avant/après chiffré : c'est le pitch qui ferme.
Le réflexe staff : ne jamais optimiser un nombre qu'on n'a pas mesuré. Avant de toucher au code, on instrumente (
resp.usage→ coût/req, cache hit rate, distribution de difficulté). On optimise dans cet ordre de ROI décroissant : prompt caching → model tiering → batch → semantic cache → compression. Le caching et le tiering capturent 80% du gain pour 20% de l'effort.
🧠 Mental model
┌──────────────────────────┐
│ USER REQUEST │
└──────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ SEMANTIC CACHE │ hit (15%) │ NO CACHE │
│ (Redis+embed) │ ──────────► └────────────────┘
└────────────────┘ │
│ miss │
▼ ▼
┌────────────────────────────────────────────┐
│ ROUTER (classifier) │
│ difficulté → easy / medium / hard │
└────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Haiku 4.5│ │Sonnet 4.6│ │ Opus 4.8 │
│ 70% │ │ 25% │ │ 5% │
│ $1/$5 Mt │ │ $3/$15 Mt│ │ $5/$25 Mt│
└──────────┘ └──────────┘ └──────────┘
(in/out, $ par million de tokens)
│ │ │
└────────────┴────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ PROMPT CACHE │ │ BATCH API │
│ system+tools │ │ jobs nocturnes │
│ -90% input cost│ │ -50% total │
└────────────────┘ └────────────────┘Analogie : optimiser ton LLM, c'est comme optimiser un restaurant. Tu ne sers pas du foie gras à tous les clients (model tiering). Tu pré-prépares les sauces le matin (prompt caching). Tu fais les conserves la nuit (batch API). Tu réutilises les plats des tables voisines quand c'est compatible (semantic cache). Et tu réduits les fiches techniques cuisine de 5 pages à 2 sans perdre la recette (prompt compression).
Un dev junior optimise au moment du run : il essaie de "faire moins de tokens output". C'est minuscule. Le vrai gain est architectural : décider quel modèle, avec quel context, en streaming ou batch, avec ou sans cache. C'est 70% du coût qui se joue avant même d'écrire le premier prompt.append("Tu es un assistant...").
Le levier par effort vs gain (ce qu'un staff attaque en premier)
| Levier | Effort | Gain typique | Risque qualité | Quand l'appliquer |
|---|---|---|---|---|
| Prompt caching | Très faible (1 flag) | Cache read ≈ 0.1× input sur la partie répétée | Aucun | Toujours. System + tools stables ≥ seuil min. |
| Model tiering | Moyen (classifier + eval) | -60 à -80% sur le volume routé bas | Élevé sans eval par tier | Volume mixte, tâches de difficulté hétérogène |
| Batch API | Faible | -50% | Aucun (mais latence 1-24h) | Tout ce qui n'est pas user-facing temps réel |
| Semantic cache | Élevé (embed + vector store + tuning seuil) | -10 à -65% selon répétitivité | Élevé (faux positifs) | Volume > 50k req/mois, questions répétées |
| Prompt compression | Moyen | -15 à -40% sur l'input | Moyen (perte de contexte) | Prompts gras (> 4k tok), tools MCP nombreux |
| Fine-tune narrow | Très élevé (dataset + train + eval + déploiement) | -80%+ sur une tâche unique | Élevé (drift, maintenance) | UNE tâche ultra-récurrente, volume massif |
Le coût d'une requête se décompose ainsi — c'est le modèle mental à avoir en tête avant d'optimiser :
coût = input_non_caché × prix_in
+ cache_read × (0.1 × prix_in)
+ cache_write × (1.25 à 2 × prix_in)
+ output × prix_outChaque levier attaque un terme : le caching déplace input_non_caché vers cache_read ; le tiering baisse prix_in/prix_out ; la compression réduit input ; le cap de max_tokens borne output. Le batch divise toute la facture par 2. Quand tu chiffres un "avant/après" client, fais-le terme par terme — c'est ce qui rend le pitch crédible en réunion.
Comment un staff raisonne sur la latence (pas juste le coût)
Optimiser le coût peut dégrader la latence — et inversement. Le classifier ajoute un round-trip avant la vraie réponse ; le semantic cache lookup ajoute un embed + une recherche vectorielle. Un staff garde ce budget en tête :
- Le classifier doit être asynchrone-friendly et minuscule (Haiku 4.5,
max_tokens: 10). S'il prend 400 ms, tu l'as ajouté à chaque requête non-cachée. - Le semantic cache gagne sur les deux fronts quand il hit (pas d'appel LLM → coût 0 ET latence ~embed seule). C'est le seul levier où coût ↓ et latence ↓ vont ensemble. Mais sur un miss, tu paies l'overhead pour rien.
- Le caching réduit aussi le TTFT (time-to-first-token) : un prefix caché est préfill plus vite. Pré-warm si la 1ère requête est user-facing.
- Le batch tue la latence (1-24h) : jamais pour du synchrone. C'est un arbitrage binaire, pas un curseur.
🛠️ Code minimal
Prompt caching Anthropic + model tiering + semantic cache simple.
// libs/llm/cost-aware-call.ts
import Anthropic from "@anthropic-ai/sdk";
import { Redis } from "ioredis";
import { embed } from "@app/embeddings"; // wrapper Voyage / OpenAI
const anthropic = new Anthropic();
const redis = new Redis(process.env.REDIS_URL!);
type Difficulty = "easy" | "medium" | "hard";
const MODEL_BY_DIFFICULTY: Record<Difficulty, string> = {
easy: "claude-haiku-4-5",
medium: "claude-sonnet-4-6",
hard: "claude-opus-4-8",
};
// 1. Semantic cache lookup
async function semanticCacheGet(question: string, threshold = 0.92): Promise<string | null> {
const qVec = await embed(question);
// simple top-1 lookup; in prod use Qdrant / pgvector
const candidates = await redis.zrange("sem:keys", 0, -1);
for (const key of candidates) {
const stored = JSON.parse((await redis.get(key))!) as { vec: number[]; answer: string };
const sim = cosine(qVec, stored.vec);
if (sim >= threshold) return stored.answer;
}
return null;
}
async function semanticCacheSet(question: string, answer: string): Promise<void> {
const qVec = await embed(question);
const key = `sem:${hash(question)}`;
await redis.set(key, JSON.stringify({ vec: qVec, answer }), "EX", 3600 * 24 * 7);
await redis.zadd("sem:keys", Date.now(), key);
}
// 2. Difficulty classifier (cheap call)
async function classify(question: string): Promise<Difficulty> {
const res = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 10,
messages: [{
role: "user",
content:
`Classify difficulty (easy|medium|hard) of: "${question}"\n` +
`easy = factual lookup, simple summary\n` +
`medium = multi-step reasoning\n` +
`hard = legal nuance, creative, long context\n` +
`Reply with one word only.`,
}],
});
const text = res.content[0].type === "text" ? res.content[0].text.trim().toLowerCase() : "medium";
return (["easy", "medium", "hard"].includes(text) ? text : "medium") as Difficulty;
}
// 3. Main call with prompt caching
export async function answer(question: string, systemPrompt: string, tools: any[]) {
const cached = await semanticCacheGet(question);
if (cached) return { answer: cached, source: "cache", costEur: 0 };
const diff = await classify(question);
const model = MODEL_BY_DIFFICULTY[diff];
const res = await anthropic.messages.create({
model,
max_tokens: 800,
system: [
{
type: "text",
text: systemPrompt,
// cache read ≈ 0.1× le prix input ; write ≈ 1.25× (TTL 5 min) ou 2× (TTL 1h).
// Min cacheable : 4096 tok (Opus 4.8 / Haiku 4.5), 2048 tok (Sonnet 4.6) —
// en-dessous le cache ne s'écrit PAS (cache_creation_input_tokens = 0, sans erreur).
cache_control: { type: "ephemeral" },
},
],
// ⚠️ tools render AVANT system : un cache_control sur le system cache déjà tools+system.
// Mettre le flag sur le DERNIER bloc stable de la chaîne tools→system→messages.
tools: tools.map(t => ({ ...t, cache_control: { type: "ephemeral" } })),
messages: [{ role: "user", content: question }],
});
// Toujours logger usage pour le FinOps : input_tokens = remainder NON caché.
// Taille prompt réelle = input + cache_creation + cache_read.
// Si cache_read = 0 sur des requêtes à prefix identique → invalidateur silencieux
// (timestamp dans le system, JSON non trié, set de tools qui varie).
const text = res.content[0].type === "text" ? res.content[0].text : "";
await semanticCacheSet(question, text);
return {
answer: text,
source: model,
costEur: estimateCost(res.usage, model), // prend en compte cache_read_input_tokens
};
}Ce qui se passe :
semanticCacheGetévite l'appel LLM si une question proche a déjà été répondue (hit rate typique 10-25% sur du support).classifyroute vers le bon modèle (Haiku, Sonnet, Opus).cache_control: ephemeralcache le system + tools côté Anthropic → re-lecture à -90% pendant 5 min.
🎬 Cas d'usage concrets
Cas 1 — Helpdesk e-commerce avec 100k req/mois (passage 30k€ → 6k€/mois)
Le client : marketplace fashion FR, ~100k requêtes/mois sur le chatbot SAV. Stack initial : tout passe par GPT-4o avec un system prompt de 4500 tokens (politique retour, tableau de tailles, charte ton, FAQ longue).
Coût initial :
- Input : 100k × 4500 tokens × 5$/1M = 2 250 $/mois en system seul.
- 100k × 600 tokens (historique) × 5$ = 300 $.
- Output : 100k × 300 tokens × 15$/1M = 450 $.
- Total : ~3 000 $/mois (≈ 28 k€/an).
Refactor coût :
- Prompt caching : tout le system prompt + tools dans un bloc
cache_control. Hit rate ~85% (les questions arrivent en burst). Coût input system : -90% → 230 $/mois. - Model tiering : classifier détecte que 65% des questions sont triviales ("où est ma commande", "comment retourner") → routées vers GPT-4o-mini (10x moins cher).
- Semantic cache : hit rate 18% sur les questions ultra-fréquentes ("livraison combien de temps", "code promo") → -18% appels.
Coût final :
- Cache hits : 18k req gratuites.
- Easy (4o-mini) : 53k × tokens × prix → 220 $/mois.
- Medium/hard (4o avec prompt cache) : 29k × tokens → 340 $/mois.
- Total : ~560 $/mois (≈ 6 200 €/an).
Économie : 22 k€/an, ROI mission 18 k€ en 10 mois.
Cas 2 — RAG juridique avec batch processing nightly
Le client : éditeur LegalTech FR, fournit des résumés automatiques de jurisprudence + notes de synthèse pour cabinets. Volume : 8 000 nouveaux documents/jour (Cour de cass, CA Paris, conseils prud'hommes). Aucun n'est temps-réel : les avocats consomment le matin suivant.
Architecture initiale : pipeline temps-réel sur Claude Sonnet, ~12 k€/mois.
Refactor :
- Batch API Anthropic : tous les documents partent dans un batch nocturne (lance à 22h, livraison à 6h). Tarif -50%.
- Model tier split : Sonnet pour les arrêts de cassation (qualité juridique max), Haiku pour les jugements simples (premier degré, divorce). Classifier sur le
juridictiondu document → router. - Prompt caching : la trame de résumé (15 pages de consignes : structurer en faits/moyens/portée, citer articles, etc.) cachée → -90% sur la partie input commune.
Avant : ~12 k€/mois. Après : ~3 800 €/mois. Économie : 100 k€/an. Mission 35 k€, payback 4 mois.
Cas 3 — Agent compta avec semantic cache sur PME (TPE-cabinet 80% questions répétées)
Le client : ESN d'expertise comptable, agent IA pour 200 TPE-clients. Constat : les TPE posent toujours les mêmes questions ("comment classer ma facture Amazon ?", "écriture pour mon abonnement OVH ?", "TVA collectée sur mes ventes Stripe ?"). Estimation : 60-70% de questions sémantiquement identiques.
Solution semantic cache :
- Cache Redis indexé par embedding Voyage. Clé = embedding question, valeur = réponse.
- Seuil cosine 0.91 (calibré : précis sans faux positifs sur cas comptables sensibles).
- TTL 7 jours (plan comptable peut évoluer en cas de réforme).
- Important : invalidation par tag → "TVA 2026" peut purger les questions TVA d'un coup.
- Tenant-aware : le cache est partagé entre tous les clients (questions génériques) mais chaque cabinet peut désactiver le sharing (paranoïa) via flag.
Résultat : 64% hit rate après 3 semaines. Coût LLM divisé par 3. Latence moyenne : 1.8s → 0.4s (encore mieux). Mission 18 k€ (cache + tuning + dashboard hit rate).
🛠️ Exemple end-to-end — Refactor d'un agent customer support 80k€/an → 15k€/an
Contexte : SaaS B2B FR (gestion de planning RH), 500 clients, chatbot intégré dans le produit. ~200k req/mois. Stack actuelle : tout passe par GPT-4o, prompt system 6000 tokens, pas de cache, pas de batch. Facture OpenAI : ~6600 €/mois (78 k€/an).
Objectif : passer à <15 k€/an sans dégrader le CSAT (actuellement 4.2/5).
1) Audit initial (semaine 1)
Tu instrumentes Langfuse pour 7 jours, tu obtiens :
- Distribution des tokens input : moyenne 4800, p95 6200 (system prompt énorme).
- Distribution des outputs : moyenne 180 tokens.
- Distribution des types de question (clustering via embeddings) :
- 38% : statut planning / "qui travaille demain" (trivial).
- 24% : règles légales (35h, repos, conventions).
- 18% : configuration produit ("comment ajouter un site").
- 12% : escalade vers humain.
- 8% : autres.
- Latence moyenne : 2.4s. P95 : 4.1s.
2) Refactor architecture cible
USER QUERY
│
▼
┌──────────────────┐
│ SEMANTIC CACHE │ hit (20% target)
│ (Redis+Voyage) │ ─────────► return
└──────────────────┘
│ miss
▼
┌──────────────────┐
│ ROUTER (Haiku) │
│ classify intent │
└──────────────────┘
│ │ │
┌──────────┘ │ └───────────┐
▼ ▼ ▼
TRIVIAL STANDARD COMPLEX
intents intents (legal, edge)
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Haiku 4.5│ │Sonnet 4.6│ │ Opus 4.8 │
│ +cache │ │ +cache │ │ +cache │
└──────────┘ └──────────┘ └──────────┘
60% traffic 30% traffic 10% traffic3) Implementation NestJS (extraits)
// libs/llm/cost-router/cost-router.service.ts
import { Injectable } from "@nestjs/common";
import { SemanticCache } from "./semantic-cache";
import { IntentClassifier } from "./intent-classifier";
import { LlmClient } from "./llm-client";
type Intent = "trivial" | "standard" | "complex" | "escalate";
const ROUTE: Record<Intent, { model: string; provider: "anthropic" | "openai" }> = {
trivial: { model: "claude-haiku-4-5", provider: "anthropic" },
standard: { model: "claude-sonnet-4-6", provider: "anthropic" },
complex: { model: "claude-opus-4-8", provider: "anthropic" },
escalate: { model: "noop", provider: "anthropic" }, // handoff human
};
@Injectable()
export class CostRouter {
constructor(
private cache: SemanticCache,
private classifier: IntentClassifier,
private llm: LlmClient,
) {}
async ask(input: { tenantId: string; question: string; history: string[] }) {
// 1. semantic cache
const cached = await this.cache.get({
tenantNamespace: input.tenantId,
question: input.question,
threshold: 0.92,
});
if (cached) return { answer: cached, source: "cache", costEur: 0 };
// 2. classify
const intent = await this.classifier.classify(input.question);
if (intent === "escalate") return { handoff: true, source: "router" };
// 3. route
const { model, provider } = ROUTE[intent];
const res = await this.llm.complete({
provider,
model,
system: SYSTEM_PROMPT,
systemCache: true,
tools: TOOLS,
messages: [{ role: "user", content: input.question }],
tenantId: input.tenantId,
});
// 4. cache the result for future similar questions (only if not too volatile)
if (intent !== "complex") {
await this.cache.set({
tenantNamespace: input.tenantId,
question: input.question,
answer: res.text,
ttlSec: 3600 * 24 * 7,
});
}
return { answer: res.text, source: model, costEur: res.costEur };
}
}// libs/llm/cost-router/semantic-cache.ts
import { Injectable, Inject } from "@nestjs/common";
import { Redis } from "ioredis";
import { embed } from "@app/embeddings";
import { cosine } from "@app/utils/math";
@Injectable()
export class SemanticCache {
constructor(@Inject("REDIS") private redis: Redis) {}
async get(opts: { tenantNamespace: string; question: string; threshold: number }) {
const qVec = await embed(opts.question);
const indexKey = `semcache:${opts.tenantNamespace}:idx`;
const keys = await this.redis.zrange(indexKey, 0, -1);
let best: { sim: number; answer: string } | null = null;
for (const k of keys) {
const raw = await this.redis.get(k);
if (!raw) continue;
const { vec, answer } = JSON.parse(raw) as { vec: number[]; answer: string };
const sim = cosine(qVec, vec);
if (sim > (best?.sim ?? 0)) best = { sim, answer };
}
return best && best.sim >= opts.threshold ? best.answer : null;
}
async set(opts: { tenantNamespace: string; question: string; answer: string; ttlSec: number }) {
const vec = await embed(opts.question);
const key = `semcache:${opts.tenantNamespace}:${Date.now()}-${Math.random()}`;
await this.redis.set(
key,
JSON.stringify({ vec, answer: opts.answer }),
"EX",
opts.ttlSec,
);
await this.redis.zadd(`semcache:${opts.tenantNamespace}:idx`, Date.now(), key);
}
}// libs/llm/cost-router/llm-client.ts (extrait — Anthropic avec prompt caching)
async complete(opts: CompleteOpts) {
if (opts.provider === "anthropic") {
const res = await this.anthropic.messages.create({
model: opts.model,
max_tokens: 600,
system: opts.systemCache
? [{ type: "text", text: opts.system, cache_control: { type: "ephemeral" } }]
: opts.system,
tools: opts.tools?.map(t => ({ ...t, cache_control: { type: "ephemeral" } })),
messages: opts.messages,
});
const usage = res.usage;
return {
text: extractText(res),
costEur: priceAnthropicEur({
model: opts.model,
inputTokens: usage.input_tokens,
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
outputTokens: usage.output_tokens,
}),
};
}
// openai branch ...
}4) Pricing helper
// libs/llm/pricing.ts
// Prix publics Anthropic 2026, en USD par million de tokens (input / output).
// cacheRead ≈ 0.1× input ; cacheWrite ≈ 1.25× input (TTL 5 min) ou 2× (TTL 1h).
// ⚠️ Les prix bougent : re-vérifier sur platform.claude.com tous les ~3 mois,
// et idéalement charger ce barème depuis un fichier de config versionné, pas en dur.
const PRICES_USD_PER_MTOK = {
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
} as const;
// Pour la facturation client en €, applique le taux de change au moment de la conso
// (ne pas figer un taux : USD/EUR a bougé de >10% sur certaines années).
const USD_TO_EUR = 0.92;
export function priceAnthropicEur(args: {
model: string;
inputTokens: number; // = usage.input_tokens (remainder NON caché)
cacheReadTokens: number; // = usage.cache_read_input_tokens
cacheWriteTokens: number; // = usage.cache_creation_input_tokens
outputTokens: number; // = usage.output_tokens
}): number {
const p = PRICES_USD_PER_MTOK[args.model as keyof typeof PRICES_USD_PER_MTOK];
if (!p) return 0;
const M = 1_000_000;
const usd =
(args.inputTokens * p.input) / M +
(args.cacheReadTokens * p.cacheRead) / M +
(args.cacheWriteTokens * p.cacheWrite) / M +
(args.outputTokens * p.output) / M;
return usd * USD_TO_EUR;
}Piège du calcul de coût :
usage.input_tokensne contient que les tokens facturés au plein tarif. La taille réelle du prompt =input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Si ton dashboard somme uniquementinput_tokens, tu sous-estimes la conso et tu crois économiser plus que la réalité. Et inversement : un agent qui tourne des heures avecinput_tokensà 4k a en fait servi l'essentiel depuis le cache — vérifie la somme des trois.
5) Dashboard FinOps
Tu exposes 4 panneaux Grafana :
- Coût / jour par modèle (stacked).
- Coût / tenant (top 20).
- Cache hit rate (semantic + prompt cache) journalier.
- Coût par requête (moyenne, p50, p95, p99).
Alerte si coût/req > 50% au-dessus de la baseline 7j.
6) Résultat client
Après 4 semaines de prod :
- 21% semantic cache hit.
- 92% prompt cache hit (system + tools).
- 58% trafic sur Haiku 4.5, 33% Sonnet 4.6, 9% Opus 4.8.
- Facture LLM mensuelle : 1180 €/mois (vs 6600 €/mois avant).
- Économie annuelle : 65 k€.
- Mission facturée : 22 k€ + bonus 8 k€ au déblocage du seuil <20 k€/an. ROI 8 mois.
🎯 Patterns courants
- Prompt caching first : c'est le gain n°1, quasi zéro code, juste un flag
cache_control. Toujours commencer par ça. - Cache les gros blocs stables (min 4096 tok sur Opus 4.8 / Haiku 4.5, 2048 sur Sonnet 4.6). System + tools sont parfaits car ils rendent avant les messages.
- TTL court (5 min) par défaut : suffit pour la rafale d'utilisateurs simultanés. Passe à
ttl: "1h"seulement si le trafic est en bursts espacés (le write coûte alors 2× : il faut ≥3 reads pour être rentable, vs ≥2 reads en 5 min). - Pré-warm le cache au démarrage (
max_tokens: 0avec lecache_controlsur le prefix partagé) si la latence de la 1ère requête est user-facing. - Multi-tier model routing : classifier en Haiku → décide qui appelle. Économise 60-80% sur les volumes.
- Semantic cache : Voyage embeddings (FR-friendly) + Redis ou Qdrant. Threshold 0.90-0.93 selon le domaine.
- Cache namespacing : par tenant ou par feature pour éviter pollution.
- Batch API : pour tout ce qui n'est pas user-facing temps réel (ingestion, summarization nocturne, reporting).
- Token budget hard cap :
max_tokensréaliste (300 si la réponse fait 300 mots, pas 4096). - Prompt compression : factoriser les tools (un méta-tool "ops" au lieu de 20 outils dispersés), réduire les exemples few-shot redondants.
- Compresser les outputs structurés : XML compact ou JSON minimal au lieu de Markdown verbeux pour le system-to-system.
- Fine-tune narrow task : pour une tâche ultra-récurrente (classifier intents), un petit modèle fine-tuné (Mistral 7B FR) battra GPT-4o-mini en coût/perf.
- Embedding cache : si tu re-embeddes les mêmes textes, cache le
(text_hash, model) -> vector(économie sèche).
🔄 Versions & écosystème 2026
- Anthropic prompt caching :
cache_control: { type: "ephemeral" }, TTL 5 min par défaut outtl: "1h". Cache read ≈ 0.1× le prix input ; cache write ≈ 1.25× (5 min) ou 2× (1h). Min cacheable selon le modèle : 4096 tok (Opus 4.8, Haiku 4.5), 2048 tok (Sonnet 4.6). Max 4 breakpoints par requête, fenêtre de lookback 20 blocs. Top-levelcache_controlsurmessages.create()auto-place le flag sur le dernier bloc cacheable (le plus simple). Vérifie viausage.cache_read_input_tokens. - Anthropic Batch API : -50% sur batch jobs (latence < 24h, souvent < 1h). Jusqu'à 100k requêtes / 256 MB par batch ; résultats dispo 29 jours.
- OpenAI prompt caching : automatique depuis 2024 sur prompts ≥1024 tokens, -50% sur cache hit. Pas besoin de flag.
- OpenAI Batch API : -50%, 24h SLA.
- Google Gemini caching : explicit cache avec TTL configurable.
- Mistral La Plateforme : prompts caching depuis fin 2025.
- Voyage embeddings (acquis par Anthropic en 2024) : excellent en FR, prix attractifs pour cache embed.
- LLMLingua-2 : compression de prompt, jusqu'à -5x tokens sans perte significative.
- GPTCache : lib open-source pour semantic cache LLM.
- Helicone / Portkey : proxies avec cache intégré, alerting coût.
Le bon défaut 2026 : prompt caching toujours, batch API quand possible, model tiering toujours, semantic cache si volume > 50k req/mois.
⚠️ Pitfalls
- Cache control mal placé : le
cache_controlcache du début (ordre de rendutools→system→messages) jusqu'au bloc flaggé inclus. Le flag doit donc être sur le DERNIER bloc stable, pas au milieu de contenu volatil. Mettre le flag après un timestamp/UUID dans le system → tout ce qui suit est non-cacheable. - Invalidateur silencieux : prompt caching est un prefix match — un seul octet qui change en tête invalide tout le reste.
datetime.now()dans le system, JSON non trié (json.dumpssanssort_keys), un set de tools qui varie par user →cache_read_input_tokens = 0sans aucune erreur. Tu crois cacher, tu paies plein pot. Diff les octets rendus de deux requêtes pour trouver le coupable. - Cache invalidation oubliée : tu changes le system prompt → tu paies un cache_write (1.25× le prix input en TTL 5 min) et perds le bénéfice tant que le hit rate ne remonte pas. Changer de modèle ou le set de tools force aussi un rebuild complet (le cache est model-scoped et tools rendent en position 0).
- Semantic cache trop laxiste : threshold 0.85 → cache hit sur des questions différentes → réponses fausses. Domaine sensible (legal, médical) → threshold ≥ 0.93 + audit.
- Cache PII multi-tenant : tu caches une réponse contenant des données d'un tenant → un autre tenant la récupère. TOUJOURS namespacer.
- Batch pour temps-réel : envoyer du user-facing dans le batch → 1h-24h latence. Use only pour offline.
- Model tiering sans eval : tu routes 60% en Haiku, puis la qualité s'effondre. Toujours eval par tier (cf chapitre 02).
- Classifier coûteux : un classifier en Opus 4.8 pour décider entre Haiku et Sonnet → tu paies plus que tu n'économises. Le classifier doit être le modèle le moins cher (Haiku 4.5), avec
max_tokensminimal (10-20) car il ne sort qu'un label. - Output max_tokens trop haut partout : tu ne paies que les tokens générés MAIS le LLM utilise parfois tout le budget. Cap réaliste par route = économie observée. (Pour un classifier : 10 ; pour une réponse de 300 mots : ~400, pas 4096.)
- Cache hit rate non monitoré : tu crois économiser, mais ton hit rate est à 3%. Toujours dashboard.
- Ignorer la dimension temporelle : les prix changent. Re-faire le math tous les 3 mois, et charger le barème depuis une config versionnée plutôt qu'en dur dans le code.
💰 Pricing / ROI client
Mission types :
- Audit FinOps LLM (1 semaine, livrable : rapport + plan d'action chiffré) : 8-12 k€.
- Refactor cost stack (router + cache + batch + prompt caching + dashboard) : 18-35 k€.
- Maintenance / tuning : 1500-2000€/j, 1-2j/mois.
Argumentaire ROI client :
- Hypothèse facture LLM 5 000 €/mois (60 k€/an).
- Économie typique 60-75% : 36-45 k€/an récurrents.
- Mission 22 k€ → payback 6-8 mois.
- Post-payback : pur ROI chaque mois (le client garde l'économie).
C'est l'argumentaire le plus facile à vendre en freelance : tu produis un chiffre avant/après en Excel, tu signes en 2 réunions.
🧪 Testing / Eval
- A/B coût/qualité : ancien stack vs nouveau, 10% de traffic chacun pendant 2 semaines, mesure CSAT + coût.
- Cache invalidation tests : changement de version du system prompt → vérifier que le cache write se fait + hit rate descend puis remonte.
- Semantic cache regression : 100 paires (q1, q2) avec ground truth "doit hitter" / "doit pas hitter". Score précision/rappel du cache.
- Stress test : monter à 5x trafic prévu → s'assurer que Redis/embed ne sature pas avant le LLM.
- Cost regression CI : à chaque PR de prompt, simule 50 requêtes, calcule coût moyen, alerte si +10%.
🔁 Quand utiliser / éviter
Utiliser tous les leviers :
- Volume > 50k req/mois.
- Budget LLM > 1 500 €/mois.
- SaaS multi-tenant.
- Workflows nocturnes (compta, ingestion docs, reporting).
Use minimal (juste prompt caching) :
- POC < 1 000 req/mois.
- One-shot scripts.
- Internal tool 5 users.
Éviter le semantic cache :
- Domaine ultra-personnalisé (chaque réponse dépend du contexte utilisateur unique).
- Données financières/médicales sans audit (risque de leak inter-tenant).
🧩 Bonus — Patterns FinOps avancés FR
A. Fine-tune narrow task (économie 80%+)
Pour une tâche ultra-récurrente (classifier intents SAV, extraire entités sur factures fournisseur), un petit modèle fine-tuné (Mistral 7B Instruct via Scaleway, ou GPT-4o-mini fine-tuned via Azure) bat un GPT-4o en coût/perf.
Calcul ROI typique :
- Coût fine-tune one-shot : 200-800 € (training).
- Coût inference par 1M tokens : ~0.10 € (vs 2.40€ GPT-4o).
- Si 50k req/mois × 2k tokens : économie ~2300 €/mois.
- Payback : < 1 mois.
À vendre comme mission spécialiste : 12-18 k€ pour le pipeline fine-tune (dataset + training + eval + déploiement).
B. Cache des descriptions d'outils MCP
Si tu utilises MCP avec 30 tools, leur description JSON pèse souvent 6-10k tokens. Tu peux :
- Les regrouper en méta-outils (
crm_opsplutôt que 8 outils CRM séparés). - Les cacher avec
cache_controlAnthropic. - Les charger conditionnellement (un user de la team compta n'a pas besoin des outils du CRM).
const toolsForUser = (role: string) => {
const tools = ALL_TOOLS.filter(t => t.allowed_roles.includes(role));
return tools.map(t => ({ ...t, cache_control: { type: "ephemeral" } }));
};C. Budget par feature, pas seulement par tenant
-- Top features cost (Grafana)
SELECT
metadata->>'feature' AS feature,
sum(total_cost) AS eur,
count(*) AS calls,
sum(total_cost) / count(*) AS avg_eur_per_call
FROM observations
WHERE type='GENERATION' AND start_time >= now() - INTERVAL 7 DAY
GROUP BY feature ORDER BY eur DESC LIMIT 20;Tu identifies que la feature "summarize_meeting" coûte 60% du budget. Tu enquêtes : 12k tokens d'input en moyenne. Tu optimises ou retire.
D. Prompt diet — audit manuel
Sur un prompt de 6000 tokens, fais ce check :
- Lignes redondantes (consignes répétées) → -15%.
- Exemples few-shot → besoin de 2 au lieu de 5 ? → -20%.
- Marqueurs de structure XML/JSON utiles ou décoratifs ? → -5%.
- Énumérations exhaustives (la liste complète des cas alors qu'on peut résumer) → -10%.
Une mission "prompt diet" de 3 jours peut économiser 30-40% du coût input sans toucher au reste.
E. Smart truncation au lieu de naive trim
Quand tu trim l'historique, ne supprime pas les premiers tours (souvent ils contiennent le contexte critique : qui est l'user, son tenant, sa langue). Use smart trim :
- Garde toujours system + 1er tour user (contexte initial).
- Garde les 3 derniers tours.
- Summarize le middle.
Économie input : -40% vs naive sliding window, avec qualité conservée.
Réflexe 2026 : avant de coder ton smart-trim à la main, regarde les primitives natives. Anthropic expose compaction (beta
compact-2026-01-12: résume server-side l'historique quand on approche la fenêtre de contexte — tu dois ré-appendresponse.contentcomplet, pas juste le texte, sinon tu perds le bloc de compaction) et context editing (purge les vieux tool results / thinking blocks). Tu n'écris du trim maison que si ces deux-là ne couvrent pas ton cas (ex. logique de rétention métier sur le 1er tour).
F. Negotiate enterprise pricing
À partir de ~5 000 €/mois facture LLM, négocie avec Anthropic ou OpenAI un enterprise contract :
- Discount 10-30% sur les prix publics.
- Burst capacity garantie.
- DPA spécifique pour la France.
Cela ne demande qu'un email + 1 call. Beaucoup de boîtes oublient.
🏭 Production : observabilité, sécurité, scale
Les leviers d'éco ci-dessus ne tiennent en prod que si tu instrumentes et que tu te protèges. Ce qu'un staff met en place avant de dire "c'est fini" :
Observabilité (sinon tu optimises à l'aveugle)
- Logger chaque
resp.usage:input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens, +model,tenant,feature. Sans ça, pas de FinOps possible. - Métrique reine : coût / requête (p50, p95, p99). La moyenne ment — un p99 à 20× la médiane = quelques requêtes qui mangent le budget.
- Cache hit rate (prompt cache ET semantic cache) en série temporelle. Une chute = invalidateur silencieux introduit par une PR. Alerte si
cache_readtombe à 0 sur un prefix censé être stable. - Trace distribuée (Langfuse / OTel) : un agent multi-tool peut faire 15 appels LLM pour une réponse. Le coût se cache dans les tours intermédiaires.
Sécurité & multi-tenant
- Semantic cache = surface de fuite inter-tenant. Une réponse cachée contenant la donnée du tenant A servie au tenant B = incident RGPD. Namespacer par tenant, ou ne partager que les réponses génériques (avec un flag opt-out par client).
- PII dans les embeddings : l'embedding d'une question peut contenir de la PII. Chiffrer au repos, TTL court, et purge par tag lors d'une demande de suppression.
- Le classifier ne doit jamais voir plus que nécessaire : il route, il ne traite pas. Lui passer tout l'historique = coût + surface d'attaque inutiles.
Scale & résilience
- Gérer les erreurs typées :
RateLimitError(429),OverloadedError(529),APITimeoutError→ retry avec backoff (le SDK le fait viamax_retries, défaut 2). Sous charge, Haiku est souvent moins saturé qu'Opus : un fallback de tier vers le bas est aussi un fallback de disponibilité. - Le vector store du semantic cache doit scaler avant le LLM : un lookup top-1 en
O(n)sur Redis (comme dans le code minimal) explose à 100k entrées. En prod → Qdrant / pgvector avec index HNSW. Stress-test à 5× le trafic prévu. - Concurrence et caching : N requêtes parallèles à prefix identique paient toutes le plein tarif (le cache n'est lisible qu'une fois la 1ère réponse en streaming). Pour un fan-out : envoie 1 requête, attends le 1er token, puis lance les N-1.
- Streaming pour les gros outputs : au-delà de ~16k
max_tokens, le non-streaming risque un timeout HTTP SDK. Stream +.finalMessage().
🏋️ Exercices
Progressifs, du concret au "défends ton chiffre". Fais-les avec un vrai compte API et un dashboard, pas sur papier.
Exercice 1 — Mesurer avant d'optimiser
Objectif : instrumenter un endpoint LLM et produire la distribution coût/req (p50/p95/p99) + cache hit rate sur 1000 requêtes simulées. Indice/Solution : wrappe messages.create, logge resp.usage dans une table (input, output, cache_creation, cache_read, model). Calcule coût = priceAnthropicEur(usage). Trace l'histogramme. Vérifie que input + cache_creation + cache_read = taille prompt réelle — si ton p95 surprend, c'est presque toujours un prompt qui n'est pas caché.
Exercice 2 — Prompt caching et chasse à l'invalidateur
Objectif : passer un system prompt de 5k tokens en cache_control, atteindre un hit rate > 80%, puis casser volontairement le cache et le prouver. Indice/Solution : place le flag sur le dernier bloc system. Envoie 10 requêtes identiques → cache_read_input_tokens doit être > 0 dès la 2ème. Puis injecte new Date().toISOString() en tête du system → observe cache_read retomber à 0 sans aucune erreur. Diff les octets rendus des deux prompts pour localiser l'octet coupable. C'est *l'*exercice qui forme l'intuition prefix-match.
Exercice 3 — Router multi-tier avec eval par tier
Objectif : construire le router Haiku→Sonnet→Opus, router 60% du trafic en bas, et prouver chiffres à l'appui que la qualité ne baisse pas. Indice/Solution : classifier Haiku 4.5 (max_tokens: 10). Crée un golden set de 100 questions étiquetées par difficulté + réponse attendue. Mesure accuracy par tier avant/après tiering. Le piège : si le classifier se trompe et envoie du hard en Haiku, l'accuracy globale s'effondre — montre la matrice de confusion du classifier, pas juste l'accuracy moyenne.
Exercice 4 — Casse-le puis répare-le : semantic cache empoisonné
Objectif : implémenter un semantic cache, le régler à un seuil trop laxiste (0.83) pour provoquer des faux positifs, mesurer le taux de réponses fausses, puis trouver le bon seuil. Indice/Solution : jeu de 100 paires (q1, q2) avec ground truth "doit hitter / doit pas hitter". À 0.83 tu auras des hits entre questions différentes ("TVA sur ventes" vs "TVA sur achats") → réponses fausses. Trace précision/rappel du cache en fonction du seuil (0.80 → 0.95). Le bon seuil maximise le rappel sous contrainte de précision = 1.0 sur le domaine sensible. Ajoute le namespacing par tenant et prouve qu'une réponse du tenant A ne fuit pas vers B.
Exercice 5 — Rendre le tout production-grade
Objectif : prendre le CostRouter du chapitre et le durcir : retry typé, fallback de disponibilité, vector store scalable, dashboard FinOps + alerte. Indice/Solution : (a) catch RateLimitError/OverloadedError/APITimeoutError, retry avec backoff, fallback vers Haiku quand Opus est overloaded (dispo > qualité ponctuellement). (b) remplace le lookup O(n) Redis par pgvector/Qdrant HNSW, stress-test à 5×. (c) expose 4 panneaux (coût/jour/modèle, coût/tenant top 20, cache hit rate, coût/req p50/p95/p99) + alerte si coût/req > baseline 7j × 1.5. Bonus : cost-regression CI qui simule 50 requêtes par PR de prompt et bloque si +10%.
Exercice 6 — Défends le chiffre (l'exercice de closing)
Objectif : produire le tableau avant/après chiffré d'un refactor (ex. 78 k€/an → 15 k€/an) décomposé terme par terme (input, cache_read, output, par modèle), et le défendre face aux objections d'un CTO sceptique. Indice/Solution : reprends la décomposition coût = input×prix_in + cache_read×0.1×prix_in + output×prix_out, divisé par 2 si batch. Pour chaque levier, isole sa contribution €. Anticipe les objections : "et si le hit rate baisse ?" (montre l'alerte), "et la qualité ?" (montre l'eval par tier), "et si les prix changent ?" (barème en config, math re-fait tous les 3 mois). Un chiffre qu'on sait décomposer et défendre ferme la mission ; un chiffre rond sorti d'un slide, non.
🎤 En entretien
Q : "Comment réduire la facture LLM d'un agent en prod sans dégrader la qualité ?" R : J'attaque par ordre de ROI/effort : prompt caching d'abord (gain immédiat, zéro risque qualité), puis model tiering avec eval par tier, puis batch pour tout l'offline, semantic cache si le volume et la répétitivité le justifient. Et je mesure resp.usage avant/après — on n'optimise pas un nombre qu'on n'a pas instrumenté.
Q : "Mon prompt caching ne marche pas — cache_read_input_tokens reste à 0. Pourquoi ?" R : Prefix match cassé par un invalidateur silencieux : un datetime.now() ou un UUID en tête du system, un JSON non trié, ou un set de tools qui varie par requête. Tout changement d'octet dans le préfixe invalide tout ce qui suit. Je diff les octets rendus de deux requêtes pour localiser le coupable, et je vérifie aussi que le prefix dépasse le seuil min (4096 tok sur Opus 4.8).
Q : "Quand le semantic cache est-il dangereux, et comment le sécuriser ?" R : Deux risques. Faux positifs (seuil trop bas → on sert la réponse d'une question proche mais différente, surtout grave en legal/médical → seuil ≥ 0.93 + audit). Et fuite inter-tenant (une réponse cachée d'un tenant servie à un autre = incident RGPD → namespacing par tenant, partage limité aux réponses génériques, TTL court, purge par tag).
Q : "Comment choisis-tu entre tiering, fine-tuning et compression pour une tâche récurrente ?" R : Fonction du volume et de la spécificité. Tiering = ROI immédiat, faible effort, pour du trafic de difficulté hétérogène. Compression = quand le prompt est gras. Fine-tune narrow = seulement pour UNE tâche ultra-récurrente à volume massif, parce que le coût total inclut dataset + train + eval + maintenance/drift — le payback peut être < 1 mois mais le coût caché est la maintenance. En doute, je commence par tiering+caching et je ne fine-tune que si le chiffre le justifie.
🔗 Liens
- Anthropic Prompt Caching : https://docs.anthropic.com/claude/docs/prompt-caching
- Anthropic Batch API : https://docs.anthropic.com/claude/docs/batch-api
- OpenAI Batch API : https://platform.openai.com/docs/guides/batch
- LLMLingua-2 : https://github.com/microsoft/LLMLingua
- GPTCache : https://github.com/zilliztech/GPTCache
- Voyage AI : https://docs.voyageai.com
- Portkey AI gateway : https://portkey.ai
- Helicone : https://docs.helicone.ai
- FinOps Foundation (LLM) : https://www.finops.org/articles/finops-for-ai
- Mistral fine-tuning : https://docs.mistral.ai/capabilities/finetuning
- OpenAI fine-tuning : https://platform.openai.com/docs/guides/fine-tuning
- Article FR : "FinOps LLM 2026" (Octo Talks)