Поделиться через


Учебник: Оценка качества отклика с помощью кэширования и составления отчетов

В этом руководстве вы создадите приложение MSTest для оценки ответа чата модели OpenAI. Тестовое приложение использует библиотеки Microsoft.Extensions.AI.Evaluation для выполнения вычислений, кэширования ответов модели и создания отчетов. В этом руководстве используются встроенные и пользовательские оценщики. Встроенные вычислители качества (из пакета Microsoft.Extensions.AI.Evaluation.Quality) используют LLM для выполнения оценок; пользовательский вычислитель не использует ИИ.

Предпосылки

Настройка службы ИИ

Чтобы подготовить службу и модель Azure OpenAI с помощью портала Azure, выполните действия, описанные в статье Создание и развертывание ресурса Azure OpenAI Service. На шаге "Развернуть модель" выберите модель gpt-5.

Создание тестового приложения

Выполните следующие действия, чтобы создать проект MSTest, который подключается к модели ИИ.

  1. В окне терминала перейдите в каталог, в котором вы хотите создать приложение, и создайте новое приложение MSTest с dotnet new помощью команды:

    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:

    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>
    

    (В зависимости от среды идентификатор клиента может быть не нужен. В этом случае удалите его из кода, создающего экземпляр 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 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. Настройте функциональные возможности отчетов.

    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, связанные с выполнением сценария.
    • Имя запуска, которое следует использовать при отчетности о результатах выполнения сценария.

    В этом тесте используется дисковая конфигурация отчетов.

  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 производит подсчет количества слов, присутствующих в ответе. В отличие от некоторых оценщиков, он не основан на ИИ. Метод 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, что будет использоваться для оценки, передается методу GetAstronomyConversationAsync для получения кэширования ответа основного ответа LLM, который оценивается. Кроме того, это позволяет кэшировать ответы для LLM, которые эксперты используют для проведения своих внутренних оценок. При кэшировании ответов возвращается ответ LLM:

      • Непосредственно из конечной точки LLM в первом запуске текущего теста или в последующих запусках, если кэшируемая запись истекла (14 дней по умолчанию).
      • Из дискового кэша ответов, который был настроен в s_defaultReportingConfiguration при последующих запусках теста.
    • Запускает оценщики по отношению к ответу. Как и в ответе LLM, при последующих запусках оценка извлекается из кэша ответов, основанного на диске, который был настроен в s_defaultReportingConfiguration.

    • Выполняет некоторую базовую проверку результатов оценки.

      Этот шаг является необязательным и главным образом для демонстрационных целей. В реальных оценках может не потребоваться проверить отдельные результаты, так как ответы LLM и оценки могут меняться со временем по мере развития продукта (и используемых моделей). Возможно, вы не хотите, чтобы отдельные оценочные тесты «проваливались» и блокировали сборки в конвейерах CI/CD. Вместо этого лучше полагаться на созданный отчет и отслеживать общие тенденции оценочных показателей в разных сценариях с течением времени (и только при сбое отдельных сборок, когда происходит значительное падение оценочных баллов в нескольких различных тестах). То есть есть некоторые нюансы здесь и выбор того, следует ли проверять отдельные результаты или не может отличаться в зависимости от конкретного варианта использования.

    При возврате метода объект scenarioRun освобождается, а результат оценки сохраняется в хранилище результатов на диске, настроенное в s_defaultReportingConfiguration.

Запуск теста или оценки

Запустите тест с помощью предпочтительного рабочего процесса тестирования, например с помощью команды dotnet test CLI или обозревателя тестов.

Создание отчета

  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). В каждой из этих папок используется папка для каждого имени сценария. В этом случае используется только один метод теста в проекте. Эта папка содержит JSON-файл со всеми данными, включая сообщения, ответ и результат оценки.
  • Расширьте оценку. Вот несколько идей: