◈ TRIADIC.LIFE / Engenharia de IA / Módulo 3 0/9 aulas
Módulo 03 · 24h estimadas · novo

Avaliação (Evals) e Testes de Sistemas com LLM

Você mudou o prompt e "parece melhor". Trocou de modelo e "parece igual". Sem medição, essas frases são palpites. Este módulo ensina a transformar qualidade de IA em número confiável: o que separa um protótipo de demonstração de um sistema em que uma empresa aposta dinheiro.

7 aulas 4 laboratórios Projeto em TypeScript Pré-requisito: Módulo 2 (o projeto dele é avaliado aqui)

Ao final do módulo você vai

  • Montar um dataset de avaliação e auditá-lo antes de confiar nele.
  • Escrever avaliadores por código que não são nem rígidos nem frouxos demais.
  • Usar um LLM como juiz e medir o quanto ele concorda com pessoas.
  • Rodar evals no CI e bloquear regressões antes do merge.
  • Comparar modelos e prompts sabendo quando a diferença é real e quando é ruído.
  • Fechar o ciclo entre o que falha em produção e o que entra no dataset.

Preparando o ambiente

cd o-arco/curso-ia/codigo/modulo-03

pip install -r python/requirements.txt     # labs (só o 3.4 chama a API)
cd projeto && npm install && cd ..         # instala também o projeto do Módulo 2

3.1 Por que "parece bom" não é métrica

Leitura · 40 min

Em software tradicional, soma(2, 2) devolve 4 sempre, e um teste unitário prova isso. Em sistemas com LLM, a mesma entrada pode gerar saídas diferentes, "certo" muitas vezes é uma questão de grau, e uma melhora num caso pode piorar outros dez. O olhômetro (rodar três exemplos e achar bonito) falha de formas previsíveis:

  • Viés de seleção: você testa os exemplos de que lembra, que costumam ser os fáceis.
  • Regressões invisíveis: o ajuste que conserta o caso A quebra os casos B e C, que você não olhou.
  • Ruído confundido com sinal: uma resposta melhor pode ser só sorte da amostragem.
  • Decisões caras sem base: "o modelo barato serve?" vira opinião contra opinião.

As quatro peças de um eval

Dataset: entradas + o que se espera→ Executor: roda o sistema real→ Avaliadores: transformam saídas em notas→ Relatório: números com margem de erro
TipoPergunta que respondeQuando roda
Teste unitárioO código ao redor do LLM funciona? (parsing, validação, custo)A cada commit, sem chamar a API
Eval de regressãoA mudança piorou algo que funcionava?A cada PR que mexe no prompt, modelo ou código de IA
Eval de capacidadeO sistema resolve bem o problema? Qual modelo é melhor?Antes de decisões: troca de modelo, lançamento
MonitoramentoEstá funcionando com os usuários de verdade?Continuamente, em produção (aula 3.7)
Comece pequeno, mas comece Vinte casos bem escolhidos, rodando com um comando, valem mais que um plano de mil casos que nunca sai do papel. O eval cresce junto com o sistema: cada bug encontrado vira um caso novo.

Quiz rápido

Você melhorou o prompt e testou 3 exemplos: todos ficaram melhores. O que isso prova?

É um bom indício para continuar investigando, não uma conclusão. O eval é o que transforma indício em evidência.

3.2 Montando o dataset de avaliação

Leitura + lab · 3h

De onde vêm os casos

  1. Tráfego real (anonimizado): a melhor fonte, porque é o que o sistema enfrenta de verdade.
  2. Casos de borda escritos à mão: o recibo sem CNPJ, o OCR ruim, o texto que nem é recibo, o documento com instrução maliciosa.
  3. Bugs que já aconteceram: cada falha em produção vira um caso, para nunca voltar.
  4. Casos sintéticos gerados por LLM: úteis para variedade, mas revise uma amostra. Se algo sai errado, corrija o gerador, não caso a caso.

