🧠FFVAcademy
📊

Avaliando RAG: recall@k, nDCG e LLM-as-judge

16 min de leitura·+80 XP
Pré-requisitos (0/1)0%

Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.

Um RAG sem eval harness é um RAG que você acha que funciona. Pipeline bonito, demo convincente, e 3 meses depois ninguém sabe por que a qualidade caiu. Avaliação não é overhead — é o instrumento que transforma RAG em engenharia. Este módulo é o mapa das métricas certas, como montar golden dataset, e como rodar eval em CI todo PR.

As duas dimensões que importam

🗺️ Eval de RAG: retrieval + generation

                   ┌─────────────────────┐
                   │  Query              │
                   └─────────┬───────────┘
                             │
                  ┌──────────┴──────────┐
                  ▼                     ▼
         ╔════════════════╗    ╔════════════════╗
         ║   Retrieval    ║    ║   Generation   ║
         ║   métricas     ║    ║   métricas     ║
         ╠════════════════╣    ╠════════════════╣
         ║ recall@k       ║    ║ faithfulness   ║
         ║ MRR            ║    ║ context prec.  ║
         ║ nDCG@k         ║    ║ answer rel.    ║
         ║ hit rate       ║    ║ hallucination  ║
         ║ context recall ║    ║ completeness   ║
         ╚════════════════╝    ╚════════════════╝
                  │                     │
                  └─────────┬───────────┘
                            ▼
                ┌──────────────────────┐
                │  Score agregado +    │
                │  drill-down por tipo │
                └──────────────────────┘
💡
Regra ouro: só meça agregado depois de conseguir separar retrieval de generation. Quando a resposta final sai errada, você precisa saber qual dos dois elos quebrou.

Métricas de retrieval — quando o que você mede é posição

MétricaO que medeQuando usar
hit rate @ kFração de queries cujo gabarito aparece em top-k (binário)Smoke test inicial, diagnóstico rápido
recall @ kFração dos docs relevantes recuperados no top-kQuando há múltiplos docs relevantes por query
precision @ kFração do top-k que é relevanteQuando top-k é pequeno e você quer zero ruído
MRR (Mean Reciprocal Rank)Média de 1/rank do primeiro hitQueries com exatamente 1 doc relevante (FAQ)
nDCG @ kDiscounted Cumulative Gain normalizadoQuando posição no top-k importa muito (padrão em RAG)
context recall (RAGAS)Quantos dos "pedaços" do gabarito estão no contexto recuperadoGabarito é uma resposta longa fragmentada em claims
python
# Métricas de retrieval — implementação mínima
import math

def hit_rate_at_k(retrieved: list[str], gold: list[str], k: int) -> float:
    return 1.0 if any(d in gold for d in retrieved[:k]) else 0.0

def recall_at_k(retrieved: list[str], gold: list[str], k: int) -> float:
    return len(set(retrieved[:k]) & set(gold)) / max(len(gold), 1)

def mrr(retrieved: list[str], gold: list[str]) -> float:
    for i, d in enumerate(retrieved, start=1):
        if d in gold:
            return 1.0 / i
    return 0.0

def dcg_at_k(retrieved: list[str], gold: list[str], k: int) -> float:
    return sum(
        (1.0 if d in gold else 0.0) / math.log2(i + 1)
        for i, d in enumerate(retrieved[:k], start=1)
    )

def ndcg_at_k(retrieved: list[str], gold: list[str], k: int) -> float:
    ideal = dcg_at_k(gold[:k], gold, k)
    return dcg_at_k(retrieved, gold, k) / ideal if ideal > 0 else 0.0

Métricas de generation — quando o que você mede é a resposta

MétricaO que medeComo calcular
FaithfulnessToda claim da resposta está no contexto?Extrai claims da resposta, checa cada uma contra o contexto (LLM ou NLI)
Context precision% dos chunks recuperados que foram úteisPara cada chunk, LLM decide se contribui para a resposta
Answer relevanceA resposta responde à pergunta?Gera queries hipotéticas a partir da resposta, mede similaridade com a query original
CompletenessA resposta cobre todos os aspectos da pergunta?LLM compara resposta com gabarito, lista o que faltou
Hallucination rate% de claims inventadas (não suportadas)1 − faithfulness, útil para SLO
⚠️
Faithfulness e answer relevance são ortogonais. Uma resposta pode ser 100% fiel ao contexto e 0% relevante ("não tenho informação" é fiel, mas inútil se o doc certo estava lá). Trackeie as duas em dashboards separados.

