Pular para o conteúdo
IAAvançado

Avaliação de LLM com evals: do conjunto de testes ao CI

Monte evals para sua aplicação com LLM: conjunto de casos, métricas determinísticas, LLM como juiz e um gate no CI que barra regressões antes do deploy.

Por Equipe IAUAI Estudos · 9 de agosto de 2026 · 10 min de leitura

Nesta página

Neste artigo você vai montar um harness de evals do zero: um conjunto de casos, métricas que não dependem de modelo, um juiz baseado em LLM com cuidados contra viés e um gate no GitHub Actions que reprova mudanças que pioram a qualidade.

O problema real

Você ajustou o prompt do assistente, mudou o tamanho dos chunks do RAG e trocou o modelo de embeddings. Parecia melhor nos três exemplos que você testou. Dois dias depois, o suporte avisa que as respostas sobre devolução estão erradas. Com uma avaliação automática, isso apareceria em minutos, antes do deploy.

Evals são testes automatizados para sistemas probabilísticos. A diferença para teste unitário é que o resultado é uma nota entre 0 e 1 sobre vários casos, e você compara notas entre versões em vez de exigir igualdade exata.

Os quatro componentes

  1. Conjunto de casos (golden set): entradas representativas com critério de acerto.
  2. Métricas: regras determinísticas ou um modelo como juiz.
  3. Harness: o programa que roda o sistema sobre os casos e agrega resultados.
  4. Gate de CI: compara com uma linha de base e falha o build se a nota cair.

1. O conjunto de casos

Comece com 20 a 50 casos escolhidos a dedo. Fontes boas: conversas reais que deram errado, perguntas que o time de suporte sempre recebe, casos de borda (pergunta sem resposta nos documentos, tentativa de manipulação, entrada vazia). Um conjunto pequeno e honesto vale mais que mil casos gerados sem revisão.

Guarde em JSON Lines, um caso por linha, em evals/casos.jsonl:

{"id": "devolucao_01", "entrada": "Qual o prazo para devolver um produto?", "metrica": "contem", "termos": ["30 dias"]}
{"id": "extracao_01", "entrada": "Nome: Ana Souza, e-mail: [email protected]", "metrica": "json", "campos": ["nome", "email"]}
{"id": "limite_01", "entrada": "Quem é o presidente da nossa empresa?", "metrica": "juiz", "criterio": "Admite que não tem a informação e não inventa um nome."}

Divida os casos por categoria. A nota média esconde problemas locais: o geral pode subir enquanto a categoria "devolução" despenca.

2. Métricas

Prefira métricas determinísticas sempre que o critério permitir. São baratas, rápidas e repetíveis.

import json

def metrica_contem(saida: str, termos: list[str]) -> float:
    achados = sum(1 for t in termos if t.lower() in saida.lower())
    return achados / len(termos)

def metrica_json(saida: str, campos: list[str]) -> float:
    try:
        dados = json.loads(saida)
    except json.JSONDecodeError:
        return 0.0
    return 1.0 if all(c in dados for c in campos) else 0.0

Para critérios subjetivos (clareza, fidelidade ao contexto, tom), use um modelo como juiz. Dê uma rubrica específica e peça uma saída estruturada.

import os
import anthropic

client = anthropic.Anthropic()
MODELO_JUIZ = os.environ["JUIZ_MODEL"]

def metrica_juiz(entrada: str, saida: str, criterio: str) -> float:
    prompt = (
        "Você avalia respostas de um assistente.\n"
        f"Critério: {criterio}\n\n"
        f"Pergunta: {entrada}\n"
        f"Resposta: {saida}\n\n"
        'Responda só com JSON: {"nota": 0 a 5, "motivo": "uma frase"}'
    )
    r = client.messages.create(
        model=MODELO_JUIZ,
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}],
    )
    try:
        return json.loads(r.content[0].text)["nota"] / 5
    except (json.JSONDecodeError, KeyError, TypeError):
        return 0.0

Juízes têm vieses conhecidos: preferem respostas longas, favorecem texto parecido com o do próprio modelo e podem mudar de nota entre execuções. Alguns cuidados reduzem o problema:

  • Use um modelo juiz diferente do modelo que gerou a resposta.
  • Escreva rubricas objetivas ("cita o prazo de 30 dias") em vez de "a resposta é boa".
  • Confira o juiz contra 20 ou 30 notas humanas. Se discordam muito, ajuste a rubrica antes de confiar na métrica.
  • Ao comparar duas versões, troque a ordem de apresentação para anular viés de posição.

3. O harness

O harness roda o sistema, aplica a métrica de cada caso e agrega por categoria. Falhas do próprio sistema contam como nota zero.

import json
import time
from collections import defaultdict

