Métricas de avaliação de RAG: retrieval, reranking e generation

Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.

Um sistema RAG com filtros avariados pode funcionar durante meses sem gerar qualquer alerta operacional. Continua a devolver respostas e cumpre o objetivo de latência, mas as respostas baseiam-se em evidência incompleta. Recall@k contra o gold set original expõe essa perda. Os dashboards de latência e disponibilidade não.

Para engenheiros que operam ou avaliam sistemas RAG multi-stage, esta referência associa falhas no parsing de documentos, filtering, retrieval, reranking e generation à métrica que identifica cada uma, mostrando depois onde essa medição deve ser incluída num release gate ou numa monitorização.

Quer avançar e executar o código?

O repositório executável slavadubrov/rag-evals-demo aplica as métricas ao SciFact. make eval executa a suite e make benchmark compara configurações de chunking, embedding e LLM. Os notebooks 00–09 isolam cada métrica. A demo usa Qdrant embedded, pelo que não requer Docker.

Em resumo

  • Uma etapa sem métrica é uma etapa que falha silenciosamente.
  • Uma stack de avaliação útil abrange ingestion, retrieval, grounding da generation, conformidade com a ontologia e sinais do sistema. RAGAS, TruLens, DeepEval, Arize Phoenix e o TREC 2024 RAG Track fornecem tooling. Não escolhem as suas métricas por si.
  • Em RAG baseado em metadata e ontologias, uma tag errada ou um hard predicate frágil pode reduzir o recall a zero. O Recall@k standard deteta a perda quando mantém o gold set original. Uma métrica de false-exclusion de filters identifica a causa. Faithfulness pode ainda avaliar claims contra um context incompleto, mas não consegue diagnosticar a causa no filter ou no retrieval. Uma recusa vazia pode não produzir statements e NaN, dependendo da implementação.

As secções seguem a ordem do pipeline. Comece pela tabela de decisão e use as secções seguintes como referência para cada etapa.


Tabela de decisão para avaliação de RAG

Use esta tabela como ponto de partida antes de escolher um framework. A métrica certa depende do modo de falha que pretende detetar, não do nome da ferramenta.

PerguntaFamília de métricasUse quandoAtenção a
O parsing preservou a fonte?Completude da extração, cobertura de tabelas/figurasPDFs, slides, scans e páginas HTML entram no corpusTexto com bom aspeto pode ainda perder captions, notas de rodapé ou estrutura de tabelas
O retrieval encontrou a evidência certa?Recall@k, nDCG@k, MRR, context precision/recallPode etiquetar chunks ou documentos relevantesUm hard metadata filter pode remover o documento correto antes de começar o ranking
O reranking melhorou a shortlist?Uplift do reranker, Precision@1, delta de nDCGCross-encoders ou LLM rankers são usados após o retrievalMeça latência e custo juntamente com o ganho de qualidade
A resposta utilizou a evidência?Faithfulness, groundedness, suporte de citaçõesA resposta cita documentos ou apresenta factos do contextFaithfulness não consegue diagnosticar parsing ou retrieval incorretos
O sistema é estável em produção?Drift, regeneration, fallback, latência p95, custo por respostaO tráfego muda após o lançamentoA telemetria de produção precisa de revisão humana por amostragem para se manter calibrada

Para uma comparação mais curta entre ferramentas, consulte Best RAG Evaluation Tools: Ragas, DeepEval, and TruLens.

Parte 1: Defina o sucesso antes da arquitetura

Crie o eval set antes do diagrama da arquitetura. Isso dá a cada escolha posterior de componente um objetivo mensurável.

Não pode escolher entre BM25 e dense retrieval, chunking recursivo e semântico, ou Cohere Rerank e BGE sem saber o que está a otimizar. “Respostas melhores” não é uma métrica. Um contrato ilustrativo seria “faithfulness ≥ 0,85 num golden set de 200 queries que cobre as nossas três principais intents, com latência p95 < 1,5 s e uma taxa de false-exclusion de filters < 2%”. Os valores são placeholders; o importante é que qualidade, cobertura, latência e filtering tenham gates explícitos.

Defina o harness antes de escrever o código de retrieval. O primeiro harness estará errado e irá revê-lo. Rever uma métrica é muito mais barato do que rever um sistema que já colocou em produção.

Três camadas do pipeline e dois modos de execução

O RAG moderno é um pipeline, pelo que a avaliação também tem de ser um pipeline. Nenhum número isolado deteta todos os modos de falha.

A avaliação em produção tem três camadas de pipeline. A avaliação de ingestion pergunta se o corpus e o índice preservam a fonte. A avaliação em tempo de query pergunta se o rewriting, filtering, retrieval, reranking e a montagem do context encontraram a evidência certa. A avaliação da resposta e da produção pergunta se a resposta utilizou essa evidência e se a qualidade se mantém com tráfego real. Se reduzir as camadas a um único score, um bug de normalização pode desaparecer dentro de um score de resposta aceitável.

Os três pontos onde um sistema RAG pode perder evidência: o corpus e o índice, o caminho de retrieval e a resposta e o tráfego realOs três pontos onde um sistema RAG pode perder evidência: o corpus e o índice, o caminho de retrieval e a resposta e o tráfego real

Estas camadas descrevem onde ocorre uma falha. Offline e online descrevem quando e contra que dados a verificação é executada. A avaliação offline usa um dataset fixo com ground truth conhecido; é reprodutível e pertence à seleção de componentes, às comparações A/B e aos CI gates. A avaliação online avalia tráfego real amostrado e capta regeneration, dwell time, feedback explícito e drift real das queries. É mais ruidosa e mais difícil de instrumentar.

Cada camada do pipeline pode contribuir com verificações offline e online. Um corpus de ingestion fixo deteta regressões do parser antes do release, enquanto os monitores de freshness e de falhas de parsing cobrem atualizações reais. Um conjunto fixo de queries mede o retrieval antes do release, enquanto traces reais amostrados expõem drift em produção. Apenas offline não deteta alterações reais; apenas online torna as regressões difíceis de reproduzir.

Avaliação ao nível do componente vs. end-to-end

Há dois erros comuns. Uma avaliação exclusivamente end-to-end diz-lhe que o sistema está avariado, mas não onde. Uma avaliação apenas ao nível dos componentes pode mostrar todas as partes a passar enquanto o sistema completo continua a falhar. A solução é usar algumas métricas end-to-end principais para decisões go/no-go, além de métricas de componentes para diagnóstico. As métricas de retrieval detetam regressões do retriever. As métricas de generation detetam regressões do generator. A correção end-to-end da resposta deteta falhas de integração.

Os frameworks de referência (uma análise opinativa)

FrameworkMelhor emOnde falha
RAGASO meu critério de seleção: um vocabulário partilhado para faithfulness, answer relevancy e context precision/recall (métricas)Custo do LLM-judge; componentes de score opacos durante o debugging; alterações de versão
ARESO meu critério de seleção: um classifier judge específico da tarefa justifica o custo de treino e anotação (artigo); a precisão reportada está limitada ao benchmarkSetup mais pesado; é necessário treinar efetivamente modelos
TruLensO meu critério de seleção: feedback functions associadas a traces e integração com OpenTelemetry são mais importantes do que um catálogo de métricas de RAG (projeto)Menos métricas específicas de RAG prontas a usar do que o RAGAS
DeepEvalO meu critério de seleção: integração com test runners e métricas customizadas são mais importantes do que o default de um framework (projeto)Uso intensivo de LLM-judge = picos de custo
Arize PhoenixO meu critério de seleção: tracing e visualizações de embeddings ajudam a investigar uma hipótese de drift (projeto)Tem de trazer as suas próprias definições de métricas
TREC 2024 RAG TrackBenchmark público para avaliação de nuggets (AutoNuggetizer), avaliação de suporte e fluência no MS MARCO Segment v2.1Não é uma ferramenta de runtime; é um benchmark para calibração

A minha stack default é RAGAS para o vocabulário de métricas, DeepEval para CI gates, Phoenix para tracing em produção e código customizado para métricas específicas da ontologia. Irá ultrapassar qualquer solução com que comece. Escolha o framework que facilite a criação de métricas customizadas.

Para benchmarks, use BEIR (Thakur et al., NeurIPS 2021) para generalização zero-shot de retrieval, MTEB para qualidade geral de embeddings, MIRACL para retrieval multilingue e o TREC 2024 RAG Track para avaliação end-to-end de RAG.


Parte 2: Mapeie os pontos de avaliação no pipeline

Um sistema RAG de produção é maior do que “fazer embedding dos documentos, fazer retrieval dos chunks e chamar um LLM”. Todas as etapas entre a aquisição do documento e a entrega da resposta podem falhar.

O pipeline RAG agrupado em ingestion, tempo de query e resposta, com métricas de diagnóstico junto a cada etapaO pipeline RAG agrupado em ingestion, tempo de query e resposta, com métricas de diagnóstico junto a cada etapa

Cada etapa do diagrama tem pelo menos uma métrica. Uma etapa sem métrica pode falhar sem que ninguém repare.

As três lanes correspondem aos pontos onde a evidência pode ser perdida. A lane de ingestion cobre parsing, cleaning, chunking, embedding e indexing. A lane de tempo de query cobre rewriting, filtering, retrieval, reranking e montagem do context. A lane de resposta e produção cobre faithfulness, verificação de citações, sinais dos utilizadores, drift, latência e custo.

