Évaluer la performance RAG dans Fabric

Ce tutoriel montre comment évaluer un système de génération augmentée par récupération (RAG) dans Microsoft Fabric. L’exemple mesure deux éléments : si le récupérateur trouve le document source correct dans Recherche Azure AI et si la réponse générée reste ancrée dans le contexte récupéré. À la fin du tutoriel, vous disposez d’un workflow de benchmark répétable pour comparer les paramètres de prompt et de récupération, suivre les régressions et évaluer la qualité de la réponse avant l’utilisation en production.

Ce tutoriel se concentre sur :

  1. Configurer les services Azure OpenAI et Recherche Azure AI
  2. Chargez les données de benchmark à partir de l’ensemble de données QA de CMU pour créer un petit ensemble d’évaluation
  3. Effectuez un test rapide avec une seule requête pour confirmer que le flux RAG fonctionne de bout en bout
  4. Définir les métriques déterministes et assistées par l’IA
  5. Point de contrôle 1 : évaluer les performances du module de récupération à l’aide de la précision top-N
  6. Check-in 2 : évaluer la génération de réponses à l’aide de métriques d’ancrage factuel, de pertinence et de similarité
  7. Sauvegardez et comparez les résultats d’évaluation dans OneLake

Prerequisites

Avant de commencer, assurez-vous de disposer de ces ressources et autorisations :

  • Un espace de travail et une maison lacustre Microsoft Fabric
  • Un projet Azure AI Foundry avec une ressource OpenAI Azure et des modèles déployés de chat et d’intégration
  • Un service Recherche Azure AI avec un index vectoriel contenant le contenu intégré créé dans le tutoriel précédent
  • Valeurs des points de terminaison, clés API et noms de déploiement pour les deux services
  • Autorisation de lire le lakehouse, d’accéder à la ressource Azure OpenAI et d’interroger le service Recherche Azure AI

Si vous n’avez pas déjà ces ressources, terminez d’abord le démarrage rapide RAG . Ce démarrage rapide crée le lakehouse et l’indice vectoriel utilisés dans ce benchmark.

Diagramme montrant le flux d’une conversation utilisateur via le système RAG.

Définissez les points de terminaison et les clés requises. Importez les bibliothèques et fonctions requises. Instanciez des clients pour Azure OpenAI et Recherche Azure AI. Définissez un wrapper de fonction avec une invite pour interroger le système RAG.

# Enter your Azure OpenAI service values.
# Use the deployment names you created in Azure OpenAI, not the base model name.
aoai_endpoint = "https://<your-resource-name>.openai.azure.com"  
aoai_key = "<your-azure-openai-key>"  
aoai_deployment_name_embeddings = "<your-embedding-deployment-name>"
aoai_deployment_name_query = "<your-chat-deployment-name>"
aoai_deployment_name_metrics = "<your-chat-deployment-name>"
aoai_api_version = "2025-04-01-preview"

# Set up Azure AI Search.
aisearch_index_name = "<your-index-name>"  
aisearch_api_key = "<your-search-key>"  
aisearch_endpoint = "https://<your-resource-name>.search.windows.net"  
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

import os, requests, json

from datetime import datetime, timedelta
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient

from pyspark.sql import functions as F
from pyspark.sql.functions import to_timestamp, current_timestamp, concat, col, split, explode, udf, monotonically_increasing_id, when, rand, coalesce, lit, input_file_name, regexp_extract, concat_ws, length, ceil
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType, ArrayType, FloatType
from pyspark.sql import Row
import pandas as pd
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.models import (
    VectorizedQuery,
)
from azure.search.documents.indexes.models import (  
    SearchIndex,  
    SearchField,  
    SearchFieldDataType,  
    SimpleField,  
    SearchableField,   
    SemanticConfiguration,  
    SemanticPrioritizedFields,
    SemanticField,  
    SemanticSearch,
    VectorSearch,  
    HnswAlgorithmConfiguration,
    HnswParameters,  
    VectorSearchProfile,
    VectorSearchAlgorithmKind,
    VectorSearchAlgorithmMetric,
)

import openai
from openai import AzureOpenAI
from synapse.ml.fabric.credentials import get_openai_httpx_sync_client
import uuid
import matplotlib.pyplot as plt
from synapse.ml.featurize.text import PageSplitter
import ipywidgets as widgets
from IPython.display import display as w_display

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 6, Finished, Available, Finished)

