次の方法で共有


チュートリアル: キャッシュとレポートを使用して応答品質を評価する

このチュートリアルでは、OpenAI モデルのチャット応答を評価する MSTest アプリを作成します。 テスト アプリでは 、Microsoft.Extensions.AI.Evaluation ライブラリを使用して評価を実行し、モデルの応答をキャッシュし、レポートを作成します。 このチュートリアルでは、組み込みエバリュエーターとカスタム エバリュエーターの両方を使用します。 ( Microsoft.Extensions.AI.Evaluation.Quality パッケージの) 組み込みの品質エバリュエーターは、LLM を使用して評価を実行します。カスタム エバリュエーターは AI を使用しません。

[前提条件]

AI サービスを構成する

Azure portal を使用して Azure OpenAI サービスとモデルをプロビジョニングするには、「Azure OpenAI Service リソースの作成とデプロイ」 記事の手順を実行します。 [モデルのデプロイ] ステップで、 gpt-4o モデルを選択します。

テスト アプリを作成する

gpt-4o AI モデルに接続する MSTest プロジェクトを作成するには、次の手順を実行します。

  1. ターミナル ウィンドウで、アプリを作成するディレクトリに移動し、 dotnet new コマンドを使用して新しい MSTest アプリを作成します。

    dotnet new mstest -o TestAIWithReporting
    
  2. TestAIWithReporting ディレクトリに移動し、必要なパッケージをアプリに追加します。

    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. 次のコマンドを実行して、Azure OpenAI エンドポイント、モデル名、テナント 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>
    

    (環境によっては、テナント ID が必要ない場合があります。その場合は、 DefaultAzureCredentialをインスタンス化するコードから削除します。

  4. 任意のエディターで新しいアプリを開きます。

テスト アプリ コードを追加する

  1. Test1.cs ファイルの名前を MyTests.cs に変更し、ファイルを開き、クラスの名前を MyTests に変更します。 空の TestMethod1 メソッドを削除します。

  2. 必要な using ディレクティブをファイルの先頭に追加します。

    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. クラスに TestContext プロパティを追加します。

    // The value of the TestContext property is populated by MSTest.
    public TestContext? TestContext { get; set; }
    
  4. GetAzureOpenAIChatConfiguration メソッドを追加します。エバリュエーターがモデルとの通信に使用するIChatClientを作成します。

    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. レポート機能を設定します。

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

    シナリオ名

    シナリオ名は、現在のテスト メソッドの完全修飾名に設定されます。 ただし、 CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken)を呼び出すときに任意の任意の文字列に設定できます。 シナリオ名を選択する際の考慮事項を次に示します。

    • ディスク ベースのストレージを使用する場合、シナリオ名は、対応する評価結果が格納されるフォルダーの名前として使用されます。 そのため、名前を適度に短くし、ファイル名とディレクトリ名で使用できない文字は避けてください。
    • 既定では、生成された評価レポートは . のシナリオ名を分割して、適切なグループ化、入れ子、集計を含む階層ビューに結果を表示できるようにします。 これは、シナリオ名が対応するテスト メソッドの完全修飾名に設定されている場合に特に便利です。これは、結果を階層内の名前空間とクラス名でグループ化できるためです。 ただし、独自のカスタム シナリオ名にピリオド (.) を含めて、シナリオに最適なレポート階層を作成することで、この機能を利用することもできます。

    実行名

    実行名は、評価結果が格納されるときに、同じ評価実行 (またはテスト実行) の一部である評価結果をグループ化するために使用されます。 ReportingConfigurationの作成時に実行名を指定しない場合、すべての評価実行で同じ既定の実行名Defaultが使用されます。 この場合、ある実行の結果は次の実行によって上書きされ、異なる実行間で結果を比較する機能が失われます。

    この例では、実行名としてタイムスタンプを使用します。 プロジェクトに複数のテストがある場合は、テスト全体で使用されるすべてのレポート構成で同じ実行名を使用して、結果が正しくグループ化されていることを確認します。

    より現実的なシナリオでは、複数の異なるアセンブリに存在し、異なるテスト プロセスで実行される評価テスト間で同じ実行名を共有することもできます。 このような場合は、テストを実行する前に、スクリプトを使用して、適切な実行名 (CI/CD システムによって割り当てられている現在のビルド番号など) で環境変数を更新できます。 または、ビルド システムで単調に増加するアセンブリ ファイルのバージョンが生成される場合は、テスト コード内から AssemblyFileVersionAttribute を読み取り、実行名として使用して、異なる製品バージョン間で結果を比較できます。

    レポートの構成

    ReportingConfigurationは以下を識別します。

    このテストでは、ディスク ベースのレポート構成を使用します。

  6. 別のファイルに、 WordCountEvaluator クラスを追加します。これは、 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));
        }
    }
    

    WordCountEvaluatorは、応答に存在する単語の数をカウントします。 一部のエバリュエーターとは異なり、AI に基づいていません。 EvaluateAsync メソッドは、ワード カウントを含むEvaluationResultを含むNumericMetricを返します。

    EvaluateAsync メソッドでは、既定の解釈もメトリックにアタッチされます。 既定の解釈では、検出された単語数が 6 から 100 の間にある場合、メトリックは適切 (許容可能) と見なされます。 それ以外の場合、メトリックは失敗したと見なされます。 この既定の解釈は、必要に応じて呼び出し元によってオーバーライドできます。

  7. MyTests.csに戻り、評価で使用するエバリュエーターを収集するメソッドを追加します。

    private static IEnumerable<IEvaluator> GetEvaluators()
    {
        IEvaluator relevanceEvaluator = new RelevanceEvaluator();
        IEvaluator coherenceEvaluator = new CoherenceEvaluator();
        IEvaluator wordCountEvaluator = new WordCountEvaluator();
    
        return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator];
    }
    
  8. システム プロンプト ChatMessageを追加し、 チャット オプションを定義し、特定の質問に対する応答をモデルに求めるメソッドを追加します。

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

    このチュートリアルのテストでは、天文学の質問に対する LLM の応答を評価します。 ReportingConfigurationでは応答キャッシュが有効になっており、指定されたIChatClientはこのレポート構成を使用して作成されたScenarioRunから常にフェッチされるため、テストの LLM 応答はキャッシュされ、再利用されます。 応答は、対応するキャッシュ エントリの有効期限が切れる (既定では 14 日以内)、または LLM エンドポイントや質問などの要求パラメーターが変更されるまで再利用されます。

  9. 応答を検証するメソッドを追加します。

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

    ヒント

    各メトリックには、スコアの理由を説明する Reason プロパティが含まれています。 その理由は 生成されたレポート に含まれており、対応するメトリックのカードの情報アイコンをクリックして表示できます。

  10. 最後に、 テスト メソッド 自体を追加します。

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

    このテスト メソッド:

    • ScenarioRunを作成します。 await usingを使用すると、ScenarioRunが正しく破棄され、この評価の結果が結果ストアに正しく保持されます。

    • 特定の天文学の質問に対する LLM の応答を取得します。 評価に使用されるのと同じIChatClientが、評価対象のプライマリ LLM GetAstronomyConversationAsync取得するために、 メソッドに渡されます。 (さらに、これにより、評価者が内部的に評価を実行するために使用する LLM ターンの応答キャッシュが有効になります)。応答キャッシュでは、LLM 応答は次のいずれかをフェッチします。

      • 現在のテストの最初の実行で LLM エンドポイントから直接、またはキャッシュされたエントリの有効期限が切れている場合 (既定では 14 日間) 以降の実行。
      • テストの後続の実行で s_defaultReportingConfiguration で構成された (ディスク ベースの) 応答キャッシュから。
    • 応答に対してエバリュエーターを実行します。 LLM 応答と同様に、後続の実行では、評価は、 s_defaultReportingConfigurationで構成された (ディスク ベースの) 応答キャッシュからフェッチされます。

    • 評価結果に対して基本的な検証を実行します。

      この手順は省略可能であり、主にデモンストレーション用です。 実際の評価では、製品 (および使用されるモデル) の進化に伴って LLM の応答と評価スコアが変化する可能性があるため、個々の結果を検証したくない場合があります。 このような場合、個々の評価テストが「失敗」となり、CI/CD パイプライン内でビルドをブロックするのを避けたいことがあります。 代わりに、生成されたレポートに依存し、時間の経過に伴うさまざまなシナリオでの評価スコアの全体的な傾向を追跡することをお勧めします (また、複数の異なるテストで評価スコアが大幅に低下した場合にのみ個々のビルドが失敗します)。 しかし、ここではいくつかの微妙な違いがあり、個々の結果を検証するかどうかの選択は、特定のユース ケースによって異なる場合があります。

    メソッドから制御が戻ると、scenarioRun オブジェクトが破棄され、その評価結果が s_defaultReportingConfiguration で構成された(ディスクベースの)結果ストアに格納されます。