LLM-as-judge: como usar sem se enganar

LLM-as-judge é o método mais escalável para métricas que exigiriam humano (faithfulness, answer relevance). Mas não é verdade revelada — é uma estimativa. Quatro regras evitam cair em armadilhas.

  1. Juiz de família diferente do gerador. Se o RAG usa Claude, julgue com GPT (ou inverso). Reduz viés de "mesma voz".
  2. Calibre com humanos. Avalie 50-100 itens manualmente, correlacione com o juiz. Se correlação for <0.7, melhore a rubrica antes de confiar.
  3. Rubrica explícita, não escore aberto. "Escala 0-4, onde 0=... 1=... 4=..." vence "dê nota de 0 a 10".
  4. Ground truth quando existe. Passe gabarito para o juiz. "Compare com referência" é muito mais confiável que "avalie em abstrato".
python
# Prompt de LLM-as-judge para faithfulness — com rubrica e ground truth
from anthropic import Anthropic

client = Anthropic()

JUDGE_PROMPT = """Você é um juiz objetivo avaliando se uma resposta é FIEL ao contexto fornecido.

Uma resposta é FIEL se toda afirmação factual nela pode ser derivada diretamente do contexto. Opiniões, interpretações ou conexões não presentes no contexto tornam a resposta NÃO FIEL.

<contexto>
{context}
</contexto>

<pergunta>
{question}
</pergunta>

<resposta>
{answer}
</resposta>

Liste cada afirmação factual da resposta. Para cada uma, diga: SUPORTADA (está no contexto), INFERIDA (razoável mas não explícita) ou NÃO SUPORTADA (não está no contexto).

Ao final, dê o veredito em JSON:
{{"faithfulness_score": 0.0 a 1.0, "unsupported_claims": ["..."]}}

faithfulness_score = claims SUPORTADAS / total de claims factuais."""

def judge_faithfulness(context: str, question: str, answer: str) -> dict:
    r = client.messages.create(
        model="claude-sonnet-4-6",             # juiz ≠ gerador
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": JUDGE_PROMPT.format(
                context=context, question=question, answer=answer
            ),
        }],
    )
    # parse do JSON final (em produção use structured output)
    import json, re
    match = re.search(r"\{[^{}]*faithfulness_score[^{}]*\}", r.content[0].text)
    return json.loads(match.group(0)) if match else {"faithfulness_score": 0.0}

RAGAS: o framework que padronizou o eval de RAG

RAGAS é lib Python open-source que implementa as métricas canônicas com LLM-as-judge por trás. Vale como ponto de partida — você ganha rapidez no setup e perde algum controle fino. Para produção madura, costuma evoluir para eval custom (fica mais transparente).

python
# pip install ragas datasets
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# Seu golden set: query, contexto recuperado, resposta gerada, ground truth
data = Dataset.from_dict({
    "question":   ["Como cancelo minha conta?", ...],
    "contexts":   [["<chunk1>", "<chunk2>"], ...],
    "answer":     ["Para cancelar, acesse ...", ...],
    "ground_truth": ["Em Configurações > Conta > Encerrar.", ...],
})

result = evaluate(
    data,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)
# → {"faithfulness": 0.87, "answer_relevancy": 0.92, ...}
💡
RAGAS usa OpenAI por default. Para PT-BR, troque o LLM e embedder explicitamente (ragas.llms.LangchainLLMWrapper) — senão você avalia em inglês por dentro e os scores ficam enviesados.

Golden dataset: como montar o seu em 1 dia

  1. Colete queries reais. Últimas 500 queries dos logs do produto. Se ainda não está em produção, gere 100-200 queries a partir dos documentos (prompt de "faça pergunta que este doc responderia").
  2. Curate 100 diversas. Amostre cobrindo: queries curtas/longas, fáceis/ambíguas, com match lexical/semântico, dentro e fora da base ("negative" — deve retornar "não sei").
  3. Escreva gabarito à mão. Para cada query: doc_id relevante(s) + resposta esperada em 1-3 frases. Isso é trabalho humano, sem atalho. 100 itens: 4-6h de trabalho bem feito.
  4. Versione como código. JSONL no repo, com schema fixo. Nunca edite em planilha que pode ser perdida.
  5. Revise trimestralmente. A distribuição de queries evolui; queries antigas perdem relevância. 15 min/trimestre para podar e adicionar.
