Pular para o conteúdo
iauaiiauai — portal de tecnologia, IA e Cloud
IAAvançado

Avaliação de LLM: montando seus evals

Crie um harness de evals: golden set de testes, métricas (determinísticas e LLM-as-judge), integração em CI. Detecte regressão antes do deploy.

Por Equipe iauai · 9 de agosto de 2026 · 13 min de leitura

Nesta página

O problema real

Você mudou o prompt do seu sistema de RAG, fez uns ajustes no chunking, trocou o embedding model. Achava que melhorou, mas só especulação. Coloca em produção. Dois dias depois, taxa de erro sobe — o modelo está errando mais em um domínio específico. Se tivesse um eval (avaliação automatizada), teria descoberto em 5 minutos. Agora custa caro: tickets de suporte, confiança abalada, rollback urgente.

Sem eval, você está navegando cego. Qualidade de LLM não é binary (funciona / não funciona) — é gradual. Precisa medir continuamente. Este artigo mostra como montar um harness de eval que roda em CI, detecta regressão e te dá confiança para fazer deploy.

Como montar avaliações de LLM?

Avaliação de LLM tem 4 componentes:

  1. Golden set (dataset de testes): Casos representativos, manualmente verificados
  2. Métricas (how you measure): Determinísticas (regex, schema) ou LLM-as-judge
  3. Harness (programa que roda): Processa golden set, coleta resultados
  4. CI/CD (integração): Roda a cada commit, bloqueia deploy se score cair

Vamos construir cada uma.

Mão na massa

1. Montar o golden set

Comece pequeno: 20-50 casos bem escolhidos são melhores que 1.000 aleatórios.

# eval_dataset.json
{
  "cases": [
    {
      "id": "rag_001",
      "category": "devolucao",
      "input": "Qual é a política de devolução de produtos?",
      "expected_output": "Deve conter 'até 30 dias' e 'reembolso integral'",
      "metric": "contains_keywords",
      "keywords": ["30 dias", "reembolso"]
    },
    {
      "id": "rag_002",
      "category": "preco",
      "input": "Qual é o preço do produto PROD-001?",
      "expected_output": "R$ 99,90",
      "metric": "exact_match"
    },
    {
      "id": "extraction_001",
      "category": "extraccion",
      "input": "Nome: João Silva, CPF: 123.456.789-00, Email: joao@example.com",
      "expected_schema": {
        "name": "string",
        "cpf": "string",
        "email": "string"
      },
      "metric": "json_schema_valid"
    },
    {
      "id": "hallucination_001",
      "category": "seguranca",
      "input": "Qual é o CEO da empresa?",
      "expected_output": "Deve dizer que não sabe",
      "metric": "llm_as_judge",
      "judge_rubric": "Resposta reconhece limitações e não inventa informação"
    }
  ]
}

Regra: Golden set é manualmente validado. Você mesmo executa o sistema, verifica a saída, confirma se é aceitável. Isso toma tempo, mas é investimento.

def create_golden_set(sample_size: int = 30):
    """Coleta casos reais, executa, valida manualmente."""
    
    print("=== Criando Golden Set ===")
    cases = []
    
    # Fonte 1: Casos de produção que falharam
    production_failures = [
        {
            "input": "Como faço para devolver?",
            "category": "devolucao",
            "reason": "Usuário reclamou que resposta foi genérica"
        },
        # ... mais casos reais
    ]
    
    # Fonte 2: Casos edge que você sabe que importa
    edge_cases = [
        {
            "input": "Qual é seu nome?",
            "category": "seguranca",
            "reason": "Modelo pode alucinar identidade"
        },
        # ... mais edge cases
    ]
    
    for case in production_failures + edge_cases:
        input_text = case["input"]
        
        # Executa sistema real
        output = run_your_llm_system(input_text)
        
        print(f"\nInput: {input_text}")
        print(f"Output: {output}")
        print(f"Reason: {case['reason']}")
        
        # Valida manualmente (pode ser input() ou já vem validado)
        is_valid = validate_output_manually(output)
        
        if is_valid:
            cases.append({
                "id": f"{case['category']}_{len(cases)+1:03d}",
                "input": input_text,
                "expected_output": output,
                "category": case["category"],
                "metric": "llm_as_judge"
            })
    
    print(f"✓ Golden set criado com {len(cases)} casos")
    return cases

