Självstudie: Utvärdera svarskvalitet med cachelagring och rapportering

I den här självstudien skapar du en MSTest-app för att utvärdera chattsvaret för en OpenAI-modell. Testappen använder biblioteken Microsoft.Extensions.AI.Evaluation för att utföra utvärderingarna, cachelagrar modellsvaren och skapar rapporter. I självstudien används både inbyggda och anpassade utvärderare. De inbyggda kvalitetsutvärderingarna (från paketet Microsoft.Extensions.AI.Evaluation.Quality) använder en LLM för att utföra utvärderingar. den anpassade utvärderaren använder inte AI.

Förutsättningar

Konfigurera AI-tjänsten

Om du vill etablera en Azure OpenAI-tjänst och modell med hjälp av Azure-portalen slutför du stegen i artikeln Skapa och distribuera en Azure OpenAI-tjänstresurs. I steget "Distribuera en modell" väljer du gpt-4o modellen.

Skapa testappen

Slutför följande steg för att skapa ett MSTest-projekt som ansluter till gpt-4o AI-modellen.

  1. I ett terminalfönster navigerar du till katalogen där du vill skapa din app och skapar en ny MSTest-app med dotnet new kommandot :

    dotnet new mstest -o TestAIWithReporting
    
  2. Gå till katalogen TestAIWithReporting och lägg till nödvändiga paket i din 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. Kör följande kommandon för att lägga till apphemligheter för din Azure OpenAI-slutpunkt, modellnamn och klientorganisations-ID:

    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>
    

    (Beroende på din miljö kanske klientorganisations-ID:t inte behövs. I så fall tar du bort den från koden som instansierar DefaultAzureCredential.)

  4. Öppna den nya appen i valfri redigerare.