# Configure access to the Azure OpenAI endpoint.
# In Fabric, use the built-in HTTP client helper so authentication is handled correctly.
embed_client = AzureOpenAI(
    api_version=aoai_api_version,
    azure_endpoint=aoai_endpoint,
    api_key=aoai_key,
    http_client=get_openai_httpx_sync_client(),
)

chat_client = AzureOpenAI(
    azure_endpoint=aoai_endpoint,
    api_key=aoai_key,
    api_version=aoai_api_version,
    http_client=get_openai_httpx_sync_client(),
)

# Configure access to Azure AI Search.
search_client = SearchClient(
    aisearch_endpoint,
    aisearch_index_name,
    credential=AzureKeyCredential(aisearch_api_key)
)

Une fois la configuration terminée, confirmez ce qui suit avant de continuer :

  • Les clients Azure OpenAI sont créés sans erreurs.
  • Le nom de l’index Recherche Azure AI correspond à celui que vous avez créé.
  • Une requête d’exemple retourne un contexte non vide à partir de get_context_source.

Si une vérification échoue, revoyez les valeurs du point de terminaison, les clés API et les noms de déploiement avant de passer au banc d’essai.

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 7, Finished, Available, Finished)

Les fonctions suivantes implémentent les deux principaux composants RAG - retriever (get_context_source) et générateur de réponse (get_answer). Le code est similaire au didacticiel précédent. Le topN paramètre vous permet de définir le nombre de ressources pertinentes à récupérer (ce didacticiel utilise 3, mais la valeur optimale peut varier selon le jeu de données) :

# Implement retriever
def get_context_source(question, topN=3):
    """
    Retrieves contextual information and sources related to a given question using embeddings and a vector search.  
    Parameters:  
    question (str): The question for which the context and sources are to be retrieved.  
    topN (int, optional): The number of top results to retrieve. Default is 3.  
      
    Returns:  
    List: A list containing two elements:  
        1. A string with the concatenated retrieved context.  
        2. A list of retrieved source paths.  
    """
    query_embedding = embed_client.embeddings.create(
        input=question,
        model=aoai_deployment_name_embeddings,
    ).data[0].embedding

    vector_query = VectorizedQuery(vector=query_embedding, k_nearest_neighbors=topN, fields="Embedding")

    results = search_client.search(   
        vector_queries=[vector_query],
        top=topN,
    )

    retrieved_context = ""
    retrieved_sources = []
    for result in results:
        retrieved_context += result['ExtractedPath'] + "\n" + result['Chunk'] + "\n\n"
        retrieved_sources.append(result['ExtractedPath'])

    return [retrieved_context, retrieved_sources]

# Implement response generator
def get_answer(question, context):
    """  
    Generates a response to a given question using provided context and an Azure OpenAI model.  
    
    Parameters:  
        question (str): The question that needs to be answered.  
        context (str): The contextual information related to the question that will help generate a relevant response.  
    
    Returns:  
        str: The response generated by the Azure OpenAI model based on the provided question and context.  
    """
    messages = [
        {
            "role": "system",
            "content": "You are a chat assistant. Use provided text to ground your response. Give a one-word answer when possible ('yes'/'no' is OK where appropriate, no details). Unnecessary words incur a $500 penalty."
        }
    ]

    messages.append(
        {
            "role": "user", 
            "content": question + "\n" + context,
        },
    )

    chat_completion = chat_client.chat.completions.create(
        model=aoai_deployment_name_query,
        messages=messages,
    )

    return chat_completion.choices[0].message.content

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 8, Finished, Available, Finished)

Dataset

La version 1.2 du jeu de données de l’Université Carnegie Mellon Question-Answer est un corpus d’articles Wikipédia avec des questions factuelles et des réponses écrites manuellement. Il est hébergé dans le Stockage Blob d'Azure sous licence GFDL. Le jeu de données utilise une table avec ces champs :

  • ArticleTitle: Nom de l’article Wikipédia les questions et réponses proviennent de
  • Question: question écrite manuellement sur l’article
  • Answer: réponse écrite manuellement en fonction de l’article
  • DifficultyFromQuestioner: Évaluation de la difficulté attribuée par l'auteur de la question
  • DifficultyFromAnswerer: évaluation de la difficulté que l’évaluateur assigne ; peut différer de DifficultyFromQuestioner
  • ExtractedPath: Chemin d’accès à l’article d’origine (un article peut avoir plusieurs paires question-réponse)
  • text: Texte de l’article Wikipédia nettoyé

