Oktatóanyag: Válaszminőség kiértékelése gyorsítótárazással és jelentéskészítéssel

Ebben az oktatóanyagban egy MSTest-alkalmazást hoz létre egy OpenAI-modell csevegési válaszának kiértékeléséhez. A tesztalkalmazás a Microsoft.Extensions.AI.Evaluation kódtárakat használja az értékelések elvégzéséhez, a modellválaszok gyorsítótárazásához és jelentések létrehozásához. Az oktatóanyag beépített és egyéni kiértékelőket is használ. A beépített minőségi kiértékelők (a Microsoft.Extensions.AI.Evaluation.Quality csomag) LLM-et használnak a kiértékelések végrehajtásához; az egyéni kiértékelő nem alkalmaz mesterséges intelligenciát.

Előfeltételek

Az AI-szolgáltatás konfigurálása

Ha Azure OpenAI service és modellt szeretne kiépíteni a Azure portálon, végezze el a A Azure OpenAI Service erőforrás létrehozása és üzembe helyezése cikkben leírt lépéseket. A "Modell üzembe helyezése" lépésben válassza ki a gpt-5 modellt.

A tesztalkalmazás létrehozása

Hajtsa végre az alábbi lépéseket egy AI-modellhez csatlakozó MSTest-projekt létrehozásához.

  1. Egy terminálablakban lépjen arra a könyvtárra, ahol létre szeretné hozni az alkalmazást, és hozzon létre egy új MSTest-alkalmazást a dotnet new következő paranccsal:

    dotnet new mstest -o TestAIWithReporting
    
  2. Lépjen a TestAIWithReporting könyvtárra, és adja hozzá a szükséges csomagokat az alkalmazáshoz:

    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
    dotnet add package Microsoft.Extensions.Configuration
    dotnet add package Microsoft.Extensions.Configuration.UserSecrets
    
  3. Futtassa a következő parancsokat alkalmazás titkos kulcsainak hozzáadásához a Azure OpenAI-végponthoz és a bérlőazonosítóhoz:

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

    (A környezettől függően előfordulhat, hogy a bérlőazonosítóra nincs szükség. Ebben az esetben távolítsa el a DefaultAzureCredential példányt a jelölő kódból.)

  4. Nyissa meg az új alkalmazást a választott szerkesztőben.

