Lição 6: Mão na massa: construa uma trilha de eval para o seu agente
Objetivos de aprendizado:
- Ligar conjuntos de avaliação, correção estratificada e loops de harness em um
eval-runner.mjs executável — uma tarefa de eval por loop independente
- Fazer os relatórios capturarem não só a taxa de aprovação, mas também duração por tarefa, número de chamadas de ferramenta, consumo de tokens e erros de ferramenta, e usar essas colunas para diagnosticar problemas
- Usar esta trilha para medir o impacto real de uma mudança no system prompt e pegar um verificador estrito demais que reprova saídas corretas
Pré-requisitos: Ter lido as lições 1–5 e ter à mão, rodando, o loop de harness do curso 7 | Anterior: Lição 5 <<
As cinco primeiras lições foram todas componentes: verificar o estado final e não etapa por etapa (lição 2), verificações determinísticas primeiro e atenção a verificadores estritos demais (lição 3), texto livre só recebe juízes LLM (lição 4), conjuntos de avaliação começam com umas vinte tarefas reais (lição 5). Cada um faz sentido por conta própria, mas, depois de mudar o seu prompt, você ainda não tem aquela coisa única que você roda com um comando só para os números lhe dizerem “melhor ou pior”.
Esta lição solda os componentes. O que você recebe é um arquivo de trezentas linhas que roda em menos de dois segundos. A orientação oficial sobre “como rodar evals” é direta: use chamadas programáticas diretas à API do LLM; use loops agênticos simples — while-loops embrulhando chamadas de LLM e chamadas de ferramenta alternadas — uma tarefa de eval por loop1. Isso é exatamente o loop dirigido por stop_reason do curso 7 desta série. Você pode transplantá-lo como está.
Como fica quando roda
Salve o eval-runner.mjs completo, mais adiante nesta lição, e depois node eval-runner.mjs:
Isto não é um exemplo feito à mão — foi copiado literalmente de uma execução real num diretório temporário. Copie o código completo e rode uma vez; tudo, exceto a coluna “Duração” (tempo de relógio real, que varia com a carga da máquina), vai bater até o milissegundo. Os números são os mesmos porque o cliente stub devolve respostas fixas.
Esta saída contém tudo o que esta lição ensina: cinco tarefas rodando cada uma o seu próprio loop, dois modos de correção misturados numa tabela só, taxa de aprovação mais quatro colunas de diagnóstico, e a diferença entre duas versões condensada numa tabela de comparação. O resto desta lição desmonta isso.
As cinco peças de uma trilha
- Sistema sob teste: definições de ferramenta, implementações reais das ferramentas e os dados por trás delas. O eval roda “o agente usa as suas ferramentas para trabalhar” — as ferramentas fazem parte do que você está testando.
- Cliente stub: um
messages.create falso que devolve respostas fixas numa fila predefinida, tornando toda a trilha reprodutível.
- Conjunto de avaliação: um array
tasks, cada entrada é {id, prompt, verify}. Exigência oficial: cada prompt de eval deve vir emparelhado com uma resposta ou resultado verificável1 — um prompt sem verificador não é uma tarefa de eval, é uma demo.
- Correção: o que pode ser corrigido deterministicamente vai para uma função
verify; texto livre vai para o juiz.
- Loop e relatório: uma tarefa, um while-loop; ao terminar, agregue as métricas numa tabela.
Uma coisa para fixar primeiro: as tarefas não compartilham messages. O messages de cada tarefa começa apenas com o prompt de usuário daquela tarefa, roda o próprio loop e depois é descartado1. Por que isso importa tanto — o quiz do meio vai perguntar diretamente.
Peça um: ferramentas e os dados por trás delas
O sistema sob teste é um assistente de pedidos, quatro pedidos, duas ferramentas: search_orders (busca por nome de cliente ou por status e devolve a lista de números de pedido) e get_order (consulta o detalhe de um pedido pelo número). Dois detalhes são deliberados: search_orders só devolve números de pedido, sem valores, forçando o agente a chamar get_order de novo para cada pedido — a coluna “Chamadas” do relatório vai expor essa falha de projeto. O outro: ela lança erro quando as duas condições de filtro estão vazias:
Este é o erro de ferramenta de “parâmetro inválido”. A orientação oficial diz que esses erros se aglomerando normalmente significa que as descrições das ferramentas deveriam estar mais claras ou precisam de exemplos1. Vamos vê-lo no relatório daqui a pouco. Erros de ferramenta não são travamentos — o bloco de execução da ferramenta captura a exceção, embrulha num tool_result com is_error: true, devolve ao modelo e incrementa um contador. tool_use e tool_result se emparelham pelo tool_use_id — essa é a base assentada no curso 7; aqui só acrescentamos dois contadores.
Peça dois: cliente stub e o interlúdio de verificação
É preciso pausar aqui, senão todos os números abaixo não se sustentam.
O Claude de verdade é não determinístico: o mesmo prompt rodado duas vezes pode seguir caminhos completamente diferentes2. Isso é bom em produção e desastroso numa lição de demonstração — você roda hoje e tira 3/5, amanhã 4/5, e não sabe dizer se a diferença veio da mudança de prompt ou do humor do modelo. Por isso as lições práticas dos cursos 8 e 9 usam todas o mesmo método: trocar o modelo por um stub que devolve respostas fixas numa fila predefinida, tornando o comportamento testado uma variável controlada. Isso verifica a lógica de controle que você escreveu, não o desempenho do modelo naquele dia.
Quando a fila esgota ele lança erro, sem resposta de fallback — se o loop girar uma vez a mais, você vê imediatamente Error: [stub] fila de respostas de v1/t2-pending esgotada (requisições emitidas: 1) (esse é o texto de erro real depois que apaguei a última resposta da fila do t2), e não um end_turn falso passando batido. Cada resposta carrega o próprio latency_ms; o stub de fato dorme esse tempo, para que a coluna “Duração” meça quantos turnos o loop levou. Cada tarefa recebe um cliente novo com o próprio script; os cursores não atravessam tarefas.
A diferença entre as duas versões de prompt está fixada nas duas filas de respostas do stub. Num cenário real você muda o system prompt e o comportamento do modelo acompanha; aqui eu não tenho modelo, então escrevi de antemão SCRIPT_V1 e SCRIPT_V2, deixando a v2 devolver respostas diferentes em duas tarefas — “suponha que o prompt v2 faça efeito e o modelo responda assim” está codificado como dado:
Use a sintaxe de espalhamento para herdar da v1 e liste apenas as entradas que mudaram — quem lê o código vê o escopo da diferença de imediato. Esta trilha verifica a própria trilha: se os verificadores corrigem certo, se as métricas registram com precisão, se os relatórios calculam direito, se duas execuções podem ser comparadas. Quando você trocar por um cliente real, a trilha não muda — só os números começam a pular.
Peça três: conjunto de avaliação — quatro comuns mais um caso extremo
A lição 5 disse que conjuntos de avaliação devem espelhar a distribuição real e cobrir casos extremos3; a documentação oficial também alertou contra ambientes de sandbox simplistas demais, que não estressam as ferramentas com complexidade suficiente1. Aqui cabem apenas cinco tarefas por questão de espaço, mas a estrutura segue a de conjuntos de avaliação reais:
t3-no-orderid merece menção especial. O prompt é “Confere pra mim o status daquele pedido” — qual deles? Não foi especificado. O comportamento ideal é pedir o número do pedido em vez de chutar um e consultar. A documentação oficial é cuidadosa quanto a esse comportamento: se o prompt do usuário não fornece informação suficiente para preencher todos os parâmetros obrigatórios, o Claude Opus é muito mais propenso a reconhecer o parâmetro faltante e pedi-lo, mas esse comportamento não é garantido, especialmente para prompts mais ambíguos e modelos menos capazes4. Comportamentos “não garantidos” são exatamente o que os conjuntos de avaliação devem cobrir — o que é garantido não precisa de teste.
O r que verify recebe contém não só answer, mas também toolCalls, toolErrors e tokens, de modo que o verificador pode conferir “estado final mais métricas-chave” e não apenas texto: o t3 de fato confere “chamou zero ferramentas”, o t5 confere “relatou exatamente um erro e disse com honestidade que não encontrou” — o “estado final primeiro” da lição 2 se concretiza através desses campos. note é para humanos; quando uma tarefa reprova, o relatório imprime o critério ao lado da resposta real do agente.
Se o seu dever de casa da lição 5 usou o conjunto de campos {id, prompt, expected, verifier, rubricRef, tags, split}, mapeie agora para evitar confusão: o verifier da lição 5 se chama grader aqui e serve apenas para exibição — o tipo de correção de fato é determinado por a tarefa ter uma função verify ou judge: true. As asserções declarativas de expected são escritas aqui diretamente no corpo da função verify (as asserções de cada tarefa são diferentes; escrevê-las como funções é mais simples do que projetar um formato universal de asserção). rubricRef está embutido como JUDGE_PROMPT, já que a suíte inteira tem apenas um caso de juiz. tags e split foram omitidos por brevidade; a disciplina de held-out é repetida como sempre na seção “Escopo”. O seu JSON da lição 5 não está obsoleto — ele é a versão declarativa deste array TASKS. Avançar significa traduzir cada asserção em uma função.
Peça quatro: correção estratificada, determinística primeiro
Os métodos de correção têm uma ordenação: a correção por código é a mais rápida e mais confiável, escala muitíssimo bem, mas carece de nuance para julgamentos complexos; a correção por LLM é rápida e flexível, dá conta de julgamento complexo, mas teste a confiabilidade primeiro e só então escale; a correção humana é a mais flexível e de maior qualidade, mas lenta e cara, evite se possível3.
Então a regra é: o que pode ser corrigido por código nunca vai para um juiz. Quatro das cinco tarefas aqui usam verify; só o t4-refund-note, aquele pedaço de texto livre, vai para o juiz — “este parágrafo pode ser enviado a um cliente” não é respondível por casamento de string. O formato do juiz segue a lição 4: rubrica travada em três itens, formato de saída travado em JSON, primeiro o raciocínio e depois a nota:
Cada ponto tem uma fonte: fazer o juiz raciocinar primeiro e pontuar depois, e então descartar o raciocínio — isso melhora a qualidade da correção, especialmente em tarefas que exigem julgamento complexo3; a saída deve ser empírica ou específica, não avaliação puramente qualitativa3; e “uma única chamada de LLM, um único prompt, saída de nota de 0.0 a 1.0 mais um aprovado/reprovado” é a combinação que a fonte oficial achou mais consistente e mais alinhada com julgamentos humanos depois de testar vários esquemas de juiz no sistema de pesquisa multiagente deles2.
O juiz aqui também é um stub: a resposta da v1 não tem prazo de chegada, dois de três itens dão 0.67 e reprovam; a v2 acrescentou, os três itens batem e dão 1.00, aprovado. A nota é autoconsistente com a rubrica — três itens binários promediados só podem cair em 0, 0.33, 0.67 ou 1.00; uma nota de 0.85 significaria que o juiz não seguiu a matemática da rubrica. O próprio juiz queima tokens; o consumo dele é somado aos tokens daquela tarefa, e é por isso que o t4 chama apenas uma ferramenta mas não tem tokens baixos.
Mais uma disciplina da lição 4: o modelo que trabalhou não deve se corrigir. A fonte oficial diz para uma instância nova do modelo tentar refutar o resultado — quem faz o trabalho não é quem corrige5. Em código: o juiz usa o próprio cliente, o próprio system prompt, o próprio array de messages, vê apenas o prompt da tarefa e a resposta a corrigir, e não vê a transcrição de chamadas de ferramenta do agente.
Peça cinco: loop e relatório
O loop é o loop do curso 7 literalmente, esqueleto inalterado — só acrescentamos o model e o max_tokens que a API real exige (o stub os ignora) e depois embrulhamos com contadores:
messages é uma variável local dentro de runTask; a função retorna e ela some. Essa é a implementação inteira de “as tarefas não compartilham contexto” — nenhum mecanismo extra é necessário, basta não içá-la para fora.
Quanto a métricas, a lista de conferência oficial é: além da acurácia de topo, colete também o tempo total de execução de chamadas de ferramenta e de tarefas individuais, o número total de chamadas de ferramenta, o consumo total de tokens e os erros de ferramenta1. As colunas da tabela do relatório seguem exatamente essa lista. A taxa de aprovação só lhe diz “passou ou não”; essas colunas lhe dizem “como passou” — uma tarefa que passa chamando doze ferramentas e uma que passa com duas chamadas são dois níveis de qualidade. Essas colunas também se autoexplicam: muitas chamadas de ferramenta redundantes normalmente sugerem que os parâmetros de paginação ou de limite de tokens precisam ser redimensionados; muitos erros de ferramenta por parâmetro inválido normalmente sugerem que as descrições das ferramentas poderiam ser mais claras ou precisam de exemplos melhores1. Os exercícios vão usar isso diretamente.
O relatório ser legível por humanos tem valor intrínseco. A sugestão oficial é: faça o Claude mostrar evidência em vez de afirmações de sucesso — a saída do teste, o comando que ele rodou e o que ele devolveu, ou uma captura de tela do resultado; revisar evidência é mais rápido do que refazer a verificação por conta própria, e funciona para sessões que você não estava acompanhando5. Esta tabela de relatório é essa evidência — cole numa descrição de PR ou mande para um colega, e ele consegue julgar sem rodar de novo. (A única pegadinha da impressão é que caracteres CJK de largura plena contam como largura 2, e o padEnd cru desalinha — o código tem um pad sensível à largura.)
O eval-runner.mjs completo
Copie e salve como eval-runner.mjs; node eval-runner.mjs roda direto. Sem dependências, sem package.json, Node 18+ (usa await de nível superior, então a extensão precisa ser .mjs).
Recuperando a armadilha da lição 3: verificadores estritos demais
A lição 3 cobriu uma armadilha, nas palavras exatas da fonte oficial: evite verificadores estritos demais, que rejeitam respostas corretas por diferenças espúrias como formatação, pontuação ou formulações alternativas válidas1. Soa como bom senso, mas é quase inevitável no código, porque verificadores estritos demais são os mais fáceis de escrever.
A trilha tem um embutido. O t1-total tem duas versões de verificador; a antiga é pass: r.answer.includes("1280.00") — parece à prova de balas: a resposta correta é 1280.00, então confira se a resposta contém essa string. Rode node eval-runner.mjs --strict-verify (colando abaixo só o relatório da v1; o relatório da v2 e a tabela de variação são impressos como de costume):
Isto também vem de uma execução real. Olhe o detalhe do t1-total: o agente respondeu “somando ¥1,280.00” — valor correto, pedidos corretos, redação normal. O único crime dele foi pôr uma vírgula de separação de milhar entre o 1 e o 280, e por isso includes("1280.00") devolve false, e uma resposta inteiramente correta é reprovada.
Neste ponto conserte o verificador, não o agente. Relatórios só lhe dizem “t1 reprovou”, não lhe dizem de quem é a culpa; o jeito de saber é ler as palavras reais do agente no detalhe — que é exatamente por que os relatórios imprimem a resposta bruta. A correção é normalização. A descrição oficial de casamento exato já inclui essa etapa: o casamento exato avalia se a saída do modelo bate com uma resposta correta predefinida, tipicamente depois de normalizar espaços em branco e caixa3. Cenários de valor precisam de mais lavagem — símbolos de moeda, separadores de milhar, unidades —, então o verificador corrigido lava o ruído primeiro, extrai os números e compara numericamente:
Tire o --strict-verify e rode de novo; o t1-total vira de 0.00 para 1.00, e a linha de base da v1 sobe de 2/5 de volta para 3/5 — e, no meio disso, o agente não mudou um único caractere, e a fila de respostas do stub não mudou um único caractere. A nota mudou mas o sistema sob teste não — esse é o teste decisivo de “problema do verificador”.
Um aparte sobre escopo: normalização não é quanto mais frouxa melhor. Afrouxe para “parece conter 1280, passa”, e o agente respondendo “total de 1280 pedidos, valor desconhecido” também passa. Verificadores devem ficar em “deixe passar diferenças irrelevantes, barre erros substantivos” — e o único método para achar essa posição é tentar com respostas reais.
Mude um ponto do prompt e veja a nota se mexer
Trilha calibrada, pronta para trabalho de verdade. Mudei um só ponto — o system prompt, acrescentando duas regras depois da v1:
Essas duas não foram inventadas; foram lidas nos “Casos reprovados” do relatório da v1: o t3 reprova porque chutou um número de pedido com os parâmetros incompletos, e o t4 levou desconto porque faltou o prazo de chegada. O relatório diz o quê, você muda o quê — essa é a diferença mais concreta entre ter e não ter uma trilha. Sem trilha, depois de mudar o prompt você dá uma olhada na saída e sente que “parece melhor”; com trilha, “qual melhorou, qual ficou parado, alguma coisa regrediu” são três linhas de números.
Rode de novo, e a tabela de variação é o último trecho da saída de abertura: taxa de aprovação de 60% para 100%, duas tarefas viram de reprovado para aprovado, as outras três não se mexem. Essa última meia-frase importa tanto quanto a primeira — ela diz que esta mudança não quebrou o que já funcionava. Sem trilha, depois de mudar o prompt você só olha a saída uma vez e acha que “ficou melhor”; com trilha, “qual melhorou / qual ficou parado / alguma regressão” são três linhas de números.
A formulação oficial para isso é: com evals você consegue medir o impacto da sua engenharia de prompt com muito mais confiança; até refinamentos pequenos nas descrições de ferramenta podem render melhorias dramáticas1. Há uma barganha a agarrar aqui também: no início do desenvolvimento de agentes, as mudanças tendem a ter impacto dramático porque ainda há fruta madura em abundância ao alcance da mão — um ajuste de prompt pode elevar a taxa de sucesso de 30% para 80%; com efeitos desse tamanho você consegue enxergar mudanças com apenas alguns casos de teste2. Você tem apenas cinco tarefas agora — isso não é déficit, é o ponto de partida.
Olhe as colunas de métrica de novo: as chamadas de ferramenta da v2 caíram de 8 para 6, os erros de ferramenta de 2 para 1, os tokens quase mil a menos, porque o t3 não chuta mais às cegas para chamar ferramentas. A mesma mudança melhorou simultaneamente a acurácia e o custo — esse tipo de coisa só fica visível quando você registra essas colunas juntas.
Escopo: o que esta trilha dá conta e o que não dá
O que ela dá conta: um agente, um lote de tarefas, rodado uma vez na sua máquina, produzindo um relatório legível por humanos.
Trocar por um modelo real — a estrutura da trilha não muda. Substitua stubClient(...) pelo cliente real do @anthropic-ai/sdk; o while-loop de runTask não muda uma linha — ele já está escrito no formato de stop_reason / tool_use / tool_result da API real; os parâmetros obrigatórios model e max_tokens já estão lá (o stub os ignora, o cliente real os usa). Depois da troca, duas coisas mudam: as notas vão tremer, porque agentes são não determinísticos entre execuções mesmo com prompts idênticos2, então não leia demais em execuções únicas; e rodar uma rodada custa dinheiro e tempo, cinco tarefas não pesam, mas duzentas exigem pensar em concorrência e custo.
O que ela não dá conta: pendurar os evals na CI, rodar a cada commit, comparar com versões históricas, bloquear merges quando a nota cai abaixo de um limiar — essas são práticas comuns de engenharia e funcionam bem, mas esta lição não se estende nelas. O nível 2 dos exercícios vai levar você por “comparar dois relatórios”; a orquestração restante é trabalho da sua CI.
Mais uma disciplina da lição 5 para repetir: não afine contra o conjunto held-out. Você segue os relatórios para mudar prompts; depois de várias rodadas as notas certamente vão subir, mas a subida pode ser só “notas nestas cinco tarefas”. A prática oficial é apoiar-se em conjuntos de teste held-out para garantir que não houve overfitting nas avaliações de “treino”1. Então, numa montagem real, as tarefas devem se dividir em duas pilhas: uma roda diariamente para orientação, a outra fica trancada e só abre quando você acha que “esta versão deve funcionar” — as notas da primeira pilha são navegação, as notas da segunda são veredito.
Último lembrete de sempre: os evals automatizados vão deixar coisas passar. Testadores humanos sempre esbarram em casos extremos que os evals deixam passar — alucinações em consultas incomuns, falhas sistêmicas, vieses sutis de seleção de fontes2. A trilha rodando lisa não significa parar de usar você mesmo.
💻 Exercícios
Recapitulação
- O formato padrão para rodar evals é chamadas programáticas diretas à API mais loops agênticos simples — uma tarefa de eval por loop; as tarefas não compartilham
messages, senão o contexto da tarefa anterior contamina a seguinte e os resultados deixam de ser comparáveis1.
- Cada prompt de eval deve vir emparelhado com um resultado verificável; os verificadores formam um espectro que vai da comparação exata de strings até pedir ao modelo que julgue — o que puder ser corrigido por código nunca vai para um juiz, porque a correção por código é a mais rápida e mais confiável e escala muitíssimo bem1 3.
- Texto livre vai para o juiz; o formato é uma única chamada, um único prompt, saída de nota de 0.0 a 1.0 mais aprovado/reprovado; a rubrica precisa raciocinar primeiro e pontuar depois, com formato de saída travado2 3.
- Além da taxa de aprovação, os relatórios precisam registrar duração da tarefa, número de chamadas de ferramenta, consumo de tokens e erros de ferramenta; essas colunas se autoexplicam — chamadas redundantes apontam para parâmetros de paginação/volume de retorno precisando de ajuste, e erros de parâmetro inválido apontam para descrições de ferramenta precisando de clareza1.
- Verificadores estritos demais rejeitam respostas corretas: formato, pontuação e formulações diferentes razoáveis podem todos derrubar uma comparação literal; faça normalização antes do casamento exato1 3. A nota mudou mas o sistema sob teste não — a culpa é do verificador.
- Com uma trilha, o impacto de uma mudança de prompt vira mensurável; até refinamentos pequenos podem render melhorias dramáticas; os efeitos no início são grandes, e poucos casos bastam para enxergar diferenças1 2. O próprio relatório é evidência revisável por outra pessoa, mais rápida do que refazer a verificação, e funciona para sessões que você não estava acompanhando5.
- Siga os relatórios para mudar prompts e as notas vão subir, mas a subida pode ser só neste lote de tarefas; tranque o conjunto held-out para impedir overfitting1. Os evals automatizados têm pontos cegos; testadores humanos ainda pegam casos extremos que os evals deixam passar2.
Depois de concluir este curso
Olhando para trás, a linha principal é curta. A lição 1 separou “parece pronto” de “está pronto” — sem verificações executáveis, “parece pronto” é o único sinal disponível, e você vira a etapa de verificação5. A lição 2 fixou o que verificar: agentes podem percorrer caminhos razoáveis completamente diferentes até o mesmo objetivo, então avalie o estado final, não confira a trajetória etapa por etapa2. A lição 3 transformou “verificações” em verificadores determinísticos executáveis que produzem passa/falha, e também alertou que verificadores estritos demais rejeitam respostas corretas1. A lição 4 cuidou do texto livre — rubricas, formato de saída, e o modelo que trabalhou não deve se corrigir2 5. A lição 5 resolveu “com quantos casos verificar”: umas vinte tarefas reais já dão para começar, não espere acumular centenas para começar2. Esta lição soldou as cinco primeiras num arquivo de trezentas linhas.
Esse arquivo não é complexo, roda em menos de dois segundos, mas o que ele muda é concreto: a partir de hoje, quando você mudar uma versão de prompt, não vai depender de “ler alguns parágrafos de saída e sentir que melhorou” para julgar — rode um comando, e a tabela de variação de v1 para v2 fala por você, exatamente como desta vez, em que o t3 e o t4 ficaram verdes enquanto os outros três permaneceram estáveis. Da próxima vez que o seu agente disser “pronto”, você tem dois comandos e um código de saída para verificar essa afirmação.
Da próxima vez que o seu agente disser “pronto”, você tem uma trilha executável para verificar.