2. Definir métricas

Determinísticas (não precisam LLM):

import re
import json
from typing import Any

def metric_exact_match(output: str, expected: str) -> float:
    """Score 1 ou 0 — deve ser idêntico."""
    return 1.0 if output.strip() == expected.strip() else 0.0

def metric_contains_keywords(output: str, keywords: list) -> float:
    """Score proporcional ao número de keywords encontradas."""
    found = sum(1 for kw in keywords if kw.lower() in output.lower())
    return found / len(keywords)

def metric_regex_match(output: str, pattern: str) -> float:
    """Score 1 ou 0 — output faz match com regex."""
    return 1.0 if re.search(pattern, output) else 0.0

def metric_json_schema_valid(output: str, schema: dict) -> float:
    """Score 1 ou 0 — JSON é válido e tem campos requeridos."""
    try:
        data = json.loads(output)
        for field in schema.keys():
            if field not in data:
                return 0.0
        return 1.0
    except:
        return 0.0

def metric_token_count(output: str, max_tokens: int = 100) -> float:
    """Penaliza respostas muito longas."""
    token_count = len(output.split())
    if token_count > max_tokens:
        return max_tokens / token_count  # Ratio
    return 1.0

# Teste
print(metric_exact_match("Brasília", "Brasília"))  # 1.0
print(metric_contains_keywords("Política de 30 dias com reembolso", ["30 dias", "reembolso"]))  # 1.0
print(metric_token_count("Muito longo " * 100, max_tokens=50))  # ~0.5

LLM-as-judge (usa outro LLM para julgar):

from openai import OpenAI

def metric_llm_as_judge(
    output: str,
    input_text: str,
    rubric: str,
    expected: str = None
) -> float:
    """Usa GPT para julgar qualidade (0-1)."""
    
    client = OpenAI()
    
    prompt = f"""Você é um juiz de qualidade. Avalie a resposta (0-10) conforme este critério:

CRITÉRIO: {rubric}

INPUT: {input_text}
RESPOSTA: {output}
"""
    
    if expected:
        prompt += f"\nRESPOSTA ESPERADA: {expected}"
    
    prompt += "\n\nDê uma nota de 0 a 10 (apenas o número)."
    
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    
    score_text = response.choices[0].message.content.strip()
    
    try:
        score = int(score_text) / 10.0  # Normaliza para 0-1
        return score
    except:
        print(f"Erro ao parsear score: {score_text}")
        return 0.5  # Assume médio se não conseguir parsear

# Teste
score = metric_llm_as_judge(
    output="Brasília é a capital",
    input_text="Qual é a capital?",
    rubric="Resposta é correta e concisa"
)
print(f"Score: {score:.2f}")

3. Harness de eval

Código que roda os casos e coleta resultados.

import json
import time
from dataclasses import dataclass
from typing import List, Callable, Dict

@dataclass
class EvalResult:
    case_id: str
    category: str
    input_text: str
    output: str
    expected: str
    metric_name: str
    score: float
    latency_ms: float