json
// golden_set.jsonl — um item por linha
{
  "id": "q_001",
  "query": "Como resetar minha senha?",
  "type": "how-to",
  "relevant_doc_ids": ["doc_42", "doc_118"],
  "ground_truth_answer": "Na tela de login, clique em 'esqueci a senha'. Você receberá um email com link válido por 1h.",
  "difficulty": "easy",
  "should_refuse": false
}
{
  "id": "q_027",
  "query": "Qual a receita da empresa em 2025?",
  "type": "out-of-scope",
  "relevant_doc_ids": [],
  "ground_truth_answer": "Não tenho essa informação na base.",
  "difficulty": "medium",
  "should_refuse": true
}

Eval harness em CI: fail the build on regression

Eval offline é inútil se não roda a cada mudança. Integre no CI todo PR que toca retrieval, prompt ou chunking. Promova uma baseline; PR que degrada >2pp em métrica crítica falha.

yaml
# .github/workflows/rag-eval.yml
name: RAG Eval
on:
  pull_request:
    paths: ["src/rag/**", "prompts/**", "ingest/**"]

jobs:
  eval:
    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

      - name: Run RAG eval on golden set
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python -m eval.run --out eval_results.json

      - name: Compare with baseline
        run: |
          python -m eval.compare \
            --baseline eval/baselines/main.json \
            --current  eval_results.json \
            --threshold 0.02

      - name: Post results to PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const r = JSON.parse(fs.readFileSync('eval_results.json'));
            const body = `### RAG eval\n\n` +
              `- recall@5: ${r.recall_at_5.toFixed(3)}\n` +
              `- nDCG@5:   ${r.ndcg_at_5.toFixed(3)}\n` +
              `- faithfulness: ${r.faithfulness.toFixed(3)}\n`;
            github.rest.issues.createComment({
              ...context.repo, issue_number: context.issue.number, body
            });
Amostragem inteligente salva custo: rode full eval (100 itens) em PRs a código core, e 20 itens em cada commit de branch. Full roda em ~US$1-3 com Haiku/gpt-4o-mini como juiz, 20 itens custa centavos.

Debug: quando a métrica cai, o que olhar primeiro

SintomaPrimeira suspeitaComo confirmar
recall@k despencaChunking ou embedder mudouRoda eval só de retrieval; compara com versão anterior do índice
recall@k OK, faithfulness caiPrompt do generator mudouGit blame no prompt; A/B com versão antiga do prompt
Answer relevance cai, faithfulness OKContexto vem mas LLM ignoraCheca ordem do contexto, prompt de "use apenas o contexto"
Queries out-of-scope começam a ser respondidasThreshold de refusal sumiuTesta "should_refuse" queries isoladamente
Métricas estáveis mas qualidade percebida caiuGolden set está estagnadoAmostra 30 queries novas dos logs, avalia à mão

Perguntas típicas

Posso confiar em BLEU/ROUGE para RAG?

Não. BLEU e ROUGE medem overlap de n-gramas — funcionam mal quando a resposta certa pode ser parafraseada de muitos jeitos. Para RAG, priorize faithfulness e answer relevance (semânticas). Use BLEU/ROUGE só em traduções ou sumarizações com referência fixa.

Qual o tamanho mínimo útil de golden set?

~30 itens já detectam regressões grosseiras (quebrou retrieval). 100 itens permitem medir diferenças de ~5pp com confiança razoável. 300+ se você quer distinguir variações sutis entre pipelines. Prefira 100 bem curados a 1000 ruins.

Eval online (em produção) substitui eval offline?

Complementa, não substitui. Offline compara versões antes do deploy. Online (thumbs up/down, tempo de leitura, repetição de query) mede o mundo real, com lag. Produção madura tem os dois, com feedback online realimentando o golden set.

Quanto custa rodar eval harness semanal?

Para 100 itens com Haiku 4.5 ou gpt-4o-mini como juiz, ~US$1-3 por rodada. 10 runs semanais: ~US$10-30/mês. É desprezível comparado ao custo de um regressão silenciosa que só aparece em ticket de cliente.
Take-aways.Eval divide em retrieval (recall@k, nDCG) e generation (faithfulness, answer relevance). LLM-as-judge é escalável mas exige rubrica, juiz diferente do gerador e calibração humana. RAGAS para começar; eval custom para amadurecer. Golden set de 100 itens bem curados é suficiente. CI que falha em regressão >2pp é o que transforma RAG em engenharia. Próximo bloco da trilha: agents — o retrieval que decide sozinho o que buscar.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo