Pular para o conteúdo principal
Os evals dizem se uma mudança deixou o ChatCLI melhor ou pior, com números em vez de impressão. Esta receita percorre o ciclo completo numa suíte pequena:
  1. Escrever dois casos: um de coder, verificado por testes Go reais, e um de chat, avaliado por um LLM-juiz. O código vai inline na suíte ou fica em arquivos de verdade que a suíte lê com !file.
  2. Rodar e salvar o resultado como baseline.
  3. Montar uma pasta por mudança a testar, cada uma com uma cópia da suíte com aquela única mudança.
  4. Testar uma mudança de configuração que parece perigosa, mas que no fim só custa mais.
  5. Testar uma mudança de policy que quebra o coder de verdade e ver a execução sair com exit code 3.
  6. Comparar as três execuções, offline, e ligar o gate no CI.
Todas as saídas abaixo vêm de execuções reais: Claude Haiku 5.5 como candidato e Claude Sonnet 5.5 como juiz. As três execuções juntas custaram cerca de US$ 0,07 e levaram mais ou menos um minuto cada.
Evals gastam tokens de verdade. chatcli eval validate e chatcli eval list não custam nada: use-os enquanto escreve a suíte, e ponha um teto em toda execução com --max-cost.

1. Escreva a suíte

Crie evals-demo/demo.yaml. O caso de coder entrega ao agente um módulo Go cuja Reverse inverte bytes, o que quebra acentos, ideogramas e emoji; os testes já cobrem esses casos e falham. O caso de chat pede uma explicação curta em português.
evals-demo/demo.yaml
Para que serve cada parte:

Prefere arquivos de verdade? Leia-os com !file

Para manter o módulo como arquivos normais que você edita no IDE, coloque-os numa pasta ao lado da suíte e marque cada entrada de files: com !file. A suíte fica sem nenhum código:
Sem a tag, o valor é conteúdo. go.mod: testdata/strutil/go.mod grava um go.mod cujo texto é esse caminho, e o módulo deixa de compilar. Escreva !file antes do caminho.
  • Os caminhos são resolvidos em relação ao arquivo da suíte, como no fixture:, e o !file mantém as permissões do arquivo de origem, então um script que você roda no setup: continua executável.
  • Para a pasta inteira, use fixture: testdata/strutil: copia a árvore numa linha. O fixture: precisa apontar para um diretório; apontar para um arquivo falha com uma mensagem que diz isso e sugere o !file.
  • Os dois se misturam. A fixture é copiada primeiro, depois os files: são gravados por cima, depois o setup: roda. Mantenha o módulo numa fixture e troque um arquivo por caso, inline ou com !file.
  • Chame a pasta de testdata/. go build, go test ./... e go vet a ignoram, então o módulo quebrado fica fora do repositório que hospeda a suíte.
O chatcli eval validate lê todo !file enquanto confere a suíte, então um caminho errado, um diretório ou uma tag escrita errada (!fiel) aparecem antes de você gastar qualquer coisa. Arquivos no disco ou inline lista todas as mensagens, e Padrões de workspace mostra sete formas de combinar fixture, files e setup, além de como conferir um workspace sem chamar modelo.

2. Confira de graça

Nenhum dos dois comandos sobe um provider. Uma chave escrita errada dá erro aqui, e nunca vira uma checagem ignorada em silêncio.

3. Rode e salve um baseline

Use um juiz que seja um modelo diferente do candidato: um modelo avaliando a si mesmo carrega os próprios pontos cegos, e o relatório marca uma execução assim como self_judged. Duas tentativas por caso mostram se a aprovação é consistente.
Como ler:
  • Cada linha de tentativa e de caso mostra o custo do candidato mais o do juiz. As tentativas de chat custaram uns US0,0035,masquasetudoeˊdojuiz:ocandidatogastoucercadeUS 0,0035, mas quase tudo é do juiz: o candidato gastou cerca de US 0,00014 por resposta. A linha de resumo separa os dois (custo $0.0157 + juiz $0.0065).
  • 2/2 tentativas com a pass_policy: all padrão é o pass^k: todas as tentativas passaram. Duas em três mostrariam o caso como instável, e ele reprovaria.
  • O run1.json é o seu baseline. Guarde-o.

4. Leia o relatório

O relatório JSON guarda toda checagem de toda tentativa, o raciocínio do juiz, as ferramentas chamadas e a resposta final. Algumas linhas de jq respondem à maioria das perguntas:
O juiz explica cada nota. Na primeira tentativa de chat ele deu 0,90:
E a resposta final do coder disse o que mudou:
O harness não confiou nessa frase: tests pass e tests untouched rodaram por conta própria no sandbox.

5. Monte o baseline e as variantes

