Phase 11 - Lesson 07

RAG Avançado (Chunking, Reranking, Busca Híbrida)

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

O RAG básico recupera os top-k chunks mais semelhantes. Isso funciona para perguntas simples. No entanto, falha para raciocínio multi-hop (múltiplas etapas), consultas ambíguas e grandes corpora. O RAG avançado é a diferença entre uma demonstração que funciona em 10 documentos e um sistema que funciona em 10 milhões.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 11, Lição 06 (RAG) Tempo: ~90 minutos Relacionado: A Fase 5 · 23 (Estratégias de Chunking para RAG) cobre todos os seis algoritmos de chunking — recursivo, semântico, sentença, documento-pai, late chunking, recuperação contextual — com benchmarks da Vectara/Anthropic. Esta lição constrói sobre essa base: busca híbrida, reranking, transformação de consultas.

Objetivos de Aprendizado

  • Implementar estratégias avançadas de chunking (semântico, recursivo, pai-filho) que preservem a estrutura e o contexto do documento
  • Construir um pipeline de busca híbrida combinando a correspondência de palavras-chave do BM25 com busca vetorial semântica e um reranker cross-encoder
  • Aplicar técnicas de transformação de consultas (HyDE, multi-query, step-back) para melhorar a recuperação em perguntas ambíguas ou complexas
  • Diagnosticar e corrigir falhas comuns de RAG: chunk incorreto recuperado, resposta ausente no contexto, quebra no raciocínio multi-hop

O Problema

Você construiu um pipeline RAG básico na Lição 06. Ele funciona para perguntas diretas em um corpus pequeno. Agora tente estes casos:

Consulta ambígua: "What was revenue last quarter?" A busca semântica retorna chunks sobre estratégia de receita, projeções de receita e os pensamentos do CFO sobre o crescimento da receita. Todos semanticamente semelhantes à palavra "receita". Nenhum contendo o número real. O chunk correto diz "$47.2M em Q3 2025", mas usa a palavra "lucros" (earnings) em vez de "receita". O modelo de embedding acha que "estratégia de receita" está mais próximo da consulta do que "os lucros do Q3 foram de $47.2M".

Pergunta multi-hop: "Which team had the highest customer satisfaction score improvement?" Isso requer encontrar as pontuações de satisfação de cada equipe, compará-las e identificar a máxima. Nenhum chunk sozinho contém a resposta. As informações estão espalhadas pelos relatórios das equipes.

Problema de corpus grande: Você tem 2 milhões de chunks. A resposta correta está no chunk nº 1.847.293. Sua recuperação top-5 traz os chunks nº 14, nº 89.201, nº 1.200.000, nº 44 e nº 901.333. Próximos no espaço de embedding, mas nenhum contém a resposta. Nessa escala, a busca de vizinhos mais próximos aproximados (ANN) introduz erro suficiente para que os resultados relevantes sejam empurrados para fora do top-k.

O RAG básico falha porque a similaridade vetorial não é o mesmo que relevância. Um chunk pode ser semanticamente semelhante a uma consulta sem ser útil para respondê-la. O RAG avançado resolve isso com quatro técnicas: busca híbrida (adiciona correspondência de palavras-chave), reranking (pontua os candidatos com mais critério), transformação de consultas (corrige a consulta antes da busca) e melhor chunking (recupera na granularidade certa).

O Conceito

Busca Híbrida: Semântica + Palavra-chave

A busca semântica (similaridade vetorial) é boa para entender o significado. "How do I cancel my subscription?" corresponde a "Passos para encerrar seu plano", mesmo que não compartilhem nenhuma palavra. Mas ela perde correspondências exatas. O "código de erro E-4021" pode não corresponder a um chunk contendo "E-4021" se o modelo de embedding tratá-lo como ruído.

A busca por palavra-chave (BM25) é o oposto. Ela se destaca em correspondências exatas. "E-4021" corresponde perfeitamente. Mas "cancelar minha assinatura" retorna zero resultados se o documento disser "encerrar seu plano".

A busca híbrida executa ambas e depois combina os resultados.

