- 01Sans mesure, un système LLM en prod est une boîte noire : coût, latence et échecs invisibles
- 02Logguer par appel : modèle, tokens, coût estimé, durée, succès/échec, tâche
- 03Le coût se calcule depuis l'usage tokens renvoyé par l'API × le tarif du modèle
Un système LLM en production sans instrumentation, c'est une boîte noire : tu ne sais pas ce qu'il te coûte, s'il ralentit, ni quand il commence à échouer. À la fin de ce tuto, tu auras une couche d'observabilité simple qui trace chaque appel — coût, latence, tokens, statut — et te permet de repérer une dérive. Exemple en TypeScript + SQLite, adaptable à n'importe quel stockage.
Étape 1 — Décider quoi mesurer
Pour chaque appel LLM, on veut au minimum :
- Modèle et tâche (pour ventiler les coûts).
- Tokens entrée / sortie (source du coût).
- Coût estimé en euros.
- Latence en millisecondes.
- Statut : succès, ou type d'erreur.
Étape 2 — Une table de traces
CREATE TABLE IF NOT EXISTS llm_traces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL,
task TEXT NOT NULL,
model TEXT NOT NULL,
tokens_in INTEGER,
tokens_out INTEGER,
cost_eur REAL,
latency_ms INTEGER,
status TEXT NOT NULL -- 'ok' | 'error:rate_limit' | 'error:parse' ...
);
Étape 3 — Envelopper l'appel
Enveloppe ton callLLM centralisé (voir le tuto sur le routage LLM) dans une fonction qui mesure et enregistre :
// Tarifs indicatifs en € par million de tokens — à mettre à jour selon ton fournisseur.
const PRIX: Record<string, { in: number; out: number }> = {
"mistral-small-latest": { in: 0.2, out: 0.6 },
"claude-haiku-4-5-20251001": { in: 0.9, out: 4.5 },
};
function coutEur(model: string, tin: number, tout: number): number {
const p = PRIX[model] ?? { in: 0, out: 0 };
return (tin * p.in + tout * p.out) / 1_000_000;
}
async function traced(task: string, model: string, run: () => Promise<any>) {
const t0 = Date.now();
try {
const res = await run();
const u = res.usage ?? {}; // l'API renvoie prompt_tokens / completion_tokens
db.prepare(
`INSERT INTO llm_traces (ts, task, model, tokens_in, tokens_out, cost_eur, latency_ms, status)
VALUES (?, ?, ?, ?, ?, ?, ?, 'ok')`
).run(
new Date().toISOString(), task, model,
u.prompt_tokens ?? null, u.completion_tokens ?? null,
coutEur(model, u.prompt_tokens ?? 0, u.completion_tokens ?? 0),
Date.now() - t0
);
return res;
} catch (err: any) {
db.prepare(
`INSERT INTO llm_traces (ts, task, model, latency_ms, status)
VALUES (?, ?, ?, ?, ?)`
).run(new Date().toISOString(), task, model, Date.now() - t0, `error:${err.code ?? "unknown"}`);
throw err;
}
}
Le coût vient de l'usage réel renvoyé par l'API, pas d'une estimation à la louche.
Étape 4 — Lire les tendances
Quelques requêtes te donnent l'essentiel :
-- Coût par jour et par tâche
SELECT substr(ts,1,10) AS jour, task, ROUND(SUM(cost_eur),3) AS eur
FROM llm_traces GROUP BY jour, task ORDER BY jour DESC;
-- Latence médiane approx. et taux d'échec sur 24h
SELECT model,
AVG(latency_ms) AS latence_moy,
AVG(CASE WHEN status = 'ok' THEN 0 ELSE 1 END) AS taux_echec
FROM llm_traces WHERE ts >= datetime('now','-1 day') GROUP BY model;
Un pic de taux_echec ou de latence te prévient d'une régression avant les plaintes utilisateurs.
Adapter à ton cas
- Alerting : un cron qui lit ces requêtes et t'envoie un mail si le coût du jour ou le taux d'échec dépasse un seuil.
- Dashboard : branche ces requêtes sur une page admin avec un graphe par jour.
- Traçage fin : ajoute un
trace_idpour relier plusieurs appels d'une même requête utilisateur (utile pour les agents multi-étapes). - Outil dédié : pour un gros volume, des plateformes spécialisées (type LangSmith/Langfuse) font ça clé en main — mais la table maison suffit longtemps.
En cas de souci
usageabsent : en streaming, certains fournisseurs ne renvoient l'usage qu'à la fin (dernier événement) — récupère-le là, ou estime à partir de la longueur.- Coût faux : tes tarifs
PRIXsont périmés. Centralise-les et tiens-les à jour ; c'est la seule partie qui bouge. - La table grossit : ajoute une purge (garde 90 jours) comme pour n'importe quel log.
- Le logging ralentit les appels : écris la trace de façon non bloquante si besoin, mais un INSERT SQLite local est négligeable devant la latence réseau d'un LLM.
Articles liés
Mettre un RAG en production : chunking, embeddings et reranking
Les leviers qui font passer un RAG du prototype au système fiable : chunking structuré, recherche hybride vecteurs + mots-clés, reranking, et mesure du retrieval. Avec le code des étapes clés et les métriques à suivre.
Fine-tuning, RAG ou prompting : quel levier pour adapter un modèle
Une méthode reproductible pour choisir entre prompting, RAG et fine-tuning : diagnostiquer le vrai problème, puis appliquer le bon levier sur un cas concret.
Router entre plusieurs LLM selon la tâche et le coût
Diriger chaque appel vers le bon modèle selon la tâche, avec bascule automatique sur un modèle de secours en cas de panne ou de crédits épuisés.