
Observabilité d'un LLM : tokens, latence, coût, hallucinations
J’ai passé des mois à construire des applications avec des LLM sans vraiment les observer. Je savais que ça répondait, parfois que c’était lent, mais je n’avais aucune idée du coût réel par requête, ni de la répartition exacte de la latence, ni surtout de la fiabilité des réponses. Le jour où j’ai branché une vraie observabilité sur mon assistant, j’ai découvert des choses que je n’aurais jamais soupçonnées. Cet article est le fruit de cette expérience : ce qu’il faut observer dans une application qui s’appuie sur un LLM, pourquoi, et comment le faire concrètement.
Le problème avec les LLM, c’est qu’ils sont une boîte noire dans le flux. Une requête classique passe par plusieurs étapes (embedding de la question, recherche, génération, post-traitement) et chaque étape a son coût, sa latence et son taux d’échec. Sans instrumentation, vous voyez le résultat final mais aucune des causes. Et quand un LLM “ hésite “ ou répond de travers, impossible de savoir si c’est le modèle, le contexte, ou le prompt.
Ce qui rend l’observabilité d’un LLM spécifique
Avant d’entrer dans le détail, il faut comprendre pourquoi observer un LLM ne ressemble pas à observer une API classique. Une API HTTP ordinaire, vous mesurez le statut, le temps de réponse, le nombre d’appels. Pour un LLM, la notion de “ bon fonctionnement “ est plus floue.
- La réponse n’a pas de statut fiable : une réponse peut être techniquement réussie (HTTP 200) et pourtant complètement fausse, voire hallucinée. Le code ne dit rien de la qualité du contenu.
- La latence est très variable : le temps de première réponse peut être court (les premiers tokens sortent vite) mais le temps total dépend du nombre de tokens générés, lui-même imprévisible à l’avance.
- Le coût dépend de la langue : le token est l’unité de facturation, et le nombre de tokens d’un même texte varie selon la langue, le modèle et même le format (code vs prose).
- Il y a des limites de rate limiting : chaque modèle a des quotas de requêtes par minute (RPM) et de tokens par minute (TPM). Les dépasser provoque des erreurs 429 qu’il faut surveiller de près.
En résumé : une application avec LLM combine les caractéristiques d’un système distribué (latence, rate limiting) et celles d’un composant non déterministe (qualité variable). Les deux faces demandent une instrumentation spécifique.
Les quatre familles de métriques à observer
Je structure l’observabilité de mes applications LLM autour de quatre blocs, que je détaille ci-dessous : les tokens, la latence, le coût, et la qualité (hallucinations). Chacun répond à une question différente.
1. L’usage des tokens
Le token est l’unité de consommation d’un LLM. Tout ce que vous lui envoyez et tout ce qu’il produit est découpé en tokens. C’est la première chose à tracer, car c’est elle qui pilote le coût et, indirectement, la latence.
Il y a trois compteurs distincts à suivre :
- Les tokens d’entrée (prompt) : ce que vous envoyez. Cela inclut le prompt système, le contexte RAG, l’historique de conversation, la question. C’est souvent le poste le plus lourd, surtout si vous injectez beaucoup de contexte (documents, historique).
- Les tokens de sortie (completion) : ce que le modèle génère. Correspond à la réponse.
- Les tokens totaux : la somme des deux, qui est l’unité de facturation la plus courante.
La distinction entrée/sortie est cruciale. Si votre facture explose, il faut savoir si c’est parce que vous envoyez trop de contexte (tokens d’entrée) ou parce que la réponse est trop verbeuse (tokens de sortie). Les deux n’ont pas du tout le même remède.
Un exemple avec l’API OpenAI (JS) :
import OpenAI from 'openai'; |
L’objet usage est quasi systématiquement renvoyé par les API de LLM (OpenAI, Anthropic, et les serveurs locaux comme Ollama renvoient une valeur équivalente). Il ne faut jamais le jeter : c’est votre source de vérité pour le coût et l’usage.
Ce que j’ai appris en pratique : la taille du contexte d’entrée est l’élément le plus volatile. Un assistant auquel on colle un historique de 50 tours sans le tronquer voit ses tokens d’entrée exploser de façon linéaire, alors que la qualité de la réponse sature. Observer prompt_tokens dans le temps révèle immédiatement ce genre de dérive.
2. La latence
La latence d’un appel LLM n’est pas un nombre unique : elle se décompose en plusieurs phases dont les caractéristiques diffèrent radicalement.
- TTFT (Time To First Token) : le temps entre l’envoi de la requête et la réception du premier token. C’est le temps caché : il inclut la file d’attente, le traitement du prompt par le modèle (prefill), et la latence réseau. C’est lui qui domine la perception de réactivité : tant que le premier token n’arrive pas, l’utilisateur voit un écran vide.
- Le temps de génération : le temps entre le premier et le dernier token. Il dépend du nombre de tokens de sortie et de la vitesse de génération, exprimée en tokens par seconde.
- La latence totale : la somme, du premier appel à la réponse complète. C’est celle que mesure l’utilisateur.
La métrique clé à côté de ces temps bruts est le débit de génération (tokens/s). Deux modèles peuvent avoir le même TTFT mais des vitesses de génération très différentes. À tokens de sortie constant, un modèle à 30 tokens/s finira deux fois plus vite qu’un modèle à 15 tokens/s.
Mesurer la décomposition en JavaScript :
const t0 = performance.now(); |
Le TTFT dépend fortement de la longueur du prompt : plus le contexte d’entrée est long, plus le préfill est coûteux, et plus le TTFT s’allonge. Observer TTFT en fonction de prompt_tokens permet de comprendre si une lenteur perçue vient du modèle ou de votre contexte.
Je recommande de tracer systématiquement : ttft_ms, generation_ms, total_ms, et tokens_per_second. Ce sont les quatre nombres qui permettent de répondre à la question : “pourquoi c’est lent ?”.
3. Le coût
Le coût d’un appel LLM se calcule presque toujours de la même façon : un prix par million de tokens d’entrée, et un prix par million de tokens de sortie. Les tokens de sortie sont presque toujours plus chers que les tokens d’entrée (jusqu’à 4 fois plus sur certains modèles), car la génération est plus coûteuse en calcul que l’ingestion.
const PRICES = { |
Le point crucial : ne calculez jamais le coût manuellement dans un outil externe à partir de logs bruts. Calculez-le au moment de la requête et stockez le résultat, car les grilles tarifaires changent. Si vous recalculez à froid six mois plus tard avec une grille obsolète, vos chiffres seront faux. Mieux vaut figer le coût au moment de l’appel.
La métrique de coût la plus utile est le coût par requête et son agrégat par heure ou par jour. Elle permet de répondre à des questions opérationnelles directes :
- Quel est le coût mensuel estimé de mon assistant ?
- Le coût par utilisateur ou par feature est-il raisonnable ?
- Un changement de modèle (passer de
gpt-4oàgpt-4o-mini) a-t-il fait baisser la facture autant que prévu ?
J’ai été surpris de voir à quel point le coût peut être dominé par des appels discrets : des résumés de contexte, des titres auto-générés, des retitrages. Ce ne sont pas les appels centraux mais la somme de milliers de petits appels qui fait gonfler la facture. L’observabilité par coût le révèle immédiatement.
4. Les rate limits et les erreurs
Les fournisseurs de LLM imposent des quotas : limite de requêtes par minute (RPM) et de tokens par minute (TPM). Au-delà, on obtient une erreur HTTP 429 avec un header Retry-After. C’est une source classique de latence et d’échecs invisibles si on ne les compte pas.
À surveiller :
- Le nombre de 429 par minute : un indicateur que vous approchez des quotas, souvent déclenché par un pic d’usage ou un bug de boucle (un retry sans backoff peut générer des centaines de 429).
- Le taux d’utilisation du quota : (tokens consommés / quota TPM). Si vous tournez à 85 % du quota pendant les heures de pointe, vous êtes à un pic près de la panne.
- Les erreurs 5xx : côté fournisseur, moins fréquentes mais réelles ; il faut les distinguer des 429.
async function callWithRetry(createFn, { maxRetries = 3 } = {}) { |
Chaque retry doit être loggé avec son attempt et son delay : c’est ainsi qu’on découvre qu’un faux sentiment de lenteur vient en réalité de multiples retries dus à un quota approché.
Détecter et estimer les hallucinations
C’est la partie la plus délicate, car il n’existe pas de compteur hallucination fourni par l’API. Il faut construire des heuristiques. Voici les approches que j’utilise, par ordre croissant de sophistication.
Le manque de source citée
Quand un LLM doit répondre à partir d’un contexte RAG, une réponse de qualité cite ses sources. Une réponse qui n’en cite aucune, ou qui cite une source qui n’existe pas dans le contexte fourni, est un fort signal d’hallucination.
L’idée : vérifier que chaque citation dans la réponse correspond bien à une source présente dans le contexte. J’utilise une fonction de correspondance approximative :
function sourcesCited(response, contextDocs) { |
Un ratio faible (beaucoup de sources disponibles mais aucune citée) signale une réponse qui s’appuie peu sur le contexte, donc plus susceptible d’être librement inventée.
Le score de confiance
Beaucoup d’API exposent des métadonnées de confiance ou de logprobs. Sans descendre dans les probabilités par token, on peut estimer une confiance globale :
function confidenceEstimate(response) { |
Un score bas indique que le modèle hésite sur beaucoup de tokens : c’est un terrain fertile pour l’erreur et l’hallucination. Cela ne détecte pas toutes les hallucinations (une hallucination peut être dite avec une grande confiance), mais c’est un signal complémentaire utile.
L’écart de récupération (retrieval gap)
Pendant une requête RAG, je mesure l’écart entre la question et les documents retrouvés : le score de similarité moyen des morceaux récupérés. Si ce score est faible, les documents retrouvés ne correspondent pas bien à la question, donc le LLM répond avec un contexte faiblement pertinent, et l’hallucination devient plus probable.
function retrievalGap(queryVector, docVectors) { |
Ce n’est pas une détection parfaite, mais croiser ces trois signaux (absence de citation, faible confiance, mauvais contexte) donne une note de risque d’hallucination par réponse. On peut alors tracer cette note comme une métrique et se faire alerter quand elle se dégrade sur une période.
Exemple de structure de métrique de risque :
{ |
Le combined_risk est une agrégation pondérée. Je le calcule ainsi : plus on cite de sources, plus la confiance est haute, plus le contexte est pertinent, plus le risque est faible. En pratique, tracer ce nombre dans le temps révèle des dérives (une source de documents qui devient obsolète, un modèle remplacé qui cite moins) que les métriques classiques ignorent.
La structure des logs et des métriques
Une bonne instrumentation repose sur deux voies complémentaires : les logs (événements riches) et les métriques (agrégats numériques). Voici comment je les organise.
Le log de requête
Chaque appel LLM doit produire un log structuré (JSON), contenant exactement le prompt, la métadonnée, la réponse, et les coûts. C’est ce log qui permet de rejouer un problème plus tard.
{ |
Chaque champ sert à une question précise : request_id pour corréler, usage pour le coût, latency pour la performance, hallucination_risk pour la qualité, prompt/response pour le diagnostic. Ne jamais logguer le prompt complet si vous avez des données personnelles à protéger ; on peut le tronquer ou le hasher partiellement.
Les métriques agrégées
Au lieu d’explorer des millions de logs, on agrège en métriques numériques interrogeables dans le temps :
llm_tokens_total{model, direction="input"} # compteur |
Avec ces séries, une question comme “combien m’a coûté hier l’assistant sur le modèle mini ?” se répond par une requête d’agrégation en quelques secondes, sans parcourir les logs.
Réflexion finale
Ce qui m’a le plus apporté dans tout ça, c’est la capacité à expliquer une anomalie plutôt qu’à la subir. Avant, si une requête était lente, je cherchais au hasard. Maintenant, je regarde le TTFT, le nombre de tokens de contexte, les retries : la cause apparaît en dix secondes.
L’observabilité d’un LLM n’est pas un luxe : c’est ce qui transforme un appel d’API opaque en un composant compréhensible et pilotable. Les quatre piliers (tokens, latence, coût, qualité) couvrent les questions que tout le monde se pose tôt ou tard en production. Si vous commencez, je vous conseille de partir de l’usage des tokens et du coût par requête : ce sont les plus simples et les plus vite rentabilisés.
Pour aller plus loin
- Prompts et logging sélectif : stockez toujours les prompts, mais pensez à la rétention et à l’anonymisation pour les données sensibles.
- Corrélation traces : si vous utilisez OpenTelemetry, rattachez le
request_iddu LLM à la trace HTTP parente (voir mon article sur l’observabilité avec OpenTelemetry). - Évaluation automatique de la qualité : à terme, un LLM juge qui évalue les réponses sur une grille (pertinence, fidélité) peut automatiser la détection d’hallucination mieux que les heuristiques.
- Dashboards : une fois les métriques exposées, je construis un dashboard Grafana dédié pour visualiser le tout en un coup d’oeil.