O BM25 (Best Matching 25) é o algoritmo padrão de busca por palavra-chave. Tem sido a espinha dorsal dos motores de busca desde os anos 1990. A fórmula:

BM25(q, d) = sum over terms t in q:
    IDF(t) * (tf(t,d) * (k1 + 1)) / (tf(t,d) + k1 * (1 - b + b * |d| / avgdl))

Onde tf(t,d) é a frequência do termo t no documento d, IDF(t) é a frequência inversa do documento, |d| é o comprimento do documento, avgdl é o comprimento médio do documento, k1 controla a saturação da frequência do termo (padrão 1.2) e b controla a normalização de comprimento (padrão 0.75).

Em termos simples: o BM25 pontua os documentos mais alto quando eles contêm termos da consulta (especialmente os raros), mas com retornos decrescentes para termos repetidos. Um documento com a palavra "receita" 50 vezes não é 50 vezes mais relevante do que um que a contém apenas uma vez.

Fusão de Ranks Recíprocos (RRF)

Você tem duas listas ranqueadas: uma da busca vetorial e outra do BM25. Como você as combina? A Fusão de Ranks Recíprocos (Reciprocal Rank Fusion) é a abordagem padrão.

RRF_score(d) = sum over rankings R:
    1 / (k + rank_R(d))

Onde k é uma constante (geralmente 60) que impede que o resultado melhor posicionado domine.

Um documento ranqueado em 1º lugar na busca vetorial e em 5º lugar no BM25 recebe: 1/(60+1) + 1/(60+5) = 0,0164 + 0,0154 = 0,0318

Um documento ranqueado em 3º lugar na busca vetorial e em 2º lugar no BM25 recebe: 1/(60+3) + 1/(60+2) = 0,0159 + 0,0161 = 0,0320

O RRF equilibra naturalmente os dois sinais. Um documento que ranqueia bem em ambas as listas obtém a melhor pontuação. Um documento que ranqueia em 1º lugar em uma lista, mas está ausente na outra, recebe uma pontuação moderada. Isso é robusto porque utiliza posições (ranks), não as pontuações brutas (raw scores), fazendo com que as diferenças nas distribuições de pontuação entre os dois sistemas não importem.

Reranking

A recuperação (seja vetorial, por palavra-chave ou híbrida) é rápida, mas imprecisa. Ela usa bi-encoders: a consulta e cada documento são embutidos (embedded) de forma independente e depois comparados. Os embeddings são computados uma vez e armazenados em cache. Isso escala para milhões de documentos.

O reranking usa cross-encoders: a consulta e um documento candidato são alimentados juntos em um modelo que gera uma pontuação de relevância. O modelo analisa ambos os textos simultaneamente e consegue capturar interações detalhadas entre eles. Um cross-encoder consegue entender que "Quais foram os lucros do Q3?" é altamente relevante para um chunk contendo "$47.2M no Q3", mesmo que um bi-encoder tenha perdido essa conexão.

O trade-off: os cross-encoders são de 100 a 1000 vezes mais lentos que os bi-encoders porque processam o par consulta-documento de forma conjunta. Não é possível pré-computar pontuações de cross-encoder para um milhão de documentos. A solução: recuperar um conjunto maior de candidatos (top-50 da busca híbrida) e depois reranquear com um cross-encoder para obter o top-5 final.

graph LR
    Q["Consulta"] --> H["Busca Híbrida"]
    H --> C50["Top 50 candidatos"]
    C50 --> RR["Reranker Cross-Encoder"]
    RR --> C5["Top 5 resultados finais"]
    C5 --> P["Construir prompt"]
    P --> LLM["Gerar resposta"]

Modelos comuns de reranking (portfólio de 2026):

  • Cohere Rerank 3.5: API gerenciada, multilíngue, melhor ganho de recall em corpora mistos
  • Voyage rerank-2.5: API gerenciada, menor latência entre as opções hospedadas
  • Jina-Reranker-v2 Multilingual: pesos abertos (open-weight), mais de 100 idiomas
  • bge-reranker-v2-m3: pesos abertos, linha de base (baseline) sólida
  • cross-encoder/ms-marco-MiniLM-L-6-v2: pesos abertos, roda em CPU para prototipagem
  • ColBERTv2 / Jina-ColBERT-v2: rerankers multi-vetor de interação tardia (late-interaction) — O(tokens) e não O(docs) no momento da pontuação

