Aula 1, Pipeline de RAG

Esta aula abre o módulo de RAG, a técnica que dá ao LLM uma memória externa de documentos. Em vez de confiar só no que o modelo aprendeu, recuperamos trechos relevantes e os entregamos a ele para responder. Vamos montar um pipeline de RAG mínimo, de ponta a ponta.

Os módulos anteriores nos deram um LLM e a habilidade de conversar com ele por prompts. Mas um LLM tem dois problemas sérios para um assistente educacional. Primeiro, ele só sabe o que viu no treino, e pode estar desatualizado ou não conhecer o material específico da sua disciplina. Segundo, ele às vezes inventa, produzindo respostas que soam confiantes mas estão erradas, o que chamamos de alucinação.

O RAG, Retrieval-Augmented Generation, ataca os dois problemas. A ideia, formalizada por Lewis e colegas, é simples e poderosa, antes de responder, buscamos em uma base de documentos os trechos mais relevantes para a pergunta, e os fornecemos ao modelo junto com o pedido. Assim, o modelo responde com base no material certo, não só na memória, reduzindo alucinações e trazendo conhecimento atualizado e privado. Este é o primeiro módulo com um projeto de assistente completo, e nesta aula montamos o esqueleto dele.


Objetivos

Ao final desta aula, você deve ser capaz de:

  • Explicar o que é RAG e quais problemas do LLM ele resolve.
  • Descrever as duas fases do pipeline, indexação e consulta.
  • Implementar um RAG mínimo de ponta a ponta.
  • Reconhecer o papel de cada etapa, do trecho recuperado à resposta gerada.

Teoria

Um sistema de RAG tem duas fases. A primeira é a indexação, feita uma vez sobre os documentos. Quebramos cada documento em pedaços, os chunks, transformamos cada pedaço em um vetor por meio de um modelo de embeddings, e guardamos esses vetores em uma base, em geral um banco vetorial. A segunda é a consulta, feita a cada pergunta. Transformamos a pergunta em um vetor, recuperamos os pedaços de vetor mais parecido, montamos um prompt que junta esses pedaços à pergunta, e pedimos ao LLM que responda com base neles.

flowchart LR
    D[Documentos] --> C[Quebra em pedaços]
    C --> E[Embeddings]
    E --> B[(Base de vetores)]
    Q[Pergunta] --> QE[Embedding da pergunta]
    QE --> R[Recupera pedaços relevantes]
    B --> R
    R --> P[Monta prompt com contexto]
    P --> L[LLM gera a resposta]

Cada etapa reaproveita algo da trilha. Os embeddings vêm do Módulo 4. A busca por similaridade do cosseno também. A montagem do prompt vem do Módulo 8. O LLM, do Módulo 7. O RAG é, em boa medida, a costura inteligente de peças que você já conhece, com o acréscimo de uma base de vetores para guardar e buscar o conhecimento.

Explicação Intuitiva

Pense na diferença entre fazer uma prova de cabeça e fazer uma prova com consulta. De cabeça, você depende só do que lembra, e pode errar ou inventar. Com consulta, você procura no material o trecho certo e responde com base nele. O RAG transforma o LLM em um aluno que faz prova com consulta, ele primeiro encontra a página relevante e só então responde.

A parte da busca é como ter um índice remissivo muito bom. Em vez de folhear o livro inteiro, você vai direto aos trechos que falam do assunto da pergunta. E como a busca é por significado, e não por palavra exata, ela encontra o trecho certo mesmo quando a pergunta usa outras palavras. É essa combinação, achar o material certo e responder com base nele, que torna o RAG tão útil para assistentes que precisam ser confiáveis.

Explicação Matemática

O coração da recuperação é a similaridade entre a pergunta e cada pedaço. Representamos a pergunta por um vetor $\mathbf{q}$ e cada pedaço por um vetor $\mathbf{d}_i$, e medimos a proximidade pela similaridade do cosseno,

\[\text{sim}(\mathbf{q}, \mathbf{d}_i) = \frac{\mathbf{q} \cdot \mathbf{d}_i}{\lVert \mathbf{q} \rVert \, \lVert \mathbf{d}_i \rVert}.\]

Recuperamos os $k$ pedaços de maior similaridade, os top-k, e os concatenamos no prompt. A qualidade do RAG depende de dois fatores, a qualidade dos vetores, que determina se a busca acha o pedaço certo, e a montagem do prompt, que determina se o modelo usa bem o que foi recuperado. A geração em si segue a previsão da próxima palavra de sempre, agora condicionada ao contexto recuperado.

Exemplo Prático