Lägg till testappkoden

  1. Byt namn på filen Test1.cs till MyTests.cs och öppna sedan filen och byt namn på klassen till MyTests. Ta bort den tomma TestMethod1 metoden.

  2. Lägg till nödvändiga using direktiv överst i filen.

    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. Lägg till egenskapen TestContext i klassen.

    // The value of the TestContext property is populated by MSTest.
    public TestContext? TestContext { get; set; }
    
  4. Lägg till metoden GetAzureOpenAIChatConfiguration , som skapar den IChatClient som utvärderaren använder för att kommunicera med modellen.

    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. Konfigurera rapporteringsfunktionen.

    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);
    

    Scenarionamn

    Scenarionamnet är inställt på det fullständigt kvalificerade namnet på den aktuella testmetoden. Du kan dock ange den till valfri sträng när du anropar CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken). Här följer några saker att tänka på när du väljer ett scenarionamn:

    • När du använder diskbaserad lagring används scenarionamnet som namnet på den mapp under vilken motsvarande utvärderingsresultat lagras. Därför är det en bra idé att hålla namnet någorlunda kort och undvika tecken som inte tillåts i fil- och katalognamn.
    • Som standard delar den genererade utvärderingsrapporten scenarionamn på . så att resultaten kan visas i en hierarkisk vy med lämplig gruppering, kapsling och aggregering. Detta är särskilt användbart i fall där scenarionamnet är inställt på det fullständigt kvalificerade namnet på motsvarande testmetod, eftersom det gör att resultaten kan grupperas efter namnområden och klassnamn i hierarkin. Du kan dock också dra nytta av den här funktionen genom att inkludera perioder (.) i dina egna anpassade scenarionamn för att skapa en rapporteringshierarki som fungerar bäst för dina scenarier.

    Körningsnamn

    Körningsnamnet används för att gruppera utvärderingsresultat som ingår i samma utvärderingskörning (eller testkörning) när utvärderingsresultaten lagras. Om du inte anger något körningsnamn när du skapar en ReportingConfigurationanvänder alla utvärderingskörningar samma standardkörningsnamn som Default. I det här fallet skrivs resultatet från en körning över av nästa och du förlorar möjligheten att jämföra resultat mellan olika körningar.

    I det här exemplet används en tidsstämpel som körningsnamn. Om du har fler än ett test i projektet kontrollerar du att resultaten grupperas korrekt med samma körningsnamn i alla rapporteringskonfigurationer som används i testerna.

    I ett mer verkligt scenario kanske du också vill dela samma körningsnamn mellan utvärderingstester som finns i flera olika sammansättningar och som körs i olika testprocesser. I sådana fall kan du använda ett skript för att uppdatera en miljövariabel med ett lämpligt körningsnamn (till exempel det aktuella versionsnumret som tilldelats av CI/CD-systemet) innan du kör testerna. Eller om byggsystemet producerar monotont ökande sammansättningsfilversioner kan du läsa AssemblyFileVersionAttribute inifrån testkoden och använda den som körningsnamn för att jämföra resultat mellan olika produktversioner.

    Rapporteringskonfiguration

    En ReportingConfiguration identifierar: följande

    Det här testet använder en diskbaserad rapporteringskonfiguration.

  6. I en separat fil lägger du till WordCountEvaluator klassen, som är en anpassad utvärderare som implementerar 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));
        }
    }
    

    Räknar WordCountEvaluator antalet ord som finns i svaret. Till skillnad från vissa utvärderare baseras den inte på AI. Metoden EvaluateAsync returnerar en EvaluationResult innehåller en NumericMetric som innehåller ordantalet.

    Metoden EvaluateAsync kopplar också en standardtolkning till måttet. Standardtolkningen anser att måttet är bra (acceptabelt) om det identifierade ordantalet är mellan 6 och 100. Annars anses metrik vara misslyckad. Den här standardtolkningen kan åsidosättas av anroparen om det behövs.

  7. Återvänd till MyTests.cs och lägg till en metod för att samla in de utvärderare som ska användas vid utvärderingen.

    private static IEnumerable<IEvaluator> GetEvaluators()
    {
        IEvaluator relevanceEvaluator = new RelevanceEvaluator();
        IEvaluator coherenceEvaluator = new CoherenceEvaluator();
        IEvaluator wordCountEvaluator = new WordCountEvaluator();
    
        return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator];
    }
    
  8. Lägg till en metod för att lägga till en systemprompt ChatMessage, definiera chattalternativen och be modellen om ett svar på en viss fråga.

    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);
    }
    

    Testet i denna handledning utvärderar LLM:s svar på en astronomifråga. Eftersom svarscachelagringen ReportingConfiguration är aktiverad och eftersom den angivna IChatClient alltid hämtas från ScenarioRun som skapats med den här rapporteringskonfigurationen, cachelagras och återanvänds LLM-svaret för testet. Svaret återanvänds tills motsvarande cachepost upphör att gälla (om 14 dagar som standard) eller tills någon begäransparameter, till exempel LLM-slutpunkten eller frågan som ställs, ändras.

  9. Lägg till en metod för att verifiera svaret.

    /// <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);
    }
    

    Tips/Råd

    Måtten innehåller var och en en Reason egenskap som förklarar resonemanget för poängen. Orsaken ingår i den genererade rapporten och kan visas genom att klicka på informationsikonen på motsvarande måttkort.

  10. Lägg slutligen till själva testmetoden .

    [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);
    }
    

    Den här testmetoden:

    • Skapar ScenarioRun. Användningen av await using säkerställer att ScenarioRun är korrekt bortskaffad och att resultatet av den här utvärderingen sparas korrekt i resultatarkivet.

    • Hämtar LLM:s svar på en specifik astronomifråga. Samma IChatClient som kommer att användas för utvärdering skickas till GetAstronomyConversationAsync-metoden för att möjliggöra svarscachelagring för det primära LLM-svaret som utvärderas. (Dessutom möjliggör detta cachelagring av svar för DE LLM-svängar som utvärderarna använder för att utföra sina utvärderingar internt.) Med cachelagring av svar hämtas LLM-svaret antingen:

      • Direkt från LLM-slutpunkten vid den första körningen av det aktuella testet, eller vid efterföljande körningar om den cachelagrade posten har utgått (14 dagar, som standard).
      • Från den (diskbaserade) svarscache som konfigurerades i s_defaultReportingConfiguration vid senare körningar av testet.
    • Kör utvärderarna mot svaret. Liksom LLM-svaret hämtas utvärderingen vid efterföljande körningar från den (diskbaserade) svarscache som konfigurerades i s_defaultReportingConfiguration.

    • Kör viss grundläggande validering på utvärderingsresultatet.

      Det här steget är valfritt och främst i demonstrationssyfte. I verkliga utvärderingar kanske du inte vill verifiera enskilda resultat eftersom LLM-svar och utvärderingspoäng kan ändras med tiden när din produkt (och de modeller som används) utvecklas. Du kanske inte vill att enskilda utvärderingstester ska "misslyckas" och blockera byggen i DINA CI/CD-pipelines när detta inträffar. I stället kan det vara bättre att använda den genererade rapporten och spåra de övergripande trenderna för utvärderingspoäng över olika scenarier över tid (och endast misslyckas enskilda byggen när det finns en betydande minskning av utvärderingspoängen i flera olika tester). Med detta sagt finns det en viss nyans här och valet om att validera enskilda resultat eller inte kan variera beroende på det specifika användningsfallet.

    När metoden returnerar scenarioRun tas objektet bort och utvärderingsresultatet för utvärderingen lagras i det (diskbaserade) resultatarkivet som har konfigurerats i s_defaultReportingConfiguration.

Kör testet/utvärderingen

Kör testet med ditt önskade testarbetsflöde, till exempel med hjälp av CLI-kommandot dotnet test eller via Test Explorer.

Generera en rapport

  1. Installera verktyget Microsoft.Extensions.AI.Evaluation.Console .NET genom att köra följande kommando från ett terminalfönster:

    dotnet tool install --local Microsoft.Extensions.AI.Evaluation.Console
    

    Tips/Råd

    Du kan behöva skapa en manifestfil först. Mer information om detta och installation av lokala verktyg finns i Lokala verktyg.

  2. Generera en rapport genom att köra följande kommando:

    dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.html
    
  3. Öppna report.html-filen. Det borde se ut ungefär så här.

    Skärmbild av utvärderingsrapporten som visar konversations- och måttvärden.

Nästa steg