Téléchargez les fichiers LICENSE-S08 et LICENSE-S09 à partir du même emplacement pour obtenir les détails de la licence.

Historique et citation

Utilisez cette citation pour le jeu de données :

CMU Question/Answer Dataset, Release 1.2
August 23, 2013
Noah A. Smith, Michael Heilman, and Rebecca Hwa
Question Generation as a Competitive Undergraduate Course Project
In Proceedings of the NSF Workshop on the Question Generation Shared Task and Evaluation Challenge, Arlington, VA, September 2008. 
Available at http://www.cs.cmu.edu/~nasmith/papers/smith+heilman+hwa.nsf08.pdf.
Original dataset acknowledgments:
This research project was supported by NSF IIS-0713265 (to Smith), an NSF Graduate Research Fellowship (to Heilman), NSF IIS-0712810 and IIS-0745914 (to Hwa), and Institute of Education Sciences, U.S. Department of Education R305B040063 (to Carnegie Mellon).
cmu-qa-08-09 (modified version)
June 12, 2024
Amir Jafari, Alexandra Savelieva, Brice Chung, Hossein Khadivi Heris, Journey McDowell
This release uses the GNU Free Documentation License (GFDL) (http://www.gnu.org/licenses/fdl.html).
The GNU license applies to all copies of the dataset.

Créer un benchmark

Importez le benchmark. Pour cette démonstration, utilisez un sous-ensemble de questions provenant des compartiments S08/set1 et S08/set2. Pour conserver une question par article, appliquez df.dropDuplicates(["ExtractedPath"]). Supprimez les questions dupliquées. Le processus de curation ajoute des étiquettes de difficulté ; cet exemple les limite à medium.

df = spark.sql("SELECT * FROM data_load_tests.cmu_qa")

# Filter the DataFrame to include the specified paths
df = df.filter((col("ExtractedPath").like("S08/data/set1/%")) | (col("ExtractedPath").like("S08/data/set2/%")))

# Keep only medium-difficulty questions.
df = df.filter(col("DifficultyFromQuestioner") == "medium")


# Drop duplicate questions and source paths.
df = df.dropDuplicates(["Question"])
df = df.dropDuplicates(["ExtractedPath"])

num_rows = df.count()
num_columns = len(df.columns)
print(f"Number of rows: {num_rows}, Number of columns: {num_columns}")

# Persist the DataFrame
df.persist()
display(df)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 9, Finished, Available, Finished)Number of rows: 20, Number of columns: 7SynapseWidget(Synapse.DataFrame, 47aff8cb-72f8-4a36-885c-f4f3bb830a91)

Le résultat est un DataFrame avec 20 lignes : le benchmark de démonstration. Les champs clés sont Question, Answer (réponse de vérité au sol organisée par l’homme) et ExtractedPath (document source). Ajustez les filtres pour inclure d’autres questions et modifiez la difficulté d’un exemple plus réaliste. Essaie.

Exécuter un test simple de bout en bout

Commencez par un test de fumée de bout en bout de la génération d’extraction augmentée (RAG).

question = "How many suborders are turtles divided into?"
retrieved_context, retrieved_sources = get_context_source(question)
answer = get_answer(question, retrieved_context)
print(answer)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 10, Finished, Available, Finished)Three

Ce test de fumée vous aide à trouver des problèmes dans l’implémentation RAG, tels que des informations d’identification incorrectes, un index vectoriel manquant ou vide ou des interfaces de fonction incompatibles. Si le test échoue, vérifiez les problèmes. Sortie attendue : Three. Si le test de fumée réussit, accédez à la section suivante pour évaluer RAG plus loin.

Établir des métriques

Définissez une métrique déterministe pour évaluer le récupérateur. Il est inspiré par les moteurs de recherche. Elle vérifie si la liste des sources récupérées inclut la source de vérité de base. Cette métrique est un score de précision N supérieur, car le topN paramètre définit le nombre de sources récupérées.

def get_retrieval_score(target_source, retrieved_sources):
    if target_source in retrieved_sources: 
        return 1
    else: 
        return 0

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 11, Finished, Available, Finished)

