Existe um padrão que se repete em todo projeto de IA que dá errado. Alguém mexe no prompt, testa com três perguntas, acha que melhorou e sobe. Duas semanas depois o sistema está pior num aspecto que ninguém estava olhando, e não há como saber em qual das onze mudanças isso aconteceu.

A cura é modesta: trinta casos, um script, um número. Leva meio dia para montar e transforma toda discussão de qualidade em comparação de duas colunas.

Comece pelos trinta casos

Não é preciso um conjunto de mil. Trinta casos bem escolhidos pegam quase toda regressão relevante. O que faz um bom conjunto:

💡 O conjunto cresce por incidente, não por planejamento

A melhor forma de chegar a cem casos não é sentar e escrever cem. É: toda vez que alguém reclamar de uma resposta, aquela pergunta entra no conjunto com a resposta certa anotada.

Em três meses você tem um conjunto que representa exatamente os seus problemas reais — e nenhuma regressão antiga volta sem ser detectada.

Três formas de pontuar, da mais confiável à menos

1. Verificação exata — use sempre que der

Quando a resposta certa é verificável por código, não envolva modelo nenhum no julgamento. Classificação em categorias, extração de campos, saída em JSON, cálculo numérico, "contém este código de produto".

É rápido, é de graça, é determinístico e não tem opinião. Se 60% do seu conjunto pode ser avaliado assim, avalie assim.

2. Verificação por regra — o meio-termo subestimado

Para resposta em texto livre, muita coisa ainda dá para checar com regra:

def checar(resposta, caso):
    ok = []
    ok.append(("cita_fonte",   "[doc" in resposta))
    ok.append(("tem_termo",    all(t in resposta.lower() for t in caso["deve_conter"])))
    ok.append(("sem_proibido", not any(t in resposta.lower() for t in caso["nao_pode"])))
    ok.append(("tamanho",      len(resposta.split()) <= caso.get("max_palavras", 400)))
    ok.append(("sem_invencao", "não sei" in resposta.lower()
                               if caso["sem_resposta_no_acervo"] else True))
    return dict(ok)

A última linha é a mais valiosa de todas. Coloque no conjunto uns cinco casos cuja resposta não existe no seu acervo e exija que o sistema diga que não sabe. É o teste que pega alucinação — e é o que quase ninguém faz.

3. Modelo-juiz — para o que sobrou

Para "a resposta está boa?", em que não há regra, use um modelo avaliando o outro. Funciona bem, com ressalvas importantes.

JUIZ = """Você avalia respostas de um assistente.

PERGUNTA: {pergunta}
RESPOSTA ESPERADA (referência): {esperada}
RESPOSTA DO SISTEMA: {obtida}

Avalie de 1 a 5:
- 5: correta e completa
- 4: correta, faltou detalhe secundário
- 3: parcialmente correta
- 2: majoritariamente errada
- 1: errada ou inventada

Responda em JSON: {{"nota": N, "motivo": "uma frase"}}"""
⚠️ Onde o modelo-juiz mente

Prefere resposta longa. Entre duas corretas, ele dá nota maior à mais extensa. Se você está justamente tentando encurtar as respostas, o juiz vai te penalizar por acertar.

Prefere o próprio estilo. Um modelo tende a dar nota melhor a textos parecidos com os que ele produz. Use um modelo de família diferente da que gerou a resposta.

É frouxo com número. Ele raramente confere se o valor citado bate com a referência. Cheque números com regra, nunca com juiz.

O script

import json, statistics
from concurrent.futures import ThreadPoolExecutor

casos = json.load(open("casos.json", encoding="utf-8"))

def avaliar_um(caso):
    resposta = meu_sistema(caso["pergunta"])          # o SEU sistema, como está
    regras = checar(resposta, caso)
    nota_juiz = julgar(caso, resposta) if caso.get("usa_juiz") else None
    return {
        "id": caso["id"],
        "passou_regras": all(regras.values()),
        "regras": regras,
        "nota": nota_juiz,
        "resposta": resposta,
    }

with ThreadPoolExecutor(max_workers=8) as ex:
    r = list(ex.map(avaliar_um, casos))

taxa = sum(x["passou_regras"] for x in r) / len(r)
notas = [x["nota"] for x in r if x["nota"] is not None]

print(f"regras: {taxa:.1%}")
print(f"juiz:   {statistics.mean(notas):.2f}" if notas else "")
json.dump(r, open("resultado.json", "w", encoding="utf-8"),
          ensure_ascii=False, indent=2)

for x in r:                       # o que interessa de verdade
    if not x["passou_regras"]:
        print(f"✗ {x['id']}: {[k for k,v in x['regras'].items() if not v]}")

Vinte linhas. Rode antes e depois de cada mudança e guarde o resultado.json com o nome da versão. É todo o processo.

Como ler o resultado sem se enganar

Com trinta casos, cada um vale 3,3 pontos percentuais. Isso significa que uma diferença de 3% é um caso — ou seja, ruído. Não comemore, não reverta.

Diferença observadaCom 30 casosCom 100 casos
1 a 3 pontosRuído. Ignore.Ruído. Ignore.
4 a 8 pontosTalvez. Olhe os casos que mudaram.Provavelmente real.
Acima de 10 pontosReal.Real e relevante.

E sempre abra os casos que mudaram de resultado. Duas mudanças que se anulam na média podem esconder "consertei cinco e quebrei cinco outros" — que não é empate, é troca de problema.

Fixe a temperatura em zero na avaliação. Com temperatura ligada, você vai medir sorte junto com qualidade e nunca vai saber a proporção.

O custo

Uma rodada de 30 casos, com o sistema respondendo e o juiz avaliando:

Barato o bastante para rodar a cada alteração, e não só antes de subir. Quem roda só na véspera do lançamento perdeu o principal benefício, que é descobrir a regressão no dia em que ela foi introduzida.

Rode a avaliação a cada mudança

API por token cobrada em reais, sem assinatura — uma rodada completa custa centavos e você paga só o que usar.

Criar conta →

Erros comuns na hora de montar

Conclusão

Avaliação de IA não precisa ser um projeto. Trinta casos reais, verificação por regra no que dá, modelo-juiz no resto, e um script que imprime um número e a lista de falhas.

O ganho não é técnico, é organizacional: a discussão sai de "eu acho que ficou melhor" para "84% contra 71%, e estes cinco casos regrediram". A partir daí, todo mundo para de discutir e vai olhar os cinco casos.

Continue: ajustar com LoRA · escolher embeddings · observabilidade