Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Neste tutorial, você cria um aplicativo MSTest para avaliar a resposta de bate-papo 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 um LLM para realizar 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 Azure OpenAI 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 modelo gpt-4o
.
Criar o aplicativo de teste
Conclua as etapas a seguir para criar um projeto MSTest que se conecte ao modelo de IA gpt-4o
.
Em uma janela de terminal, navegue até o diretório onde você deseja criar seu aplicativo e crie um novo aplicativo MSTest com o comando
dotnet new
:dotnet new mstest -o TestAIWithReporting
Navegue até o diretório
TestAIWithReporting
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.UserSecrets
Execute os seguintes comandos para adicionar segredos de aplicativo ao seu endpoint do Azure OpenAI, nome do modelo e ID do locatário.
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, o ID do locatário pode não ser necessário. Nesse caso, remova-o do código que instancia o DefaultAzureCredential.)
Abra o novo aplicativo no editor de sua escolha.
Adicionar o código do aplicativo de teste
Renomeie o arquivo Test1.cs para MyTests.cse, em seguida, abra o arquivo e renomeie a classe para
MyTests
. Exclua o métodoTestMethod1
vazio.Adicione as diretivas
using
necessárias à 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 método
GetAzureOpenAIChatConfiguration
, que cria a IChatClient que o avaliador usa para 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, poderá defini-lo como qualquer cadeia de caracteres da sua escolha quando 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 da avaliação correspondente são armazenados. Portanto, é uma boa ideia manter o nome razoavelmente curto e evitar caracteres que não são permitidos em nomes de arquivos e diretórios.
- Por padrão, o relatório de avaliação gerado divide os nomes dos cenários em
.
, para que os resultados possam ser exibidos numa vista hierárquica com agrupamento, subdivisão e agregação adequados. 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 ao incluir pontos (.
) nos seus próprios nomes de cenário personalizados, para criar uma hierarquia de relatórios que melhor funcione para os seus cenários.
Nome da execução
O nome de execução é usado para agrupar os resultados da 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 pela próxima e você perderá a capacidade de comparar resultados em diferentes execuções.Este exemplo utiliza um timestamp como o nome da execução. Se você tiver mais de um teste em seu projeto, certifique-se de que os resultados sejam agrupados corretamente usando o mesmo nome de execução em todas as configurações de relatório usadas nos testes.
Num cenário mais realista, poderá também querer compartilhar o mesmo nome de execução entre testes de avaliação presentes em diferentes assemblies e que são executados em distintos 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 compilação atual atribuído pelo seu sistema CI/CD) antes de executar os testes. Ou, se o seu sistema de compilação produz versões de ficheiros de assembly monotonicamente crescentes, pode ler o AssemblyFileVersionAttribute de dentro do código de teste e utilizá-lo como o nome de execução para comparar os resultados entre diferentes versões do produto.
Configuração de relatórios
Um ReportingConfiguration identifica:
- O conjunto de avaliadores que devem ser invocados para cada ScenarioRun criado através da chamada de CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken).
- O endpoint LLM que os avaliadores devem utilizar (ver ReportingConfiguration.ChatConfiguration).
- Como e onde devem ser armazenados os resultados das execuções do cenário.
- Como as respostas LLM relacionadas a execuções de cenários devem ser armazenadas em cache.
- O nome de execução que deve ser usado ao relatar resultados para as execuções do cenário.
Este teste usa uma configuração de relatório baseada em disco.
Em um arquivo separado, adicione a classe
WordCountEvaluator
, 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)); } }
O
WordCountEvaluator
conta o número de palavras presentes na resposta. Ao contrário de alguns avaliadores, não se baseia em IA. O métodoEvaluateAsync
retorna um EvaluationResult que inclui um NumericMetric que contém a contagem de palavras.O método
EvaluateAsync
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 detetada estiver entre 6 e 100. Caso contrário, a métrica é considerada falha. Essa interpretação padrão pode ser substituída pelo chamador, se necessário.De volta ao
MyTests.cs
, adicione um método para reunir os avaliadores para usar 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 do sistema ChatMessage, defina as opções de bate-papo 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 do LLM a uma pergunta de astronomia. Como o ReportingConfiguration tem o cache de resposta habilitado e como o IChatClient fornecido é sempre buscado no 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 ponto de extremidade 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); }
Sugestão
Cada métrica inclui uma propriedade
Reason
que explica o motivo da pontuação. O motivo está incluído no relatório gerado e pode ser visualizado ao clicar no ícone de informações no cartão da métrica correspondente.Finalmente, adicione o método de teste em si mesmo.
[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 ensaio:
Cria o ScenarioRun. O uso de
await using
garante que oScenarioRun
seja descartado corretamente e que os resultados dessa avaliação sejam corretamente persistidos no armazenamento de resultados.Obtém a resposta do LLM a uma pergunta específica de astronomia. O mesmo IChatClient que será usado para avaliação é passado para o método
GetAstronomyConversationAsync
, a fim de obter a cache de resposta para a resposta primária da LLM que está a ser avaliada. (Além disso, permite o cache de respostas para os turnos LLM que os avaliadores usam para realizar as suas avaliações internamente.) Com o cache de resposta, a resposta LLM é buscada:- Diretamente do ponto de extremidade da LLM na primeira execução do teste atual, ou em execuções subsequentes, se a entrada em cache tiver expirado (14 dias como padrão).
- A partir do cache de resposta (baseado em disco) que foi configurado em
s_defaultReportingConfiguration
em execuções posteriores do teste.
Executa os avaliadores contra a resposta. Como ocorre com a resposta LLM, em execuções subsequentes, a avaliação é obtida 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 do LLM e as pontuações de avaliação podem mudar ao longo do tempo à medida que seu produto (e os modelos usados) evoluem. Poderás não querer que testes de avaliação individuais "falhem" e bloqueiem as construções nos teus pipelines de CI/CD quando isto acontece. Em vez disso, pode ser 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ó reprovar compilações individuais quando há queda significativa nas pontuações de avaliação em múltiplos testes diferentes). Dito isto, há algumas nuances aqui e a escolha de validar ou não resultados individuais pode variar dependendo do caso de uso específico.
Quando o método retorna, o objeto
scenarioRun
é descartado e o resultado da avaliação é armazenado no armazenamento de resultados (baseado em disco) configurado ems_defaultReportingConfiguration
.
Executar o teste/avaliação
Execute o teste usando seu fluxo de trabalho de teste preferido, por exemplo, usando o comando CLI dotnet test
ou por meio Test Explorer.
Gerar um relatório
Instale a ferramenta Microsoft.Extensions.AI.Evaluation.Console .NET executando o seguinte comando a partir de uma janela do terminal:
dotnet tool install --local Microsoft.Extensions.AI.Evaluation.Console
Gere um relatório executando o seguinte comando:
dotnet tool run aieval report --path <path\to\your\cache\storage> --output report.html
Abra o arquivo
report.html
. Deveria ser algo assim.
Próximos passos
- Navegue até o diretório onde os resultados do teste são armazenados (que é
C:\TestReports
, a menos que você modificou o local quando criou o ReportingConfiguration). No subdiretórioresults
, 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 — neste caso, apenas o único método de teste 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 que é usado na resposta.
- Adicione outro método de teste, por exemplo, um método que avalia várias respostas do 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 chamar CreateScenarioRunAsync(String, String, IEnumerable<String>, IEnumerable<String>, CancellationToken).