Méthode d’évaluation d’un RAG

Author
Affiliation

Saint-Clair Chabert-Liddell

Published

May 16, 2024

Keywords

IA Générative, RAG, Évaluation

Le RAG (Retrieval Augmented Generation) est une technique qui améliore la génération de texte en combinant l’intelligence artificielle avec une base de connaissances externe. Avant de produire une réponse, le système RAG consulte cette base pour récupérer des informations pertinentes sur le sujet demandé. Cette approche permet d’obtenir des réponses plus précises et mieux informées, en s’appuyant sur des données à jour plutôt que sur les seules connaissances préalables du modèle. Nous expliquons dans ce document comment évaluer ces systèmes.

Étapes principales

La méthode d’évaluation repose sur le framework Ragas https://docs.ragas.io/en/latest/index.html. Elle est composée des étapes suivantes :

  • Se procurer ou simuler un ensemble de test
  • Appliquer son RAG pipeline sur les questions de l’ensemble de test
  • Appliquer les métriques d’évaluation à son ensemble de test

En cas d’amélioration des métriques d’évaluation d’un nouveau modèle, les résultats de celui-ci doivent toujours être vérifiés par un humain avant le passage en production.

Ensemble de test

Les métriques d’évaluation sont réparties en deux groupes.

Ensemble de test expert humain

Vous avez obtenu un ensemble d’au moins 50 questions/réponses (on appellera ces réponses ground truth) d’un expert du sujet portant sur le contenu de la base documentaire. Si cet ensemble de test ne comporte que des questions, vous ne pourrez pas utiliser toutes les métriques d’évaluation et vous devrez simuler un ensemble de test complet.

Simuler un ensemble de test expert LLM

Pour simuler un ensemble de test synthétique, il vous faut installer le package ragas. Cette note est valide pour la version du package 0.1.7. Vous aurez également besoin de langchain pour convertir l’ensemble de votre base documentaire sous la forme de LangchainDocument.

poetry add ragas==0.1.7

Pour générer un ensemble de question/contexte/réponse en français, vous aurez besoin d’adapter les prompts de génération en téléchargeant le dossier des prompts traduits, qui est à mettre dans le même dossier que votre scripte/notebook. N’hésitez pas à changer les modèles LLM ou d’embeddings suivant les contraintes spécifiques à votre projet (confidentialité…)

Code
from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context, conditional
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

generator_llm = ChatOpenAI(model="gpt-3.5-turbo-16k")
critic_llm = ChatOpenAI(model="gpt-4o")
embedding_model = "text-embedding-3-large"

cache_dir = '.ragas_fr' # A commenter si la langue est l'anglais
language = "french" # A commenter si la langue est l'anglais

def build_testset(
    documents = List[Langchaindocs], 
    generator_llm: ChatOpenAI,
    critic_llm: ChatOpenAI,
    chunk_size: int,
    embedding_model:str,
    distributions: dict[str, int], 
    test_size: int, 
    language: str, 
    cache_dir = str) -> Dataset:
    
    embeddings = OpenAIEmbeddings(
        model=embedding_model,
        chunk_size=chunk_size,
    )
    generator = TestsetGenerator.from_langchain(
        generator_llm, 
        critic_llm, 
        embeddings, chunk_size=chunk_size)

    generator.adapt(language, evolutions=[simple, reasoning,conditional,multi_context], cache_dir=cache_dir) # A commenter si la langue est l'anglais

    testset = generator.generate_with_langchain_docs(
        documents,
        test_size=test_size,
        distributions=distributions,
        raise_exceptions=False,
        with_debugging_logs=False,
    )
    return testset

Générer ensuite un ensemble de test de taille 30 pour chacun des 4 types de questions :

  • simple : la réponse à la question est directement récupérable dans le contexte,
  • reasoning : la réponse nécessite de faire un raisonnement à partir de l’information du contexte,
  • conditional : la réponse est conditionnée à une autre information du contexte,
  • multi-context : Le contexte est la concaténation de deux contextes différents.

Choisissez un chunk_size grand si possible (en nombre de tokens), l’extraction de document faite au préalable vous donnera beaucoup de chunks de tailles plus petites, puis sauvegarder les données sur le disque.

Code
evol_names = {simple: 'simple', 
        reasoning: 'reasoning', 
        multi_context: 'multi_context', 
        conditional: 'conditional'}
test_size = 30
chunk_size = 2048

for evol in [simple, reasoning, multi_context, conditional]:
    distributions={
        evol: 1
    }
    testset = build_testset(documents, chunk_size, distributions, test_size, language, cache_dir)
    testset.to_dataset().save_to_disk(f'my_save_folder/testset_{chunk_size}_{evol_names[evol]}')