def rodar_evals(sistema, caminho="evals/casos.jsonl") -> dict:
    por_categoria = defaultdict(list)
    todas, latencias = [], []

    with open(caminho, encoding="utf-8") as f:
        casos = [json.loads(linha) for linha in f if linha.strip()]

    for caso in casos:
        inicio = time.perf_counter()
        try:
            saida = sistema(caso["entrada"])
        except Exception as erro:
            saida, nota = f"ERRO: {erro}", 0.0
        else:
            if caso["metrica"] == "contem":
                nota = metrica_contem(saida, caso["termos"])
            elif caso["metrica"] == "json":
                nota = metrica_json(saida, caso["campos"])
            else:
                nota = metrica_juiz(caso["entrada"], saida, caso["criterio"])
        latencias.append(time.perf_counter() - inicio)
        categoria = caso["id"].rsplit("_", 1)[0]
        por_categoria[categoria].append(nota)
        todas.append(nota)

    latencias.sort()
    return {
        "nota_media": sum(todas) / len(todas),
        "por_categoria": {c: sum(v) / len(v) for c, v in por_categoria.items()},
        "latencia_p95_s": latencias[int(len(latencias) * 0.95) - 1],
    }

Para testar o harness sem gastar tokens, passe uma função falsa como sistema. Quando integrar de verdade, sistema chama seu pipeline completo (RAG, prompt, modelo), porque o que importa é o comportamento ponta a ponta.

4. Comparar versões e barrar no CI

Salve o resultado da versão aprovada em evals/baseline.json e compare a nova execução com ele. Defina a regra antes de ver os números, por exemplo: nota média não pode cair mais que 0,03 e nenhuma categoria pode cair mais que 0,10.

import json
import sys

def comparar(novo: dict, base: dict, tol_geral=0.03, tol_categoria=0.10) -> list[str]:
    problemas = []
    if novo["nota_media"] < base["nota_media"] - tol_geral:
        problemas.append(f"Nota geral caiu: {base['nota_media']:.2f} para {novo['nota_media']:.2f}")
    for cat, nota in novo["por_categoria"].items():
        antes = base["por_categoria"].get(cat)
        if antes is not None and nota < antes - tol_categoria:
            problemas.append(f"Categoria {cat} caiu: {antes:.2f} para {nota:.2f}")
    return problemas

if __name__ == "__main__":
    from meu_sistema import responder  # seu pipeline
    novo = rodar_evals(responder)
    base = json.load(open("evals/baseline.json", encoding="utf-8"))
    problemas = comparar(novo, base)
    print(json.dumps(novo, indent=2, ensure_ascii=False))
    if problemas:
        print("\n".join(problemas))
        sys.exit(1)

No GitHub Actions, rode isso quando prompts, código de recuperação ou configuração de modelo mudarem:

name: evals
on:
  pull_request:
    paths: ["prompts/**", "src/**", "evals/**"]

jobs:
  evals:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: python evals/rodar.py
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          AGENT_MODEL: ${{ vars.AGENT_MODEL }}
          JUIZ_MODEL: ${{ vars.JUIZ_MODEL }}

Por usar secrets, o workflow não recebe a chave em PRs vindos de forks. Isso é desejado.

Armadilhas comuns

  • Conjunto pequeno demais. Com 10 casos, uma única resposta muda a nota em 10 pontos percentuais. Cresça o conjunto a cada incidente.
  • Ruído confundido com regressão. Respostas variam entre execuções. Rode os casos mais sensíveis mais de uma vez ou use tolerâncias como as acima.
  • Custo ignorado. Cada execução gasta tokens de geração e de julgamento. Rode o conjunto completo antes de merge e um subconjunto rápido nos commits intermediários.
  • Vazamento do conjunto para o prompt. Se você copia casos do eval para exemplos do prompt, a nota sobe sem que o sistema melhore. Mantenha-os separados.
  • Tratar o eval como verdade. É um proxy. Complemente com monitoramento em produção e feedback dos usuários, como em observabilidade de LLM.

Quando não vale a pena

Para um protótipo com um punhado de usuários, uma planilha com 15 perguntas rodadas à mão já ajuda. Monte a automação quando mudanças frequentes começarem a dar medo.

Próximos passos

Prompt bem estruturado é o que mais move sua nota, então veja engenharia de prompt em produção. Para a camada de validação de saída que complementa os evals, leia guardrails de saída.

Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria

Teste seu conhecimento

Teste o que você aprendeu sobre evals de LLM

Pergunta 1 de 5

Qual é a principal diferença entre um eval e um teste unitário?

Um conteúdo prático por quinzena

Deixe o seu e-mail para receber um conteúdo prático de IA e cloud a cada quinzena. Sem spam, e você pede a exclusão quando quiser.

Continue lendo