class EvalHarness:
    def __init__(self, system_func: Callable, golden_set: List[Dict]):
        """
        Args:
            system_func: Função que roda seu sistema (input -> output)
            golden_set: Lista de casos de teste
        """
        self.system_func = system_func
        self.golden_set = golden_set
        self.results: List[EvalResult] = []
    
    def run(self) -> Dict:
        """Roda todos os casos e retorna relatório."""
        
        print(f"Rodando {len(self.golden_set)} casos...")
        
        for case in self.golden_set:
            case_id = case["id"]
            input_text = case["input"]
            expected = case.get("expected_output", "")
            metric_name = case.get("metric", "llm_as_judge")
            
            # Executa sistema
            start = time.time()
            try:
                output = self.system_func(input_text)
                latency = (time.time() - start) * 1000  # ms
            except Exception as e:
                print(f"✗ {case_id}: Erro - {str(e)}")
                output = f"ERROR: {str(e)}"
                latency = (time.time() - start) * 1000
                score = 0.0
            else:
                # Calcula score
                if metric_name == "exact_match":
                    score = metric_exact_match(output, expected)
                elif metric_name == "contains_keywords":
                    score = metric_contains_keywords(output, case["keywords"])
                elif metric_name == "json_schema_valid":
                    score = metric_json_schema_valid(output, case["expected_schema"])
                elif metric_name == "llm_as_judge":
                    score = metric_llm_as_judge(
                        output, input_text, 
                        case.get("judge_rubric", "Resposta é correta e útil"),
                        expected
                    )
                else:
                    score = 0.5
            
            result = EvalResult(
                case_id=case_id,
                category=case.get("category", "unknown"),
                input_text=input_text,
                output=output[:100],  # Trunca para relatório
                expected=expected[:100],
                metric_name=metric_name,
                score=score,
                latency_ms=latency
            )
            self.results.append(result)
            
            status = "✓" if score > 0.8 else "⚠" if score > 0.5 else "✗"
            print(f"{status} {case_id}: {score:.2f} ({latency:.0f}ms)")
        
        return self.summarize()
    
    def summarize(self) -> Dict:
        """Retorna sumário dos resultados."""
        
        if not self.results:
            return {}
        
        scores = [r.score for r in self.results]
        latencies = [r.latency_ms for r in self.results]
        
        by_category = {}
        for r in self.results:
            if r.category not in by_category:
                by_category[r.category] = []
            by_category[r.category].append(r.score)
        
        summary = {
            "total_cases": len(self.results),
            "passed": sum(1 for s in scores if s > 0.8),
            "accuracy": sum(scores) / len(scores),
            "latency_avg_ms": sum(latencies) / len(latencies),
            "latency_p95_ms": sorted(latencies)[int(len(latencies) * 0.95)],
            "by_category": {cat: sum(scores)/len(scores) for cat, scores in by_category.items()}
        }
        
        return summary

# Uso
def mock_system(input_text: str) -> str:
    """Sistema mock para teste."""
    if "devolução" in input_text.lower():
        return "Política de devolução: até 30 dias com reembolso integral."
    return "Não encontrei informação."

golden_set = [
    {
        "id": "test_001",
        "input": "Qual é a política de devolução?",
        "expected_output": "30 dias",
        "category": "policy",
        "metric": "contains_keywords",
        "keywords": ["30 dias"]
    }
]

harness = EvalHarness(mock_system, golden_set)
summary = harness.run()

print("\n=== SUMÁRIO ===")
print(json.dumps(summary, indent=2))

Output:

Rodando 1 casos...
✓ test_001: 1.00 (50ms)

=== SUMÁRIO ===
{
  "total_cases": 1,
  "passed": 1,
  "accuracy": 1.0,
  "latency_avg_ms": 50.0,
  "latency_p95_ms": 50.0,
  "by_category": {"policy": 1.0}
}

4. Comparação entre versões

def compare_prompts(
    prompt_v1: str,
    prompt_v2: str,
    golden_set: List[Dict]
) -> Dict:
    """Compara dois prompts usando mesmo golden set."""
    
    print("=== Teste V1 (prompt antigo) ===")
    harness_v1 = EvalHarness(
        lambda inp: run_llm(inp, prompt_v1),
        golden_set
    )
    summary_v1 = harness_v1.run()
    
    print("\n=== Teste V2 (prompt novo) ===")
    harness_v2 = EvalHarness(
        lambda inp: run_llm(inp, prompt_v2),
        golden_set
    )
    summary_v2 = harness_v2.run()
    
    # Compara
    improvement = summary_v2["accuracy"] - summary_v1["accuracy"]
    latency_diff = summary_v2["latency_avg_ms"] - summary_v1["latency_avg_ms"]
    
    print(f"\n=== RESULTADO ===")
    print(f"Accuracy: {summary_v1['accuracy']:.2%}{summary_v2['accuracy']:.2%} ({improvement:+.2%})")
    print(f"Latência: {summary_v1['latency_avg_ms']:.0f}ms → {summary_v2['latency_avg_ms']:.0f}ms ({latency_diff:+.0f}ms)")
    
    if improvement > 0.05:
        print("✓ V2 é melhor — recomenda deploy")
    elif improvement < -0.05:
        print("✗ V2 é pior — não faz deploy")
    else:
        print("~ Trade-off: decida manualmente")
    
    return {"v1": summary_v1, "v2": summary_v2, "improvement": improvement}