Vous pouvez lire et mettre sous forme de dataframe votre ensemble de test de la manière suivante :

Code
from datasets import load_from_disk
from datasets import concatenate_datasets

testsets = [load_from_disk('./my_save_folder/'+path) for path in os.listdir("./my_save_folder/")]

testset = concatenate_datasets([ts for ts in testsets])
test_df = testset.to_pandas()

Vérifier manuellement et éventuellement filtrer les réponses générées non pertinentes. Par exemple, de temps en temps, l’algorithme de génération renvoie 'nan'.

Code
index_nonan = test_df['ground_truth'] != 'nan'
test_questions = test_df['question'][index_nonan].values.tolist()
test_gt = [[item] for item in test_df["ground_truth"][index_nonan].values.tolist()]

On obtient une liste de questions et une liste de ground truth (“vraies” réponses). Il faudra effectuer une dernière phase de filtrage avec les métriques d’évaluation. Celle-ci est décrite en Section Section 3.1.1.

Évaluation

Pour l’évaluation, on se sert à nouveau de la librairie ragas (cf. Génération d’un ensemble de test). Il existe plusieurs métriques, dont l’intérêt est expliqué ci-dessous.

Code
from ragas.metrics import (
    answer_relevancy,
    faithfulness,
    context_recall,
    context_precision,
#    context_relevancy,
#    context_entity_recall,
    answer_similarity,
    answer_correctness,
    context_utilization
)

from ragas import adapt
from ragas.evaluation import evaluate
from datasets import Dataset
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

evaluation_llm = ChatOpenAI(model="gpt-4o-2024-05-13", temperature=0)
Choix du LLM d’évaluation

Pensez-bien à choisir la version du LLM d’évaluation, par exemple "gpt-4o-2024-05-13" et non "gpt-4o" afin que l’évaluation soit consistante au cours du temps.

Si votre ensemble de question/réponse est en français, alors il vous faudra changer les prompts qui sont utilisés dans le calcul des métriques d’évaluation avec la fonction adapt et le répertoire de prompt que vous aurez téléchargé.

Évaluation avec ground truth

Vous disposez d’un ensemble de tests avec questions et réponses (expert ou synthétique). Nous utilisons alors les quatre métriques suivantes :

Code
metrics_with_gt = [answer_relevancy, 
        faithfulness, 
        context_recall,       
        context_precision,
        answer_correctness]

adapt(metrics=metrics_with_gt, language="french", cache_dir='.ragas_fr')

Pour l’évaluation, il faut fournir un dataset avec quatre champs :

  • "question" - List[str] : La liste des questions de l’ensemble de test.
  • "ground_truth" - List[str] : La liste des réponses générées ou obtenues lors de la création de l’ensemble de test.
  • "answer" - List[str] : La liste des réponses générées par votre rag pipeline.
  • "contexts" - List[List[str]] : La liste des contextes (liste de chunks) retrouvés votre rag pipeline.
Code
ds_with_gt = Dataset.from_dict(
    {
        "question": test_questions,
        "answer": infered_answers, # La liste des réponses obtenu avec votre rag pipeline
        "contexts": retrieved_contexts, # La liste des contextes (liste de chunks) retrouver votre rag pipeline
        "ground_truth": [gt[0] for gt in test_gt],
    }
    )
ragas_eval_with_gt = evaluate(ds_with_gt, metrics=metrics_with_gt, llm = evaluation_llm)

ragas_eval_with_gt.to_pandas().to_json('save_folder_results_ragas/ragas_eval_with_gt.json')

Filtrage automatique du jeu de test

Si le jeu de test a été généré synthétiquement, il est important de se prémunir des éventuelles hallucinations du LLM qui a généré l’ensemble de question réponse. Pour cela on va filtrer les questions pour ne garder que celles qui ont un score parfait dans les métriques d’évaluation context_recall et faithfulness (égales à 1). Pour cela, on suppose que notre réponse answer est la ground_truth.

Code
ground_truth = [gt[0] for gt in test_gt]

ds_testset = Dataset.from_dict(
    {
        "question": test_questions,
        "answer": ground_truth, # La liste des réponses obtenu avec votre rag pipeline
        "contexts": retrieved_contexts, # La liste des contextes (liste de chunks) retrouver votre rag pipeline
        "ground_truth": ground_truth,
    }
    )
ragas_testset = evaluate(ds_with_gt, metrics=[faithfulness, context_recall], llm = evaluation_llm)

df_result_testset = ragas_testset.to_pandas()

id_faithful = (df_result_testset['faithfulness']==1) & 
                (pdf_result_testset['context_recall'] == 1)