Selon le benchmark, la réponse est contenue dans la source avec l’ID "S08/data/set1/a9". Test de la fonction sur l’exemple que nous avons exécuté ci-dessus retourne 1, comme prévu, car elle se trouvait dans les trois premiers blocs de texte pertinents.

print("Retrieved sources:", retrieved_sources)
get_retrieval_score("S08/data/set1/a9", retrieved_sources)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 12, Finished, Available, Finished)Retrieved sources: ['S08/data/set1/a9', 'S08/data/set1/a9', 'S08/data/set1/a5']1

Cette section définit les métriques assistées par l’IA. Le modèle de prompt inclut quelques exemples d’entrée (CONTEXTE et RÉPONSE) et de sorties suggérées, également appelées modèle à quelques coups. Ce schéma est une approche courante basée sur le LLM en tant que juge pour évaluer la qualité des réponses. Cette démo utilise groundedness et relevance, qui sont largement utilisés pour vérifier si une réponse est prise en charge par le contexte récupéré et répond à la question de manière appropriée. D’autres indicateurs peuvent être utiles, mais ils peuvent fournir des signaux moins intuitifs. Par exemple, les réponses n’ont pas besoin d’être similaires pour être correctes, donc similarity les scores peuvent être trompeurs. L’échelle pour toutes les métriques est de 1 à 5. Plus élevé est meilleur. Groundedness ne prend que deux entrées (contexte et réponse générée), tandis que les deux autres métriques utilisent également la vérité de base pour l’évaluation.

def get_groundedness_metric(context, answer):
    """Get the groundedness score from the LLM using the context and answer."""

    groundedness_prompt_template = """
    You are presented with a CONTEXT and an ANSWER about that CONTEXT. Decide whether the ANSWER is entailed by the CONTEXT by choosing one of the following ratings:
    1. 5: The ANSWER follows logically from the information contained in the CONTEXT.
    2. 1: The ANSWER is logically false from the information contained in the CONTEXT.
    3. an integer score between 1 and 5 and if such integer score does not exist, use 1: It is not possible to determine whether the ANSWER is true or false without further information. Read the passage of information thoroughly and select the correct answer from the three answer labels. Read the CONTEXT thoroughly to ensure you know what the CONTEXT entails. Note the ANSWER is generated by a computer system, it can contain certain symbols, which should not be a negative factor in the evaluation.
    Independent Examples:
    ## Example Task #1 Input:
    "CONTEXT": "Some are reported as not having been wanted at all.", "QUESTION": "", "ANSWER": "All are reported as being completely and fully wanted."
    ## Example Task #1 Output:
    1
    ## Example Task #2 Input:
    "CONTEXT": "Ten new television shows appeared during the month of September. Five of the shows were sitcoms, three were hourlong dramas, and two were news-magazine shows. By January, only seven of these new shows were still on the air. Five of the shows that remained were sitcoms.", "QUESTION": "", "ANSWER": "At least one of the shows that were cancelled was an hourlong drama."
    ## Example Task #2 Output:
    5
    ## Example Task #3 Input:
    "CONTEXT": "In Quebec, an allophone is a resident, usually an immigrant, whose mother tongue or home language is neither French nor English.", "QUESTION": "", "ANSWER": "In Quebec, an allophone is a resident, usually an immigrant, whose mother tongue or home language is not French."
    5
    ## Example Task #4 Input:
    "CONTEXT": "Some are reported as not having been wanted at all.", "QUESTION": "", "ANSWER": "All are reported as being completely and fully wanted."
    ## Example Task #4 Output:
    1
    ## Actual Task Input:
    "CONTEXT": {context}, "QUESTION": "", "ANSWER": {answer}
    Reminder: The return values for each task should be correctly formatted as an integer between 1 and 5. Do not repeat the context and question.  Don't explain the reasoning. The answer should include only a number: 1, 2, 3, 4, or 5.
    Actual Task Output:
    """

    metric_client = AzureOpenAI(
        api_version=aoai_api_version,
        azure_endpoint=aoai_endpoint,
        api_key=aoai_key,
        http_client=get_openai_httpx_sync_client(),
    )

    messages = [
        {
            "role": "system",
            "content": "You are an AI assistant. You will be given the definition of an evaluation metric for assessing the quality of an answer in a question-answering task. Your job is to compute an accurate evaluation score using the provided evaluation metric."
        }, 
        {
            "role": "user",
            "content": groundedness_prompt_template.format(context=context, answer=answer)
        }
    ]

    metric_completion = metric_client.chat.completions.create(
        model=aoai_deployment_name_metrics,
        messages=messages,
        temperature=0,
    )

    return metric_completion.choices[0].message.content

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 13, Finished, Available, Finished)