Regras de um bom dataset

  • Critério de sucesso inequívoco. Dois especialistas olhando a mesma saída devem concordar se passou. "Um bom resumo" não é critério; "cita o valor e o prazo" é.
  • Registre a origem do gabarito. Escrito por uma pessoa, verificado por uma pessoa, ou saída de um modelo? Nunca use as respostas do modelo A como gabarito para comparar A com B: você estaria medindo o quanto B imita A.
  • Cobertura nos dois sentidos. Se o sistema deve sinalizar recibos com problema, também precisa de casos em que não deve sinalizar. Senão, "sinalizar tudo" tira nota máxima.
  • Distribuição realista e balanceada. Compare com a produção. Informe sempre a linha de base da classe majoritária: se 72% dos casos são "dúvida", um sistema que sempre responde "dúvida" já acerta 72%.
  • Sem vazamento. A resposta não pode aparecer no texto de entrada, nos exemplos do prompt nem em lugar que o sistema consiga ler.
  • Tags por caso (ocr, desconto, seguranca): permitem ver onde o sistema falha, não só quanto.
  • Versionado junto com o código. Notas de antes e depois de mudar o dataset não são comparáveis.

LAB 3.2 — Auditando um dataset com problemas plantados

dados/dataset_suporte.jsonl tem 20 chamados de suporte rotulados por categoria, com defeitos colocados de propósito. O script encontra os que dá para encontrar com código:

python python/01_auditar_dataset.py
[campo vazio]      c13: categoria
[campo vazio]      c18: texto, categoria
[duplicata exata]  c02, c05
[quase duplicata]  c02, c05, c06
[vazamento]        c16: o texto contém o próprio rótulo 'bug'

Distribuição dos rótulos:
  duvida          13  █████████████
  cobranca         2  ██
  bug              2  ██
  cancelamento     1  █

Linha de base da classe majoritária: responder sempre 'duvida' acerta 72%.
[desbalanceado]    mais da metade dos casos é de uma única classe
  1. Leia os 20 casos. Há pelo menos um rótulo discutível que o script não pega. Qual é, e como você o resolveria?
  2. Rode o script no casos.jsonl do projeto deste módulo (adapte os nomes dos campos).
  3. Escreva 5 casos novos que equilibrem as categorias.

Quiz rápido

Para comparar o modelo atual (A) com um novo (B), alguém propõe usar as respostas de A como gabarito. Problema?

Gabarito derivado de um dos competidores faz esse competidor ganhar por construção. Use gabarito humano, verificado, ou uma rubrica.

Um eval de "deve escalar para humano?" só tem casos em que a resposta certa é "sim". O que dá errado?

Sem casos negativos, o eval recompensa o comportamento degenerado. Cubra os dois sentidos.

3.3 Avaliadores baseados em código

Leitura + lab · 3h

Sempre que der para verificar com código, verifique com código: é rápido, barato, determinístico e não tem opinião. O desafio é calibrar o rigor.

AvaliadorBom paraArmadilha
Igualdade exataIDs, enums, códigosRígido: reprova "brasilia." contra "Brasília"
Igualdade normalizadaRespostas curtasAinda reprova respostas certas com frase em volta
Número com tolerânciaValores, totais, medidasFormatos (1.234,56 vs 1234.56) precisam ser tratados
"Contém"Verificar se um fato apareceFrouxo: aprova "Não é Brasília, é o Rio"
Schema / tiposSaídas estruturadasGarante a forma, não o conteúdo
Regras de negócioSomas, datas, dígitos verificadoresSó pega o que você pensou em verificar
Executar e testarCódigo gerado, SQL, configuraçõesTestes fracos aprovam soluções erradas

Checagens atômicas, não uma nota única

Avalie cada propriedade separadamente (total, data, cnpj, sinalizacao…). Uma nota única como "7,3" esconde o que piorou. Checagens separadas mostram que a data piorou nos recibos com ano de dois dígitos, e isso aponta direto para a correção.

Teste o seu avaliador