my_ragas_results[id_faithful].to_dataset().save_to_disk(f'my_save_folder/filtered/testset_{chunk_size}_{evol_names[evol]}')

Evaluation sans ground truth

Vous disposez uniquement d’un ensemble de question de test. Il ne vous est alors pas possible d’évaluer finement la capacité de votre retriever. Nous utilisons alors les trois métriques suivantes :

Code
metrics_wo_gt = [answer_relevancy, 
        faithfulness, 
        context_utilization]

adapt(metrics=metrics_wo_gt, language="french", cache_dir='.ragas_fr')

Pour l’évaluation, il faut fournir un dataset avec trois champs :

  • "question" - List[str] : La liste des questions de l’ensemble de test.
  • "answer" - List[str] : La liste des réponses obtenues avec votre rag pipeline.
  • "contexts" - List[List[str]] : La liste des contextes (liste de chunks) retrouver votre rag pipeline.
Code
ds_wo_gt = Dataset.from_dict(
    {
        "question": test_questions,
        "answer": infered_answers, # La liste des réponses obtenu avec votre rag pipeline
        "contexts": retrieved_contexts, # La liste des contextes (liste de chunks) retrouver votre rag pipeline
    }
    )
result_with_gt = evaluate(ds_wo_gt, metrics=metrics_wo_gt, llm = evaluation_llm)

ragas_eval_with_gt.to_pandas().to_json('save_folder_results_ragas/ragas_eval_wo_gt.json')

Analyse des résultats et conseil d’amélioration

On peut analyser les fichiers que les résultats de notre protocole d’évaluation. Cette partie fournit quelques (pseudo)-codes de visualisation et des recommandations d’amélioration.

Code
df_ragas1 = pd.read_json('results_ragas/ragas_eval_with_gt1.json')
df_ragas2 = ...

res1 = df_ragas1. \
    merge(test_df, on=['question']). \
    groupby('evolution_type'). \
    mean(numeric_only=True).\
    assign(k = 2, chunk_size = 300). \
    reset_index()

res2 = ...
results = pd.concat([res1, res2,...], ignore_index=True)

Optimisation et tradeoff Précision-Recall

Le context_precision et le context_recall ne sont accessibles que dans le cas où il existe un ground_truth. Ils quantifient la qualité de notre chaine de récupération de contexte. D’une manière générale, notre objectif est de retrouver le contexte le plus petit possible (pour des raisons d’efficacité mais aussi de coût et de rapidité) qui contiennent toute l’information dont on a besoin.

  • Context recall Le rappel du contexte représente la proportion de fait présent dans le ground truth présent dans le contexte récupéré. Avoir un rappel élevé (proche de 1) est indispensable pour pouvoir générer une réponse exhaustive.
  • Context precision La précision du contexte évalue la position dans le contexte des éléments utiles pour pouvoir retrouver le ground truth. Avoir une précision élevée (proche de 1) facilite la tâche du modèle de langage, mais n’est pas nécessaire, ni ne garantis, d’obtenir une réponse correcte.
Optimisation precision-recall

Dans un premier temps, optimiser votre recall (au moins 0.9). Pour cela, augmenter la taille de votre contexte, en prenant des chunks plus grand ou bien en augmentant le k du top_k.

En augmentant le recall, on fait face à un trade-off precision-recall. Essayer de déterminer le point où le rappel est suffisamment élevé, mais où la précision n’a pas encore trop diminué.

Mauvais recall

Si le rappel reste faible ou si le tradeoff precision/recall est trop fort, il faut revoir toute la chaine de retrieval du rag. Adapter des stratégies plus complexes. N’hésitez pas à contacter la team R&D de Polynom !

Reproductibilité

Le calcul du recall nécessite qu’un LLM subdivise une réponse en fait unitaire. Cela implique une forte variance dans la métrique et un problème de reproductibilité. Il ne faut donc pas surinterpréter des petites différences.

Code
px.scatter(results, 'context_recall', 'context_precision', 
           color = 'evolution_type', facet_col= 'k', facet_row='chunk_size')

Trade off precision-recall, dans ce cas k = 10 et un chunk_size de 300 tokens parait un bon choix. Remarquons que les questions multi-contexte (en rouge) ont un rappel plus faible.

Amélioration de la faithfulness et de l’answer relevancy/correctness

  • Answer correctness Une réponse générée par notre RAG est correcte, si la réponse jugée proche du ground truth par un modèle de langage.
  • Faithfulness Juge à quel point les informations contenues dans la réponse du système de conversation sont fidèles au contexte retrouvé.
  • Answer relevancy Juge la capacité à retrouver la question originale à partir de la réponse et du contexte.
Interprétation d’answer relevancy