Vamos montar um RAG mínimo de ponta a ponta, sem nenhuma biblioteca pesada. Usamos uma pequena base de notas de aula, vetores TF-IDF como embeddings simples, e a busca por cosseno para recuperar o trecho mais relevante de uma pergunta. Ao final, montamos o prompt que entregaria esse trecho a um LLM.

Esse esqueleto, propositalmente simples, deixa claras todas as etapas do pipeline antes de trocarmos as peças por versões mais poderosas nas próximas aulas. O código está no notebook notebooks/modulo-09/01-pipeline-de-rag.ipynb, então abra-o ao lado para acompanhar.

Código Comentado

import re
import math
from collections import Counter

# Base de conhecimento: pequenos trechos de notas de aula.
documentos = [
    "A derivada mede a taxa de variação instantânea de uma função em um ponto.",
    "A regra da cadeia permite derivar funções compostas multiplicando as derivadas.",
    "Uma matriz é uma tabela de números organizada em linhas e colunas.",
    "Em Python, uma função é definida com a palavra-chave def.",
]


def tokenizar(texto):
    return re.findall(r"\w+", texto.lower())


# Indexação: TF-IDF como embedding simples de cada documento.
N = len(documentos)
df = Counter()
for d in documentos:
    for w in set(tokenizar(d)):
        df[w] += 1
idf = {w: math.log(N / f) for w, f in df.items()}


def vetor(texto):
    tf = Counter(tokenizar(texto))
    return {w: tf[w] * idf.get(w, 0.0) for w in tf}


def cosseno(a, b):
    prod = sum(a[w] * b.get(w, 0.0) for w in a)
    na = math.sqrt(sum(v * v for v in a.values()))
    nb = math.sqrt(sum(v * v for v in b.values()))
    return prod / (na * nb) if na and nb else 0.0


base = [vetor(d) for d in documentos]


def recuperar(pergunta, k=1):
    q = vetor(pergunta)
    ranking = sorted(((cosseno(q, base[i]), i) for i in range(N)), reverse=True)
    return [documentos[i] for _, i in ranking[:k]]


def montar_prompt(pergunta, trechos):
    contexto = "\n".join(f"- {t}" for t in trechos)
    return (
        "Use apenas o contexto abaixo para responder.\n"
        f"Contexto:\n{contexto}\n\n"
        f"Pergunta: {pergunta}\nResposta:"
    )


pergunta = "O que é a derivada de uma função?"
trechos = recuperar(pergunta, k=1)
print("Trecho recuperado:", trechos[0])
print("\nPrompt para o LLM:\n", montar_prompt(pergunta, trechos))

Ao rodar, a busca recupera o trecho sobre a derivada, o mais relevante para a pergunta, e o prompt resultante entrega esse trecho ao modelo com a instrução de responder apenas com base nele. Esse é o RAG inteiro em miniatura, indexar, recuperar e aumentar o prompt. Tudo o que faremos a seguir é melhorar cada peça, embeddings melhores, um banco vetorial de verdade, e uma montagem de contexto mais cuidadosa.

Exercícios

1) Conceitual: Quais dois problemas do LLM o RAG resolve, e como? 2) Conceitual: Descreva as duas fases do pipeline de RAG e o que acontece em cada uma. 3) Prático: Acrescente novos documentos à base e teste perguntas que dependam deles. 4) Prático: Aumente o k para recuperar mais de um trecho e veja como o prompt muda. 5) Extensão: Pesquise o Dense Passage Retrieval e explique como ele difere da recuperação por TF-IDF que usamos aqui.

Projeto da Aula

Construa um RAG mínimo funcional. A entrega é um programa que indexa uma pequena base de notas de aula, recupera o trecho mais relevante para uma pergunta e monta o prompt com contexto, opcionalmente enviando ao Ollama para gerar a resposta final.

Considere o projeto pronto quando, para algumas perguntas, o sistema recuperar o trecho certo e montar um prompt coerente, e quando você escrever um parágrafo sobre o que melhoraria na busca. Este esqueleto cresce ao longo do módulo até virar o assistente educacional completo do projeto final.

Leituras Recomendadas

  • O artigo de Lewis e colegas que introduziu o RAG.
  • O artigo do Dense Passage Retrieval, de Karpukhin e colegas, sobre recuperação densa.
  • Tutoriais de RAG com LangChain e LlamaIndex, para ver implementações de produção.

Referências Científicas

As referências abaixo são reais e estão registradas em references/referencias.bib. As chaves entre parênteses são as do BibTeX.

  • Lewis, P., et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS. (lewis2020rag)
  • Karpukhin, V., et al. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP. (karpukhin2020dpr)
  • Manning, C. D., Raghavan, P., e Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press. (manning2008ir)