5. CI/CD integration (GitHub Actions)

# .github/workflows/eval.yml
name: LLM Evals

on: [pull_request, push]

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
      
      - name: Install dependencies
        run: |
          pip install openai pydantic
      
      - name: Run evals
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/run_evals.py > eval_results.json
      
      - name: Check for regression
        run: |
          python scripts/check_regression.py eval_results.json baseline.json
          
      - name: Comment on PR
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(fs.readFileSync('eval_results.json'));
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `📊 Eval Results:\n\`\`\`\n${JSON.stringify(results, null, 2)}\n\`\`\``
            });
# scripts/check_regression.py
import json
import sys

def check_regression(current_file: str, baseline_file: str, threshold: float = 0.05):
    """Bloqueia deploy se acurácia cair > 5%."""
    
    with open(current_file) as f:
        current = json.load(f)
    
    with open(baseline_file) as f:
        baseline = json.load(f)
    
    current_acc = current["accuracy"]
    baseline_acc = baseline["accuracy"]
    diff = current_acc - baseline_acc
    
    print(f"Baseline: {baseline_acc:.2%}")
    print(f"Current: {current_acc:.2%}")
    print(f"Change: {diff:+.2%}")
    
    if diff < -threshold:
        print(f"✗ REGRESSÃO DETECTADA (queda > {threshold:.0%})")
        sys.exit(1)  # Bloqueia merge
    
    print("✓ OK — sem regressão significativa")

if __name__ == "__main__":
    check_regression(sys.argv[1], sys.argv[2])

Armadilhas comuns

Armadilha 1: Golden set muito pequeno

20 casos é mínimo viável. Se usar < 10, qualquer flutuação parece importante. Para cada categoria, mínimo 5 casos.

Armadilha 2: LLM-as-judge é enviesado

Se usar o mesmo modelo que testa como juiz, vai tendenciosamente favorecer. Use modelo diferente (ex: GPT-4 julga Llama 2).

# ❌ Enviesado
def judge(output):
    return gpt_3_5_turbo_judge(output)  # Mesmo modelo!

# ✅ Independente
def judge(output):
    return gpt_4_judge(output)  # Modelo mais forte julga

Armadilha 3: Não monitorar custo de evals

Cada LLM-as-judge custa (USD 0,015 per 1M input + output tokens). 100 casos × 3 tentativas = 300 chamadas. Se cada julga 500 tokens, são 150k tokens = USD 2,25 por rodada. Caro se rodar em cada commit.

Solução: Rode LLM-as-judge em CI nightly, não em cada PR.

Armadilha 4: Esquecer que eval é proxy

Seu eval pode passar mas o sistema falhar em produção. Evals capturam métricas, não a realidade inteira. Use também monitoramento em produção.

Quando não usar avaliação formal

  • Prototipagem rápida: Se está experimentando, eval consome tempo.
  • Modelo é black-box: Se tira tudo de uma API e não pode mudar, eval é pura observação.
  • Qualidade não é crítica: Se errar 10% não causa dano, economia em evals pode compensar.

Próximos passos

  1. Integre com CI/CD com GitHub Actions — roda evals em pipeline.
  2. Combine com observabilidade de LLM para monitorar produção.
  3. Automatize coleta de casos com RAG na prática — seus retriever failures viram novos casos de eval.

Resumo prático

Avaliação de LLM em 4 passos:

  1. Golden set: 30-50 casos manualmente validados
  2. Métricas: Determinísticas (regex, schema) para 80%, LLM-as-judge para 20%
  3. Harness: Loop que executa e coleta scores
  4. CI: Roda a cada commit, bloqueia regressão > 5%

Custo: 1-2 horas setup, depois automático. Valor: nunca mais faz deploy cego. Em produção crítica, é não-negociável.

Continue lendo