Évaluer un pipeline RAG
Mis à jour le 29 juillet 2026
Objectifs
- Évaluer séparément le retrieval et la génération
- Implémenter des métriques automatiques de qualité RAG
- Construire un framework d’évaluation complet
Les deux axes d’évaluation
Un pipeline RAG échoue de deux façons très différentes, et les confondre vous fera perdre des semaines. Soit le retrieval n’a pas trouvé les bons documents — la réponse est alors mauvaise pour une raison qui n’a rien à voir avec le modèle de génération, et ce sont les métriques de la leçon 10 qui le diagnostiquent. Soit les bons documents étaient bien là et c’est la génération qui a dérapé : le modèle a inventé un détail, ou a répondu à côté de la question posée.
Un utilisateur qui remonte « la réponse est fausse » ne vous dit pas laquelle des deux causes est en jeu. D’où la règle : on mesure les deux composants indépendamment, avec des métriques distinctes, et on ne touche à l’un qu’après avoir su lequel décrochait.
Métriques de génération
La fidélité (faithfulness) répond à la question qui inquiète le plus en production : la réponse est-elle entièrement soutenue par les documents fournis, ou le modèle a-t-il ajouté du sien ? Plutôt que de demander un score global — que le juge accorde trop généreusement —, on lui fait décomposer la réponse affirmation par affirmation et classer chacune comme supportée, non supportée ou contredite. Cette dernière catégorie est la plus précieuse : une affirmation qui contredit la source signale une erreur bien plus grave qu’une simple omission.
from openai import OpenAI
client = OpenAI()
def evaluer_fidelite(
question: str,
reponse: str,
contextes: list[str]
) -> dict:
"""Évalue si la réponse est fidèle aux contextes."""
contexte_complet = "\n\n---\n\n".join(contextes)
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Évalue la fidélité de cette réponse par rapport aux "
f"documents sources.\n\n"
f"Documents sources :\n{contexte_complet}\n\n"
f"Question : {question}\n\n"
f"Réponse : {reponse}\n\n"
f"Pour chaque affirmation dans la réponse, indique :\n"
f"- SUPPORTÉE : si elle est confirmée par les documents\n"
f"- NON SUPPORTÉE : si elle n'apparaît pas dans les documents\n"
f"- CONTREDITE : si elle contredit les documents\n\n"
f"Termine par un score global de 0 à 1."
)
}],
temperature=0
)
return {"évaluation": response.choices[0].message.content}
Une réponse peut être parfaitement fidèle et pourtant inutile : elle recopie fidèlement un passage qui traite d’autre chose. C’est ce que mesure la pertinence de la réponse (answer relevance), en confrontant cette fois la réponse à la question seule, sans les documents. Le prompt fixe une échelle explicite — 1,0 pour une réponse complète, 0,7 pour une réponse partielle mais utile, 0,3 pour du tangentiel, 0,0 pour du hors sujet —, car un juge à qui l’on demande simplement « note de 0 à 1 » produit des scores instables d’une exécution à l’autre.
def evaluer_pertinence(question: str, reponse: str) -> float:
"""Score de pertinence de la réponse par rapport à la question."""
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Sur une échelle de 0 à 1, à quel point cette réponse "
f"répond-elle à la question posée ?\n\n"
f"Question : {question}\n"
f"Réponse : {reponse}\n\n"
f"Critères :\n"
f"- 1.0 : réponse complète et précise\n"
f"- 0.7 : réponse partielle mais utile\n"
f"- 0.3 : tangentiellement liée\n"
f"- 0.0 : hors sujet\n\n"
f"Retourne uniquement le score numérique."
)
}],
temperature=0,
max_tokens=10
)
try:
return float(response.choices[0].message.content.strip())
except ValueError:
return 0.0
Reste la context precision, qui fait le pont entre les deux axes : parmi les cinq documents que vous avez injectés dans le prompt, combien ont réellement servi ? Si la réponse à une question sur les congés n’a exploité qu’un seul des cinq chunks, les quatre autres ont consommé du budget de contexte et ajouté du bruit. On interroge donc le juge sur chaque contexte pris isolément, par un simple oui ou non, et l’on calcule la proportion d’utiles.
def evaluer_precision_contexte(
question: str,
contextes: list[str],
reponse: str
) -> dict:
"""Évalue quels contextes ont été utiles pour la réponse."""
resultats = []
for i, ctx in enumerate(contextes):
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Ce document a-t-il été utile pour répondre "
f"à la question ?\n\n"
f"Question : {question}\n"
f"Document : {ctx[:500]}\n"
f"Réponse générée : {reponse}\n\n"
f"Réponds par OUI ou NON."
)
}],
temperature=0,
max_tokens=5
)
utile = "oui" in response.choices[0].message.content.lower()
resultats.append({"index": i, "utile": utile})
nb_utiles = sum(1 for r in resultats if r["utile"])
return {
"details": resultats,
"precision": nb_utiles / len(contextes) if contextes else 0
}
Framework d’évaluation complet
Ces trois fonctions ne servent à rien tant qu’elles restent manuelles. Le pas décisif consiste à les faire tourner sur un jeu de questions figé, à chaque modification du pipeline : c’est ce qui transforme un ressenti — « je trouve que c’est mieux depuis que j’ai changé le chunking » — en un chiffre comparable. La classe ci-dessous exécute le pipeline sur chaque item du dataset, collecte les scores et rend leurs moyennes.
Regardez l’exemple de dataset : deux questions RH, avec pour chacune la réponse attendue et l’identifiant des documents qui devraient être retrouvés. Vingt à cinquante entrées de ce type, écrites une bonne fois à partir de vraies questions d’utilisateurs, suffisent à détecter les régressions.
class EvaluateurRAG:
def __init__(self, pipeline_rag):
self.pipeline = pipeline_rag
self.client = OpenAI()
def evaluer_dataset(self, dataset: list[dict]) -> dict:
"""Évalue le pipeline sur un dataset de questions-réponses.
Chaque item : {question, reponse_attendue, docs_pertinents}
"""
resultats = {
"fidelite": [],
"pertinence": [],
"precision_contexte": [],
}
for item in dataset:
# Exécuter le pipeline
output = self.pipeline.repondre(item["question"])
reponse = output["reponse"]
contextes = [s["extrait"] for s in output["sources"]]
# Évaluer
pertinence = evaluer_pertinence(item["question"], reponse)
resultats["pertinence"].append(pertinence)
ctx_eval = evaluer_precision_contexte(
item["question"], contextes, reponse
)
resultats["precision_contexte"].append(ctx_eval["precision"])
# Moyennes
import numpy as np
return {
metrique: round(float(np.mean(scores)), 4)
for metrique, scores in resultats.items()
}
# Utilisation
dataset = [
{
"question": "Combien de jours de congés ai-je ?",
"reponse_attendue": "25 jours de congés payés par an",
"docs_pertinents": ["politique-conges"]
},
{
"question": "Puis-je télétravailler le lundi ?",
"reponse_attendue": "Oui, le télétravail est possible sauf mardi et jeudi",
"docs_pertinents": ["politique-teletravail"]
},
]
evaluateur = EvaluateurRAG(pipeline_rag)
metriques = evaluateur.evaluer_dataset(dataset)
print("Résultats :")
for k, v in metriques.items():
print(f" {k}: {v}")
Tableau de bord des métriques
Voici les cibles à viser sur un système en production, avec ce que chaque nombre signifie réellement.
| Métrique | Cible | Ce qu’elle mesure |
|---|---|---|
| Recall@5 | > 0.90 | Les bons docs sont-ils dans le top 5 ? |
| Precision@5 | > 0.70 | Les docs retournés sont-ils pertinents ? |
| Faithfulness | > 0.85 | La réponse est-elle fidèle aux sources ? |
| Answer Relevance | > 0.80 | La réponse répond-elle à la question ? |
| Context Precision | > 0.60 | Les contextes fournis sont-ils utiles ? |
Lisez ce tableau de haut en bas comme un arbre de diagnostic. Un recall en dessous de 0,90 rend inutile toute analyse des lignes suivantes : corrigez d’abord le chunking ou l’index. En revanche, un recall excellent accompagné d’une fidélité médiocre désigne le prompt de génération, pas la recherche.
Résumé
- Évaluez séparément le retrieval (recall, precision) et la génération (fidélité, pertinence)
- La fidélité détecte les hallucinations du LLM
- La pertinence vérifie que la réponse est utile
- Construisez un dataset de test et mesurez avant chaque changement