def get_relevance_metric(context, question, answer):    
    relevance_prompt_template = """
    Relevance measures how well the answer addresses the main aspects of the question, based on the context. Consider whether all and only the important aspects are contained in the answer when evaluating relevance. Given the context and question, score the relevance of the answer between one to five stars using the following rating scale:
    One star: the answer completely lacks relevance
    Two stars: the answer mostly lacks relevance
    Three stars: the answer is partially relevant
    Four stars: the answer is mostly relevant
    Five stars: the answer has perfect relevance

    This rating value should always be an integer between 1 and 5. So the rating produced should be 1 or 2 or 3 or 4 or 5.

    context: Marie Curie was a Polish-born physicist and chemist who pioneered research on radioactivity and was the first woman to win a Nobel Prize.
    question: What field did Marie Curie excel in?
    answer: Marie Curie was a renowned painter who focused mainly on impressionist styles and techniques.
    stars: 1

    context: The Beatles were an English rock band formed in Liverpool in 1960, and they are widely regarded as the most influential music band in history.
    question: Where were The Beatles formed?
    answer: The band The Beatles began their journey in London, England, and they changed the history of music.
    stars: 2

    context: The recent Mars rover, Perseverance, was launched in 2020 with the main goal of searching for signs of ancient life on Mars. The rover also carries an experiment called MOXIE, which aims to generate oxygen from the Martian atmosphere.
    question: What are the main goals of Perseverance Mars rover mission?
    answer: The Perseverance Mars rover mission focuses on searching for signs of ancient life on Mars.
    stars: 3

    context: The Mediterranean diet is a commonly recommended dietary plan that emphasizes fruits, vegetables, whole grains, legumes, lean proteins, and healthy fats. Studies have shown that it offers numerous health benefits, including a reduced risk of heart disease and improved cognitive health.
    question: What are the main components of the Mediterranean diet?
    answer: The Mediterranean diet primarily consists of fruits, vegetables, whole grains, and legumes.
    stars: 4

    context: The Queen's Royal Castle is a well-known tourist attraction in the United Kingdom. It spans over 500 acres and contains extensive gardens and parks. The castle was built in the 15th century and has been home to generations of royalty.
    question: What are the main attractions of the Queen's Royal Castle?
    answer: The main attractions of the Queen's Royal Castle are its expansive 500-acre grounds, extensive gardens, parks, and the historical castle itself, which dates back to the 15th century and has housed generations of royalty.
    stars: 5

    Don't explain the reasoning. The answer should include only a number: 1, 2, 3, 4, or 5.

    context: {context}
    question: {question}
    answer: {answer}
    stars:
    """

    metric_client = AzureOpenAI(
        api_version=aoai_api_version,
        azure_endpoint=aoai_endpoint,
        api_key=aoai_key,
        http_client=get_openai_httpx_sync_client(),
    )

    messages = [
        {
            "role": "system",
            "content": "You are an AI assistant. You are given the definition of an evaluation metric for assessing the quality of an answer in a question-answering task. Compute an accurate evaluation score using the provided evaluation metric."
        }, 
        {
            "role": "user",
            "content": relevance_prompt_template.format(context=context, question=question, answer=answer)
        }
    ]

    metric_completion = metric_client.chat.completions.create(
        model=aoai_deployment_name_metrics,
        messages=messages,
        temperature=0,
    )

    return metric_completion.choices[0].message.content

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 14, Finished, Available, Finished)

