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 as bibliotecas Microsoft.Extensions.AI.Evaluation 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 uma LLM para executar avaliações; o avaliador personalizado não usa IA.
Pré-requisitos
- .NET 8 ou uma versão posterior
- Visual Studio Code (opcional)
Configurar o serviço de IA
Para provisionar um serviço e um modelo do OpenAI do Azure usando o portal do Azure, conclua as etapas no artigo Criar e implantar um recurso do Serviço OpenAI do Azure. Na etapa "Implantar um modelo", selecione o gpt-4o modelo.
Criar o aplicativo de teste
Conclua as etapas a seguir para criar um projeto MSTest que se conecte ao gpt-4o 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 --prerelease dotnet add package Microsoft.Extensions.Configuration dotnet add package Microsoft.Extensions.Configuration.UserSecretsExecute os seguintes comandos para adicionar segredos do aplicativo para o ponto de extremidade, nome do modelo e ID do locatário do Azure OpenAI:
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>(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 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); }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-lo para qualquer cadeia de caracteres de sua escolha ao chamar CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken). Aqui estão algumas considerações para 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
.de cenário para que os resultados possam ser exibidos em uma exibição hierárquica com agrupamento, aninhamento e agregação apropriados. Isso é especialmente útil nos casos em que o nome do cenário é definido como o nome totalmente qualificado do método de teste correspondente, pois permite que os resultados sejam agrupados 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 serão substituídos pelo próximo e você perderá 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 mais real, talvez você também queira compartilhar o mesmo nome de execução em testes de avaliação que residem em vários assemblies diferentes e que são executados em diferentes 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 criado ao chamar CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken).
- A interface 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. Essa interpretação padrão pode ser substituída pelo chamador, se necessário.De volta em
MyTests.cs, adicione um método para reunir os avaliadores a serem utilizados 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 o cache de resposta do ReportingConfiguration está habilitado e, como o fornecido IChatClient é sempre buscado do ScenarioRun criado usando essa configuração de relatório, a resposta LLM para o teste é armazenada em cache e reutilizada. A resposta será reutilizada até que a entrada de cache correspondente expire (em 14 dias por padrão), 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. O uso de
await usinggarante que oScenarioRunseja descartado corretamente e que os resultados dessa avaliação sejam corretamente persistidos no repositório de resultados.Obtém a resposta da LLM a uma pergunta de astronomia específica. O mesmo IChatClient que será usado para avaliação é passado para o
GetAstronomyConversationAsyncmétodo para obter o cache de resposta para a resposta LLM primária que está sendo avaliada. (Além disso, isso permite o cache de respostas para as interações do LLM que os avaliadores usam para executar suas avaliações internamente.) Com o cache de respostas, a resposta do LLM é recuperada:- 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) que foi configurado nas execuções subsequentes do teste em
s_defaultReportingConfiguration.
Executa os avaliadores em relação à resposta. Assim como a resposta LLM, em execuções subsequentes, a avaliação é buscada do cache de resposta (baseado em disco) que foi 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, pois 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. Talvez você não queira que testes de avaliação individuais "falhem" porque isso pode bloquear as compilações em seus pipelines de CI/CD quando isso acontecer. Em vez disso, talvez seja melhor confiar no relatório gerado e acompanhar as tendências gerais para pontuações de avaliação em diferentes cenários ao longo do tempo (e só falhar compilações 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 .NET Microsoft.Extensions.AI.Evaluation.Console executando o seguinte comando em uma janela de terminal:
dotnet tool install --local Microsoft.Extensions.AI.Evaluation.ConsoleDica
Talvez seja necessário criar um arquivo de manifesto primeiro. Para obter mais informações sobre isso e como instalar ferramentas locais, consulte as ferramentas locais.
Gere 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. Deve ser algo assim.
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 um avaliador personalizado adicional, 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).