Par construction de l’embedding utilisé par la métrique, elle sera toujours élevée (au moins 0.6). De plus, la moyenne dépend beaucoup du champ applicatif. À n’utiliser que de manière relative !

Avec ground truth

Amélioration des métriques

Vous avez obtenu un recall élevé. Si vous avez également une bonne précision, alors votre chaine de retrieval est bonne. Essayez d’améliorer les prompts/prompt instructions.

Si votre contexte est très long et/ou que votre précision est mauvaise, il vous faudra probablement un LLM plus puissant pour générer la réponse.

Hallucinantion

Si vous avez un faible recall (et une mauvaise faithfulness) mais une bonne relevancy, alors votre LLM hallucine !

Code
px.scatter(results, 'faithfulness', 'answer_relevancy', 
           color = 'evolution_type', facet_col= 'k', facet_row='chunk_size')

Pour chunk size = 300 et k = 10 et pour chunk size = 128 et k = 10 ou 20, on a un très bon score. Pour un contexte plus petit, le recall était trop faible et on a une mauvaisse faithfulness pour les questions multi-contextes. Pour la vignette en haut à droite, la précision chute et la valeur des métriques représentés chutent également.
Answer correctness et answer relevancy

Attention avec la métrique d’answer correctness. Elle pénalise beaucoup les réponses réponses plus détaillées que la ground truth, cependant des études (https://tech.beatrust.com/entry/2024/05/02/RAG_Evaluation%3A_Assessing_the_Usefulness_of_Ragas) montrent que cette métrique est très corrélée avec de l’interprétation humaine quand on utilise un LLM puissant (gpt4) et qu’il vaut mieux l’utiliser à la place de l’answer relevancy.

Regardez des exemples à la main si ces deux métriques aboutissent à des conclusions différentes.

Code
res_prompt = pd.concat([res_prompt1.assign(prompt= 'tlo'),
            res_prompt2.assign(prompt= 'custom')], ignore_index=True)

px.scatter(res_prompt, x='faithfulness', y= 'answer_relevancy',
                  color = 'evolution_type', symbol='prompt', 
                    range_y=[0.8,1], range_x = [0.7,1])\
.update_traces(marker={'size': 15})

Un prompt instruction adapté améliore la faithfulness et l’answer relevancy (losange vs point)
Code
res_llm = pd.concat([res_llm_new.assign(prompt= 'gpt-3.5-new'),
            res_llm_old.assign(prompt= 'gpt-3.5-old')], ignore_index=True)

px.scatter(res_prompt, x='faithfulness', y= 'answer_relevancy',
                  color = 'evolution_type', symbol='llm', 
                    range_y=[0.8,1], range_x = [0.7,1])\
.update_traces(marker={'size': 15})

Un LLM plus puissant améliore la faithfulness et l’answer relevancy. Ici, une ancienne version de gpt-3.5-turbo, contre la dernière version au 15 mai 2024.

Sans ground truth

Lorsque l’on ne dispose pas de ground truth, on se base sur la triade de métrique suivante :

  • Faithfulness Juge à quel point les informations contenus dans la réponse du système de conversation sont fidèles au contexte retrouvé.
  • Answer relevancy Juge la capacité à retrouver la question originale à partir de la réponse et du contexte.
  • Context utilization L’équivalent de la précision, mais sans ground truth.

Il est beaucoup plus dur de cibler spécifiquement la chaine de retrieval, mais ces trois métriques peuvent être utilisées pour juger globalement la qualité de votre RAG sur un ensemble de questions communes (type extraction d’information par exemple).

Conclusion

Nous avons présenté des méthodes qui permettent d’évaluer la partie, récupération et génération d’une application RAG. Des études numériques ont permis de se forger une intuition de l’impact des différents éléments d’une chaîne de RAG simple sur les métriques. Bien entendu, des méthodes de RAG plus avancées permettent d’améliorer davantage les résultats.

Découvrez comment motre expertise en RAG peut transformer votre entreprise

Vous êtes intrigué par les possibilités offertes par le système RAG et souhaitez en savoir plus sur la façon dont cette technologie peut bénéficier à votre organisation ? Notre équipe d’experts est là pour vous accompagner à chaque étape de votre projet, de la conception à l’implémentation.

Contactez-Nous Dès Aujourd’hui !

Chez Polynom, nous sommes spécialisés dans les solutions de génération augmentée par la récupération. Que vous ayez besoin de conseils personnalisés, d’une démonstration de nos capacités ou d’un devis détaillé, nous sommes prêts à vous aider.

📧 Email : []

🌐 Site Web : [https://polynom.io]

Cliquez ici pour planifier une consultation gratuite avec l’un de nos experts et découvrez comment nous pouvons vous aider à tirer le meilleur parti de la technologie RAG.