def get_similarity_metric(question, ground_truth, answer):
    similarity_prompt_template = """
    Equivalence, as a metric, measures the similarity between the predicted answer and the correct answer. If the information and content in the predicted answer is similar or equivalent to the correct answer, then the value of the Equivalence metric should be high, else it should be low. Given the question, correct answer, and predicted answer, determine the value of Equivalence metric using the following rating scale:
    One star: the predicted answer is not at all similar to the correct answer
    Two stars: the predicted answer is mostly not similar to the correct answer
    Three stars: the predicted answer is somewhat similar to the correct answer
    Four stars: the predicted answer is mostly similar to the correct answer
    Five stars: the predicted answer is completely similar to the correct answer

    This rating value should always be an integer between 1 and 5. So the rating produced should be 1 or 2 or 3 or 4 or 5.

    The examples below show the Equivalence score for a question, a correct answer, and a predicted answer.

    question: What is the role of ribosomes?
    correct answer: Ribosomes are cellular structures responsible for protein synthesis. They interpret the genetic information carried by messenger RNA (mRNA) and use it to assemble amino acids into proteins.
    predicted answer: Ribosomes participate in carbohydrate breakdown by removing nutrients from complex sugar molecules.
    stars: 1

    question: Why did the Titanic sink?
    correct answer: The Titanic sank after it struck an iceberg during its maiden voyage in 1912. The impact caused the ship's hull to breach, allowing water to flood into the vessel. The ship's design, lifeboat shortage, and lack of timely rescue efforts contributed to the tragic loss of life.
    predicted answer: The sinking of the Titanic was a result of a large iceberg collision. This caused the ship to take on water and eventually sink, leading to the death of many passengers due to a shortage of lifeboats and insufficient rescue attempts.
    stars: 2

    question: What causes seasons on Earth?
    correct answer: Seasons on Earth are caused by the tilt of the Earth's axis and its revolution around the Sun. As the Earth orbits the Sun, the tilt causes different parts of the planet to receive varying amounts of sunlight, resulting in changes in temperature and weather patterns.
    predicted answer: Seasons occur because of the Earth's rotation and its elliptical orbit around the Sun. The tilt of the Earth's axis causes regions to be subjected to different sunlight intensities, which leads to temperature fluctuations and alternating weather conditions.
    stars: 3

    question: How does photosynthesis work?
    correct answer: Photosynthesis is a process by which green plants and some other organisms convert light energy into chemical energy. This occurs as light is absorbed by chlorophyll molecules, and then carbon dioxide and water are converted into glucose and oxygen through a series of reactions.
    predicted answer: In photosynthesis, sunlight is transformed into nutrients by plants and certain microorganisms. Light is captured by chlorophyll molecules, followed by the conversion of carbon dioxide and water into sugar and oxygen through multiple reactions.
    stars: 4

    question: What are the health benefits of regular exercise?
    correct answer: Regular exercise can help maintain a healthy weight, increase muscle and bone strength, and reduce the risk of chronic diseases. It also promotes mental well-being by reducing stress and improving overall mood.
    predicted answer: Routine physical activity can contribute to maintaining ideal body weight, enhancing muscle and bone strength, and preventing chronic illnesses. In addition, it supports mental health by alleviating stress and augmenting general mood.
    stars: 5

    Don't explain the reasoning. The answer should include only a number: 1, 2, 3, 4, or 5.

    question: {question}
    correct answer:{ground_truth}
    predicted answer: {answer}
    stars:
    """
    
    metric_client = AzureOpenAI(
        api_version=aoai_api_version,
        azure_endpoint=aoai_endpoint,
        api_key=aoai_key,
        http_client=get_openai_httpx_sync_client(),
    )

    messages = [
        {
            "role": "system",
            "content": "You are an AI assistant. You will be given the definition of an evaluation metric for assessing the quality of an answer in a question-answering task. Your job is to compute an accurate evaluation score using the provided evaluation metric."
        }, 
        {
            "role": "user",
            "content": similarity_prompt_template.format(question=question, ground_truth=ground_truth, answer=answer)
        }
    ]

    metric_completion = metric_client.chat.completions.create(
        model=aoai_deployment_name_metrics,
        messages=messages,
        temperature=0,
    )

    return metric_completion.choices[0].message.content

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 15, Finished, Available, Finished)

Testez la métrique de pertinence :

get_relevance_metric(retrieved_context, question, answer)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 16, Finished, Available, Finished)'2'

Un score de 5 signifie que la réponse est pertinente. Le code suivant obtient la métrique de similarité :

get_similarity_metric(question, 'three', answer)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 17, Finished, Available, Finished)'5'

Un score de 5 signifie que la réponse correspond à la réponse de base organisée par un expert humain. Les scores de métriques assistés par l’IA peuvent varier avec la même entrée. Ils sont plus rapides que d’utiliser des juges humains.

Évaluer la performance de RAG sur le benchmark de Q&A