Transformação de Consultas

Às vezes, o problema não é a recuperação, mas a própria consulta. "Qual era aquela coisa sobre a nova mudança de política?" é uma péssima consulta de busca. Não contém termos específicos. O embedding é vago. Nenhum sistema de recuperação consegue encontrar os documentos certos a partir disso.

Reescrita de consulta: reformular a consulta do usuário em uma consulta de busca melhor. Um LLM pode fazer isso:

User: "What was that thing about the new policy change?"
Rewritten: "Recent policy changes and updates"

HyDE (Hypothetical Document Embeddings): em vez de buscar com a consulta, gera-se uma resposta hipotética, faz-se o embedding dela e busca-se por documentos reais semelhantes.

Query: "What is the refund policy for enterprise?"
Hypothetical answer: "Enterprise customers are eligible for a full refund
within 60 days of purchase. Refunds are pro-rated based on the remaining
subscription period and processed within 5-7 business days."

Faça o embedding da resposta hipotética e busque por documentos reais semelhantes a ela. A intuição: a resposta hipotética vive mais próxima no espaço de embedding da resposta real do que a pergunta original. Perguntas e respostas têm estruturas linguísticas diferentes. Ao gerar uma resposta hipotética, você faz a ponte entre o "espaço da pergunta" e o "espaço da resposta" no embedding.

O HyDE adiciona uma chamada de LLM antes da recuperação. Isso aumenta a latência em 500-2000ms. Vale a pena quando a qualidade da recuperação é ruim em consultas brutas.

Fatiamento Pai-Filho (Parent-Child Chunking)

O chunking padrão força um trade-off: chunks pequenos para recuperação precisa, chunks grandes para contexto suficiente. O chunking pai-filho elimina esse trade-off.

Indexe chunks pequenos (128 tokens) para recuperação. Quando um chunk pequeno for recuperado, retorne seu chunk pai (512 tokens) para o prompt. O chunk pequeno corresponde à consulta com precisão. O chunk pai fornece contexto suficiente para o LLM gerar uma boa resposta.

graph TD
    P["Chunk pai (512 tokens)<br/>Seção completa sobre a política de reembolso"]
    C1["Chunk filho (128 tokens)<br/>Plano padrão: reembolso de 30 dias"]
    C2["Chunk filho (128 tokens)<br/>Enterprise: 60 dias pro-rata"]
    C3["Chunk filho (128 tokens)<br/>Tempo de processamento: 5-7 dias"]
    C4["Chunk filho (128 tokens)<br/>Como enviar uma solicitação"]

    P --> C1
    P --> C2
    P --> C3
    P --> C4

    Q["Consulta: reembolso enterprise?"] -.->|"corresponde ao filho"| C2
    C2 -.->|"retorna o pai"| P

O termo de busca "reembolso enterprise?" corresponde exatamente ao chunk filho C2. Mas o prompt recebe o chunk pai P completo, que inclui o contexto ao redor sobre o tempo de processamento e o processo de envio.

Filtragem por Metadados

Antes de executar a busca vetorial, filtre o corpus por metadados: data, fonte, categoria, autor, idioma. Isso reduz o espaço de busca e evita resultados irrelevantes.

"O que mudou na política de segurança no mês passado?" deve buscar apenas documentos dos últimos 30 dias na categoria de segurança. Sem a filtragem por metadados, você busca em todo o corpus e pode recuperar um documento de segurança de 2 anos atrás que por acaso é semanticamente semelhante.

Sistemas RAG em produção armazenam metadados ao lado de cada chunk: documento de origem, data de criação, categoria, autor, versão. Os bancos de dados vetoriais suportam pré-filtragem por metadados antes da busca por segurança, o que é crítico para o desempenho em escala.

Avaliação

Você construiu um sistema RAG. Como saber se ele funciona? Três métricas:

Relevância da recuperação (Recall@k): para um conjunto de perguntas de teste com documentos relevantes conhecidos, qual porcentagem de documentos relevantes aparece nos top-k resultados? Se a resposta para uma pergunta está no chunk nº 47, o chunk nº 47 aparece no top-5?

Fidelidade (Faithfulness): a resposta gerada é fundamentada nos documentos recuperados? Se os chunks recuperados dizem "janela de reembolso de 60 dias" e o modelo diz "janela de reembolso de 90 dias", isso é uma falha de fidelidade. O modelo alucinou apesar de ter o contexto correto.

Correção da resposta (Answer correctness): a resposta gerada corresponde à resposta esperada? Esta é a métrica ponta a ponta. Ela combina a qualidade da recuperação e a qualidade da geração.

Uma verificação simples de fidelidade: pegue cada afirmação na resposta gerada e verifique se ela aparece (em substância) nos chunks recuperados. Se a resposta contiver um fato que não está em nenhum chunk recuperado, provavelmente foi alucinado.

graph TD
    subgraph "Framework de Avaliação"
        Q["Perguntas de teste<br/>+ respostas esperadas<br/>+ IDs de docs relevantes"]
        Q --> Ret["Avaliação de recuperação<br/>Recall@k: os docs corretos<br/>foram recuperados?"]
        Q --> Faith["Avaliação de fidelidade<br/>A resposta é fundamentada<br/>nos docs recuperados?"]
        Q --> Correct["Avaliação de correção<br/>A resposta corresponde<br/>à resposta esperada?"]
    end

Mão na Massa

Passo 1: Implementação do BM25

import math
from collections import Counter

class BM25:
    def __init__(self, k1=1.2, b=0.75):
        self.k1 = k1
        self.b = b
        self.docs = []
        self.doc_lengths = []
        self.avg_dl = 0
        self.doc_freqs = {}
        self.n_docs = 0

    def index(self, documents):
        self.docs = documents
        self.n_docs = len(documents)
        self.doc_lengths = []
        self.doc_freqs = {}

        for doc in documents:
            words = doc.lower().split()
            self.doc_lengths.append(len(words))
            unique_words = set(words)
            for word in unique_words:
                self.doc_freqs[word] = self.doc_freqs.get(word, 0) + 1

        self.avg_dl = sum(self.doc_lengths) / self.n_docs if self.n_docs else 1

    def score(self, query, doc_idx):
        query_words = query.lower().split()
        doc_words = self.docs[doc_idx].lower().split()
        doc_len = self.doc_lengths[doc_idx]
        word_counts = Counter(doc_words)
        score = 0.0

        for term in query_words:
            if term not in word_counts:
                continue
            tf = word_counts[term]
            df = self.doc_freqs.get(term, 0)
            idf = math.log((self.n_docs - df + 0.5) / (df + 0.5) + 1)
            numerator = tf * (self.k1 + 1)
            denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avg_dl)
            score += idf * numerator / denominator

        return score

    def search(self, query, top_k=10):
        scores = [(i, self.score(query, i)) for i in range(self.n_docs)]
        scores.sort(key=lambda x: x[1], reverse=True)
        return scores[:top_k]

Passo 2: Fusão de Ranks Recíprocos

def reciprocal_rank_fusion(ranked_lists, k=60):
    scores = {}
    for ranked_list in ranked_lists:
        for rank, (doc_id, _) in enumerate(ranked_list):
            if doc_id not in scores:
                scores[doc_id] = 0.0
            scores[doc_id] += 1.0 / (k + rank + 1)
    fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    return fused

Passo 3: Pipeline de Busca Híbrida

def hybrid_search(query, chunks, vector_embeddings, vocab, idf, bm25_index, top_k=5, fusion_k=60):
    query_emb = tfidf_embed(query, vocab, idf)
    vector_results = search(query_emb, vector_embeddings, top_k=top_k * 3)
    bm25_results = bm25_index.search(query, top_k=top_k * 3)
    fused = reciprocal_rank_fusion([vector_results, bm25_results], k=fusion_k)
    return fused[:top_k]