テスト/評価を実行する

CLI コマンド dotnet test または テスト エクスプローラーを使用して、好みのテスト ワークフローを使用してテストを実行します。

レポートを生成する

  1. ターミナル ウィンドウから次のコマンドを実行して、 Microsoft.Extensions.AI.Evaluation.Console .NET ツールをインストールします。

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

    ヒント

    最初にマニフェスト ファイルを作成することが必要な場合があります。 その詳細とローカル ツールのインストールについては、「 ローカル ツール」を参照してください。

  2. 次のコマンドを実行してレポートを生成します。

    dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.html
    
  3. report.html ファイルを開きます。 次のようになります。

    会話とメトリックの値を示す評価レポートのスクリーンショット。

次のステップ

  • テスト結果が格納されているディレクトリに移動します (C:\TestReportsの作成時に場所を変更しない限り、ReportingConfiguration)。 resultsサブディレクトリには、タイムスタンプ (ExecutionName) で名前が付けられた各テスト実行のフォルダーがあることに注意してください。 これらの各フォルダー内には、各シナリオ名のフォルダーがあります。この場合は、プロジェクト内の 1 つのテスト メソッドだけです。 そのフォルダーには、メッセージ、応答、評価結果を含むすべてのデータを含む JSON ファイルが含まれています。
  • 評価を拡充します。 いくつかのアイデアを次に示します。