Os erros acumulam-se ao longo da cadeia: um parsing incorreto limita o que o chunking pode fazer, um chunking incorreto limita o retrieval e um retrieval incorreto limita tanto o reranking como a generation. Faithfulness mede apenas a resposta final, nunca a causa a montante.


Parte 3: Avaliação de ingestion

Muitas falhas de RAG em produção começam na ingestion. O sistema funciona com documentos de teste limpos e depois falha com PDFs, scans, tabelas e páginas de corpus reais e desorganizadas.

Aquisição e parsing de documentos

O que medir:

  • Completude da extração de texto: extracted_chars / expected_chars numa amostra etiquetada, calculado por classe de documento. Não existe um package canónico — escreva um pequeno harness que compare o output do parser com uma referência revista manualmente. Procure notas de rodapé, headers e captions em falta.

  • Precisão de OCR: CER (Character Error Rate) e WER (Word Error Rate), as métricas standard de speech/OCR:

    CER=S+D+IN,WER=Sw+Dw+IwNw\text{CER} = \frac{S + D + I}{N}, \qquad \text{WER} = \frac{S_w + D_w + I_w}{N_w}

    onde SS, DD, II são substituições, deleções e inserções ao nível dos caracteres e NN é a contagem de caracteres da referência (o subscript ww aplica-se à versão por palavra). Não aplique o mesmo limite de CER a todo o corpus. Calibre-o por classe de documento e pela perda de qualidade da resposta downstream. Texto impresso, escrita manual e material multilingue têm perfis de erro diferentes. Calcule com jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) ou com evaluate do Hugging Face. Para corpora de avaliação, FUNSD e SROIE são benchmarks públicos.

    from jiwer import cer, wer
    
    refs = ["Mars has two moons, Phobos and Deimos."]
    hyps = ["Mars has two m00ns, Phobos and Deirnos."]
    
    print(f"CER = {cer(refs, hyps):.3f}")  # CER = 0.105
    print(f"WER = {wer(refs, hyps):.3f}")  # WER = 0.286
  • Fidelidade da extração de tabelas: TEDS (Tree-Edit-Distance-based Similarity) mede quão próxima está uma árvore de tabelas HTML prevista da referência, normalizada pelo tamanho da maior árvore. De Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1EditDist(Ta,Tb)max(Ta,Tb)\text{TEDS}(T_a, T_b) = 1 - \frac{\text{EditDist}(T_a, T_b)}{\max(|T_a|, |T_b|)}

    O TEDS usa tanto a estrutura (linhas, colunas, spans) como o conteúdo das células. O TEDS-S remove o conteúdo e avalia apenas a estrutura. Implementação de referência: teds.py do PubTabNet (usa apted internamente). Para corpora de avaliação, consulte PubTabNet, FinTabNet e SciTSR. Parsers ingénuos falham frequentemente em tabelas. Faça benchmark antes de confiar neles.

  • Preservação do layout / estrutura: ordem dos headings, integridade das listas e ordem de leitura em PDFs com várias colunas. Use DocLayNet como benchmark etiquetado. Uma comparação pronta a usar pode abranger um parser de elementos como unstructured, uma biblioteca de PDF como pymupdf e um parser VLM como docling.

Compare famílias de parsers distintas, por exemplo uma baseline com Tesseract, um modelo de OCR baseado em VLM e o candidato do seu vendor. Use uma amostra estratificada de classes de documentos reais com um DPI fixo, incluindo scans limpos, fotografias, tabelas, texto multilingue, matemática e escrita manual. Reporte CER ou WER para cada classe e TEDS para páginas com tabelas.

Cleaning e normalização

  • Precisão da remoção de boilerplate: precision/recall contra spans de boilerplate etiquetados por humanos. Uma remoção agressiva elimina conteúdo relevante; uma remoção pouco rigorosa polui os embeddings. Ferramentas a comparar: trafilatura, jusText, Resiliparse. Barbaresi (2021) faz um benchmark head-to-head destas ferramentas.

  • Normalização Unicode: a percentagem de documentos que produz outputs NFC e NFKC idênticos (calculada com unicodedata.normalize, da stdlib) é um sinal útil de drift. Mismatches são a forma como zero-width joiners e caracteres visualmente semelhantes quebram o recall do retrieval.

  • Precisão da deteção de idioma: F1 numa amostra multilingue etiquetada. É crítica para índices multilingues. Use fasttext-langdetect (o lid.176 do Facebook), lingua-py ou cld3. FLORES-200 fornece texto de avaliação em 200 idiomas, mas a mistura de idiomas em produção deve determinar a sua amostra de teste.

  • Eficácia da deduplicação (MinHash / LSH): precision/recall do detetor de near-duplicates contra um conjunto etiquetado manualmente. A ideia subjacente é estimar a similaridade de Jaccard J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} entre conjuntos de shingles de documentos através de kk hashes de permutações aleatórias (Broder, 1997) e agrupar near-duplicates com banding LSH (Indyk & Motwani, 1998). Faça sweep do número de hashes e do limiar de Jaccard no seu corpus. Acompanhe separadamente a taxa de false-merge (corrompe respostas) e a taxa de missed-merge (desperdiça espaço no índice). datasketch fornece a implementação usada abaixo; os parâmetros são ilustrativos:

    from datasketch import MinHash, MinHashLSH
    
    def shingles(text: str, k: int = 5) -> set[str]:
        text = text.lower()
        return {text[i:i + k] for i in range(len(text) - k + 1)}
    
    def to_minhash(text: str, num_perm: int = 128) -> MinHash:
        m = MinHash(num_perm=num_perm)
        for s in shingles(text):
            m.update(s.encode("utf-8"))
        return m
    
    docs = {
        "d1": "Mars has two moons, Phobos and Deimos.",
        "d2": "Mars has two moons, Phobos and Deimos!",   # near-dup
        "d3": "Curiosity rover landed on Mars in 2012.",
    }
    
    lsh = MinHashLSH(threshold=0.8, num_perm=128)
    for did, text in docs.items():
        lsh.insert(did, to_minhash(text))
    
    print(sorted(lsh.query(to_minhash(docs["d1"]))))  # ['d1', 'd2']
  • Remoção de PII: precision e recall, calculados separadamente por tipo de entidade (emails, SSNs, nomes, moradas). Erros de recall criam risco de compliance; erros de precision prejudicam a qualidade das respostas. Defina o operating point com a equipa jurídica. As ferramentas candidatas incluem Microsoft Presidio, scrubadub ou um modelo NER com fine-tuning num conjunto etiquetado.

O chunking controla a qualidade do retrieval

O chunking pode criar uma lacuna de recall em vários pontos mesmo quando o modelo de embedding se mantém fixo. No benchmark de vendors da NVIDIA de 2025, o chunking ao nível da página produziu a maior precisão e a menor variância para documentos paginados. Trate esse resultado como evidência para o corpus testado, não como um vencedor universal.

O chunking semântico agrupa frases adjacentes por similaridade de embeddings e corta em fronteiras dissimilares. SemanticChunker, do LangChain, e SemanticSplitterNodeParser, do LlamaIndex, implementam esta estratégia. Pode melhorar o recall face a janelas fixas quando as fronteiras temáticas são importantes.

O recursive character splitting tenta quebras de parágrafo, depois quebras de frase e, por fim, quebras de palavra, até cada chunk caber no tamanho-alvo. RecursiveCharacterTextSplitter, do LangChain, implementa esta sequência. Escolha valores candidatos para a janela e o overlap que se adequem à estrutura dos seus documentos e deixe o golden set decidir os valores finais.

Métricas a acompanhar:

  • Coerência do chunk: coherence=cos(si,sj)withincos(si,sj)across boundary\text{coherence} = \overline{\cos(s_i, s_j)}_{\text{within}} - \overline{\cos(s_i, s_j)}_{\text{across boundary}}, onde sis_i são embeddings de frases. Chunks saudáveis são internamente semelhantes e dissimilares nas fronteiras. Calcule com sentence-transformers e com cosine_similarity de scikit-learn.
  • Qualidade das fronteiras: “é este um corte sensato?” etiquetado por humanos numa amostra, além de uma verificação estrutural que garanta que os chunks não dividem tabelas, listas ou secções numeradas.
  • Tamanho ótimo do chunk: faça sweep de tamanhos em tokens (128, 256, 512, 1024) e represente Recall@k em função do tamanho no seu golden set. Escolha o knee. Não escolha simplesmente o valor indicado num tutorial.
  • Eficácia do overlap: faça ablation de várias frações de overlap e meça Recall@k. Pare de aumentar o overlap quando a curva de recall local estabilizar ou quando o custo da duplicação superar o ganho.
  • Fidelidade da atribuição do chunk: percentagem de chunks que mantêm um ponteiro verificável para a fonte (número da página, anchor da secção, doc ID). A auditabilidade exige isto.
  • Chunking late vs. early: o late chunking (Günther et al., 2024) faz embedding do documento completo e segmenta-o depois, preservando o context global (implementação de referência em jina-embeddings-v3). O Contextual Retrieval (Anthropic, 2024) acrescenta a cada chunk context gerado por um LLM. Ambos aumentam o custo. Faça benchmark no seu corpus antes de adotar qualquer um.

Na minha opinião, o chunking estrutural (dividir por headings, tabelas e secções — implementado por parsers como o unstructured.io ou percorrendo o AST já produzido pelo seu parser) é pouco utilizado. Se os seus documentos têm estrutura, use-a antes de adicionar heurísticas de similaridade. O recursive character splitting é a baseline; o chunking semântico justifica o overhead sobretudo em prosa não estruturada.

Extração e enriquecimento de metadata

  • Precision/recall/F1 de NER: por tipo de entidade, num subconjunto etiquetado. O standard é o estilo CoNLL/MUC. Calcule com seqeval (from seqeval.metrics import f1_score) para a versão aware de tags BIO/IOB, ou com scikit-learn para comparações de span sets. CoNLL-2003 e OntoNotes 5.0 são os corpora de referência canónicos.
  • F1 de relation extraction: ainda mais importante para sistemas baseados em ontologias. Etiquete manualmente um conjunto estratificado por tipo de relação e classe de documento. TACRED e DocRED são benchmarks públicos; as implementações candidatas incluem pipelines de relações opennre e spaCy.
  • Precisão da extração de títulos / headings: exact-match mais similaridade de Levenshtein normalizada (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) contra o ground truth — python-Levenshtein ou rapidfuzz fornecem ambas numa só chamada.
  • Preservação de metadata hierárquica: percentagem de chunks que preservam corretamente a sua parent section, parent document e caminho de ancestry. Esta é a métrica que determina se o seu RAG consegue responder a perguntas do tipo “o que diz o child da policy X?”.

Geração de embeddings

  • Benchmarks de seleção de modelos: use os resultados de tarefas do MTEB (nDCG@10 é a métrica principal; o MTEB Python package permite reproduzir o leaderboard localmente), o BEIR para generalização zero-shot e o MIRACL para retrieval multilingue como pontos de comparação. Trate a transferência de MTEB em inglês para uma língua com menos recursos como uma hipótese a testar no conjunto etiquetado dessa língua.
  • Avaliação específica do domínio: não trate a posição num benchmark geral como resultado no domínio. Dimensione um golden set do domínio a partir da sua matriz de cobertura e da incerteza que a decisão tolera. Depois faça rerank dos modelos candidatos com ranx ou pytrec_eval. Um conjunto de domínio pode inverter a ordem do leaderboard, por isso publique a amostra do dataset, o protocolo de retrieval e o intervalo de confiança juntamente com o resultado.
  • Deteção de drift de embeddings: acompanhe KL distributional ou drift baseado em modelo entre uma janela de referência fixa e embeddings de produção em rolling window; meça também a estabilidade dos nearest neighbors para um conjunto fixo de probes. evidently e alibi-detect implementam detetores baseados em modelos e estatísticos. O estudo comparativo da Evidently é uma avaliação de um vendor; compare os métodos em shifts conhecidos nos seus próprios embeddings.
  • Multi-vector vs. single-vector: a late interaction preserva representações ao nível do token em vez de colapsar cada documento num único vetor; ColBERT é o design canónico, com implementações de referência em RAGatouille e PyLate. Esta representação mais rica aumenta o custo do índice e do retrieval. Compare qualidade, armazenamento e latência com uma baseline single-vector no mesmo conjunto de domínio antes de a adotar.

Construção do índice

  • Recall@k sob aproximação: compare o índice approximate-nearest-neighbour (ANN) com uma baseline exacta de brute force no mesmo k — em FAISS, trata-se de IndexHNSWFlat (ou IndexIVFFlat) vs. IndexFlatIP/IndexFlatL2. Defina a perda de recall aceitável a partir do seu orçamento de qualidade downstream. O projeto ann-benchmarks acompanha curvas de Pareto recall–QPS entre bibliotecas.
  • Tuning de HNSW: HNSW (Hierarchical Navigable Small World) é um grafo de proximidade em camadas; consulte Malkov & Yashunin, 2018. É implementado em hnswlib, em IndexHNSWFlat do FAISS e na maioria das vector DBs. HNSW expõe três parâmetros: M (fan-out do grafo), efConstruction (largura dos candidatos durante o build) e efSearch (largura dos candidatos durante a query). Comece pelos defaults documentados da biblioteca e faça sweep dos parâmetros até a curva recall–latência cumprir os requisitos do seu eval set.
  • Tuning de IVF: IVF (Inverted File index — particiona vetores com k-means em nlist células e, no momento da query, pesquisa as nprobe células mais próximas; consulte IndexIVFFlat e IndexIVFPQ do FAISS). Faça sweep de nlist e nprobe contra o recall e a latência da pesquisa exacta. Faça benchmark de queries filtradas separadamente, porque as famílias de índices e as vector databases implementam a travessia de filters de forma diferente.
  • Lag de freshness das atualizações: tempo entre o commit do documento e a sua disponibilidade para retrieval. Acompanhe p50 e p99. Em sistemas com requisitos regulamentares, acompanhe também a percentagem de queries servidas contra índices stale.

Parte 4: Avaliação em tempo de query

A lane de tempo de query contém as métricas que diagnosticam o caminho de retrieval. Recall@k, por si só, não mostra se a falha foi causada por rewriting, filtering, reranking ou montagem do context.

Compreensão e rewriting de queries

  • Qualidade da query expansion: uplift de Recall@k no seu golden set, comparando a query expandida com a raw. Defina previamente o ganho mínimo útil e a sua incerteza antes de testar. Se a expansion não ultrapassar esse gate local, não justifica a sua latência e custo. Baselines clássicas de PRF (pseudo-relevance feedback), como RM3 e Bo1, continuam a ser sanity checks úteis; a expansion baseada em LLM tem de as superar.
  • Avaliação de HyDE: HyDE (Gao et al., 2022) gera uma resposta hipotética com o LLM, faz embedding dessa resposta e faz retrieval contra ela. Acrescenta latência de generation e uma nova superfície de falha. Meça Recall@10 separadamente em slices in-domain, out-of-domain e de baixa confiança; depois decida se pertence ao caminho default, a um fallback ou a nenhum dos dois.
  • Geração de multi-query: união de Recall@k de N rewrites vs. uma única query. Faça sweep de N e escolha um ponto na sua fronteira recall–latência. Implementações: MultiQueryRetriever, do LangChain, e QueryFusionRetriever, do LlamaIndex.
  • Precisão da classificação de intent: precision/recall/F1 standard por intent (calcule com sklearn.metrics.classification_report), mas a métrica operacional é a correção do routing — é invocado o pipeline downstream correto?
  • Routing adaptativo: Adaptive-RAG (Jeong et al., NAACL 2024) defende que nem todas as queries merecem a mesma estratégia de retrieval. Acompanhe a precisão do router como um problema de classificação contra um conjunto etiquetado de “não necessita de retrieval / one-shot / iterativo”.

Métricas de retrieval

Estas são as métricas baseline. Se não as acompanhar, não consegue saber se o retrieval está a melhorar.

MétricaO que medeQuando usar
Recall@kfração dos documentos relevantes de uma query devolvidos no top kquando é importante não perder qualquer parte do conjunto relevante
Precision@kpercentagem do top-k que é relevanteútil quando a context window é o bottleneck
MRRmédia de 1/rank do primeiro documento relevantequando os utilizadores só olham para o top-1 ou top-3
nDCG@kganho descontado pela posição e ponderado pelos graus de relevânciamétrica standard de retrieval para relevância graduada
MAPmédia, entre queries, da average precisionquando todo o ranked list é importante
Hit Rate@kse aparece pelo menos um documento relevante no top kfaça a média do resultado binário entre queries como sanity metric rápida
Coveragepercentagem de golden docs alguma vez recuperados em todas as queriesdeteta lacunas sistemáticas no índice

As fórmulas, para referência (relevância binária com conjunto relevante RqR_q para a query qq e reli=1\text{rel}_i = 1 se o documento recuperado na posição ii estiver em RqR_q):

Recall@k=Rq{d1,,dk}Rq,Precision@k=Rq{d1,,dk}k\text{Recall@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{|R_q|}, \quad \text{Precision@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{k} RRq=1rank of first relevant doc,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rank of first relevant doc}}, \quad \text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \text{RR}_q DCG@k=i=1k2reli1log2(i+1),nDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}, \quad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Para relevância graduada, reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; o nDCG binário é o caso especial usado no código abaixo. MAP é a média, entre queries, de APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Consulte Manning, Raghavan, Schütze, Introduction to Information Retrieval, capítulo 8, para as derivações.

Para código de produção, use ranx, pytrec_eval ou ir_measures — implementam toda a família de métricas TREC e tratam corretamente a relevância graduada. Defina objetivos de release com base num golden set realista, na qualidade downstream da resposta e no custo de uma falha. Não herde thresholds de um tutorial.

O test harness para estas métricas é curto. Pode executá-lo a partir de um notebook antes mesmo de escolher uma vector database.

from math import log2
from statistics import mean

# synthetic gold set: query_id -> set of relevant doc ids
gold = {
    "q1": {"d3"},
    "q2": {"d7", "d2"},
    "q3": {"d11"},
    "q4": {"d5"},
}

# ranked retrieval results: query_id -> ranked list of doc ids (top-10)
runs = {
    "q1": ["d8", "d3", "d1", "d4", "d2", "d9", "d6", "d10", "d12", "d13"],
    "q2": ["d2", "d6", "d4", "d7", "d1", "d3", "d8", "d11", "d5", "d9"],
    "q3": ["d11", "d2", "d3", "d4", "d1", "d6", "d7", "d8", "d10", "d12"],
    "q4": ["d1", "d2", "d3", "d6", "d8", "d9", "d10", "d12", "d13", "d14"],
}

def recall_at_k(ranked, gold_set, k):
    if not gold_set:
        return 0.0
    hit = sum(1 for d in ranked[:k] if d in gold_set)
    return hit / len(gold_set)

def reciprocal_rank(ranked, gold_set):
    # MRR contribution per query: 1/rank of the first relevant doc.
    for rank, d in enumerate(ranked, start=1):
        if d in gold_set:
            return 1.0 / rank
    return 0.0

def ndcg_at_k(ranked, gold_set, k):
    # binary relevance: rel ∈ {0, 1}
    gains = [1.0 if d in gold_set else 0.0 for d in ranked[:k]]
    dcg = sum(g / log2(i + 2) for i, g in enumerate(gains))
    # ideal DCG: all gold docs ranked first, capped by k
    n_gold_in_topk = min(k, len(gold_set))
    idcg = sum(1.0 / log2(i + 2) for i in range(n_gold_in_topk))
    return dcg / idcg if idcg else 0.0