Passo 4: Reranker Simples

Em produção, você usaria um modelo cross-encoder. Aqui, construímos um reranker que pontua a relevância consulta-documento usando sobreposição de palavras, importância de termos e correspondência de frases.

def rerank(query, candidates, chunks):
    query_words = set(query.lower().split())
    stop_words = {"the", "a", "an", "is", "are", "was", "were", "what", "how",
                  "why", "when", "where", "do", "does", "for", "of", "in", "to",
                  "and", "or", "on", "at", "by", "it", "its", "this", "that",
                  "with", "from", "be", "has", "have", "had", "not", "but"}
    query_terms = query_words - stop_words

    scored = []
    for doc_id, initial_score in candidates:
        chunk = chunks[doc_id].lower()
        chunk_words = set(chunk.split())

        term_overlap = len(query_terms & chunk_words)

        query_bigrams = set()
        q_list = [w for w in query.lower().split() if w not in stop_words]
        for i in range(len(q_list) - 1):
            query_bigrams.add(q_list[i] + " " + q_list[i + 1])
        bigram_matches = sum(1 for bg in query_bigrams if bg in chunk)

        position_boost = 0
        for term in query_terms:
            pos = chunk.find(term)
            if pos != -1 and pos < len(chunk) // 3:
                position_boost += 0.5

        rerank_score = (
            term_overlap * 1.0
            + bigram_matches * 2.0
            + position_boost
            + initial_score * 5.0
        )
        scored.append((doc_id, rerank_score))

    scored.sort(key=lambda x: x[1], reverse=True)
    return scored

Passo 5: HyDE (Hypothetical Document Embeddings)

def hyde_generate_hypothesis(query):
    templates = {
        "what": "The answer to '{query}' is as follows: Based on our documentation, {topic} involves specific policies and procedures that define how the process works.",
        "how": "To address '{query}': The process involves several steps. First, you need to initiate the request. Then, the system processes it according to the defined rules.",
        "default": "Regarding '{query}': Our records indicate specific details and policies related to this topic that provide a comprehensive answer."
    }
    query_lower = query.lower()
    if query_lower.startswith("what"):
        template = templates["what"]
    elif query_lower.startswith("how"):
        template = templates["how"]
    else:
        template = templates["default"]

    topic_words = [w for w in query.lower().split()
                   if w not in {"what", "is", "the", "how", "do", "does", "a", "an",
                                "for", "of", "to", "in", "on", "at", "by", "and", "or"}]
    topic = " ".join(topic_words) if topic_words else "this topic"

    return template.format(query=query, topic=topic)


def hyde_search(query, chunks, vector_embeddings, vocab, idf, top_k=5):
    hypothesis = hyde_generate_hypothesis(query)
    hypothesis_emb = tfidf_embed(hypothesis, vocab, idf)
    results = search(hypothesis_emb, vector_embeddings, top_k)
    return results, hypothesis

Passo 6: Fatiamento Pai-Filho

def create_parent_child_chunks(text, parent_size=200, child_size=50):
    words = text.split()
    parents = []
    children = []
    child_to_parent = {}

    parent_idx = 0
    start = 0
    while start < len(words):
        parent_end = min(start + parent_size, len(words))
        parent_text = " ".join(words[start:parent_end])
        parents.append(parent_text)

        child_start = start
        while child_start < parent_end:
            child_end = min(child_start + child_size, parent_end)
            child_text = " ".join(words[child_start:child_end])
            child_idx = len(children)
            children.append(child_text)
            child_to_parent[child_idx] = parent_idx
            child_start += child_size

        parent_idx += 1
        start += parent_size

    return parents, children, child_to_parent

Passo 7: Avaliação de Fidelidade

