Avaliando RAG: recall@k, nDCG e LLM-as-judge
- ⬜🎯 Hybrid Search + Reranking: do BM25 ao cross-encoder(Engenharia AI-Native)
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
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étrica | O que mede | Quando usar |
|---|---|---|
| hit rate @ k | Fração de queries cujo gabarito aparece em top-k (binário) | Smoke test inicial, diagnóstico rápido |
| recall @ k | Fração dos docs relevantes recuperados no top-k | Quando há múltiplos docs relevantes por query |
| precision @ k | Fração do top-k que é relevante | Quando top-k é pequeno e você quer zero ruído |
| MRR (Mean Reciprocal Rank) | Média de 1/rank do primeiro hit | Queries com exatamente 1 doc relevante (FAQ) |
| nDCG @ k | Discounted Cumulative Gain normalizado | Quando posição no top-k importa muito (padrão em RAG) |
| context recall (RAGAS) | Quantos dos "pedaços" do gabarito estão no contexto recuperado | Gabarito é uma resposta longa fragmentada em claims |
# 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.0Métricas de generation — quando o que você mede é a resposta
| Métrica | O que mede | Como calcular |
|---|---|---|
| Faithfulness | Toda 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 úteis | Para cada chunk, LLM decide se contribui para a resposta |
| Answer relevance | A resposta responde à pergunta? | Gera queries hipotéticas a partir da resposta, mede similaridade com a query original |
| Completeness | A 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.
- Juiz de família diferente do gerador. Se o RAG usa Claude, julgue com GPT (ou inverso). Reduz viés de "mesma voz".
- Calibre com humanos. Avalie 50-100 itens manualmente, correlacione com o juiz. Se correlação for <0.7, melhore a rubrica antes de confiar.
- Rubrica explícita, não escore aberto. "Escala 0-4, onde 0=... 1=... 4=..." vence "dê nota de 0 a 10".
- Ground truth quando existe. Passe gabarito para o juiz. "Compare com referência" é muito mais confiável que "avalie em abstrato".
# 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).
# 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 () — senão você avalia em inglês por dentro e os scores ficam enviesados.
Quais são as duas dimensões que uma avaliação de RAG precisa separar?
Golden dataset: como montar o seu em 1 dia
- 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").
- 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").
- 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.
- Versione como código. JSONL no repo, com schema fixo. Nunca edite em planilha que pode ser perdida.
- Revise trimestralmente. A distribuição de queries evolui; queries antigas perdem relevância. 15 min/trimestre para podar e adicionar.
// 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.
# .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
| Sintoma | Primeira suspeita | Como confirmar |
|---|---|---|
| recall@k despenca | Chunking ou embedder mudou | Roda eval só de retrieval; compara com versão anterior do índice |
| recall@k OK, faithfulness cai | Prompt do generator mudou | Git blame no prompt; A/B com versão antiga do prompt |
| Answer relevance cai, faithfulness OK | Contexto vem mas LLM ignora | Checa ordem do contexto, prompt de "use apenas o contexto" |
| Queries out-of-scope começam a ser respondidas | Threshold de refusal sumiu | Testa "should_refuse" queries isoladamente |
| Métricas estáveis mas qualidade percebida caiu | Golden set está estagnado | Amostra 30 queries novas dos logs, avalia à mão |
Perguntas típicas
❓ Posso confiar em BLEU/ROUGE para RAG?
❓ Qual o tamanho mínimo útil de golden set?
❓ Eval online (em produção) substitui eval offline?
❓ Quanto custa rodar eval harness semanal?
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.
Perguntas frequentes
❓ Como avaliar RAG separando recuperação de geração?
❓ O que é um conjunto de referência para RAG?
❓ Fidelidade e relevância medem a mesma coisa?
Fixando
Ao usar um modelo como juiz para avaliar respostas, qual cuidado evita se enganar?
Qual é o caminho mais rápido para começar a avaliar RAG numa equipe que não tem nada hoje?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…