Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Neste tutorial, você criará um aplicativo MSTest para avaliar a resposta de chat de um modelo OpenAI. O aplicativo de teste usa o Microsoft. Extensions.AI.Evaluation bibliotecas para executar as avaliações, armazenar em cache as respostas do modelo e criar relatórios. O tutorial usa avaliadores internos e personalizados. Os avaliadores de qualidade internos (do pacote Microsoft.Extensions.AI.Evaluation.Quality) usam LLM para executar avaliações; o avaliador personalizado não usa IA.
Pré-requisitos
- .NET 8 ou posterior
- Visual Studio Code (opcional)
Configurar o serviço de IA
Para provisionar um Azure OpenAI service e um modelo usando o portal Azure, conclua as etapas no artigo Criar e implantar um recurso Serviço OpenAI do Azure. Na etapa "Implantar um modelo", selecione o gpt-5 modelo.
Criar o aplicativo de teste
Conclua as etapas a seguir para criar um projeto MSTest que se conecte a um modelo de IA.
Em uma janela de terminal, navegue até o diretório onde você deseja criar seu aplicativo e crie um novo aplicativo MSTest com o
dotnet newcomando:dotnet new mstest -o TestAIWithReportingNavegue até o
TestAIWithReportingdiretório e adicione os pacotes necessários ao seu aplicativo: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.UserSecretsExecute os seguintes comandos para adicionar os segredos do aplicativo para o ponto de extremidade, nome do modelo e ID do locatário do OpenAI do Azure:
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>(Dependendo do seu ambiente, a ID do inquilino pode não ser necessária. Nesse caso, remova-a do código que cria uma instância do DefaultAzureCredential.)
Abra o novo aplicativo em seu editor de escolha.
Adicionar o código do aplicativo de teste
Renomeie o arquivo Test1.cs para MyTests.cs e, em seguida, abra o arquivo e renomeie a classe para
MyTests. Exclua o método vazioTestMethod1.Adicione as diretivas necessárias
usingà parte superior do arquivo.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;Adicione a propriedade TestContext à classe.
// The value of the TestContext property is populated by MSTest. public TestContext? TestContext { get; set; }Adicione o
GetAzureOpenAIChatConfigurationmétodo, que cria o IChatClient que o avaliador usa para se comunicar com o modelo.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); }Configure a funcionalidade de relatório.
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);Nome do cenário
O nome do cenário é definido como o nome totalmente qualificado do método de teste atual. No entanto, você pode defini-la para qualquer cadeia de caracteres quando chamar CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken). Considere esses fatores ao escolher um nome de cenário:
- Ao usar o armazenamento baseado em disco, o nome do cenário é usado como o nome da pasta na qual os resultados de avaliação correspondentes são armazenados. Portanto, é uma boa ideia manter o nome razoavelmente curto e evitar quaisquer caracteres que não sejam permitidos em nomes de arquivo e diretório.
- Por padrão, o relatório de avaliação gerado divide os nomes dos cenários em
., para que os resultados sejam exibidos de forma hierárquica, com agrupamento, aninhamento e agregação apropriados. A exibição hierárquica é especialmente útil quando o nome do cenário é o nome totalmente qualificado do método de teste correspondente, pois agrupa resultados por namespaces e nomes de classe na hierarquia. No entanto, você também pode aproveitar esse recurso incluindo períodos (.) em seus próprios nomes de cenário personalizados para criar uma hierarquia de relatórios que funcione melhor para seus cenários.
Nome da execução
O nome da execução é usado para agrupar resultados de avaliação que fazem parte da mesma execução de avaliação (ou execução de teste) quando os resultados da avaliação são armazenados. Se você não fornecer um nome de execução ao criar um ReportingConfiguration, todas as execuções de avaliação usarão o mesmo nome de execução padrão de
Default. Nesse caso, os resultados de uma execução são substituídos pelo próximo e você perde a capacidade de comparar resultados em diferentes execuções.Este exemplo usa um carimbo de data/hora como o nome da execução. Se você tiver mais de um teste em seu projeto, verifique se os resultados são agrupados corretamente usando o mesmo nome de execução em todas as configurações de relatório usadas nos testes.
Em um cenário do mundo real, talvez você também queira compartilhar o mesmo nome de execução em testes de avaliação que residam em vários assemblies diferentes e que sejam executados em diversos processos de teste. Nesses casos, você pode usar um script para atualizar uma variável de ambiente com um nome de execução apropriado (como o número de build atual atribuído pelo seu sistema de CI/CD) antes de executar os testes. Ou, se o sistema de compilação produzir versões de arquivos de assembly que aumentam monotonicamente, você pode ler o AssemblyFileVersionAttribute dentro do código de teste e usá-lo como o nome de execução para comparar resultados entre diferentes versões do produto.
Configuração de relatórios
Um ReportingConfiguration identifica:
- O conjunto de avaliadores que deve ser invocado para cada ScenarioRun que é criado chamando CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken).
- O ponto de extremidade LLM que os avaliadores devem usar (consulte ReportingConfiguration.ChatConfiguration).
- Como e onde devem ser armazenados os resultados das execuções dos cenários.
- Como as respostas LLM relacionadas às execuções dos cenários devem ser armazenadas em cache.
- O nome de execução que deve ser usado ao relatar os resultados das execuções do cenário.
Esse teste usa uma configuração de relatório baseada em disco.
Em um arquivo separado, adicione a
WordCountEvaluatorclasse, que é um avaliador personalizado que implementa 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)); } }Conta
WordCountEvaluatoro número de palavras presentes na resposta. Ao contrário de alguns avaliadores, ele não é baseado em IA. OEvaluateAsyncmétodo retorna um EvaluationResult que inclui um NumericMetric que contém a contagem de palavras.O
EvaluateAsyncmétodo também anexa uma interpretação padrão à métrica. A interpretação padrão considera a métrica boa (aceitável) se a contagem de palavras detectada estiver entre 6 e 100. Caso contrário, a métrica será considerada com falha. O chamador pode substituir essa interpretação padrão, se necessário.De volta ao
MyTests.cs, adicione um método para reunir os avaliadores a serem usados na avaliação.private static IEnumerable<IEvaluator> GetEvaluators() { IEvaluator relevanceEvaluator = new RelevanceEvaluator(); IEvaluator coherenceEvaluator = new CoherenceEvaluator(); IEvaluator wordCountEvaluator = new WordCountEvaluator(); return [relevanceEvaluator, coherenceEvaluator, wordCountEvaluator]; }Adicione um método para adicionar um prompt ChatMessagedo sistema, defina as opções de chat e peça ao modelo uma resposta a uma determinada pergunta.
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); }O teste neste tutorial avalia a resposta da LLM a uma pergunta de astronomia. Como ReportingConfiguration tem o cache de resposta habilitado e, como o IChatClient fornecido é sempre buscado de ScenarioRun criado usando-se essa configuração de relatório, a resposta do LLM para o teste será armazenada em cache e reutilizada. A resposta é reutilizada até que a entrada de cache correspondente expire (por padrão, em 14 dias) ou até que qualquer parâmetro de solicitação, como o endpoint LLM ou a pergunta que está sendo feita, seja alterado.
Adicione um método para validar a resposta.
/// <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); }Dica
Cada uma das métricas inclui uma
Reasonpropriedade que explica o raciocínio por trás da pontuação. O motivo é incluído no relatório gerado e pode ser exibido clicando no ícone de informações no cartão da métrica correspondente.Por fim, adicione o método de teste em si.
[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); }Este método de teste:
Cria o ScenarioRun.
await usinggarante o descarte correto deScenarioRune a persistência correta dos resultados da avaliação no repositório de resultados.Obtém a resposta da LLM sobre uma pergunta de astronomia específica. O teste passa o mesmo IChatClient que é usado para avaliação para o método
GetAstronomyConversationAsynce obtém o cache de resposta para a resposta do LLM primário que está sendo avaliado. (Passar o mesmo cliente também habilita o cache de resposta para as interações de LLM que os avaliadores usam para executar suas avaliações internamente.) Com o cache de resposta, a resposta do LLM é obtida de uma das seguintes maneiras:- Diretamente do ponto de acesso LLM na primeira execução do teste em andamento ou em execuções subsequentes se a entrada armazenada em cache tiver expirado (14 dias, por padrão).
- No cache de resposta (baseado em disco) configurado em
s_defaultReportingConfigurationnas execuções posteriores do teste.
Executa os avaliadores em relação à resposta. Assim como a resposta do LLM, as execuções subsequentes buscam a avaliação do cache de resposta (baseado em disco) configurado em
s_defaultReportingConfiguration.Executa alguma validação básica no resultado da avaliação.
Esta etapa é opcional e principalmente para fins de demonstração. Em avaliações do mundo real, talvez você não queira validar resultados individuais porque as respostas llm e as pontuações de avaliação podem mudar ao longo do tempo à medida que o produto (e os modelos usados) evoluem. Pode ser importante evitar que testes de avaliação individuais "falhem" e bloqueiem os builds em seus pipelines de CI/CD quando ocorrer alteração. Em vez disso, talvez seja melhor confiar no relatório gerado e acompanhar as tendências gerais para as pontuações de avaliação em diferentes cenários ao longo do tempo (e só produzir falhas em builds individuais quando houver uma queda significativa nas pontuações de avaliação em vários testes diferentes). Dito isto, há algumas nuances aqui e a escolha de validar resultados individuais ou não pode variar dependendo do caso de uso específico.
Quando o método retorna, o
scenarioRunobjeto é descartado e o resultado da avaliação é armazenado no repositório de resultados (baseado em disco) configurado ems_defaultReportingConfiguration.
Executar o teste/avaliação
Execute o teste usando seu fluxo de trabalho de teste preferencial, por exemplo, usando o comando dotnet test da CLI ou por meio do Gerenciador de Testes.
Gerar um relatório
Instale a ferramenta Microsoft.Extensions.AI.Evaluation.Console do .NET executando o seguinte comando em uma janela de terminal:
dotnet tool install --create-manifest-if-needed Microsoft.Extensions.AI.Evaluation.ConsoleGere um relatório executando o seguinte comando:
dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.htmlAbra o arquivo
report.html. O relatório é semelhante à captura de tela a seguir.
Próximas etapas
- Navegue até o diretório onde os resultados do teste são armazenados (ou seja
C:\TestReports, a menos que você modifique o local quando criou o ReportingConfiguration).resultsNo subdiretório, observe que há uma pasta para cada execução de teste nomeada com um carimbo de data/hora (ExecutionName). Dentro de cada uma dessas pastas há uma pasta para cada nome de cenário, nesse caso, apenas o método de teste único no projeto. Essa pasta contém um arquivo JSON com todos os dados, incluindo as mensagens, a resposta e o resultado da avaliação. - Expanda a avaliação. Aqui estão algumas ideias:
- Adicione outro avaliador personalizado, como um avaliador que usa IA para determinar o sistema de medição usado na resposta.
- Adicione outro método de teste, por exemplo, um método que avalia várias respostas da LLM. Como cada resposta pode ser diferente, é bom amostrar e avaliar pelo menos algumas respostas a uma pergunta. Nesse caso, você especifica um nome de iteração sempre que chama CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken).