Você mudou o system prompt. O time de ML acha que ficou melhor. Mas “achar” não é métrica. Em infra, você não faz deploy sem rodar tests. Em AI, o equivalente é evals.

LLM Evals são testes automatizados que medem a qualidade das respostas do modelo. É o seu test suite pro AI. Sem evals, toda mudança no pipeline (prompt, modelo, RAG, chunking) é um deploy no escuro.

O mapa pro profissional de infra

Conceito EvalsO que fazEquivalente em infra
Eval datasetConjunto de perguntas + respostas esperadasTest fixtures
MetricMedida de qualidade (0-1)SLA metric (uptime, latência)
Regression testVerificar que mudanças não pioraramLoad test before/after deploy
BenchmarkComparação entre modelos/configsPerformance benchmark (ApacheBench)
Human evalHumano julga qualidadeUser acceptance testing
LLM-as-judgeOutro LLM avalia a respostaAutomated QA
Golden datasetDataset curado com respostas “certas”Test fixtures com assertions

Por que evals são difíceis (e por que fazer mesmo assim)

Em software tradicional, testes são determinísticos. assert add(2, 3) == 5. Sempre.

LLMs são estocásticos. A mesma pergunta pode gerar respostas diferentes. E “correto” muitas vezes não é binário. Uma resposta pode ser parcialmente correta, correta mas verbosa, correta mas no tom errado.

Isso não significa que não dá pra testar. Significa que os testes são estatísticos e não determinísticos. Em vez de “passou/falhou”, você mede “acertou 87% das vezes” e detecta quando cai pra 82%.

É exatamente como medir latência. Você não espera que toda request demore exatamente 50ms. Você mede p50, p95, p99, e alerta quando desviam do baseline.

Tipos de evals

1. Exact match / Contains

O mais simples. A resposta contém a informação correta?

def eval_contains(response, expected_terms):
    """Verifica se termos esperados estão na resposta."""
    score = sum(1 for term in expected_terms if term.lower() in response.lower())
    return score / len(expected_terms)

# Exemplo
resposta = "Use kubectl delete namespace meu-ns para remover o namespace"
esperado = ["kubectl", "delete", "namespace"]
score = eval_contains(resposta, esperado)  # 1.0 (contém tudo)

Bom pra: comandos CLI, fatos objetivos, referências a documentos específicos. Ruim pra: respostas complexas, tom, completude.

2. Similarity-based

Compara embedding da resposta com embedding da resposta esperada.

from openai import AzureOpenAI
import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def eval_similarity(response, reference, client):
    """Compara semântica da resposta com referência."""
    resp_embedding = client.embeddings.create(
        input=response, model="text-embedding-3-small"
    ).data[0].embedding
    
    ref_embedding = client.embeddings.create(
        input=reference, model="text-embedding-3-small"
    ).data[0].embedding
    
    return cosine_similarity(resp_embedding, ref_embedding)

Bom pra: verificar se a resposta está “na direção certa”. Ruim pra: detectar erros factuais sutis (embedding similar, mas informação errada).

3. LLM-as-judge

Usar outro LLM (tipicamente mais poderoso) pra avaliar a resposta. É o método mais flexível.

import json

def eval_llm_judge(question, response, reference, client):
    """Usa GPT-4o como juiz de qualidade."""
    
    judge_prompt = """Avalie a resposta do assistente comparando com a referência.
    
Critérios (0-5 cada):
- Correção factual: informações estão corretas?
- Completude: respondeu tudo que foi perguntado?
- Clareza: fácil de entender e aplicar?
- Segurança: não sugere ações perigosas sem avisos?

Retorne JSON: {"correção": N, "completude": N, "clareza": N, "segurança": N, "nota_final": N, "justificativa": "..."}
"""
    
    result = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": judge_prompt},
            {"role": "user", "content": f"""
Pergunta: {question}
Resposta do assistente: {response}
Resposta de referência: {reference}
"""}
        ],
        response_format={"type": "json_object"},
        temperature=0
    )
    
    return json.loads(result.choices[0].message.content)

Bom pra: avaliação nuanced, múltiplos critérios. Cuidado: o judge também pode errar. Valide com human eval periodicamente.

4. Task-specific metrics

Métricas específicas pro tipo de tarefa:

TarefaMétricaComo medir
Q&A factualAccuracy / exact matchResposta contém o fato correto
ClassificaçãoPrecision, recall e F1F1 = média harmônica entre precision e recall
SummarizationROUGEOverlap de n-grams ou subsequências com a referência
Tradução / captioningBLEUOverlap de n-grams. Útil como baseline, fraco sozinho pra julgar qualidade
Code generationPass@kProbabilidade de pelo menos uma entre k amostras passar nos testes
RAGFaithfulnessResposta é suportada pelo contexto
RAGRelevanceChunks recuperados e resposta final atacam a pergunta

BLEU e ROUGE continuam úteis quando existe uma resposta de referência boa e estável. Pra resposta aberta de LLM, eu trato os dois como sinal auxiliar, não como veredito final.

Montando um eval pipeline

Passo 1: Criar o golden dataset

Comece com 50-100 exemplos curados manualmente. Cada exemplo tem:

{
  "id": "eval-001",
  "question": "Como verificar se a replicação do PostgreSQL está saudável?",
  "expected_answer": "Executar SELECT * FROM pg_stat_replication no primário. Verificar que sent_lsn e replay_lsn estão próximos (lag < 1MB). Se replay_lsn estiver muito atrás, verificar network e I/O no standby.",
  "expected_terms": ["pg_stat_replication", "sent_lsn", "replay_lsn"],
  "category": "database",
  "difficulty": "medium"
}

Passo 2: Rodar evals automaticamente

import json
from datetime import datetime

def run_eval_suite(eval_dataset, rag_pipeline, client, output_file):
    """Roda suite de evals e salva resultados."""
    results = []
    
    for item in eval_dataset:
        # Gerar resposta do sistema
        response = rag_pipeline.query(item["question"])
        
        # Avaliar
        contains_score = eval_contains(response, item["expected_terms"])
        judge_result = eval_llm_judge(
            item["question"], response, item["expected_answer"], client
        )
        
        results.append({
            "id": item["id"],
            "category": item["category"],
            "contains_score": contains_score,
            "judge_scores": judge_result,
            "response": response,
            "timestamp": datetime.now().isoformat()
        })
    
    # Salvar resultados
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(results, f, indent=2)
    
    # Calcular agregados
    avg_score = sum(r["judge_scores"]["nota_final"] for r in results) / len(results)
    print(f"Score médio: {avg_score:.2f}/5.0")
    print(f"Contains score: {sum(r['contains_score'] for r in results)/len(results):.2%}")
    
    return results

Passo 3: Integrar no CI/CD

# .github/workflows/eval.yml
name: LLM Eval Suite
on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'rag/**'
      - 'config/**'

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run eval suite
        env:
          AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }}
          AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
        run: |
          python -m pip install -r requirements.txt
          python run_evals.py --dataset evals/golden.json --output results.json
      
      - name: Check regression
        run: |
          python check_regression.py \
            --current results.json \
            --baseline evals/baseline.json \
            --threshold 0.05
          # Falha se score caiu mais que 5% vs baseline

Métricas específicas pra RAG

Quando avalia um sistema RAG, separe as métricas em duas camadas:

Retrieval metrics (o search está funcionando?)

MétricaO que medeComo calcular
Recall@K% de docs relevantes nos top KDocs relevantes em K / total relevantes
Precision@K% dos top K que são relevantesDocs relevantes em K / K
MRRQuão cedo aparece o primeiro resultado relevanteMédia de 1/rank do primeiro relevante em cada query

Generation metrics (o modelo está respondendo bem?)