A tesztalkalmazás kódjának hozzáadása

  1. Nevezze át a Test1.cs fájlt MyTests.cs, majd nyissa meg a fájlt, és nevezze át az osztályt a következőreMyTests: . Törölje az üres TestMethod1 metódust.

  2. Adja hozzá a szükséges using irányelveket a fájl elejéhez.

    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. Adja hozzá a TestContext tulajdonságot az osztályhoz.

    // The value of the TestContext property is populated by MSTest.
    public TestContext? TestContext { get; set; }
    
  4. Adja hozzá a GetAzureOpenAIChatConfiguration metódust, amely létrehozza azt, IChatClient amelyet az értékelő a modellel való kommunikációhoz használ.

    private static ChatConfiguration GetAzureOpenAIChatConfiguration()
    {
        IConfigurationRoot config = new ConfigurationBuilder().AddUserSecrets<MyTests>().Build();
    
        string endpoint = config["AZURE_OPENAI_ENDPOINT"];
        string tenantId = config["AZURE_TENANT_ID"];
        string model = "gpt-5";
    
        // 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. A jelentéskészítési funkció beállítása.

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

    Forgatókönyv neve

    A forgatókönyv neve az aktuális tesztmetódus teljes neve. A híváskor azonban bármilyen karakterláncra beállíthatja CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken). A forgatókönyv nevének kiválasztásakor vegye figyelembe ezeket a tényezőket:

    • Lemezalapú tároló használata esetén a forgatókönyv neve annak a mappának a neve, amelyben a megfelelő kiértékelési eredményeket tárolja. Ezért érdemes a nevet viszonylag röviden tartani, és elkerülni a fájl- és könyvtárnevekben nem engedélyezett karaktereket.
    • A létrehozott kiértékelési jelentés alapértelmezés szerint felosztja a forgatókönyvek nevét . , így az eredmények hierarchikus nézetben jelennek meg, megfelelő csoportosítással, beágyazással és összesítéssel. A hierarchikus nézet különösen akkor hasznos, ha a forgatókönyv neve a megfelelő tesztmetódus teljes neve, mert névterek és osztálynevek szerint csoportosítja az eredményeket a hierarchiában. Ezt a funkciót azonban kihasználhatja úgy is, hogy pontokat (.) a saját egyéni forgatókönyvek neveibe is beiktatva létrehoz egy olyan jelentési hierarchiát, amely a legjobban megfelel a forgatókönyveinek.

    Végrehajtási név

    A végrehajtási név arra szolgál, hogy csoportosítsa a kiértékelési eredményeket, amelyek ugyanahhoz a próbafuttatáshoz (vagy tesztfuttatáshoz) tartoznak a kiértékelési eredmények tárolásakor. Ha nem ad meg végrehajtási nevet a ReportingConfigurationlétrehozáskor, az összes kiértékelési futtatás ugyanazt az alapértelmezett végrehajtási nevet Defaulthasználja. Ebben az esetben az egyik futtatás eredményeit felülírja a következő, és nem tudja összehasonlítani az eredményeket a különböző futtatások között.

    Ez a példa egy időbélyeget használ a végrehajtás neveként. Ha egynél több teszt van a projektben, győződjön meg arról, hogy az eredmények megfelelően vannak csoportosítva ugyanazzal a végrehajtási névvel a tesztek során használt összes jelentéskonfigurációban.

    Egy valósabb forgatókönyvben érdemes lehet ugyanazt a végrehajtási nevet megosztani a különböző szerelvényekben található és különböző tesztfolyamatokban végzett értékelési tesztek között. Ilyen esetekben szkripttel frissíthet egy környezeti változót a megfelelő végrehajtási névvel (például a CI/CD-rendszer által hozzárendelt aktuális buildszámmal), mielőtt futtatná a teszteket. Vagy ha a buildelési rendszer monoton módon növekvő szerelvényfájl-verziókat állít elő, a tesztkódból elolvashatja a AssemblyFileVersionAttribute forrást, és végrehajtási névként használhatja az eredményeket a különböző termékverziók eredményeinek összehasonlításához.

    Jelentéskészítési konfiguráció

    A ReportingConfiguration azonosít:

    Ez a teszt lemezalapú jelentéskészítési konfigurációt használ.

  6. Egy külön fájlban adja hozzá az WordCountEvaluator osztályt, amely egy egyéni kiértékelő, és a(z) IEvaluator-t implementálja.

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

    A WordCountEvaluator válaszban szereplő szavak számát számítja ki. Egyes kiértékelőktől eltérően ez nem AI-n alapul. A EvaluateAsync metódus egy EvaluationResult olyan értéket ad vissza, amely tartalmazza NumericMetric a szavak számát.

    A EvaluateAsync metódus egy alapértelmezett értelmezést is csatol a metrikához. Az alapértelmezett értelmezés jónak (elfogadhatónak) tekinti a metrikát, ha az észlelt szószám 6 és 100 között van. Ellenkező esetben a metrika sikertelennek minősül. A hívó szükség esetén felülbírálhatja ezt az alapértelmezett értelmezést.

  7. A MyTests.cs helyen adjon hozzá egy metódust, amely összegyűjti a kiértékelőket, hogy felhasználásra kerüljenek az értékelés során.

    private static IEnumerable<IEvaluator> GetEvaluators()
    {
        IEvaluator relevanceEvaluator = new RelevanceEvaluator();
        IEvaluator coherenceEvaluator = new CoherenceEvaluator();
        IEvaluator wordCountEvaluator = new WordCountEvaluator();
    
        return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator];
    }
    
  8. Adjon hozzá egy metódust egy rendszerkérés ChatMessagehozzáadásához, határozza meg a csevegési beállításokat, és kérjen választ a modelltől egy adott kérdésre.

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

    Az oktatóanyagban szereplő teszt kiértékeli az LLM válaszát egy csillagászati kérdésre. Mivel a ReportingConfiguration válasz gyorsítótárazása engedélyezve van, és mivel a megadott IChatClient adatokat a rendszer mindig ezzel a ScenarioRun jelentéskészítési konfigurációval hozza létre, a rendszer gyorsítótárazza és újra felhasználja a teszt LLM-válaszát. A rendszer mindaddig újra felhasználja a választ, amíg a megfelelő gyorsítótárbejegyzés le nem jár (alapértelmezés szerint 14 napon belül), vagy amíg bármilyen kérésparaméter , például az LLM-végpont vagy a feltett kérdés nem változik.

  9. Adjon hozzá egy metódust a válasz ellenőrzéséhez.

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

    Jótanács

    A metrikák mindegyike tartalmaz egy tulajdonságot Reason , amely elmagyarázza a pontszám indoklását. Ennek oka szerepel a létrehozott jelentésben , és a megfelelő metrika kártyáján az információ ikonra kattintva tekinthető meg.

  10. Végül adja hozzá magát a vizsgálati módszert .

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

    Ez a vizsgálati módszer:

    • Létrehozza a ScenarioRun. await using biztosítja a ScenarioRun kiértékelési eredmények megfelelő ártalmatlanítását és helyes megőrzését az eredménytárolóba.

    • Megkapja az LLM válaszát egy adott csillagászati kérdésre. A teszt ugyanazt IChatClient adja át, mint amelyet a kiértékeléshez használnak a GetAstronomyConversationAsync kiértékelendő elsődleges LLM-válasz válasz gyorsítótárazásának lekérésére szolgáló metódusnak. (Ugyanannak az ügyfélnek az átadása lehetővé teszi a válaszok gyorsítótárazását az LLM interakciós fordulókhoz, amelyeket a kiértékelők belső értékeléseik során alkalmaznak.) A válaszok gyorsítótárazásával az LLM válaszai a következő módon lesznek elérhetők:

      • Közvetlenül az LLM-végpontról az aktuális teszt első futtatásakor, vagy az azt követő futtatások során, ha a gyorsítótárazott bejegyzés lejárt (alapértelmezés szerint 14 nap).
      • A teszt későbbi futtatásaiban a s_defaultReportingConfiguration-ben konfigurált lemezalapú válaszgyorsítótárból.
    • A kiértékelőket futtatja a válasz ellenőrzéséhez. Az LLM-válaszhoz hasonlóan az azt követő futtatások is lekérik a kiértékelést a (lemezalapú) válaszgyorsítótárból, amely a következőben konfigurálva van s_defaultReportingConfiguration.

    • Elvégez néhány alapvető érvényesítést a kiértékelési eredményen.

      Ez a lépés nem kötelező, és elsősorban bemutató célokra szolgál. A valós értékelésekben előfordulhat, hogy nem szeretné ellenőrizni az egyes eredményeket, mert az LLM-válaszok és a kiértékelési pontszámok idővel változhatnak a termék (és a használt modellek) fejlődésével. Előfordulhat, hogy nem szeretné, hogy az egyes kiértékelési tesztek "sikertelenek", és az eredmények változásakor blokkolják a CI-/CD-folyamatok buildjeit. Ehelyett lehet, hogy célszerűbb lenne a létrehozott jelentésre támaszkodni, és az értékelési pontszámok általános trendjeit nyomon követni különböző forgatókönyvek esetén, hosszabb időszakon át (és csak akkor legyen sikertelen egyes build-ek futtatása, amikor több különböző teszt során jelentős csökkenés tapasztalható az értékelési pontszámokban). Mindazonáltal van némi árnyalat ebben, és az a választás, hogy ellenőrizzük-e az egyes eredményeket vagy sem, a konkrét használati esettől függően változhat.

    Amikor a metódus visszatér, az scenarioRun objektum el lesz helyezve, és a kiértékelési eredmény a (lemezalapú) eredménytárolóban lesz tárolva, amely a következőben s_defaultReportingConfigurationvan konfigurálva: .

A teszt/kiértékelés futtatása

Futtassa a tesztet az előnyben részesített tesztelési munkafolyamattal, például a CLI paranccsal dotnet test vagy a Test Explorer használatával.

Jelentés létrehozása

  1. Telepítse a Microsoft. Extensions.AI.Evaluation.Console .NET eszköz egy terminálablakból a következő parancs futtatásával:

    dotnet tool install --create-manifest-if-needed Microsoft.Extensions.AI.Evaluation.Console
    
  2. Hozzon létre egy jelentést a következő parancs futtatásával:

    dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.html
    
  3. Nyissa meg a(z) report.html fájlt. A jelentés az alábbi képernyőképhez hasonlóan néz ki.

    A beszélgetés és a metrikaértékeket megjelenítő kiértékelési jelentés képernyőképe.

Következő lépések