La plupart des écrits techniques sur la génération augmentée par récupération traitent le déploiement comme une destination. Vous avez livré, les evals ont passé, votre client est satisfait. Ce n'est pas à ça que ressemble la production. La production, c'est une dérive lente et souvent invisible à partir du jour où vous avez livré. Si vous ne surveillez pas le bon signal, vous ne le remarquerez qu'au moment où un client le fera.
Voici le déroulement d'un mode de défaillance contre lequel nous concevons : à quoi ressemble une dérive d'index trois semaines après une remise, comment elle apparaît dans les métriques, et les changements que cela a imposés à notre harnais d'evals standard pour que cette catégorie d'échec remonte au premier jour, pas à la troisième semaine.
Le contexte
Prenons un déploiement représentatif : une stack de support client pour une fintech de taille moyenne, environ 8 000 employés, 1,2 million de chunks de documents de support, en anglais et en français, avec un pic de charge autour de 240 QPS. Sept agents dans le graphe d'orchestration (recherche, classificateur, planificateur, exécuteur, deux spécialistes et un nœud de garde-fou). Le retrieval était hybride (BM25 plus dense) contre un cluster Qdrant, avec un re-ranker par-dessus.
Au jour 0, nous avons exécuté le harnais d'evals complet : 412 questions sur 14 catégories, scorées contre des réponses de référence figées par un LLM-as-judge avec des vérifications humaines ponctuelles. La satisfaction client (CSAT) du pilote en direct suivait nos scores hors ligne à 1,5 point près. Tout le monde s'accordait à dire que ça fonctionnait.
Le signal
Trois semaines plus tard, le tableau de bord analytique du client a signalé une baisse de CSAT de 88 à 84. Ça ressemblait d'abord à du bruit (un écart de 4 points sur 1 200 conversations reste dans un intervalle de confiance raisonnable), mais le chiffre du lendemain était 83. Le surlendemain, 82. Ce n'est plus du bruit. C'est une tendance.
Nous avons extrait les traces d'orchestration pour les conversations aux CSAT les plus bas des 48 dernières heures. Les agents n'étaient pas confus. Le planificateur avait choisi le bon chemin. L'exécuteur avait appelé les bons outils. Les générations étaient fluides, conformes à la marque, polies. Elles étaient aussi fausses : citant les mauvaises politiques, reprenant des tarifs obsolètes, ignorant une ligne de produits lancée au jour 14.
Dans chaque cas, l'échec était en amont. Le retriever ne remontait aucun chunk utile, alors le générateur hallucinait quelque chose de plausible à partir de ses priors. Les agents eux-mêmes allaient bien. C'est le corpus qui avait bougé.
Le diagnostic
L'équipe de support du client avait été discrètement héroïque. Dans les trois semaines suivant la remise, ils avaient ajouté 2 401 nouveaux documents de support, retiré 188 anciens, et réécrit la politique sur deux lignes de produits. Rien de tout cela n'avait été ré-embeddé. Rien de tout cela n'était dans l'index que le retriever interrogeait.
Notre pipeline avait un job d'ingestion. Il tournait simplement sur le mauvais calendrier. Nous avions configuré un ré-embedding nocturne pour les documents delta, mais la requête d'ingestion dépendait d'une colonne last_modified que le CMS du client ne mettait à jour qu'à la création, pas à la modification. Les nouveaux docs entraient. Les docs modifiés, non. Les docs retirés restaient dans l'index. Chaque jour, discrètement, l'index devenait un peu plus faux.
Ce que la métrique aurait dû détecter
Notre harnais d'evals mesurait la qualité de génération contre des réponses de référence figées. Si le retriever remontait de mauvais chunks, la génération était quand même scorée contre ce que nous attendions au jour 0. Le juge marquait volontiers une génération « bonne » si elle correspondait à la réponse attendue du jour 0, même quand cette réponse était devenue obsolète.
| métrique | jour 0 | jour 21 | jour 35 | signal |
|---|---|---|---|---|
| qualité de génération (juge) | 0,91 | 0,90 | 0,88 | stable ✓ |
| recall retrieval @ k=10 | 0,87 | 0,71 | 0,58 | −0,29 |
| fraîcheur chunks (âge moyen) | 4 j | 18 j | 29 j | +25 j |
| taux de correspondance des citations | 0,94 | 0,74 | 0,61 | −0,33 |
| csat en direct | 88,4 | 83,9 | 81,2 | −7,2 |
La métrique basée sur le juge a été la dernière à bouger. Le recall retrieval, la fraîcheur des chunks et le taux de correspondance des citations s'étaient tous effondrés des semaines plus tôt. Nous ne les surveillions simplement pas.
La correction
Le correctif était petit. Le changement culturel, lui, ne l'était pas. Nous avons réécrit trois choses.
- Déclencheur d'ingestion. Au lieu de faire confiance à une colonne CMS, nous calculons maintenant un hash de contenu sur chaque document chaque nuit et ré-embeddons tout chunk dont le hash a changé. Plus lent. Correct.
- Evals de retrieval. Le recall@k contre un ensemble de citations de référence tenu à l'écart est maintenant une métrique de premier plan, exécutée quotidiennement contre l'index en direct. Une dérive au-delà d'un seuil page l'astreinte.
- Jauge de fraîcheur des chunks. L'âge moyen des chunks cités est maintenant dans le tableau de bord aux côtés de la latence et du coût. Si l'index ne bouge pas alors que le corpus source bouge, quelque chose ne va pas.
Voici à quoi ressemble approximativement le harnais de recall. Il tourne en cron job sur le même compute qui gère l'index, prend environ 90 secondes sur notre taille de corpus typique, et écrit une ligne par exécution dans une petite table Postgres.
from dataclasses import dataclass from retriever import Retriever from ground_truth import load_eval_set @dataclass class RecallResult: recall: float mean_age_days: float n_queries: int def eval_recall(retriever: Retriever, k: int = 10) -> RecallResult: # held-out: ~600 (query, expected_chunk_ids) pairs eval_set = load_eval_set("recall_v3") hits, ages = 0, [] for q, expected_ids in eval_set: retrieved = retriever.search(q, k=k) retrieved_ids = {c.id for c in retrieved} hits += len(retrieved_ids & expected_ids) / len(expected_ids) ages.extend(c.age_days for c in retrieved) return RecallResult( recall=hits / len(eval_set), mean_age_days=sum(ages) / len(ages), n_queries=len(eval_set), )
Deux jours après la réécriture, le recall retrieval était remonté au-dessus de 0,85, l'âge moyen des chunks était sous 6 jours, et le CSAT avait récupéré à 89. Dans un scénario comme celui-ci, les utilisateurs finaux ne voient jamais le diagnostic. Ils constatent seulement que le système s'améliore.
Ce qui va dans les evals, exactement
Nous avons maintenant standardisé cela. Chaque système agentique que nous livrons exécute au minimum ces cinq métriques chaque jour, et chacune a un seuil documenté et une page d'astreinte en cas de dépassement.
- Recall retrieval@k contre un ensemble de référence tenu à l'écart, rafraîchi trimestriellement.
- Taux de correspondance des citations. Le chunk cité contient-il réellement la réponse.
- Fraîcheur des chunks. Âge moyen des chunks récupérés, p50 et p99.
- Qualité de génération via LLM-juge, mais seulement significative quand le retrieval est sain.
- Détecteur de dérive. Divergence KL de la distribution d'embeddings de requêtes par rapport à une ligne de base.
Rien de tout cela n'est exotique. Rien de tout cela ne nécessite des articles de recherche. Si la plupart des équipes ne les exécutent pas, ce n'est pas une question de capacité. C'est que le tableau de bord construit par l'équipe de déploiement au jour 0 ne montrait que la qualité de génération, et une fois le système « en production », personne n'est revenu vérifier le tableau de bord.
Conclusions
Si vous opérez un système RAG en production, trois demandes :
- Mesurez le recall retrieval quotidiennement contre un ensemble de référence tenu à l'écart. La qualité de génération est en aval. Si vous ne surveillez que la génération, vous regardez un indicateur retardé d'un indicateur retardé.
- Calculez une jauge de fraîcheur des chunks et affichez-la à côté de la latence et du coût. Un système dont les chunks récupérés vieillissent chaque semaine est un système qui casse en silence.
-
Ne faites pas confiance au
last_modifiedde votre CMS. Calculez des hashes de contenu. Les cycles supplémentaires sont bon marché. La semaine de production dégradée, non.
La dérive silencieuse du retrieval sur ce qui ressemble à un déploiement stable est l'un des modes de défaillance les plus courants pour un système RAG qui semble sain. Faites tourner RAG en production assez longtemps et vous rencontrerez ce mode de défaillance. L'intérêt d'un exemple illustratif est de le reconnaître avant que votre CSAT ne le fasse.
Si vous souhaitez que nous examinions votre suite d'evals, le formulaire de contact est la voie la plus rapide. Nous offrons des revues gratuites de 30 minutes pour les systèmes en production.