Antes de confiar em qualquer nota, rode duas verificações baratas:

  • Oráculo: passe o próprio gabarito pelo avaliador. Tem que dar 100%. Se não der, o avaliador ou o caso estão errados.
  • Linha de base nula: passe uma saída vazia ou boba. Tem que dar perto de 0%. Se não der, o avaliador é frouxo.

O projeto deste módulo faz as duas coisas como testes automatizados.

LAB 3.3 — Do rígido ao frouxo

python python/02_avaliadores.py
saída                                     certo?   exato  normal.  contém
"Brasília"                                     ✓       ✓        ✓       ✓
"brasilia."                                    ✓      ✗!        ✓      ✗!
"  Brasília "                                  ✓      ✗!        ✓       ✓
"A capital do Brasil é Brasília."              ✓      ✗!       ✗!       ✓
"Não é Brasília, é o Rio de Janeiro."          ✗       ✗        ✗      ✓!
"Rio de Janeiro"                               ✗       ✗        ✗       ✗

discordâncias com o humano: {'exato': 3, 'normalizado': 1, 'contem': 2}
  1. Nenhum dos três acerta tudo. Que avaliador você escreveria para essa pergunta? (Dica: talvez a melhor solução seja mudar o sistema, pedindo saída estruturada.)
  2. O avaliador de schema reprova o JSON dentro de ```json. Corrija o avaliador para aceitá-lo. Depois responda: o sistema deveria produzir essa cerca?

Quiz rápido

Seu avaliador dá 40% para o próprio gabarito. A conclusão é…

O gabarito deve passar por definição. É o teste de oráculo, que pega a maioria dos bugs de eval em minutos.

3.4 LLM como juiz, e como calibrá-lo

Leitura + lab · 4h

Algumas qualidades não cabem em código: o resumo é fiel? A explicação aponta o problema certo? O tom é adequado? Para isso, um LLM avalia a saída de outro com uma rubrica. Funciona bem, desde que você trate o juiz como mais um componente a ser testado.

Como escrever um bom juiz

  • Rubrica concreta. Critérios verificáveis ("cita o valor ou o prazo"), não "avalie se é bom".
  • Um critério por chamada quando o diagnóstico importa: fidelidade, completude e formato como notas separadas.
  • Justificativa antes do veredito, no formato estruturado: o juiz raciocina antes de decidir, e você consegue auditar.
  • Saída estruturada ({"justificativa": "...", "aprovado": true}): nada de procurar "SIM" no texto.
  • A saída avaliada é dado, não instrução. Coloque-a entre tags e diga ao juiz para ignorar instruções dentro dela.

Os vieses conhecidos

ViésO que aconteceMitigação
PosiçãoEm comparações A×B, prefere a primeira (ou a segunda) opçãoSortear a ordem, ou avaliar nas duas ordens
VerbosidadePrefere respostas mais longasDizer na rubrica que tamanho não conta; limitar tamanho como critério
AutopreferênciaPrefere textos parecidos com os que ele mesmo escreveriaJuiz diferente do modelo avaliado
Deferência a rótulosFavorece a resposta chamada de "referência" ou "humana"Não dizer ao juiz de onde veio cada resposta

Calibração: o juiz concorda com pessoas?

Peça a uma pessoa que avalie algumas dezenas de casos de forma independente e compare com o juiz. Duas métricas:

  • Concordância: % de casos em que juiz e pessoa decidiram igual.
  • Kappa de Cohen: concordância descontado o acaso. Um juiz que aprova tudo pode ter 50% de concordância e kappa 0: não sabe nada.

Regra prática: abaixo de ~90% de concordância em casos claros, melhore a rubrica antes de usar o juiz para tomar decisões. Teste também casos negativos óbvios (resposta vazia, "não sei", resposta a outra pergunta): o juiz precisa reprovar todos.

LAB 3.4 — Calibrando um juiz de resumos

dados/resumos_rotulados.jsonl tem 12 resumos do mesmo e-mail, cada um com aprovação ou reprovação de uma pessoa e o motivo. O script pede ao juiz que avalie os mesmos resumos e calcula a concordância e o kappa:

python python/03_llm_juiz.py
python python/03_llm_juiz.py --rubrica-vaga
  1. Compare concordância e kappa entre a rubrica boa e a vaga. Em quais casos a rubrica vaga erra?
  2. O resumo r06 é fiel, mas longo. A pessoa reprovou. O juiz concorda? Se não, que critério faltou?
  3. Rode o juiz duas vezes. Os vereditos mudam? Isso é variância do juiz, que se soma à do sistema avaliado.

Quiz rápido

Seu juiz tem 50% de concordância com humanos e kappa 0. O que provavelmente está acontecendo?

Kappa perto de 0 significa concordância igual à do acaso. Um juiz que aprova tudo produz exatamente isso.

3.5 Evals no CI: regressão bloqueada antes do merge

Leitura + prática · 3h

Um eval que só roda quando alguém lembra não protege nada. Coloque-o no pipeline, como os testes. Como evals custam dinheiro e tempo, use camadas:

CamadaQuandoO que rodaCusto
Testes rápidosTodo commitTestes unitários, oráculo, linha de base nula. Sem API.Zero
Eval de fumaçaPR que toca prompt, modelo ou código de IASuíte pequena, 1 repetição, com limiar mínimoCentavos
Suíte completaTodas as noites e antes de lançamentosTodos os casos, várias repetições, LLM-juizAlguns dólares

Cuidados que separam um eval de CI útil de um irritante

  • Erro de infraestrutura não é nota zero. Um 429 ou timeout vai para uma lista separada. Se houver erros demais, o job falha por infraestrutura, não por qualidade.
  • Limiar com folga. Com poucos casos o ruído é grande (aula 3.6). Um limiar exatamente na nota atual vai falhar aleatoriamente e o time vai aprender a ignorá-lo.
  • Mesma configuração de produção. O eval deve chamar o código real, com o mesmo prompt e modelo. Uma reimplementação "parecida" mede outra coisa.
  • Confira o modelo que respondeu (campo model da resposta). Uma troca silenciosa invalida o resultado.
  • Guarde tudo: entrada, saída, checagens, custo e latência por caso, como artefato do job. Quando a nota cair, você precisa ver por quê sem rodar de novo.
  • Segredo da API só como secret do CI, e nunca em PRs vindos de forks.

O projeto traz exemplo-github-actions.yml com essas camadas: testes rápidos em todo PR, eval de fumaça com limiar de 90% e suíte completa com juiz todas as noites.

Quiz rápido

A API retornou 429 em 3 de 30 casos durante o eval. Como contar esses casos?

Misturar falhas de rede com falhas do modelo contamina a nota e faz um dia de API instável parecer uma regressão.

3.6 Comparar modelos e prompts sem se enganar

Leitura + lab · 3h

Um eval com N casos dá uma estimativa da taxa real de acerto. Toda estimativa tem margem de erro, e ela é maior do que a intuição sugere. Uma aproximação útil para a meia-largura do intervalo de 95% de uma taxa: ±1/√casos (é o pior caso, com a taxa perto de 50%; perto de 0% ou 100% a margem é menor). Com 25 casos, ±20 pontos percentuais. Com 100, ±10.

Repetições não são casos novos É tentador contar casos × repetições como se fossem amostras independentes: 25 casos com 4 repetições dariam ±10 pontos, como 100 casos. Não dão. As repetições do mesmo caso são correlacionadas: se o caso é difícil, ele tende a falhar em todas. Repetir mede a variabilidade da geração (o modelo responder diferente à mesma pergunta); acrescentar casos mede a cobertura dos problemas reais. São duas fontes de incerteza diferentes, e repetir só diminui a primeira. A fórmula com casos × repetições só vale no caso extremo em que cada execução se comporta como um caso novo, e por isso subestima a incerteza.

Na prática: trate o caso como unidade. Calcule a taxa de cada caso (média das suas repetições), compare sistemas em pares nos mesmos casos e, quando a distribuição for estranha ou houver poucos casos, use reamostragem por caso (bootstrap): sorteie casos com reposição, levando junto todas as repetições de cada um, e veja quanto a média varia. O projeto deste módulo faz isso (bootstrapPorCaso em src/estatistica.ts).

LAB 3.6 — Simulando o ruído

python python/04_ruido_estatistico.py

O sistema A acerta 80% de verdade, o B acerta 85%. Quantas vezes o eval percebe isso?

    casos  B parece melhor   empate  A parece melhor  diferença detectada*
       10              49%      23%              27%                    4%
       25              61%      13%              26%                    6%
       50              70%       8%              22%                   10%
      100              80%       5%              16%                   14%
      200              89%       2%               8%                   28%
      500              98%       0%               1%                   55%
     1000             100%       0%               0%                   83%

Com 25 casos, em 26% das vezes o sistema pior parece melhor. Para detectar com confiança uma melhora de 5 pontos, são necessárias centenas de casos.

  1. Mude para A = 70% e B = 90%. Quantos casos bastam agora?
  2. Qual a menor diferença que vale a pena detectar no seu projeto? Quantos casos isso exige?

Na prática

  • Sempre informe o intervalo, não só a taxa: "82% (61%–93%)" conta uma história bem diferente de "82%".
  • Compare em pares: os dois sistemas nos mesmos casos. A diferença caso a caso tem muito menos ruído do que comparar duas notas gerais.
  • Repetições reduzem o ruído do não determinismo dentro de cada caso; mais casos reduzem o ruído de quais casos você escolheu. Não são equivalentes: se a dúvida é "o sistema funciona nos problemas reais?", só casos novos e independentes respondem. Use repetições para saber se um caso é estável, não para inflar o N.
  • Olhe caso a caso mesmo quando o agregado é inconclusivo: dois casos que falham sempre com o modelo B são uma pista concreta.
  • Custo por tarefa concluída, não por chamada: inclua as tentativas extras, o juiz e a revisão humana dos casos sinalizados.

Exemplo: comparando dois modelos no projeto

O comando npm run comparar faz a comparação pareada. Abaixo, um exemplo ilustrativo, gerado com um modelo simulado que erra o total de dois recibos:

caso                                A (claude-opus-5-5)   B (claude-haiku-5-5)
academia-desconto-percentual                       100%                     0%  ▼
farmacia-desconto                                  100%                   100%
...
posto-quantidade-decimal                           100%                     0%  ▼

Diferença B − A: -18.2 p.p.  (IC 95%: -42.1 p.p. a +5.7 p.p.)
Conferência por reamostragem de casos: IC 95% -45.5 p.p. a +0.0 p.p.
→ Inconclusivo: a diferença cabe no ruído. Mais casos independentes ajudariam; repetições só reduzem a variação dentro de cada caso.

As duas linhas de intervalo usam a mesma unidade, o caso. A primeira supõe distribuição normal; a segunda reamostra os casos e não supõe nada sobre a forma. Quando elas discordam muito, desconfie da primeira: com poucos casos e diferenças que só valem 0 ou −100%, a aproximação normal é fraca.

Repare: B errou sempre nos mesmos dois casos, mas com só 11 casos o agregado é inconclusivo. A leitura certa: "não há evidência de que B seja equivalente; há dois padrões de erro concretos (quantidade decimal e desconto percentual) para investigar e transformar em mais casos".

Quiz rápido

Prompt novo: 84%. Prompt antigo: 80%. 25 casos, 1 repetição. O que concluir?

1/√25 = ±20 p.p. Uma diferença de 4 pontos está bem dentro do ruído. Rodar os mesmos 25 casos mais vezes não resolve: diz se cada caso é estável, mas não traz problemas novos.

3.7 Avaliação em produção

Leitura · 1h30

O dataset de avaliação é uma fotografia; a produção é o filme. Usuários fazem perguntas que você não previu, os dados mudam e o comportamento do modelo pode mudar entre versões. Avaliação online completa a offline:

  • Sinais explícitos: 👍/👎, "esta resposta ajudou?", correções feitas pelo usuário.
  • Sinais implícitos: o usuário copiou a resposta? Reformulou a pergunta logo depois? Abriu um chamado humano?
  • Checagens automáticas nas respostas reais: as mesmas regras de negócio do eval (soma fecha? JSON válido?).
  • LLM-juiz por amostragem: avaliar 1% a 5% do tráfego com a rubrica já calibrada.
  • Métricas operacionais: taxa de recusa, de max_tokens, de erros, latência e custo por tarefa.

O ciclo que faz o sistema melhorar

Produção→Falhas detectadas→Viram casos no dataset→Correção→Eval no CI prova a correção↺
Privacidade Dados de produção que entram no dataset precisam ser anonimizados e tratados conforme a LGPD e as políticas da empresa. Registre de onde veio cada caso e quem pode acessar o dataset.

Quiz rápido

Um usuário reportou que o extrator leu "1O/2026" como outubro de 2016. Qual o primeiro passo?

O caso no dataset prova o bug, prova a correção e impede que ele volte. É o "teste de regressão" do mundo de IA.

Projeto do módulo: evals do extrator de recibos

Projeto · 8–10h · TypeScript

Uma suíte de avaliação completa para o extrator do Módulo 2. Ela importa e executa o extrator real, sem cópias nem reimplementações, e aplica tudo o que este módulo ensinou.

cd projeto
npm install
npm test                                        # 14 testes: avaliadores, oráculo, linha nula, estatística
npm run eval                                    # 11 casos × 1 repetição
npm run eval -- --reps 3 --juiz                 # com repetições e LLM-juiz
npm run eval -- --modelo claude-haiku-5-5 --reps 3
npm run comparar -- resultados/A.json resultados/B.json
npm run eval -- --limiar 0.9                    # código de saída 1 se ficar abaixo (CI)

O que tem dentro

ArquivoResponsabilidade
casos/casos.jsonl11 casos com tags e origem do gabarito: recibos limpos, desconto, acréscimo, OCR com soma errada, sem CNPJ, quantidade decimal, ano com 2 dígitos, prompt injection e um texto que nem é recibo
src/avaliadores.tsChecagens atômicas com tolerâncias (centavos, formatação de CNPJ) e sinalização avaliada nos dois sentidos
src/juiz.tsLLM-juiz com rubrica para a qualidade da explicação nos casos com problema, usando um modelo diferente do avaliado
src/estatistica.tsWilson, diferença pareada, reamostragem por caso (bootstrap), percentis, piso de ruído por caso
src/executar.tsExecutor com concorrência, repetições, erros de infraestrutura separados, conferência do modelo servido, relatório por checagem e por tag, limiar para CI
src/comparar.tsComparação pareada entre duas execuções, com custo e latência
test/Oráculo, linha de base nula, "sinalizar tudo não passa", tolerâncias e casos de segurança
exemplo-github-actions.ymlWorkflow em camadas para copiar para o seu repositório

Entregáveis

  1. Rode a suíte com claude-opus-5-5 e claude-haiku-5-5, com 3 repetições cada, e compare. Registre taxa com intervalo, custo por recibo e latência.
  2. Expanda para pelo menos 30 casos, incluindo os recibos que você criou no Módulo 2. Mantenha cobertura nos dois sentidos.
  3. Calibre o juiz: rotule você mesmo as explicações de 10 execuções e meça a concordância.
  4. Faça uma mudança no prompt do extrator que você acha que melhora. Prove (ou refute) com o eval e o comparador.
  5. Coloque o workflow no seu repositório e mostre um PR bloqueado por regressão.

Critérios de avaliação

CritérioPeso
Dataset de 30+ casos, com tags, origem e cobertura nos dois sentidos25%
Comparação de modelos com intervalos e custo por tarefa20%
Calibração do juiz com concordância medida20%
Mudança de prompt provada ou refutada com o comparador20%
CI funcionando15%

Recursos para aprofundar

Todos gratuitos