def evaluate_faithfulness(answer, retrieved_chunks):
    answer_sentences = [s.strip() for s in answer.split(".") if len(s.strip()) > 10]
    if not answer_sentences:
        return 1.0, []

    grounded = 0
    ungrounded = []
    context = " ".join(retrieved_chunks).lower()

    for sentence in answer_sentences:
        words = set(sentence.lower().split())
        stop_words = {"the", "a", "an", "is", "are", "was", "were", "and", "or",
                      "to", "of", "in", "for", "on", "at", "by", "it", "this", "that"}
        content_words = words - stop_words
        if not content_words:
            grounded += 1
            continue

        matched = sum(1 for w in content_words if w in context)
        ratio = matched / len(content_words) if content_words else 0

        if ratio >= 0.5:
            grounded += 1
        else:
            ungrounded.append(sentence)

    score = grounded / len(answer_sentences) if answer_sentences else 1.0
    return score, ungrounded


def evaluate_retrieval_recall(queries_with_relevant, retrieval_fn, k=5):
    total_recall = 0.0
    results = []

    for query, relevant_indices in queries_with_relevant:
        retrieved = retrieval_fn(query, k)
        retrieved_indices = set(idx for idx, _ in retrieved)
        relevant_set = set(relevant_indices)
        hits = len(retrieved_indices & relevant_set)
        recall = hits / len(relevant_set) if relevant_set else 1.0
        total_recall += recall
        results.append({
            "query": query,
            "recall": recall,
            "hits": hits,
            "total_relevant": len(relevant_set)
        })

    avg_recall = total_recall / len(queries_with_relevant) if queries_with_relevant else 0
    return avg_recall, results

Como Usar

Com um cross-encoder real para reranking:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank_with_cross_encoder(query, candidates, chunks, top_k=5):
    pairs = [(query, chunks[doc_id]) for doc_id, _ in candidates]
    scores = reranker.predict(pairs)
    scored = list(zip([doc_id for doc_id, _ in candidates], scores))
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored[:top_k]

Com o reranker gerenciado da Cohere:

import cohere

co = cohere.Client()

def rerank_with_cohere(query, candidates, chunks, top_k=5):
    docs = [chunks[doc_id] for doc_id, _ in candidates]
    response = co.rerank(
        model="rerank-english-v3.0",
        query=query,
        documents=docs,
        top_n=top_k
    )
    return [(candidates[r.index][0], r.relevance_score) for r in response.results]

Para HyDE com um LLM real:

import anthropic

client = anthropic.Anthropic()

def hyde_with_llm(query):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=256,
        messages=[{
            "role": "user",
            "content": f"Write a short paragraph that would be a good answer to this question. Do not say you don't know. Just write what the answer would look like.\n\nQuestion: {query}"
        }]
    )
    return response.content[0].text

Para busca híbrida em produção com Weaviate:

import weaviate

client = weaviate.connect_to_local()

collection = client.collections.get("Documents")
response = collection.query.hybrid(
    query="enterprise refund policy",
    alpha=0.5,
    limit=10
)

O parâmetro alpha controla o equilíbrio: 0.0 = palavra-chave pura (BM25), 1.0 = vetor puro, 0.5 = peso igual. A maioria dos sistemas em produção usa alpha entre 0.3 e 0.7.

Entregáveis

Esta lição produz:

  • outputs/prompt-advanced-rag-debugger.md -- um prompt para diagnosticar e corrigir problemas de qualidade de RAG
  • outputs/skill-advanced-rag.md -- uma skill para construir RAG de nível de produção com busca híbrida e reranking

Exercícios

  1. Compare BM25 vs busca vetorial vs busca híbrida nos documentos de exemplo. Para cada uma das 5 consultas de teste, registre qual abordagem retorna o chunk mais relevante na posição nº 1. A busca híbrida deve vencer em pelo menos 3 de 5.

  2. Implemente um filtro de metadados. Adicione um campo "category" (categoria) a cada documento (security, billing, api, product). Antes de executar a busca vetorial, filtre os chunks apenas para a categoria relevante. Teste com "What encryption is used?" e verifique se ele pesquisa apenas chunks da categoria security.

  3. Construa um pipeline HyDE completo usando a função simples de geração da Lição 06. Compare a qualidade de recuperação (relevância top-3) entre a busca direta por consulta e a busca HyDE em todas as 5 consultas de teste. O HyDE deve melhorar os resultados para consultas vagas.

  4. Implemente a estratégia de chunking pai-filho nos documentos de exemplo. Use child_size=30 e parent_size=100. Busque com chunks filhos, mas retorne os chunks pais no prompt. Compare as respostas geradas com o chunking padrão com chunk_size=50.

  5. Crie um conjunto de dados de avaliação: 10 perguntas com chunks de respostas conhecidos. Meça Recall@3, Recall@5 e Recall@10 para (a) apenas busca vetorial, (b) apenas BM25, (c) busca híbrida, (d) híbrida + reranking. Plote os resultados e identifique onde o reranking ajuda mais.