MétricaO que medeComo calcular
FaithfulnessResposta é suportada pelo contexto?LLM-judge: “esta afirmação é suportada pelos docs?”
RelevanceResposta responde a pergunta?LLM-judge: “a resposta endereça a pergunta?”
Hallucination rate% de respostas com info inventadaLLM-judge: “info não presente no contexto?”
def eval_rag_faithfulness(response, context_chunks, client):
    """Avalia se a resposta é fiel ao contexto fornecido."""
    
    prompt = f"""Analise se CADA afirmação na resposta é suportada pelo contexto.

Contexto fornecido:
{chr(10).join(context_chunks)}

Resposta do assistente:
{response}

Para cada afirmação na resposta, indique:
- SUPORTADA: está no contexto
- NÃO SUPORTADA: não está no contexto (possível alucinação)
- PARCIAL: parcialmente suportada

Retorne JSON: {{"afirmacoes": [...], "score_faithfulness": 0.0-1.0}}
"""
    
    result = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
        temperature=0
    )
    
    return json.loads(result.choices[0].message.content)

Eval anti-patterns

Eval dataset pequeno demais

  • Com 10 exemplos você percebe um desastre, não uma regressão sutil. Pra começar, 50-100 casos já dão um sinal bem melhor.

Eval dataset viesado

  • Só testar perguntas fáceis dá falsa confiança. Inclua edge cases, perguntas ambíguas, perguntas que o sistema não deveria responder.

Não versionar o baseline

  • Se você não sabe qual era o score antes da mudança, não sabe se melhorou ou piorou.

Avaliar só com LLM-as-judge

  • O judge pode concordar com alucinações. Faça human eval em 10-20% dos casos regularmente.

Otimizar pro eval em vez do usuário

  • Se o eval dataset não reflete perguntas reais dos usuários, você está overfitting pro test. Use logs de produção pra atualizar o dataset.

Ferramentas e frameworks

FerramentaO que fazQuando usar
Azure AI Evaluation SDKEvals integrados ao Azure AIJá usa Azure AI Studio
promptfooOpen-source, CLI-first evalTimes que querem controle, CI/CD
RagasMétricas específicas pra RAGAvaliando pipelines RAG
DeepEvalFramework Python pra LLM evalsIntegração com pytest
Custom scriptsPython puroMáximo controle, necessidades específicas
# promptfoo: exemplo de eval via CLI
npm install -g promptfoo

# Configurar eval
cat > promptfooconfig.yaml << 'EOF'
providers:
  - id: azure-gpt-4o
    provider: azure-openai
    apiKey: $AZURE_OPENAI_API_KEY
    apiHost: https://meu-endpoint.openai.azure.com
    deployment: gpt-4o
    apiVersion: "2024-06-01"
    model: gpt-4o

prompts:
  - file://prompts/system-v1.txt
  - file://prompts/system-v2.txt

tests:
  - vars:
      question: "Como verificar replicação PostgreSQL?"
    assert:
      - type: contains
        value: "pg_stat_replication"
      - type: llm-rubric
        value: "A resposta inclui comandos SQL específicos e explica o que verificar"
  - vars:
      question: "Qual o procedimento de failover?"
    assert:
      - type: contains
        value: "DR-003"
      - type: llm-rubric
        value: "Menciona pré-requisitos e tem passos claros"
EOF

# Rodar
promptfoo eval
promptfoo view  # abre dashboard com resultados

O que levar pra segunda-feira

  • Evals são o test suite do AI. Sem eles, você não sabe se suas mudanças melhoraram ou pioraram.
  • Comece simples. 50 exemplos + contains + LLM-as-judge já dão um raio-X bem útil.
  • Meça retrieval e generation separadamente. Se o modelo erra, primeiro descubra se o problema é no search ou na geração.
  • Integre no CI/CD. Mudanças em prompts devem passar por evals antes de ir pra produção, igual código passa por tests.
  • Trate evals como métricas de SLA. Defina thresholds por categoria, alerte quando cair e investigue root cause.

O próximo post é sobre Machine Learning System Design: como projetar sistemas de ML completos, da ingestão de dados ao serving em produção.

Leitura complementar