K = 5
print(f"Recall@{K}: {mean(recall_at_k(runs[q], gold[q], K) for q in gold):.3f}")
print(f"MRR:       {mean(reciprocal_rank(runs[q], gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}:  {mean(ndcg_at_k(runs[q], gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR:       0.625
# nDCG@5:    0.627

Esse é o seu retrieval CI gate. Ligue-o a um subset rápido orientado por coverage em cada PR e execute o golden set completo no release gate mais lento. Bloqueie um merge quando uma métrica pré-registada ultrapassar o seu orçamento de regressão.

O repositório complementar fixa os valores exatos acima (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) como unit test em tests/test_retrieval_metrics.py; o notebook 01 faz sweep de Recall@k / MRR / nDCG num índice SciFact real e o harness com formato de produção está em evaluation/retrieval.py.

Hybrid retrieval e reciprocal rank fusion

BM25 é um scorer lexical esparso que combina correspondência de termos exatos, ponderação de termos e normalização pelo comprimento. Está disponível em rank_bm25, Elasticsearch, OpenSearch e na maioria dos motores de pesquisa.

A Reciprocal Rank Fusion (Cormack, Clarke e Buettcher, SIGIR 2009) combina rankings BM25 e dense pela posição. A configuração original k=60 é uma baseline útil. RRF é agnóstica aos scores, evitando a normalização entre lanes exigida pela interpolação linear. Com um conjunto etiquetado suficientemente grande para estimar um delta estável, teste também uma combinação convexa e faça tuning de α.

A minha hipótese é que hybrid retrieval mais um cross-encoder reranker pode ajudar em corpora técnicos, de logs e de código. O ganho pode ser pequeno em corpora fortemente semânticos. Meça contra as lanes dense-only e sparse-only, porque uma configuração de fusion deficiente pode ter pior desempenho do que qualquer uma das entradas. O notebook SciFact complementar é um teste delimitado, não um resultado geral.

A implementação cabe em poucas linhas.

from collections import defaultdict

# two retrieval lanes: dense embeddings and BM25.
dense  = ["d3", "d7", "d1", "d4", "d2", "d9", "d10"]
sparse = ["d2", "d3", "d8", "d1", "d11", "d4", "d6"]

def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).

    score(d) = sum over rankings of 1 / (k + rank(d))
    Score-agnostic: only rank position matters. k=60 is the canonical default.
    """
    scores: dict[str, float] = defaultdict(float)
    for ranking in rankings:
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] += 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)

fused = rrf([dense, sparse], k=60)
for doc, score in fused[:5]:
    print(f"{doc}  score={score:.5f}")
# d3  score=0.03252   <- rank 1 dense, rank 2 sparse
# d2  score=0.03178   <- rank 5 dense, rank 1 sparse
# d1  score=0.03150

Repare no que o RRF não faz: nunca consulta os raw similarity scores. Um dense retriever que devolve cosine 0,98 e uma lane BM25 que devolve score 17,4 não são diretamente comparáveis. Se os normalizar com z-scores ou min-max scaling, pode acabar por favorecer a lane com maior variância nesse batch.

O RRF usa apenas o rank. Se um retriever coloca um documento na posição 2, esse voto vale 1 / (60 + 2), independentemente do raw score que o produziu.

Hybrid + RRF no SciFact: o notebook 02 compara dense vs. BM25 vs. RRF com deltas por query. O fuser com formato de produção está em retrieval/hybrid_rrf.py; tests/test_rrf.py fixa a ordenação canónica de d3 / d2 / d1 em k=60.

Reranking

  • ΔnDCG / ΔMRR: uplift face a não aplicar rerank, no seu golden set e à profundidade efetivamente usada pela aplicação. Calcule executando as métricas de retrieval com e sem o reranker, sobre conjuntos de candidatos idênticos.
  • Cross-encoder vs. bi-encoder: um bi-encoder faz embedding da query e do documento independentemente (um vetor por lado) e calcula o score através do produto escalar; um cross-encoder concatena query+doc e executa uma única forward pass que atende conjuntamente a ambos. Os cross-encoders trocam uma forward pass por candidato por uma interação query–documento mais rica. Implementação de referência: sentence-transformers CrossEncoder. Faça benchmark da relevância e da latência em hardware, batch size e profundidade de candidatos identificados; não transfira o resultado de um modelo ou serviço gerido para outro ambiente.
  • Listwise vs. pointwise: pointwise calcula o score de cada par (query, doc) independentemente; listwise calcula o score de toda a lista de candidatos conjuntamente, permitindo ao modelo comparar candidatos. Avalie ambos sobre os mesmos conjuntos de candidatos. Calibre qualquer threshold de score por modelo e corpus, em vez de tratar um exemplo publicado como portátil.
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

query = "How do I rotate database credentials in production?"
candidates = [
    "Production database credentials are rotated via Vault every 30 days.",
    "The new logo was unveiled at the all-hands meeting.",
    "To rotate prod DB creds, run the `rotate-secrets` GitHub Action.",
]

scores = reranker.predict([(query, c) for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
for doc, score in ranked:
    print(f"{score:+.3f}  {doc}")

Um reranker ajuda frequentemente um pipeline RAG básico, mas não é uma melhoria garantida. Meça o ΔPrecision@1 e o ΔnDCG no seu golden set e mantenha-o apenas se o ganho cumprir o orçamento de latência e custo. Compare esse ganho medido com alterações menores no retrieval antes de escolher a próxima otimização.

ΔnDCG e ΔPrecision@1 de um cross-encoder no SciFact: notebook 03; módulo: retrieval/reranker.py.

Construção do context e lost-in-the-middle

Muitas falhas de “bom retrieval, má resposta” começam na construção do context.

  • Relevância do context: score de relevância por chunk de RAGAS ContextRelevance ou de um cross-encoder, agregado como média e como percentagem de chunks abaixo de um threshold.
  • Utilização do context: dos chunks colocados no context, quantos foram efetivamente citados ou utilizados na resposta. Calcule como cited chunksretrieved chunks\frac{|\text{cited chunks}|}{|\text{retrieved chunks}|} numa amostra etiquetada. Defina o threshold operacional a partir da qualidade da resposta e do custo em tokens, em vez de usar uma percentagem universal.
  • Deteção de lost-in-the-middle: avaliação sintética em que coloca o gold chunk nas posições {primeira, intermédia, última} de um context longo e mede a correção da resposta. O estudo citado de Liu et al. (TACL 2023) reporta uma degradação em U nas suas condições de context longo. Trate o mesmo padrão num modelo atual como uma hipótese a testar. Mitigações: faça rerank e depois reordene o top-k para que o chunk com maior score fique primeiro ou último (LongContextReorder, do LangChain, faz exatamente isto) ou comprima agressivamente os chunks intermédios. Meça com uma avaliação estratificada por posição, não apenas com um score agregado. Uma avaliação executável e detalhada, estratificada por posição, está no notebook 06 (módulo: evaluation/lost_in_middle.py).
  • Compressão do context: reporte o compression ratio (tokens de input / tokens de output) juntamente com a correção da resposta. As ferramentas incluem ContextualCompressionRetriever, do LangChain, e LongLLMLingua. Defina previamente a maior perda de correção aceitável a partir do risco e do orçamento de tokens da aplicação e rejeite configurações que a ultrapassem.

Parte 5: Taxa de false-exclusion de filters

Esta métrica tem uma secção própria porque os scores agregados de retrieval não conseguem atribuir uma falha ao filter.

Um hard metadata filter como tenant_id = X AND product = Y AND locale = en-US pode reduzir o recall efetivo a zero. Um Recall@k corretamente implementado deteta a perda, porque o denominador continua a ser o conjunto original de documentos relevantes. Não indica se a causa foi o filter, o retriever ou o ranker. Faithfulness avalia claims contra o context recuperado. Pode ainda atribuir um score a claims suportadas por esse context incompleto, mas não consegue diagnosticar a causa no filter ou no retrieval. Uma recusa vazia pode não produzir statements e NaN, dependendo da implementação; não a trate como evidência de que faithfulness aprovou a recusa.

O ramo destacado representa a falha comum: o documento correto existe, mas o filter remove-o antes do retrieval. Recall@k regista a queda; apenas a taxa de exclusão a atribui ao predicate.

Falhas silenciosas de RAG mapeadas do corpus de origem, passando pelo filtering, ranking e generation, até à métrica que identifica cada origemFalhas silenciosas de RAG mapeadas do corpus de origem, passando pelo filtering, ranking e generation, até à métrica que identifica cada origem

A métrica

filter_false_exclusion_rate =
    (# queries where all gold docs were excluded by metadata filter) /
    (# queries with at least one gold doc)

Esta definição ao nível da query conta exclusões catastróficas: nenhum documento relevante sobrevive. Para queries com múltiplos gold docs, o Recall@k standard continua a expor a perda parcial; acrescente uma taxa de exclusão por documento se essa fronteira for importante. Para calcular qualquer uma das taxas, precisa de (a) IDs de documentos de ground truth para cada query de avaliação e (b) instrumentação que registe os predicados de filter aplicados, não apenas os resultados finais. Defina o objetivo a partir do custo de excluir uma resposta válida e do intervalo de confiança da amostra de produção.

Eis uma implementação funcional. Compara o recall standard correto com um evaluator inválido que redefine a relevância depois do filtering.

# A small worked example where hard filters remove relevant documents.
docs = [
    {"id": "d1", "tenant": "acme",   "locale": "en-US"},
    {"id": "d2", "tenant": "acme",   "locale": "en-GB"},
    {"id": "d3", "tenant": "globex", "locale": "en-US"},
    {"id": "d4", "tenant": "acme",   "locale": "en-US"},
    {"id": "d5", "tenant": "acme",   "locale": "de-DE"},
]

queries = [
    # the gold doc lives in en-GB but the dynamic filter forced en-US
    {"qid": "q1", "gold": {"d2"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc is correctly within the tenant filter
    {"qid": "q2", "gold": {"d4"}, "filter": lambda d: d["tenant"] == "acme"},
    # the gold doc is in a different tenant and gets dropped
    {"qid": "q3", "gold": {"d3"}, "filter": lambda d: d["tenant"] == "acme"},
    # the gold doc passes the filter (de-DE locale match)
    {"qid": "q4", "gold": {"d5"}, "filter": lambda d: d["locale"] == "de-DE"},
]

def filter_false_exclusion_rate(queries, docs):
    n_with_gold, n_excluded = 0, 0
    for q in queries:
        if not q["gold"]:
            continue
        n_with_gold += 1
        survivors = {d["id"] for d in docs if q["filter"](d)}
        if not (q["gold"] & survivors):
            n_excluded += 1
    return n_excluded / n_with_gold if n_with_gold else 0.0

rate = filter_false_exclusion_rate(queries, docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%

# Correct Recall@k keeps the original gold set as its denominator.
def standard_recall_at_k(queries, docs, k=10):
    recalls = []
    for q in queries:
        # demo only: the survivor set stands in for a ranked run.
        # A real harness ranks the survivors first, then slices to k.
        survivors = [d for d in docs if q["filter"](d)][:k]
        survivor_ids = {d["id"] for d in survivors}
        recalls.append(len(q["gold"] & survivor_ids) / len(q["gold"]))
    return sum(recalls) / len(recalls) if recalls else 0.0

print(f"standard recall@10 = {standard_recall_at_k(queries, docs):.2%}")
# standard recall@10 = 50.00%

# INVALID: rebuilding the gold set after filtering changes the question.
# It drops queries whose relevant documents did not survive, then scores 100%.
def invalid_recall_over_filtered_gold(queries, docs, k=10):
    recalls = []
    all_doc_ids = {d["id"] for d in docs}
    for q in queries:
        all_survivors = {d["id"] for d in docs if q["filter"](d)}
        filtered_gold = q["gold"] & all_doc_ids & all_survivors
        if not filtered_gold:
            continue
        top_k_ids = set(list(all_survivors)[:k])
        recalls.append(len(filtered_gold & top_k_ids) / len(filtered_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

invalid = invalid_recall_over_filtered_gold(queries, docs)
print(f"INVALID recall (filtered gold) = {invalid:.2%}")
# INVALID recall (filtered gold) = 100.00%

assert rate == 0.5
assert standard_recall_at_k(queries, docs) == 0.5
assert invalid == 1.0

Metade das queries perde o respetivo gold doc devido ao filter, pelo que o Recall@10 correto cai para 50%. Esse score deteta o sintoma, mas não consegue atribuir-lhe uma causa. A taxa de false-exclusion mostra que o predicate removeu duas respostas antes de o retriever ser executado. O evaluator deliberadamente inválido reporta 100% apenas porque elimina essas falhas do gold set. Nenhum modelo consegue recuperar um documento que foi filtrado.

A taxa de 50% acima é reproduzida como unit test no repositório complementar: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. O Notebook 04 executa-o no SciFact com metadata sintética, para poder observar um filter real a reduzir o recall a zero; a métrica de runtime (com precision/recall do predicate como métrica complementar) está em evaluation/filter_exclusion.py.

Métrica complementar: precision e recall do predicate

Quando o filtering é dinâmico (por exemplo, um LLM extrai predicados de filter da query), trate o extractor de predicados como um modelo de classificação e avalie-o como tal. Meça precision e recall dos predicados contra um conjunto etiquetado de pares (query, correct predicate). Uma taxa de erro de predicados não corresponde diretamente à mesma perda pontual no recall de retrieval; meça com que frequência esses erros excluem um gold document. Assim que um hard filter remove o gold document, nenhum reranking consegue ajudar.

Soft boost vs. hard filter

Esta métrica obriga a uma decisão de design. Use hard filters quando a correção é binária: jurisdição legal, fronteiras de ACL, publicado-versus-draft. Use soft boosts quando a relevância é graduada: preferência de locale, recência, versão. Sem medir a taxa de exclusão, a escolha errada é difícil de detetar.

A regra de decisão, mensurável:

For each filter predicate F:
  hard_recall_F  = retrieval_recall@k with F as a hard filter
  soft_recall_F  = retrieval_recall@k with F as a +0.X rerank boost
  hard_precision = relevant_in_top_k / k under hard filter
  soft_precision = relevant_in_top_k / k under soft boost
  exclusion_rate = % of queries where the gold doc was filtered out (hard)

Use hard filter only if exclusion_rate < ε AND hard_precision >> soft_precision.
Otherwise prefer soft boost.

Escolha ε com base no dano de uma false exclusion, no benefício de maior precision e no tamanho da amostra de avaliação. Está planeado um artigo dedicado a este trade-off; consulte os follow-ups listados no final.


Parte 6: Avaliação de generation

As métricas de retrieval dizem-lhe que o sistema poderia responder corretamente. Não dizem que respondeu. As métricas de generation cobrem essa lacuna.

Faithfulness e groundedness

A faithfulness do RAGAS decompõe a resposta em claims atómicos (statements factuais curtos e autocontidos) e verifica cada um contra o context recuperado através de um LLM judge:

faithfulness=claims supported by contexttotal claims\text{faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims}|}

A percentagem de claims suportadas é o score. A estrutura é mais útil do que qualquer número isolado, porque mostra quais claims não têm suporte. O código de produção está no package ragas. O exemplo seguinte é um esboço de API legacy sem execução para RAGAS 0.4.3, não um programa copy-paste. Para o executar, fixe ragas==0.4.3, instale um provider client, configure as credenciais, crie um RAGAS LLM suportado por um provider e passe-o como evaluator_llm. A configuração fica intencionalmente fora do bloco e é específica do provider; a função recebe o objeto configurado como argumento, em vez de pressupor um default. Para código novo, use a API atual baseada em collections, que o RAGAS documenta como caminho de migração da API legacy de métricas:

# No-run: legacy RAGAS 0.4.3 API shape.
# Before calling this function, configure a provider-backed RAGAS LLM.
# For example, with the provider credentials already set:
# from openai import AsyncOpenAI
# from ragas.llms import llm_factory
# evaluator_llm = llm_factory("gpt-4o-mini", client=AsyncOpenAI())
from ragas import EvaluationDataset, evaluate
from ragas.metrics import (
    Faithfulness,
    LLMContextPrecisionWithoutReference,
    ResponseRelevancy,
)

def run_legacy_ragas_043(evaluator_llm):
    dataset = EvaluationDataset.from_list([
        {
            "user_input": "How many moons does Mars have?",
            "response": "Mars has two moons, Phobos and Deimos.",
            "retrieved_contexts": ["Mars has two moons named Phobos and Deimos."],
            "reference": "Mars has two moons.",
        }
    ])

    return evaluate(
        dataset,
        metrics=[Faithfulness(), ResponseRelevancy(), LLMContextPrecisionWithoutReference()],
        llm=evaluator_llm,
    )

O RAGAS renomeou estes fields na versão 0.2 (questionuser_input, answerresponse, contextsretrieved_contexts, ground_truthreference). Código escrito para a versão 0.1 falha de forma confusa: as keys antigas são descartadas em vez de rejeitadas, pelo que o erro identifica as novas columns como ausentes.

Segue-se o mesmo loop expandido com um judge determinístico de substituição, para mostrar a estrutura end-to-end.

def extract_claims(answer: str) -> list[str]:
    # Production: an LLM call that decomposes the answer.
    # Demo: split on sentence-final punctuation.
    return [c.strip() for c in answer.replace("?", ".").replace("!", ".").split(".") if c.strip()]

def verify_claim(claim: str, context: str) -> bool:
    # Production: an NLI (natural-language inference) model or LLM judge.
    # Demo: a deterministic stand-in so the example runs offline.
    entailed_pairs = {
        "Mars has two moons": True,
        "Phobos and Deimos orbit Mars": True,
        "Mars has a thick atmosphere": False,  # unsupported by context
        "Curiosity landed in 2012": True,
    }
    for k, v in entailed_pairs.items():
        if k.lower() in claim.lower() or claim.lower() in k.lower():
            return v
    words = [w.lower() for w in claim.split() if len(w) > 3]
    return all(w in context.lower() for w in words) if words else False

context = (
    "Mars has two moons, Phobos and Deimos. NASA's Curiosity rover "
    "landed on Mars in 2012."
)
answer = (
    "Mars has two moons. Phobos and Deimos orbit Mars. "
    "Mars has a thick atmosphere. Curiosity landed in 2012."
)

claims = extract_claims(answer)
verdicts = [(c, verify_claim(c, context)) for c in claims]
faithfulness = sum(1 for _, ok in verdicts if ok) / len(verdicts)
for c, ok in verdicts:
    print(f"  [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}")
# faithfulness = 0.75   (one unsupported claim about the atmosphere)

A estrutura é importante. Em produção, verify_claim passa a ser um modelo NLI ou uma chamada a um LLM. O resto do harness permanece igual: extrair, verificar, agregar.

Extração e verificação end-to-end de claims em respostas SciFact geradas: notebook 05; módulo: evaluation/faithfulness.py. O repositório executa o mesmo loop através de duas famílias de judges — o próprio modelo do generator e um judge de outra família (RAG_EVALS_JUDGE_MODEL) — além de uma baseline lexical determinística, para poder observar onde as famílias discordam.

Uma alternativa criada especificamente para o efeito de LLM-as-judge é o HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), um classifier com fine-tuning para deteção de hallucinations. O model card documenta o checkpoint, o raw score entre 0 e 1 que emite e os resultados de balanced accuracy em AggreFact e RAGTruth. Não publica uma decision boundary default, pelo que essa escolha é sua. Trate esses valores como evidência do model card, não como garantia no seu corpus: calibre o threshold com labels locais e compare-o com o judge escolhido antes do deployment.

Avaliação de factos atómicos

O FActScore (Min et al., EMNLP 2023) decompõe generations longas em factos atómicos, recupera evidência para cada facto, etiqueta cada um como supported / not-supported e reporta a fração suportada:

FActScore=supported atomic factstotal atomic facts\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Implementação de referência: shmsw25/FActScore. Funciona bem para biografias, sumários e outros outputs longos. Atenção: factos triviais repetitivos podem inflacionar o score e ataques “MontageLie” (factos verdadeiros numa ordem enganadora) podem derrotá-lo. O VeriScore trata claims com modificadores necessários; o filter Core ajuda a evitar fact-padding.

Precisão das citações

Acompanhe citation precision (os spans citados suportam efetivamente a claim) e citation recall (as claims que deveriam ser citadas são citadas):

cite_precision=cited spans that support a claimcited spans,cite_recall=claims with at least one supporting cited spanclaims that should be cited\text{cite\_precision} = \frac{|\text{cited spans that support a claim}|}{|\text{cited spans}|}, \quad \text{cite\_recall} = \frac{|\text{claims with at least one supporting cited span}|}{|\text{claims that should be cited}|}

O TREC 2024 RAG Track define um protocolo reproduzível de support evaluation. Thakur et al. (SIGIR 2025) reportam que o GPT-4o concordou com judges humanos em 56% dos casos numa avaliação manual feita de raiz, subindo para 72% com pós-edição das previsões do LLM. Isso é útil como force multiplier nas condições testadas, não como substituto da avaliação humana em contextos de alto risco. Para uma aproximação automática, o ALCE (Gao et al., EMNLP 2023) implementa citation precision/recall com verificação baseada em NLI.

Correção, completude e recusa da resposta

  • Correção da resposta vs. ground truth: quando disponível, exact match ou token-F1 para tarefas de resposta curta (evaluate.load("squad")), similaridade semântica para respostas abertas (bert-score, cosine de embeddings através de sentence-transformers ou AnswerCorrectness do RAGAS).
  • Completude através de nuggets: um “nugget” é uma unidade atómica de informação que qualquer resposta correta tem de conter (por exemplo, para “Quando foi fundada a empresa?”, os nuggets poderiam ser {year: 1994, founder: Jane Doe}). O AutoNuggetizer do TREC extrai de uma referência os gold nuggets de uma resposta correta e calcula que fração é coberta pelo sistema — forte correlação com avaliação manual em 21 tópicos × 45 runs no TREC 2024.
  • Comportamento de recusa: queries sem resposta no corpus devem produzir abstention, não hallucination. Acompanhe abstention precision (recusas corretas) e abstention recall (queries fora do âmbito que desencadearam uma recusa). NoMIRACL é o benchmark público; no seu próprio domínio, etiquete um slice de queries fora do âmbito e acompanhe a precisão da abstention.

Verificação pós-generation

Os ganhos de reliability mais baratos resultam frequentemente de post-checks determinísticos, não de modelos maiores.

  • Verificação de grounding de entidades: todas as named entities da resposta têm de aparecer no context recuperado (ou ser deriváveis dele). Uma verificação simples com regex + exact-match (ou ents de spaCy contra uma string de context normalizada) deteta uma fração surpreendentemente grande de hallucinations.
  • Verificação de claims: extraia claims, execute NLI contra o context e falhe ou sinalize qualquer uma abaixo do threshold. Modelos NLI-as-faithfulness: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Acrescenta latência. Vale a pena em domínios de alto risco.
  • Self-consistency (Wang et al., ICLR 2023): faça sampling de várias generations com temperature > 0; reporte a taxa de agreement (por exemplo, a proporção de generations que coincide com a resposta modal ou o BERTScore pairwise); escolha o número de samples a partir da curva de estabilidade–custo e sinalize respostas com baixo agreement para revisão humana.
  • Calibração da confiança: recolha a confiança verbalizada (“Quão confiante está, de 0 a 1?”) e compare-a com a correção efetiva no eval set. Represente uma curva de calibração e reporte o Expected Calibration Error: ECE=m=1MBmnacc(Bm)conf(Bm)\text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n} |\text{acc}(B_m) - \text{conf}(B_m)|, onde BmB_m são bins de confiança. Implementações: netcal, torchmetrics.CalibrationError. Um modelo que reporta confiança 0,9 deve estar correto em aproximadamente 90% dos casos comparáveis; meça o desvio em vez de presumir calibração.

Parte 7: Avaliação de RAG baseado em ontologia

As métricas standard acima cobrem RAG sobre corpora abertos. Se o seu RAG faz retrieval sobre uma ontologia estruturada, taxonomia ou knowledge graph, essas métricas são necessárias, mas não suficientes. Exemplos incluem produtos num catálogo, condições no SNOMED, componentes numa BOM e técnicas de segurança no MITRE ATT&CK. Também precisa de medir a camada da ontologia.

Precisão do entity linking

A primeira tarefa é mapear uma menção numa query para uma entidade da ontologia (“Aspirin” → wikidata:Q18216, “the 737” → aircraft:Boeing_737).

  • Precision/recall/F1 ao nível da menção: standard, contra spans de menções gold (calcule com seqeval ou com um comparador de span sets).
  • Precisão da disambiguation: entre as menções detetadas corretamente, que fração é mapeada para o entity ID correto? As referências públicas incluem ReFinED, REL e GENRE; benchmarks como AIDA-CoNLL e BELB mostram que os resultados variam por sistema e domínio.
  • Tratamento de NIL: precision/recall em “entidade não presente na ontologia”. Meça separadamente o over-linking para entidades próximas mas erradas e a abstention correta.

Avaliação aware da hierarquia

A accuracy simples trata “prever Sedan quando a verdade é Hatchback” da mesma forma que “prever Sedan quando a verdade é Submarine”. Esses erros não são equivalentes.

  • Precision/recall/F1 hierárquicos (Kosmopoulos et al., 2015): atribua crédito a ancestors e descendants no DAG da ontologia. Com P^q\hat{P}_q, o nó previsto mais todos os seus ancestors, e TqT_q, o nó verdadeiro mais todos os seus ancestors:

    hP=qP^qTqqP^q,hR=qP^qTqqTq,hF1=2hPhRhP+hRhP = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |\hat{P}_q|}, \quad hR = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |T_q|}, \quad hF1 = \frac{2 \cdot hP \cdot hR}{hP + hR}

    Implemente com networkx sobre o grafo da ontologia: acrescente os ancestors a cada prediction e a cada label e calcule os overlaps dos conjuntos acima.

  • Similaridade de Wu-Palmer entre a entidade prevista e a entidade gold na taxonomia (Wu & Palmer, 1994):

    WuP(c1,c2)=2depth(LCA(c1,c2))depth(c1)+depth(c2)\text{WuP}(c_1, c_2) = \frac{2 \cdot \text{depth}(\text{LCA}(c_1, c_2))}{\text{depth}(c_1) + \text{depth}(c_2)}

    onde LCA é o lowest common ancestor na taxonomia. Está disponível out of the box no NLTK para WordNet (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); para taxonomias customizadas, calcule o LCA com networkx.

  • Taxa de confusão com siblings/parent: acompanhe separadamente confusões com siblings, parents e children — count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. Use exemplos revistos para testar se os erros com siblings resultam de menções ambíguas ou se os erros com parents resultam de generalização excessiva.

Taxa de false-exclusion de filters (reprise, agora crítica)

Em sistemas baseados em ontologias, os hard filters provêm frequentemente da própria ontologia (“fazer retrieval apenas de documentos etiquetados com a categoria X”). A métrica de taxa de exclusão (definida na Parte 5) torna-se um sinal primário de correção. Uma previsão de categoria errada pode reduzir o recall a zero; a taxa de exclusão atribui essa perda ao filter.

Conformidade da generation constrained

Quando o output tem de estar em conformidade com uma ontologia (todos os nomes de entidades na resposta têm de ser membros válidos da ontologia; todos os predicates têm de provir de um vocabulário fechado), meça:

  • Taxa de validade do schema: percentagem de outputs que fazem parse e validam contra o schema da ontologia. Valide com jsonschema ou pydantic. JSONSchemaBench é o benchmark público para structured output geral; para schemas específicos de uma ontologia, crie o seu próprio validator.
  • Conformidade do vocabulário: percentagem de entidades nomeadas no output que são ontology IDs válidos — uma verificação de set membership contra o vocabulário fechado.
  • Conformidade semântica: um output sintaticamente válido pode ainda escolher a entidade errada, embora válida. Combine conformidade com a correção da resposta downstream.

Frameworks de constrained decoding (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) foram concebidos para impor a validade do schema. O JSONSchemaBench compara eficiência, cobertura e qualidade entre implementações. Volte a executar os seus casos que correspondem aos schemas e ao serving backend, porque a cobertura e a latência dependem de ambos.

Auditabilidade

Para sistemas baseados em ontologias cujas respostas são sujeitas a revisão:

  • Completude das citações: percentagem de claims factuais com pelo menos uma citação verificável.
  • Profundidade da proveniência: percentagem de citações que permitem chegar até um documento de origem com um ID estável, e não apenas a um chunk hash.
  • Taxa de reprodutibilidade: voltar a executar a mesma query num snapshot fixo devolve a mesma resposta. Fixe a versão do modelo, o runtime, a configuração de decoding e a seed; depois defina a taxa de repetição necessária a partir das exigências de auditabilidade do workflow. Temperature zero, por si só, não garante determinismo. Uma falha pode resultar da generation, do serving runtime ou de qualquer etapa a montante.

Parte 8: Avaliação ao nível do sistema

Qualidade holística da resposta

  • LLM-as-judge (Zheng et al., NeurIPS 2023): uma abordagem de avaliação baseada em modelos e escalável. O G-Eval (Liu et al., EMNLP 2023) deriva um rubric a partir de um critério em linguagem natural. Depois atribui scores com output ponderado por log-prob. O agreement depende do judge, da tarefa, do prompt e do calibration set.
  • Preferência pairwise: apresente ao judge a resposta A contra a resposta B e registe a preferência. Isto evita problemas de calibração de scores absolutos. O MT-Bench reportou agreement do judge GPT-4 superior a 80% tanto com preferências humanas como com agreement humano–humano nas condições do benchmark; não transfira essa taxa para outro domínio sem calibração.

LLM-as-judge tem biases reais:

  • Position bias: os judges preferem a primeira ou a segunda resposta independentemente da qualidade. Mitigação: randomize a ordem ou execute ambas as ordens e faça a média.
  • Verbosity bias: os judges podem confundir comprimento com qualidade. Um estudo controlado de 2026 encontrou comportamento heterogéneo em pares de expansion. Três judges preferiram respostas mais longas, o Claude preferiu respostas concisas e o GPT-4o foi aproximadamente neutro. Os cinco tiveram bons resultados nos controlos de truncation. Estes resultados estão limitados ao benchmark; indique ao judge como tratar completude e filler e reporte performance controlada pelo comprimento no seu próprio rubric.
  • Self-preference bias: o GPT-4 prefere outputs do GPT-4; o bias correlaciona-se com a perplexity do output (os judges preferem texto que lhes é familiar). Mitigação: use uma família de judges diferente da do sistema avaliado. Não use um modelo para se avaliar a si próprio.

Receita prática: selecione um judge com dados de calibração etiquetados por humanos, randomize a ordem das respostas, mascare as identidades dos modelos e indique a política de comprimento no rubric. Repita casos apenas quando as amostras adicionais reduzirem materialmente a incerteza. Em avaliações de alto risco, compare judges de famílias de modelos diferentes e analise as discordâncias contra labels humanos.

Schema-Guided Reasoning para judges

O output free-form é uma fonte de variação nas execuções do judge. Duas execuções sobre a mesma resposta podem organizar o rubric de forma diferente e produzir scores diferentes. O Schema-Guided Reasoning (SGR) torna esse rubric explícito: defina as etapas da avaliação como um schema Pydantic e use constrained output através de Outlines, XGrammar, structured outputs do vLLM ou response_format da OpenAI, para que cada execução devolva os mesmos fields pela mesma ordem.

Na avaliação de RAG, o schema decompõe o julgamento em fields explícitos e auditáveis, em vez de permitir que o modelo salte diretamente para um número:

from pydantic import BaseModel, Field
from typing import Literal

class FaithfulnessJudgment(BaseModel):
    extracted_claims: list[str] = Field(
        description="Atomic factual claims in the answer, one per item."
    )
    supported_claims: list[str] = Field(
        description="Subset of extracted_claims that are entailed by the context."
    )
    unsupported_claims: list[str] = Field(
        description="Subset that is NOT entailed by the context."
    )
    failure_mode: Literal[
        "none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
    ]
    score: float = Field(ge=0.0, le=1.0)
    rationale: str

Os fields estruturados tornam o score recuperável como len(supported) / len(extracted) e mostram exatamente sobre que claims dois judges discordaram. O modelo Pydantic também torna uma alteração do rubric visível como um code diff. O constrained output garante a forma, não um veredicto sem bias; por isso, a randomização da posição, os judges de famílias diferentes e a calibração humana continuam a ser necessários.

Isto funciona para qualquer judge baseado em rubric, não apenas para faithfulness. Preferência pairwise, suporte de citações e correção de recusas beneficiam do mesmo tratamento.

Um harness de G-Eval / pairwise / position-bias / cross-family judge está no notebook 07; módulo: evaluation/llm_judge.py. O benchmark sweep (make benchmark no repositório) liga três modelos (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) a um A/B pairwise com judge rotativo, para que cada modelo avalie os outros dois e a self-preference apareça como um número.

Latência e custo

  • p50, p95, p99 em cada etapa do pipeline. Escolha o percentil do SLO e o threshold de alertas a partir da jornada do utilizador, do volume de tráfego e do error budget.
  • Time-to-first-token vs. tempo total de generation. Os utilizadores preocupam-se com TTFT numa UX de streaming.
  • Decomposição por etapa: retrieval, reranking, generation e post-processing. Use o trace para localizar a cauda, em vez de assumir qual a etapa que a causou; registe o dispositivo do reranker e o batch size ao comparar execuções.
  • Total $/query = embedding + retrieval + rerank + generation + armazenamento amortizado. Acompanhe p50 e p99; é na long tail que o budget é consumido.
  • Taxas de cache hit nos níveis de embedding cache, retrieval cache e KV-cache. Defina objetivos separados a partir da repetição observada, da política de invalidação e do custo evitado em cada camada.

p50/p95/p99 por etapa, com decomposição, está incluído no notebook 08 e no runner em evaluation/latency.py; o benchmark report combina latência com faithfulness numa única matriz que pode voltar a executar com make benchmark.

Testes A/B

  • Unidade de randomização: escolha a unidade a partir do estimand, do carryover e da interferência. Use atribuição por utilizador ou por sessão quando a exposição repetida puder alterar o comportamento ou criar uma UX inconsistente. A atribuição por query é defensável apenas quando esses efeitos são negligenciáveis e a análise modela observações repetidas.
  • Métricas primárias, guardrails e exploratórias: faça pré-registo. Escolha a medida primária a partir do resultado do produto; proxies de satisfação incluem thumbs, regenerations e dwell. Trate latência e custo como guardrails quando condicionam a experiência.
  • Tamanho da amostra: faça power analysis antes do lançamento, a partir do efeito mínimo que vale a pena detetar, da variância baseline, da unidade de atribuição e da stopping rule.

Parte 9: Construção do conjunto de testes

Uma métrica só é tão boa quanto o test set em que é executada. Se o seu golden set cobre três intents e o tráfego de produção abrange doze, Recall@10 mede apenas essas três intents. Pior ainda, um test set overfit a perguntas fáceis (“Qual é a política de reembolsos da empresa?”) pode aprovar um sistema que falha nas difíceis (“Elegibilidade para reembolso de um cancelamento parcial ao abrigo do EU Digital Services Act de 2023, faturado em EUR e com origem na Irlanda?”). O score agregado sobe enquanto o sistema continua a falhar numa parte importante do tráfego de produção.

O mesmo problema afeta o ground truth. Se os SMEs etiquetaram os documentos óbvios, mas falharam os documentos relevantes da long tail, o Recall@k irá penalizar um retriever que, na realidade, os encontrou. Otimiza para as labels, não para a verdade.

Construa primeiro o test set em torno da distribuição real e da dificuldade das queries. Depois escolha métricas que respondam aos modos de falha-alvo e faça tuning do sistema com base nelas.

Geração sintética de queries

Use um LLM para gerar perguntas a partir do seu corpus:

  • Por chunk: “Gere 3 perguntas que um utilizador poderia fazer e às quais este chunk responde.”
  • Multi-hop: selecione dois chunks e gere uma pergunta que exija ambos.
  • Adversarial: gere perguntas com entidades distractoras, formulações quase duplicadas e menções ambíguas.

O RAGAS tem uma distribuição integrada por tipo de pergunta (reasoning, conditional, multi-context). O DataMorgana gera benchmarks sintéticos configuráveis entre categorias de utilizador e de pergunta. Os dados sintéticos são úteis para cold starts e testes de cobertura. Não substituem queries reais dos utilizadores.

Construção do golden dataset

Dados selecionados por humanos ancoram o golden set.

  1. Faça sampling de queries reais dos utilizadores (ou simuladas, se o produto ainda não foi lançado), estratificadas por intent.
  2. Peça a SMEs que respondam a cada pergunta e identifiquem que documento(s) contêm a resposta.
  3. Dimensione o conjunto a partir da matriz de cobertura e do intervalo de confiança necessário para decisões de release; a cobertura é mais importante do que um número de queries copiado de outro contexto.
  4. Volte a fazer curation quando a cadência de releases, os sinais de drift, o risco do domínio e a capacidade de anotação o justificarem.

Test sets adversariais

  • Counterfactuals: troque entidades-chave na query. O sistema recupera os chunks certos para a query alterada?
  • Distractors: queries em que o corpus contém uma resposta plausível mas errada que não deveria ser recuperada. É isto que o RGB (Chen et al., AAAI 2024) submete a stress test: robustez ao ruído, rejeição de negativos, integração de informação e robustez contrafactual.
  • Negação e quantifiers: queries com “não”, “exceto” e “apenas”. Os dense retrievers têm frequentemente dificuldades com estas construções.
  • Fora do âmbito: queries sem resposta no corpus. O sistema deve dizer “Não sei”, não fazer hallucination. O NoMIRACL cobre este caso. Avalie explicitamente a abstention nos tipos de queries de produção.

Cobertura e avaliação contínua

  • Construa uma matriz de cobertura: query intent × document type × ontology branch. Procure ter ≥1 query por célula. Células vazias são regiões não monitorizadas onde as regressões se escondem.
  • Execute um subset de regressão limitado e rápido em cada PR e a suite completa segundo um calendário mais lento.
  • Agende a avaliação do golden set completo a partir da cadência de releases e do custo da avaliação; os release candidates são gates naturais.
  • Agende a avaliação de drift a partir do volume de tráfego, da alteração esperada e do risco. Use uma amostra rolling de produção e estratifique por feedback, em vez de alterar silenciosamente a distribuição-alvo.

Parte 10: Monitorização em produção

A suite de avaliação que coloca em produção descreve o sistema no lançamento. O tráfego de produção muda depois disso.

Feedback implícito e explícito

  • Click-through / open rate nas fontes citadas (se a UI as expuser).
  • Dwell time na resposta.
  • Taxa de regeneration: percentagem de respostas que o utilizador volta a perguntar ou pede ao sistema para refazer. Trate-a como um sinal de insatisfação e calibre-a contra conversas revistas.
  • Taxas de copy / share / export — sinais positivos fortes.
  • Padrões de follow-up: padrões como “Tem a certeza?” ou “Mas e X?” sugerem desconfiança.
  • Thumbs up/down com categorias de motivo opcionais (errado, incompleto, fora do tópico, prejudicial, lento). As edições inline, quando permitidas pela UI, geralmente contêm mais informação do que qualquer outro sinal de feedback.

Deteção de drift

  • Drift de queries: acompanhe a distribuição dos embeddings das queries face a uma janela de referência usando divergência KL, MMD ou um detetor baseado em modelo. Gere um alerta quando houver shift e faça debugging segmentado.
  • Drift de embeddings: fixe um conjunto de probes de documentos; periodicamente volte a fazer embedding e meça o cosine face aos embeddings originais. Mesmo um drift pequeno entre versões do modelo do provider pode quebrar silenciosamente o retrieval. Armazenamento versionado de embeddings (snapshots imutáveis por versão) é a mitigação mais barata.
  • Drift de performance: acompanhe métricas equivalentes às de produção (taxa de regeneration por intent) ao longo do tempo. Subidas súbitas significam que algo avariou; drifts lentos significam que o mundo mudou.

Shadow evaluation e human-in-the-loop

Execute o sistema candidato em paralelo com a produção, compare os outputs offline e não os sirva aos utilizadores. Isto deteta regressões antes do lançamento. Tem custo adicional de inference, mas não tem impacto nos clientes.

Para revisão human-in-the-loop (HITL):

  • Envie outputs de baixa confiança para uma review queue.
  • Inclua uma amostra aleatória do tráfego de produção para blind review; defina a taxa a partir do volume de tráfego, do risco e da capacidade dos reviewers.
  • Dê maior peso aos outputs com thumbs-down.
  • Use os outputs revistos para ampliar o golden set.

O conjunto mínimo de guardrails

Gere alertas para estes sinais, por ordem de prioridade:

  1. Score de Faithfulness/HHEM abaixo do threshold numa amostra rolling de produção.
  2. Latência p95 acima do SLO.
  3. Taxa de false-exclusion de filters acima do threshold (baseada em amostra).
  4. Taxa de regeneration fora de uma control band calibrada localmente que tenha em conta o tamanho da janela, o tráfego, a sazonalidade e o orçamento de falsos alertas.
  5. Custo/query acima do budget.

Se um alerta disparar sem uma alteração correspondente de código ou modelo, é provável que tenha drift. Se disparar depois de uma alteração, é provável que tenha uma regressão. Em qualquer caso, obtém um sinal antes de chegarem tickets ao suporte.


Ressalvas

  • Os objetivos são locais, não universais. Qualquer número identificado como ilustrativo neste guia é uma configuração de exemplo ou um resultado trabalhado, não um threshold de release. Calibre os thresholds ao seu domínio, ao risco, à incerteza do eval set e às expectativas dos utilizadores.
  • O espaço de frameworks evolui rapidamente. Versões do HHEM, nomes das métricas do RAGAS, model cards e ordem dos leaderboards podem mudar depois da publicação. Volte a verificar as fontes ligadas e faça novo benchmark antes de se comprometer.
  • Os números de agreement de LLM-as-judge têm asteriscos. O valor de 80% entre GPT-4 e humanos provém das condições do MT-Bench / Chatbot Arena. Em domínios de nicho e casos adversariais, o agreement diminui acentuadamente. Use judges como force multiplier, não como substituto de spot-checking.
  • Os uplifts de benchmarks de vendors nem sempre são reproduzíveis de forma independente. Reproduza-os nos seus próprios dados antes de acreditar num número, sobretudo para rerankers e sistemas de OCR mais recentes.
  • Nenhuma métrica substitui a observação dos outputs. Agende blind review de uma amostra aleatória de produção segundo o tráfego, o risco e a capacidade dos reviewers. As métricas escalam esse hábito; não o substituem.

Próximos artigos desta série

Este foi o índice. Os follow-ups que estou a planear:

  • Soft Boosts vs. Hard Filters: análise aprofundada da taxa de false-exclusion de filters, com código, exemplos reais de produção e um decision framework.
  • Chunking Is the Hidden Variable: uma experiência controlada com chunking recursivo, semântico, late e estrutural em três corpora.
  • Reranker Selection in 2026: BGE vs. Cohere vs. ZeRank vs. modelos cross-encoder atuais, comparados head-to-head em custo, latência e uplift.
  • Ontology-Grounded RAG: An End-to-End Walkthrough: construção do harness de avaliação completo para um sistema de retrieval baseado em entidades.
  • LLM-as-Judge Without the Self-Preference Trap: receitas práticas para avaliação automática sem bias.
  • Online Evaluation in Production: padrões de instrumentação, políticas de alertas e os dashboards que detetam regressões reais.

Referências

Frameworks e benchmarks

Retrieval e ranking

Generation, faithfulness e judges

Drift e produção

Código complementar

  • slavadubrov/rag-evals-demo — harness executável para todas as métricas deste artigo no corpus SciFact, além de um benchmark sweep de chunking × embedding × LLM. Notebooks 00–09, unit tests que fixam os exemplos trabalhados acima e um índice Qdrant embedded, para executar sem Docker.