Termos-Chave

Termo Lo que diz a gente O que realmente significa
BM25 "Busca por palavra-chave" Um algoritmo probabilístico de ranqueamento que pontua documentos com base na frequência do termo, frequência inversa do documento e normalização do comprimento do documento
Busca híbrida "O melhor dos dois mundos" Execução paralela de busca semântica (vetorial) e por palavra-chave (BM25), fundindo os resultados com fusão de ranks
Fusão de Ranks Recíprocos "Mesclar listas ranqueadas" Combinação de múltiplas listas ranqueadas somando 1/(k + rank) para cada documento em todas as listas
Reranking "Pontuação de segunda passagem" Uso de um modelo cross-encoder mais caro para repontuar um conjunto de candidatos da recuperação inicial
Cross-encoder "Modelo conjunto de consulta-documento" Um modelo que recebe uma consulta e um documento como entrada única, gerando uma pontuação de relevância; mais preciso que bi-encoders, mas lento demais para busca em todo o corpus
Bi-encoder "Modelo de embedding independente" Um modelo que gera embeddings de consultas e documentos de forma independente; rápido porque os embeddings são pré-computados, mas menos preciso que cross-encoders
HyDE "Buscar com uma resposta falsa" Geração de uma resposta hipotética para a consulta, geração de seu embedding e busca por documentos reais semelhantes a ela
Chunking pai-filho "Busca pequena, contexto grande" Indexação de chunks pequenos para recuperação precisa, mas retorno do chunk pai maior para fornecer contexto suficiente
Filtragem por metadados "Estreitar antes de buscar" Filtragem de documentos por atributos (data, fonte, categoria) antes de executar a busca vetorial para reduzir o espaço de busca
Fidelidade "Permaneceu fundamentado" Se a resposta gerada é apoiada pelos documentos recuperados, em oposição a ser alucinada a partir dos dados de treinamento do modelo

Leituras Adicionais

  • Robertson & Zaragoza, "The Probabilistic Relevance Framework: BM25 and Beyond" (2009) -- a referência definitiva para BM25, explicando os fundamentos probabilísticos por trás da fórmula
  • Cormack et al., "Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods" (2009) -- o artigo original sobre RRF mostrando que ele supera métodos de fusão mais complexos
  • Gao et al., "Precise Zero-Shot Dense Retrieval without Relevance Labels" (2022) -- o artigo sobre HyDE demonstrando que embeddings de documentos hipotéticos melhoram a recuperação sem nenhum dado de treinamento
  • Nogueira & Cho, "Passage Re-ranking with BERT" (2019) -- mostrou que o reranking com cross-encoders sobre o BM25 melhora significativamente a qualidade da recuperação
  • Khattab et al., "DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines" (2023) -- trata a construção de prompts e a seleção de pesos como um problema de otimização sobre pipelines de recuperação; leia isto para "programar LLMs" em vez de "criar prompts para LLMs".
  • Edge et al., "From Local to Global: A Graph RAG Approach to Query-Focused Summarization" (Microsoft Research 2024) -- artigo sobre GraphRAG: extração de entidade-relação + detecção de comunidade Leiden para sumarização focada em consultas; a distinção entre recuperação global vs local.
  • Asai et al., "Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection" (ICLR 2024) -- RAG autoavaliativo com tokens de reflexão; a fronteira dos agentes além do modelo estático recuperar-depois-gerar.
  • LangChain Query Construction blog -- como traduzir consultas de linguagem natural em consultas estruturadas de banco de dados (Text-to-SQL, Cypher) como uma etapa prévia de recuperação.
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).