このチュートリアルでは、OpenAI モデルのチャット応答を評価する MSTest アプリを作成します。 テスト アプリでは、Microsoft.Extensions.AI.Evaluation ライブラリを利用して、評価を実行し、モデルの応答をキャッシュし、レポートを作成します。 このチュートリアルでは、組み込みエバリュエーターとカスタム エバリュエーターの両方を使用します。 組み込みの品質エバリュエーター (
[前提条件]
AI サービスを構成する
Azure ポータルを使用してAzure OpenAI serviceとモデルをプロビジョニングするには、「Create and deploy an Azure OpenAI Service resource」の手順を実行します。 [モデルのデプロイ] ステップで、 gpt-5 モデルを選択します。
テスト アプリを作成する
AI モデルに接続する MSTest プロジェクトを作成するには、次の手順を実行します。
ターミナル ウィンドウで、アプリを作成するディレクトリに移動し、
dotnet newコマンドを使用して新しい MSTest アプリを作成します。dotnet new mstest -o TestAIWithReportingTestAIWithReportingディレクトリに移動し、必要なパッケージをアプリに追加します。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次のコマンドを実行して、Azure OpenAI エンドポイントとテナント ID にapp シークレットを追加します。
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>(環境によっては、テナント ID が必要ない場合があります。その場合は、 DefaultAzureCredentialをインスタンス化するコードから削除します。
任意のエディターで新しいアプリを開きます。
テスト アプリ コードを追加する
Test1.cs ファイルの名前を MyTests.cs に変更し、ファイルを開き、クラスの名前を
MyTestsに変更します。 空のTestMethod1メソッドを削除します。必要な
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;クラスに TestContext プロパティを追加します。
// The value of the TestContext property is populated by MSTest. public TestContext? TestContext { get; set; }GetAzureOpenAIChatConfigurationメソッドを追加します。エバリュエーターがモデルとの通信に使用するIChatClientを作成します。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); }レポート機能を設定します。
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は以下を識別します。
- ScenarioRunを呼び出すことによって作成される各CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken)に対して呼び出す必要があるエバリュエーターのセット。
- エバリュエーターが使用する LLM エンドポイント ( ReportingConfiguration.ChatConfigurationを参照)。
- シナリオの実行結果を格納する方法と場所。
- シナリオの実行に関連する LLM 応答をキャッシュする方法。
- シナリオの実行の結果を報告するときに使用する実行名。
このテストでは、ディスク ベースのレポート構成を使用します。
別のファイルに、
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 の間にある場合、メトリックは適切 (許容可能) と見なされます。 それ以外の場合、メトリックは失敗したと見なされます。 呼び出し元は、必要に応じて、この既定の解釈をオーバーライドできます。MyTests.csに戻り、評価で使用するエバリュエーターを収集するメソッドを追加します。private static IEnumerable<IEvaluator> GetEvaluators() { IEvaluator relevanceEvaluator = new RelevanceEvaluator(); IEvaluator coherenceEvaluator = new CoherenceEvaluator(); IEvaluator wordCountEvaluator = new WordCountEvaluator(); return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator]; }システム プロンプト 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 エンドポイントや質問の質問などの要求パラメーターが変更されるまで再利用されます。
応答を検証するメソッドを追加します。
/// <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プロパティが含まれています。 その理由は 生成されたレポート に含まれており、対応するメトリックのカードの情報アイコンをクリックして表示できます。最後に、 テスト メソッド 自体を追加します。
[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を
GetAstronomyConversationAsyncメソッドに渡し、評価対象のプライマリLLM応答の応答キャッシュを取得します。 (同じクライアントを渡すと、評価者が内部的に評価を実行するために使用する LLM ターンの応答キャッシュも有効になります)。応答キャッシュでは、LLM 応答は次のいずれかをフェッチします。- 現在のテストの最初の実行で LLM エンドポイントから直接、またはキャッシュされたエントリの有効期限が切れている場合 (既定では 14 日間) 以降の実行。
- テストの後続の実行で
s_defaultReportingConfigurationに構成された (ディスク ベースの) 応答キャッシュから。
応答に対してエバリュエーターを実行します。 LLM 応答と同様に、後続の実行では、
s_defaultReportingConfigurationで構成された (ディスク ベースの) 応答キャッシュから評価がフェッチされます。評価結果に対して基本的な検証を実行します。
この手順は省略可能であり、主にデモンストレーション用です。 実際の評価では、製品 (および使用されるモデル) の進化に伴って LLM の応答と評価スコアが変化する可能性があるため、個々の結果を検証したくない場合があります。 結果が変化したときに、個々の評価テストで CI/CD パイプライン内のビルドを "失敗" したりブロックしたりしたくない場合があります。 代わりに、生成されたレポートに依存し、時間の経過に伴うさまざまなシナリオでの評価スコアの全体的な傾向を追跡することをお勧めします (また、複数の異なるテストで評価スコアが大幅に低下した場合にのみ個々のビルドが失敗します)。 しかし、ここではいくつかの微妙な違いがあり、個々の結果を検証するかどうかの選択は、特定のユース ケースによって異なる場合があります。
メソッドが返すと、
scenarioRunオブジェクトは破棄され、評価結果はs_defaultReportingConfigurationで構成されている (ディスク ベースの) 結果ストアに格納されます。
テスト/評価を実行する
CLI コマンド dotnet test または テスト エクスプローラーを使用して、好みのテスト ワークフローを使用してテストを実行します。
レポートを生成する
Microsoft.Extensions.AI.Evaluation.Console .NET ツールをインストールするには、ターミナル ウィンドウで次のコマンドを実行してください。
dotnet tool install --create-manifest-if-needed Microsoft.Extensions.AI.Evaluation.Console次のコマンドを実行してレポートを生成します。
dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.htmlreport.htmlファイルを開きます。 レポートは次のスクリーンショットのようになります。
次のステップ
- テスト結果が格納されているディレクトリに移動します (
C:\TestReportsの作成時に場所を変更しない限り、ReportingConfiguration)。resultsサブディレクトリには、タイムスタンプ (ExecutionName) で名前が付けられた各テスト実行のフォルダーがあることに注意してください。 これらの各フォルダー内には、各シナリオ名のフォルダーがあります。この場合は、プロジェクト内の 1 つのテスト メソッドだけです。 そのフォルダーには、メッセージ、応答、評価結果を含むすべてのデータを含む JSON ファイルが含まれています。 - 評価を拡充します。 いくつかのアイデアを次に示します。
- AI を使用して応答で使用される測定システムを決定するエバリュエーターなど、別のカスタム エバリュエーターを追加します。
- LLM からの 複数の応答を評価するメソッド など、別のテスト メソッドを追加します。 各回答は異なる場合があるため、質問に対する少なくともいくつかの回答をサンプリングして評価することをお勧めします。 この場合、 CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken)を呼び出すたびにイテレーション名を指定します。
.NET