Créez des enveloppes de fonctions pour les exécuter à grande échelle. Encapsulez chaque fonction qui se termine par _udf (court pour user-defined function) afin qu’elle soit conforme aux exigences Spark (@udf(returnType=StructType([ ... ]))) et exécute des calculs sur des données volumineuses plus rapidement sur le cluster.

# UDF wrappers for RAG components
@udf(returnType=StructType([  
    StructField("retrieved_context", StringType(), True),  
    StructField("retrieved_sources", ArrayType(StringType()), True)  
]))
def get_context_source_udf(question, topN=3):
    return get_context_source(question, topN)

@udf(returnType=StringType())
def get_answer_udf(question, context):
    return get_answer(question, context)


# UDF wrapper for retrieval score
@udf(returnType=StringType())
def get_retrieval_score_udf(target_source, retrieved_sources):
    return get_retrieval_score(target_source, retrieved_sources)


# UDF wrappers for AI-assisted metrics
@udf(returnType=StringType())
def get_groundedness_metric_udf(context, answer):
    return get_groundedness_metric(context, answer)

@udf(returnType=StringType())
def get_relevance_metric_udf(context, question, answer): 
    return get_relevance_metric(context, question, answer)

@udf(returnType=StringType())
def get_similarity_metric_udf(question, ground_truth, answer):
    return get_similarity_metric(question, ground_truth, answer)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 18, Finished, Available, Finished)

Suivi n° 1 : performance du module de récupération

Le code suivant crée les colonnes result et retrieval_score dans le DataFrame de référence. Ces colonnes incluent la réponse générée par RAG et un indicateur indiquant si le contexte fourni au LLM inclut l’article sur lequel repose la question.

df = df.withColumn("result", get_context_source_udf(df.Question)).select(df.columns+["result.*"])
df = df.withColumn('retrieval_score', get_retrieval_score_udf(df.ExtractedPath, df.retrieved_sources))
print("Aggregate Retrieval score: {:.2f}%".format((df.where(df["retrieval_score"] == 1).count() / df.count()) * 100))
display(df.select(["question", "retrieval_score",  "ExtractedPath", "retrieved_sources"]))

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 19, Finished, Available, Finished)Aggregate Retrieval score: 100.00%SynapseWidget(Synapse.DataFrame, 14efe386-836a-4765-bd88-b121f32c7cfc)

** Pour toutes les questions, l'extracteur récupère le contexte correct et, dans la plupart des cas, c'est la première entrée. La recherche Azure AI fonctionne bien. Vous pouvez vous demander pourquoi, dans certains cas, le contexte a deux ou trois valeurs identiques. Ce n’est pas une erreur : cela signifie que le récupérateur extrait des fragments du même article qui ne tiennent pas dans un bloc lors du fractionnement.

Suivi n° 2 : performance du générateur de réponses

Passez la question et le contexte au LLM pour générer une réponse. Stockez-la dans la generated_answer colonne dans le DataFrame :

df = df.withColumn('generated_answer', get_answer_udf(df.Question, df.retrieved_context))

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 20, Finished, Available, Finished)

Utilisez la réponse générée, la réponse de base, la question et le contexte pour calculer les métriques. Affichez les résultats de l’évaluation pour chaque paire question-réponse :

df = df.withColumn('gpt_groundedness', get_groundedness_metric_udf(df.retrieved_context, df.generated_answer))
df = df.withColumn('gpt_relevance', get_relevance_metric_udf(df.retrieved_context, df.Question, df.generated_answer))
df = df.withColumn('gpt_similarity', get_similarity_metric_udf(df.Question, df.Answer, df.generated_answer))
display(df.select(["question", "answer", "generated_answer", "retrieval_score", "gpt_groundedness","gpt_relevance", "gpt_similarity"]))

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 21, Finished, Available, Finished)SynapseWidget(Synapse.DataFrame, 22b97d27-91e1-40f3-b888-3a3399de9d6b)

