Condividi tramite


Esercitazione: Valutare la qualità della risposta con la memorizzazione nella cache e la creazione di report

In questa esercitazione si crea un'app MSTest per valutare la risposta della chat di un modello OpenAI. L'app di test usa le librerie di Microsoft.Extensions.AI.Evaluation per eseguire le valutazioni, memorizzare nella cache le risposte del modello e creare report. L'esercitazione usa valutatori predefiniti e personalizzati. Gli analizzatori di qualità predefiniti (dal pacchetto Microsoft.Extensions.AI.Evaluation.Quality) usano un LLM per eseguire valutazioni; l'analizzatore personalizzato non usa l'intelligenza artificiale.

Prerequisiti

Configurare il servizio di intelligenza artificiale

Per effettuare il provisioning di un servizio e un modello OpenAI di Azure usando il portale di Azure, completare la procedura descritta nell'articolo creare e distribuire una risorsa del servizio OpenAI di Azure. Nel passaggio "Distribuisci un modello" selezionare il modello di gpt-4o.

Creare l'app di test

Completare i passaggi seguenti per creare un progetto MSTest che si connette al modello di intelligenza artificiale gpt-4o.

  1. In una finestra del terminale passare alla directory in cui si vuole creare l'app e creare una nuova app MSTest con il comando dotnet new:

    dotnet new mstest -o TestAIWithReporting
    
  2. Passare alla directory TestAIWithReporting e aggiungere i pacchetti necessari all'app:

    dotnet add package Azure.AI.OpenAI
    dotnet add package Azure.Identity
    dotnet add package Microsoft.Extensions.AI.Abstractions
    dotnet add package Microsoft.Extensions.AI.Evaluation
    dotnet add package Microsoft.Extensions.AI.Evaluation.Quality
    dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting
    dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
    dotnet add package Microsoft.Extensions.Configuration
    dotnet add package Microsoft.Extensions.Configuration.UserSecrets
    
  3. Esegui i seguenti comandi per aggiungere segreti dell'applicazione per il nome del modello, l'endpoint e l'ID cliente di Azure OpenAI.

    dotnet user-secrets init
    dotnet user-secrets set AZURE_OPENAI_ENDPOINT <your-Azure-OpenAI-endpoint>
    dotnet user-secrets set AZURE_OPENAI_GPT_NAME gpt-4o
    dotnet user-secrets set AZURE_TENANT_ID <your-tenant-ID>
    

    A seconda dell'ambiente, l'ID tenant potrebbe non essere necessario. In tal caso, rimuoverlo dal codice che effettua l'istanza del DefaultAzureCredential.

  4. Aprire la nuova app nell'editor preferito.