Para testar uma mudança, rode a mesma suíte com aquela única mudança contra o baseline. Guarde cada variante numa pasta própria, ao lado da suíte que ela altera:
Por que esse layout funciona:
  • Uma mudança por variante. Quando uma variante regride, você sabe qual mudança causou. Duas mudanças na mesma variante deixam você adivinhando.
  • O mesmo name de suíte e os mesmos ids de caso em toda cópia. A comparação casa os casos por <suíte>/<id>. Renomeie um dos dois e o compare informa o caso como removido e um novo como adicionado, em vez de compará-los.
  • Variantes em subpastas ficam fora das execuções normais. chatcli eval run evals-demo/ carrega só os *.yaml que estão diretamente naquela pasta; subpastas não são varridas. A execução do baseline nunca pega uma variante por acidente, e cada variante só roda quando você indica o arquivo dela.
  • Um relatório por execução, numerado. Toda variante é comparada com o mesmo run1.json.
Crie uma variante copiando a suíte e fazendo a única mudança, e confira com diff que nada mais mudou:
chatcli eval validate badcfg/demo.yaml confere a cópia de graça, antes de você gastar qualquer coisa com ela.

6. Teste uma mudança que só custa mais

Imagine que alguém baixa o teto de tokens de saída para economizar. Coloque a mudança no env: da suíte, não na sua linha de comando:
Defina o que está testando no env:, não no seu shell. Cada candidato é um processo chatcli separado, que recebe o seu ambiente mais o env: da suíte. O juiz roda dentro do próprio processo do chatcli eval, então ANTHROPIC_MAX_TOKENS=200 chatcli eval run … também limitaria o juiz, truncaria os vereditos dele e corromperia as notas. Uma variável no env: chega só aos candidatos.
Mantenha o name da suíte e os ids dos casos, para que a comparação case caso a caso, e rode contra o baseline:
Continua passando, com exit code 0. O loop do coder percebe uma resposta cortada no limite de tokens, aumenta o limite e continua, então um teto baixo não o quebra. Mesmo assim a mudança aparece no relatório, como turnos a mais:
Aprovar ou reprovar é só metade do que um eval diz. Uma mudança pode manter todos os casos verdes e ainda custar mais turnos, tokens ou tempo; compare turns, output_tokens e cost_usd entre os relatórios para ver isso.

7. Teste uma mudança que quebra o coder

Agora imagine que alguém endurece a policy do coder de modo que ele não consiga mais alterar arquivos. O policy: de um caso instala regras só para aquele caso, mescladas sobre a sua policy global:
write, patch e multipatch são os subcomandos que editam arquivos. O exec também é negado: sem isso, o agente ainda conseguiria editar pelo shell.
O que a execução mostra:
  • Só o caso afetado reprova. O caso de chat não usa ferramentas e continua passando.
  • A nota diz o quanto piorou. Quatro das seis checagens continuam passando (os testes estão intactos, o @coder foi chamado, turnos e custo ficaram no orçamento), então a tentativa fica com 0,67, não 0. O veredito continua sendo reprovação, porque uma tentativa só passa quando todas as checagens passam.
  • Exit code 3 significa regressão contra o baseline. É o código que faz um job de CI falhar.
  • O comportamento do agente diante do bloqueio também fica visível. A resposta final dele não afirmou ter corrigido e não ficou tentando outras formas de contornar a regra:
    Num eval ninguém pode responder à pergunta do agente, então o caso reprova. É exatamente o ponto: foi a mudança de policy que o quebrou.

8. Compare as três execuções

Lado a lado, os três relatórios contam três histórias diferentes: Os turnos vêm dos relatórios (jq '.cases[].trials[] | .turns' runN.json). Nem a taxa de aprovação nem o exit code mostram o custo da segunda mudança; os turnos mostram.

Offline, sem chamar modelo

O compare refaz a comparação a partir de dois relatórios salvos, sem chamar modelo, com os mesmos gates e exit codes do run:
Um pipe esconde o exit code. Em chatcli eval compare … | tail -5; echo $?, o $? é o exit code do tail, que é sempre 0. Leia o exit code sem pipe, ou ligue set -o pipefail em scripts e passos de CI.

9. Gate de pull requests no CI

Commite a suíte e um baseline gerado na main, e rode a suíte contra esse baseline em todo pull request. Um job de GitHub Actions:
.github/workflows/evals.yml
  • O --max-cost limita cada execução: quando o gasto de candidato mais juiz chega ao valor, nenhuma tentativa nova começa e as restantes aparecem como puladas.
  • O --markdown grava o resumo, o delta contra o baseline e as checagens que falharam, pronto para o resumo do job ou um comentário de pull request.
  • Atualize o baseline de propósito, depois de uma mudança que você aceita, rodando de novo na main com --out evals/baseline.json.

Armadilhas


Relacionados

Evals

Todo campo, checagem, flag e exit code.

Segurança do Coder

As regras de policy que o campo policy: de um caso usa.