Que montrent ces valeurs ? Pour faciliter leur interprétation, tracez des histogrammes de fiabilité, de pertinence et de similarité. Le LLM est plus détaillé que les réponses humaines, ce qui diminue la métrique de similarité - environ la moitié des réponses sont sémantiquement correctes, mais reçoivent quatre étoiles en tant que principalement similaires. La plupart des valeurs pour les trois métriques sont 4 ou 5, ce qui suggère que les performances RAG sont bonnes. Il existe quelques valeurs hors norme (par exemple, pour la question How many species of otter are there?, le modèle a généré There are 13 species of otter, qui est correct avec une pertinence élevée et une similarité (5). Pour une raison quelconque, GPT considérait qu’il était mal ancré dans le contexte fourni et lui a donné une étoile. Dans les trois autres cas, où il existe au moins une métrique d'une étoile assistée par l'IA, le faible score indique une mauvaise réponse. Le LLM évalue parfois incorrectement, mais évalue généralement avec précision.

# Convert Spark DataFrame to Pandas DataFrame
pandas_df = df.toPandas()

selected_columns = ['gpt_groundedness', 'gpt_relevance', 'gpt_similarity']
trimmed_df = pandas_df[selected_columns].astype(int)

# Define a function to plot histograms for the specified columns
def plot_histograms(dataframe, columns):
    # Set up the figure size and subplots
    plt.figure(figsize=(15, 5))
    for i, column in enumerate(columns, 1):
        plt.subplot(1, len(columns), i)
        # Filter the dataframe to only include rows with values 1, 2, 3, 4, 5
        filtered_df = dataframe[dataframe[column].isin([1, 2, 3, 4, 5])]
        filtered_df[column].hist(bins=range(1, 7), align='left', rwidth=0.8)
        plt.title(f'Histogram of {column}')
        plt.xlabel('Values')
        plt.ylabel('Frequency')
        plt.xticks(range(1, 6))
        plt.yticks(range(0, 20, 2))


# Call the function to plot histograms for the specified columns
plot_histograms(trimmed_df, selected_columns)

# Show the plots
plt.tight_layout()
plt.show()

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 24, Finished, Available, Finished)

Capture d’écran des histogrammes montrant la distribution de la pertinence gpT et des scores de similarité pour les questions évaluées.

En guise d'étape finale, sauvegardez les résultats du benchmark dans une table de votre lakehouse. Cette étape est facultative mais fortement recommandée : elle rend vos résultats plus utiles. Lorsque vous modifiez un élément dans le RAG (par exemple, modifiez l’invite, mettez à jour l’index ou utilisez un autre modèle GPT dans le générateur de réponse), mesurez l’impact, quantifiez les améliorations et détectez les régressions.

# create name of experiment that is easy to refer to
friendly_name_of_experiment = "rag_tutorial_experiment_1"

# Note the current date and time  
time_of_experiment = current_timestamp()

# Generate a unique GUID for all rows
experiment_id = str(uuid.uuid4())

# Add two new columns to the Spark DataFrame
updated_df = df.withColumn("execution_time", time_of_experiment) \
                        .withColumn("experiment_id", lit(experiment_id)) \
                        .withColumn("experiment_friendly_name", lit(friendly_name_of_experiment))

# Store the updated DataFrame in the default lakehouse as a table named 'rag_experiment_runs'
table_name = "rag_experiment_run_demo1" 
updated_df.write.format("parquet").mode("append").saveAsTable(table_name)

Sortie de cellule :StatementMeta(, 21cb8cd3-7742-4c1f-8339-265e2846df1d, 28, Finished, Available, Finished)

Revenez aux résultats de l’expérience à tout moment pour les examiner, comparer avec de nouvelles expériences et choisir la configuration qui fonctionne le mieux pour la production.

Troubleshooting

Si le carnet échoue, vérifiez les éléments suivants :

  • Identifiants ou points de terminaison OpenAI Azure invalides : confirmez le nom de la ressource, la clé API et les noms de déploiement.
  • Résultats vides ou manquants d’Recherche Azure AI : vérifiez le nom de l’index, le schéma de l’index vectoriel, et que les données sont chargées.
  • Mauvais noms de modèles : assurez-vous que les déploiements d’intégration et de chat existent et correspondent aux valeurs du code.
  • Scores métriques faibles ou valeurs inattendues : confirmez que le contexte récupéré contient la source attendue et que la question de référence correspond aux documents indexés.
  • Résultats de récupération dupliqués : confirmez que les fragments ont été créés correctement et que l’index contient les métadonnées attendues des fragments.

Si une étape échoue, arrêtez-vous et validez la cellule précédente avant de passer à la section suivante.

Résumé

Utilisez les métriques assistées par IA et le taux de récupération top-N pour créer votre solution de génération d’extraction augmentée (RAG).