Aggiungere il codice dell'app di test

  1. Rinominare il file Test1.cs in MyTests.cse quindi aprire il file e rinominare la classe in MyTests. Eliminare il metodo TestMethod1 vuoto.

  2. Aggiungere le direttive using necessarie all'inizio del file.

    using Azure.AI.OpenAI;
    using Azure.Identity;
    using Microsoft.Extensions.AI.Evaluation;
    using Microsoft.Extensions.AI;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.AI.Evaluation.Reporting.Storage;
    using Microsoft.Extensions.AI.Evaluation.Reporting;
    using Microsoft.Extensions.AI.Evaluation.Quality;
    
  3. Aggiungere la proprietà TestContext alla classe .

    // The value of the TestContext property is populated by MSTest.
    public TestContext? TestContext { get; set; }
    
  4. Aggiungere il metodo GetAzureOpenAIChatConfiguration, che crea il IChatClient usato dall'analizzatore per comunicare con il modello.

    private static ChatConfiguration GetAzureOpenAIChatConfiguration()
    {
        IConfigurationRoot config = new ConfigurationBuilder().AddUserSecrets<MyTests>().Build();
    
        string endpoint = config["AZURE_OPENAI_ENDPOINT"];
        string model = config["AZURE_OPENAI_GPT_NAME"];
        string tenantId = config["AZURE_TENANT_ID"];
    
        // Get an instance of Microsoft.Extensions.AI's <see cref="IChatClient"/>
        // interface for the selected LLM endpoint.
        AzureOpenAIClient azureClient =
            new(
                new Uri(endpoint),
                new DefaultAzureCredential(new DefaultAzureCredentialOptions() { TenantId = tenantId }));
        IChatClient client = azureClient.GetChatClient(deploymentName: model).AsIChatClient();
    
        // Create an instance of <see cref="ChatConfiguration"/>
        // to communicate with the LLM.
        return new ChatConfiguration(client);
    }
    
  5. Configurare la funzionalità di creazione di report.

    private string ScenarioName => $"{TestContext!.FullyQualifiedTestClassName}.{TestContext.TestName}";
    
    private static string ExecutionName => $"{DateTime.Now:yyyyMMddTHHmmss}";
    
    private static readonly ReportingConfiguration s_defaultReportingConfiguration =
        DiskBasedReportingConfiguration.Create(
            storageRootPath: "C:\\TestReports",
            evaluators: GetEvaluators(),
            chatConfiguration: GetAzureOpenAIChatConfiguration(),
            enableResponseCaching: true,
            executionName: ExecutionName);
    

    nome scenario

    Il nome dello scenario è impostato sul nome completamente qualificato del metodo di test corrente. Tuttavia, è possibile impostarlo su qualsiasi stringa di propria scelta quando si chiama CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken). Ecco alcune considerazioni per la scelta di un nome di scenario:

    • Quando si usa l'archiviazione basata su disco, il nome dello scenario viene usato come nome della cartella in cui vengono archiviati i risultati di valutazione corrispondenti. È quindi consigliabile mantenere il nome ragionevolmente breve ed evitare eventuali caratteri non consentiti nei nomi di file e directory.
    • Per impostazione predefinita, il report di valutazione generato suddivide i nomi degli scenari in . in modo che i risultati possano essere visualizzati in una visualizzazione gerarchica con raggruppamento, annidamento e aggregazione appropriati. Ciò è particolarmente utile nei casi in cui il nome dello scenario è impostato sul nome completo del metodo di test corrispondente, poiché consente di raggruppare i risultati per namespace e nomi di classe nella gerarchia. Tuttavia, è anche possibile sfruttare questa funzionalità includendo i periodi (.) nei nomi degli scenari personalizzati per creare una gerarchia di report ottimale per gli scenari.

    Nome dell'esecuzione

    Il nome dell'esecuzione viene usato per raggruppare i risultati della valutazione che fanno parte della stessa esecuzione di valutazione (o esecuzione di test) quando vengono archiviati i risultati della valutazione. Se non si specifica un nome di esecuzione durante la creazione di un ReportingConfiguration, tutte le esecuzioni di valutazione useranno lo stesso nome di esecuzione predefinito di Default. In questo caso, i risultati di un'esecuzione verranno sovrascritti dalla successiva e si perde la possibilità di confrontare i risultati tra diverse esecuzioni.

    In questo esempio viene usato un timestamp come nome di esecuzione. Se nel progetto sono presenti più test, assicurarsi che i risultati vengano raggruppati correttamente usando lo stesso nome di esecuzione in tutte le configurazioni di report usate nei test.

    In uno scenario più reale, è anche possibile condividere lo stesso nome di esecuzione tra i test di valutazione che risiedono in più assembly diversi e eseguiti in processi di test diversi. In questi casi, è possibile usare uno script per aggiornare una variabile di ambiente con un nome di esecuzione appropriato (ad esempio il numero di build corrente assegnato dal sistema CI/CD) prima di eseguire i test. In alternativa, se il sistema di compilazione produce versioni dei file di assembly che aumentano monotonicamente, è possibile leggere il AssemblyFileVersionAttribute dall'interno del codice di test e usarlo come nome di esecuzione per confrontare i risultati tra diverse versioni del prodotto.

    Configurazione Reportistica

    Un ReportingConfiguration identifica:

    Questo test utilizza una configurazione di generazione di report basata su disco.

  6. In un file separato aggiungere la classe WordCountEvaluator, che è un analizzatore personalizzato che implementa IEvaluator.

    using System.Text.RegularExpressions;
    using Microsoft.Extensions.AI;
    using Microsoft.Extensions.AI.Evaluation;
    
    namespace TestAIWithReporting;
    
    public class WordCountEvaluator : IEvaluator
    {
        public const string WordCountMetricName = "Words";
    
        public IReadOnlyCollection<string> EvaluationMetricNames => [WordCountMetricName];
    
        /// <summary>
        /// Counts the number of words in the supplied string.
        /// </summary>
        private static int CountWords(string? input)
        {
            if (string.IsNullOrWhiteSpace(input))
            {
                return 0;
            }
    
            MatchCollection matches = Regex.Matches(input, @"\b\w+\b");
            return matches.Count;
        }
    
        /// <summary>
        /// Provides a default interpretation for the supplied <paramref name="metric"/>.
        /// </summary>
        private static void Interpret(NumericMetric metric)
        {
            if (metric.Value is null)
            {
                metric.Interpretation =
                    new EvaluationMetricInterpretation(
                        EvaluationRating.Unknown,
                        failed: true,
                        reason: "Failed to calculate word count for the response.");
            }
            else
            {
                if (metric.Value <= 100 && metric.Value > 5)
                    metric.Interpretation = new EvaluationMetricInterpretation(
                        EvaluationRating.Good,
                        reason: "The response was between 6 and 100 words.");
                else
                    metric.Interpretation = new EvaluationMetricInterpretation(
                        EvaluationRating.Unacceptable,
                        failed: true,
                        reason: "The response was either too short or greater than 100 words.");
            }
        }
    
        public ValueTask<EvaluationResult> EvaluateAsync(
            IEnumerable<ChatMessage> messages,
            ChatResponse modelResponse,
            ChatConfiguration? chatConfiguration = null,
            IEnumerable<EvaluationContext>? additionalContext = null,
            CancellationToken cancellationToken = default)
        {
            // Count the number of words in the supplied <see cref="modelResponse"/>.
            int wordCount = CountWords(modelResponse.Text);
    
            string reason =
                $"This {WordCountMetricName} metric has a value of {wordCount} because " +
                $"the evaluated model response contained {wordCount} words.";
    
            // Create a <see cref="NumericMetric"/> with value set to the word count.
            // Include a reason that explains the score.
            var metric = new NumericMetric(WordCountMetricName, value: wordCount, reason);
    
            // Attach a default <see cref="EvaluationMetricInterpretation"/> for the metric.
            Interpret(metric);
    
            return new ValueTask<EvaluationResult>(new EvaluationResult(metric));
        }
    }
    

    Il WordCountEvaluator conta il numero di parole presenti nella risposta. A differenza di alcuni analizzatori, non si basa sull'intelligenza artificiale. Il metodo EvaluateAsync restituisce un EvaluationResult che include un NumericMetric che contiene il conteggio delle parole.

    Il metodo EvaluateAsync associa anche un'interpretazione predefinita alla metrica. L'interpretazione predefinita considera la metrica valida (accettabile) se il conteggio delle parole rilevate è compreso tra 6 e 100. In caso contrario, la metrica viene considerata non riuscita. Questa interpretazione predefinita può essere sostituita dal chiamante, se necessario.

  7. Torna in MyTests.cs, aggiungi un metodo per raccogliere i valutatori da usare nella valutazione.

    private static IEnumerable<IEvaluator> GetEvaluators()
    {
        IEvaluator relevanceEvaluator = new RelevanceEvaluator();
        IEvaluator coherenceEvaluator = new CoherenceEvaluator();
        IEvaluator wordCountEvaluator = new WordCountEvaluator();
    
        return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator];
    }
    
  8. Aggiungere un metodo per aggiungere un prompt di sistema ChatMessage, definire le opzioni di chat e chiedere al modello una risposta a una determinata domanda.

    private static async Task<(IList<ChatMessage> Messages, ChatResponse ModelResponse)> GetAstronomyConversationAsync(
        IChatClient chatClient,
        string astronomyQuestion)
    {
        const string SystemPrompt =
            """
            You're an AI assistant that can answer questions related to astronomy.
            Keep your responses concise and under 100 words.
            Use the imperial measurement system for all measurements in your response.
            """;
    
        IList<ChatMessage> messages =
            [
                new ChatMessage(ChatRole.System, SystemPrompt),
                new ChatMessage(ChatRole.User, astronomyQuestion)
            ];
    
        var chatOptions =
            new ChatOptions
            {
                Temperature = 0.0f,
                ResponseFormat = ChatResponseFormat.Text
            };
    
        ChatResponse response = await chatClient.GetResponseAsync(messages, chatOptions);
        return (messages, response);
    }
    

    Il test in questa esercitazione valuta la risposta dell'LLM a una domanda di astronomia. Poiché il ReportingConfiguration ha abilitato la memorizzazione nella cache delle risposte e poiché il IChatClient fornito viene sempre recuperato dal ScenarioRun creato usando questa configurazione di creazione di report, la risposta LLM per il test viene memorizzata nella cache e riutilizzata. La risposta verrà riutilizzata fino alla scadenza della voce della cache corrispondente (in 14 giorni per impostazione predefinita) o fino a quando non viene modificato qualsiasi parametro di richiesta, ad esempio l'endpoint LLM o la domanda da porre.

  9. Aggiungere un metodo per convalidare la risposta.

    /// <summary>
    /// Runs basic validation on the supplied <see cref="EvaluationResult"/>.
    /// </summary>
    private static void Validate(EvaluationResult result)
    {
        // Retrieve the score for relevance from the <see cref="EvaluationResult"/>.
        NumericMetric relevance =
            result.Get<NumericMetric>(RelevanceEvaluator.RelevanceMetricName);
        Assert.IsFalse(relevance.Interpretation!.Failed, relevance.Reason);
        Assert.IsTrue(relevance.Interpretation.Rating is EvaluationRating.Good or EvaluationRating.Exceptional);
    
        // Retrieve the score for coherence from the <see cref="EvaluationResult"/>.
        NumericMetric coherence =
            result.Get<NumericMetric>(CoherenceEvaluator.CoherenceMetricName);
        Assert.IsFalse(coherence.Interpretation!.Failed, coherence.Reason);
        Assert.IsTrue(coherence.Interpretation.Rating is EvaluationRating.Good or EvaluationRating.Exceptional);
    
        // Retrieve the word count from the <see cref="EvaluationResult"/>.
        NumericMetric wordCount = result.Get<NumericMetric>(WordCountEvaluator.WordCountMetricName);
        Assert.IsFalse(wordCount.Interpretation!.Failed, wordCount.Reason);
        Assert.IsTrue(wordCount.Interpretation.Rating is EvaluationRating.Good or EvaluationRating.Exceptional);
        Assert.IsFalse(wordCount.ContainsDiagnostics());
        Assert.IsTrue(wordCount.Value > 5 && wordCount.Value <= 100);
    }
    

    Suggerimento

    Le metriche includono una proprietà Reason che spiega il motivo del punteggio. Il motivo è incluso nel report generato e può essere visualizzato facendo clic sull'icona delle informazioni nella scheda della metrica corrispondente.

  10. Aggiungere infine il metodo di test stesso.

    [TestMethod]
    public async Task SampleAndEvaluateResponse()
    {
        // Create a <see cref="ScenarioRun"/> with the scenario name
        // set to the fully qualified name of the current test method.
        await using ScenarioRun scenarioRun =
            await s_defaultReportingConfiguration.CreateScenarioRunAsync(
                ScenarioName,
                additionalTags: ["Moon"]);
    
        // Use the <see cref="IChatClient"/> that's included in the
        // <see cref="ScenarioRun.ChatConfiguration"/> to get the LLM response.
        (IList<ChatMessage> messages, ChatResponse modelResponse) = await GetAstronomyConversationAsync(
            chatClient: scenarioRun.ChatConfiguration!.ChatClient,
            astronomyQuestion: "How far is the Moon from the Earth at its closest and furthest points?");
    
        // Run the evaluators configured in <see cref="s_defaultReportingConfiguration"/> against the response.
        EvaluationResult result = await scenarioRun.EvaluateAsync(messages, modelResponse);
    
        // Run some basic validation on the evaluation result.
        Validate(result);
    }
    

    Questo metodo di test:

    • Crea il ScenarioRun. L'uso di await using garantisce che la ScenarioRun venga eliminata correttamente e che i risultati di questa valutazione vengano mantenuti correttamente nell'archivio risultati.

    • Ottiene la risposta dell'LLM a una domanda di astronomia specifica. La stessa IChatClient che verrà utilizzata per la valutazione viene passata al metodo GetAstronomyConversationAsync per ottenere il caching delle risposte della risposta principale LLM valutata. Ciò consente inoltre la memorizzazione nella cache delle risposte per i turni LLM usati dagli analizzatori per eseguire le proprie valutazioni internamente. Con la memorizzazione nella cache delle risposte, la risposta LLM viene recuperata:

      • Direttamente dall'endpoint LLM nella prima esecuzione del test corrente o nelle esecuzioni successive se la voce memorizzata nella cache è scaduta (14 giorni, per impostazione predefinita).
      • Dalla cache delle risposte (basata su disco) configurata in s_defaultReportingConfiguration nelle esecuzioni successive del test.
    • Esegue i valutatori sulla risposta. Analogamente alla risposta LLM, nelle esecuzioni successive, la valutazione viene recuperata dalla cache delle risposte (basata su disco) configurata in s_defaultReportingConfiguration.

    • Esegue una convalida di base sul risultato della valutazione.

      Questo passaggio è facoltativo e principalmente a scopo dimostrativo. Nelle valutazioni reali, è possibile non convalidare i singoli risultati perché le risposte e i punteggi di valutazione LLM possono cambiare nel tempo man mano che il prodotto (e i modelli usati) si evolvono. Potresti non voler che i singoli test di valutazione "falliscano" e blocchino le compilazioni nelle pipeline CI/CD quando ciò accade. Al contrario, potrebbe essere preferibile basarsi sul report generato e monitorare le tendenze generali dei punteggi di valutazione nei diversi scenari nel tempo (e consentire il fallimento delle singole compilazioni solo quando c'è un calo significativo dei punteggi di valutazione in più test diversi). Detto questo, c'è una certa sfumatura qui e la scelta di se convalidare singoli risultati o meno può variare a seconda del caso d'uso specifico.

    Quando il metodo termina, l'oggetto scenarioRun viene smaltito e il risultato della valutazione viene archiviato nell'archivio dei risultati (su disco) configurato in s_defaultReportingConfiguration.

Eseguire il test/valutazione

Eseguire il test usando il flusso di lavoro di test preferito, ad esempio usando il comando dell'interfaccia della riga di comando dotnet test o tramite Esplora Test.

Generare un rapporto

  1. Installare lo strumento Microsoft.Extensions.AI.Evaluation.Console .NET eseguendo il comando seguente da una finestra del terminale:

    dotnet tool install --local Microsoft.Extensions.AI.Evaluation.Console
    
  2. Generare un report eseguendo il comando seguente:

    dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.html
    
  3. Aprire il file report.html. Dovrebbe avere un aspetto simile al seguente.

    Screenshot del report di valutazione che mostra i valori della conversazione e delle metriche.

Passaggi successivi