Phase 11 - Lesson 06
RAG (Retrieval-Augmented Generation)
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Seu LLM conhece tudo até o limite de seu treinamento. Ele não sabe nada sobre os documentos da sua empresa, sua base de código ou as notas da reunião da semana passada. O RAG resolve isso recuperando documentos relevantes e inserindo-os no prompt. É o padrão mais implantado na IA de produção. Se você for construir uma única coisa deste curso, construa um pipeline de RAG.
Type: Build Languages: Python Prerequisites: Phase 10 (LLMs from Scratch), Phase 11 Lessons 01-05 Time: ~90 minutes Related: Phase 5 · 23 (Chunking Strategies for RAG) para os seis algoritmos de divisão em blocos (chunking) e quando cada um é mais indicado. Phase 5 · 22 (Embedding Models Deep Dive) para a escolha do modelo de incorporação (embedding). Phase 11 · 07 (Advanced RAG) para busca híbrida, re-ranqueamento e transformação de consulta.
Objetivos de Aprendizagem
- Construir um pipeline de RAG completo: carregamento de documentos, divisão em blocos (chunking), incorporação (embedding), armazenamento vetorial, recuperação e geração
- Implementar busca semântica usando um banco de dados vetorial (ChromaDB, FAISS ou Pinecone) com indexação adequada
- Explicar por que o RAG é preferido em relação ao ajuste fino (fine-tuning) para aplicações baseadas em conhecimento (custo, atualização, atribuição)
- Avaliar a qualidade do RAG usando métricas de recuperação (precisão, revocação) e métricas de geração (fidelidade, relevância)
O Problema
Você constrói um chatbot para sua empresa. Um cliente pergunta: "Qual é a política de reembolso para planos corporativos (enterprise)?" O LLM responde com uma resposta genérica sobre políticas típicas de reembolso de SaaS. A política real, enterrada em um wiki interno de 200 páginas, diz que os clientes corporativos têm uma janela de 60 dias com reembolsos proporcionais. O LLM nunca viu esse documento. Ele não pode saber o que não foi treinado para saber.
O ajuste fino (fine-tuning) é uma solução. Pegar o LLM, treiná-lo com seus documentos internos e implantar o modelo atualizado. Isso funciona, mas apresenta problemas sérios. O ajuste fino custa milhares de dólares em computação. O modelo fica desatualizado no momento em que um documento é alterado. Você não tem como saber de qual fonte o modelo extraiu a informação. E se a empresa adquirir outra linha de produtos no mês seguinte, você terá que fazer o ajuste fino novamente.
O RAG é a outra solução. Deixe o modelo intocado. Quando uma pergunta chega, busque passagens relevantes em seu armazenamento de documentos, cole-as no prompt antes da pergunta e deixe o modelo responder usando essas passagens como contexto. O armazenamento de documentos pode ser atualizado em minutos. Você pode ver exatamente quais documentos foram recuperados. O modelo em si nunca muda. É por isso que o RAG é o padrão dominante em produção: é mais barato, mais atualizado, mais auditável e funciona com qualquer LLM.
O Concept
O Padrão RAG
Todo o padrão cabe em quatro etapas:
graph LR
Q["Consulta do Usuário"] --> R["Recuperar"]
R --> A["Aumentar Prompt"]
A --> G["Gerar"]
G --> Ans["Resposta"]
subgraph "Recuperar"
R --> Embed["Incorporar consulta (Embed)"]
Embed --> Search["Buscar no banco vetorial"]
Search --> TopK["Retornar blocos top-k"]
end
subgraph "Aumentar"
TopK --> Format["Formatar blocos no prompt"]
Format --> Combine["Combinar com a pergunta do usuário"]
end
subgraph "Gerar"
Combine --> LLM["LLM gera resposta"]
LLM --> Cite["Resposta baseada nos docs recuperados"]
end
Consulta -> Recuperar -> Aumentar prompt -> Gerar. Todo sistema de RAG segue esse padrão. As diferenças entre os sistemas RAG em produção estão nos detalhes de cada etapa: como você divide em blocos, como faz a incorporação (embedding), como busca e como constrói o prompt.
Por que o RAG é Melhor que o Ajuste Fino (Fine-Tuning)
| Preocupação | Ajuste Fino (Fine-Tuning) | RAG |
|---|---|---|
| Custo | ,000- 00,000+ por execução de treinamento |
$0.01-$0.10 por consulta (embedding + LLM) |
| Atualização | Desatualizado até que seja retreinado | Atualizado em minutos reindexando os docs |
| Auditabilidade | Não é possível rastrear a resposta até a fonte | Pode mostrar as passagens exatas recuperadas |
| Alucinação | Ainda alucina livremente | Baseado nos documentos recuperados |
| Privacidade de dados | Dados de treinamento integrados aos pesos | Os documentos permanecem no seu armazenamento vetorial |
O ajuste fino altera os pesos do modelo permanentemente. O RAG altera o contexto do modelo temporariamente. Para a maioria das aplicações, o contexto temporário é o que você deseja.
O único caso em que o ajuste fino vence é quando você precisa que o modelo adote um estilo, tom ou padrão de raciocínio específico que não pode ser alcançado apenas por meio de engenharia de prompt. Para recuperação de conhecimento factual, o RAG vence todas as vezes.
Modelos de Incorporação (Embedding Models)
Um modelo de incorporação (embedding) converte texto em um vetor denso. Textos semelhantes produzem vetores que estão próximos uns dos outros nesse espaço multidimensional. "Como faço para redefinir minha senha?" e "Preciso alterar minha senha" produzem vetores quase idênticos, apesar de compartilharem poucas palavras. "O gato sentou no tapete" produz um vetor muito diferente.
Modelos de embedding comuns (linha de 2026 — veja Phase 5 · 22 para análise completa):
| Modelo | Dimensões | Provedor | Notas |
|---|---|---|---|
| text-embedding-3-small | 1536 (Matryoshka) | OpenAI | Melhor relação preço/desempenho para a maioria dos casos de uso |
| text-embedding-3-large | 3072 (Matryoshka) | OpenAI | Maior precisão, truncável para 256/512/1024 |
| Gemini Embedding 2 | 3072 (Matryoshka) | Principal recuperação MTEB; contexto de 8K | |
| voyage-4 | 1024/2048 (Matryoshka) | Voyage AI | Variantes de domínio (código, finanças, jurídico) |
| Cohere embed-v4 | 1024 (Matryoshka) | Cohere | Forte suporte multilíngue, contexto de 128K |
| BGE-M3 | 1024 (dense + sparse + ColBERT) | BAAI (open-weight) | Três perspectivas a partir de um único modelo |
| Qwen3-Embedding | 4096 (Matryoshka) | Alibaba (open-weight) | Maior pontuação de recuperação em open-weight |
| all-MiniLM-L6-v2 | 384 | Open-weight (Sentence Transformers) | Linha de base para prototipagem |
Para esta lição, construiremos nosso próprio embedding simples usando TF-IDF. Não porque o TF-IDF seja o que os sistemas de produção usam, mas porque ele torna o conceito concreto: o texto entra, um vetor sai, textos semelhantes produzem vetores semelhantes.
Semelhança Vetorial (Vector Similarity)
Dados dois vetores, como você mede a semelhança? Três opções:
Similaridade de cosseno: o cosseno do ângulo entre dois vetores. Varia de -1 (oposto) a 1 (idêntico). Ignora a magnitude, importando-se apenas com a direção. Este é o padrão para RAG.
cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)
Produto escalar (Dot product): o produto interno bruto. Vetores maiores recebem pontuações mais altas. Útil quando a magnitude carrega informação (documentos mais longos podem ser mais relevantes).
dot(a, b) = sum(a_i * b_i)
Distância L2 (Euclidiana): a distância em linha reta no espaço vetorial. Menor distância = mais semelhante. Sensível a diferenças de magnitude.
L2(a, b) = sqrt(sum((a_i - b_i)^2))
A similaridade de cosseno é o padrão. Ela lida com documentos de tamanhos diferentes de forma elegante porque normaliza pela magnitude. Quando alguém fala "busca vetorial", quase sempre se refere à similaridade de cosseno.
Estratégias de Divisão em Blocos (Chunking Strategies)
Os documentos são muito longos para serem incorporados como vetores únicos. Um PDF de 50 páginas pode produzir um embedding ruim porque contém dezenas de tópicos. Em vez disso, você divide os documentos em blocos (chunks) e incorpora cada bloco separadamente.
Divisão de tamanho fixo (Fixed-size chunking): divide a cada N tokens. Simples e previsível. Um bloco de 512 tokens com sobreposição (overlap) de 50 tokens significa que o bloco 1 contém os tokens 0-511, o bloco 2 contém os tokens 462-973 e assim por diante. A sobreposição garante que você não divida uma frase em um limite desfavorável.
Divisão semântica (Semantic chunking): divide em limites naturais. Parágrafos, seções ou cabeçalhos de markdown. Cada bloco é uma unidade coerente de significado. Mais complexo de implementar, mas produz uma melhor recuperação.
Divisão recursiva (Recursive chunking): tenta dividir primeiro no maior limite (cabeçalhos de seção). Se uma seção ainda for muito grande, divide nos limites de parágrafo. Se um parágrafo ainda for muito grande, divide nos limites de frase. Esta é a abordagem do RecursiveCharacterTextSplitter do LangChain e funciona bem na prática.
O tamanho do bloco importa mais do que as pessoas pensam:
- Muito pequeno (64-128 tokens): cada bloco carece de contexto. "Aumentou 15% no último trimestre" não significa nada sem saber ao que "ele/ela" se refere.
- Muito grande (2048+ tokens): cada bloco cobre vários tópicos, diluindo a relevância. Quando você busca por dados de receita, recebe um bloco que é 10% sobre receita e 90% sobre número de funcionários.
- Ponto ideal (256-512 tokens): contexto suficiente para ser autossuficiente, focado o suficiente para ser relevante.
A maioria dos sistemas de RAG em produção usa blocos de 256-512 tokens com sobreposição de 50 tokens. As diretrizes de RAG da Anthropic recomendam essa faixa.
Bancos de Dados Vetoriais (Vector Databases)
Depois de ter as incorporações (embeddings), você precisa de um lugar para armazená-las e buscá-las. Opções:
| Banco de Dados | Tipo | Melhor para |
|---|---|---|
| FAISS | Biblioteca (em processo) | Prototipagem, conjuntos de dados pequenos a médios |
| Chroma | Banco de dados leve | Desenvolvimento local, implantações pequenas |
| Pinecone | Serviço gerenciado | Produção sem sobrecarga operacional |
| Weaviate | Banco de dados de código aberto | Produção auto-hospedada |
| pgvector | Extensão do Postgres | Já utiliza o Postgres |
| Qdrant | Banco de dados de código aberto | Auto-hospedagem de alto desempenho |
Para esta lição, construiremos um armazenamento vetorial em memória simples. Ele armazena vetores em uma lista e faz uma busca por força bruta usando similaridade de cosseno. Isso é equivalente ao FAISS com um índice plano. Ele escala para cerca de 100.000 vetores antes de se tornar lento. Sistemas de produção usam algoritmos de vizinho mais próximo aproximado (ANN), como HNSW, para buscar milhões de vetores em milissegundos.
O Pipeline Completo
graph TD
subgraph "Indexação (offline)"
D["Documentos"] --> C["Dividir em blocos (Chunk)"]
C --> E["Incorporar cada bloco"]
E --> S["Armazenar vetores + texto"]
end
subgraph "Consulta (online)"
Q["Consulta do usuário"] --> QE["Incorporar consulta"]
QE --> VS["Busca vetorial (top-k)"]
VS --> P["Construir prompt com os blocos"]
P --> LLM["LLM gera resposta"]
end
S -.->|"mesmo espaço vetorial"| VS
A fase de indexação é executada uma vez por documento (ou quando os documentos são atualizados). A fase de consulta é executada a cada solicitação do usuário. Em produção, a indexação pode processar milhões de documentos ao longo de horas. A consulta deve responder em menos de um segundo.
Números Reais
A maioria dos sistemas de RAG em produção usa estes parâmetros:
- k = 5 a 10 blocos recuperados por consulta
- Tamanho do bloco = 256 a 512 tokens com sobreposição de 50 tokens
- Orçamento de contexto: 2.500-5.000 tokens de conteúdo recuperado por consulta
- Prompt total: ~8.000-16.000 tokens (prompt do sistema + blocos recuperados + histórico de conversa + consulta do usuário)
- Dimensão do embedding: 384-3072 dependendo do modelo
- Rendimento de indexação: 100-1.000 documentos por segundo com embeddings de API
- Latência de consulta: 50-200ms para recuperação, 500-3000ms para geração
Construa
Passo 1: Divisão de Documentos em Blocos (Document Chunking)
def chunk_text(text, chunk_size=200, overlap=50):
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + chunk_size
chunk = " ".join(words[start:end])
chunks.append(chunk)
start += chunk_size - overlap
return chunks
Passo 2: Embeddings TF-IDF
Construiremos uma função de embedding simples. O TF-IDF (Term Frequency-Inverse Document Frequency) não é um embedding neural, mas converte texto em vetores de uma forma que captura a importância das palavras. Palavras frequentes em um documento recebem um TF mais alto. Palavras raras em todo o corpus recebem um IDF mais alto. O produto resulta em um vetor onde palavras importantes e distintas têm valores altos.
import math
from collections import Counter
def build_vocabulary(documents):
vocab = set()
for doc in documents:
vocab.update(doc.lower().split())
return sorted(vocab)
def compute_tf(text, vocab):
words = text.lower().split()
count = Counter(words)
total = len(words)
return [count.get(word, 0) / total for word in vocab]
def compute_idf(documents, vocab):
n = len(documents)
idf = []
for word in vocab:
doc_count = sum(1 for doc in documents if word in doc.lower().split())
idf.append(math.log((n + 1) / (doc_count + 1)) + 1)
return idf
def tfidf_embed(text, vocab, idf):
tf = compute_tf(text, vocab)
return [t * i for t, i in zip(tf, idf)]
Passo 3: Busca por Similaridade de Cosseno (Cosine Similarity Search)
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
def search(query_embedding, stored_embeddings, top_k=5):
scores = []
for i, emb in enumerate(stored_embeddings):
sim = cosine_similarity(query_embedding, emb)
scores.append((i, sim))
scores.sort(key=lambda x: x[1], reverse=True)
return scores[:top_k]
Passo 4: Construção do Prompt (Prompt Construction)
É aqui que acontece o "aumento" no RAG. Pegamos os blocos recuperados, formatamos em um prompt e pedimos ao LLM para responder com base no contexto fornecido.
def build_rag_prompt(query, retrieved_chunks):
context = "\n\n---\n\n".join(
f"[Source {i+1}]\n{chunk}"
for i, chunk in enumerate(retrieved_chunks)
)
return f"""Answer the question based ONLY on the following context.
If the context doesn't contain enough information, say "I don't have enough information to answer that."
Context:
{context}
Question: {query}
Answer:"""
Passo 5: O Pipeline de RAG Completo (The Complete RAG Pipeline)
class RAGPipeline:
def __init__(self):
self.chunks = []
self.embeddings = []
self.vocab = []
self.idf = []
def index(self, documents):
all_chunks = []
for doc in documents:
all_chunks.extend(chunk_text(doc))
self.chunks = all_chunks
self.vocab = build_vocabulary(all_chunks)
self.idf = compute_idf(all_chunks, self.vocab)
self.embeddings = [
tfidf_embed(chunk, self.vocab, self.idf)
for chunk in all_chunks
]
def query(self, question, top_k=5):
query_emb = tfidf_embed(question, self.vocab, self.idf)
results = search(query_emb, self.embeddings, top_k)
retrieved = [(self.chunks[i], score) for i, score in results]
prompt = build_rag_prompt(
question, [chunk for chunk, _ in retrieved]
)
return prompt, retrieved
Passo 6: Geração (simulada) (Generation (simulated))
Em produção, é aqui que você chama a API do LLM. Para esta lição, simularemos a geração extraindo a frase mais relevante do contexto recuperado.
def simple_generate(prompt, retrieved_chunks):
query_words = set(prompt.lower().split("question:")[-1].split())
best_sentence = ""
best_score = 0
for chunk in retrieved_chunks:
for sentence in chunk.split("."):
sentence = sentence.strip()
if not sentence:
continue
words = set(sentence.lower().split())
overlap = len(query_words & words)
if overlap > best_score:
best_score = overlap
best_sentence = sentence
return best_sentence if best_sentence else "I don't have enough information."
Use
Com um modelo de embedding real e um LLM, o código quase não muda:
from openai import OpenAI
client = OpenAI()
def embed(text):
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def generate(prompt):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0
)
return response.choices[0].message.content
Ou com a Anthropic:
import anthropic
client = anthropic.Anthropic()
def generate(prompt):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
O pipeline é o mesmo. Troque a função de embedding. Troque a função de geração. A lógica de recuperação, divisão em blocos, construção de prompt — tudo idêntico, independentemente de quais modelos você usar.
Para armazenamento vetorial em escala, substitua a busca por força bruta por um banco de dados vetorial adequado:
import chromadb
client = chromadb.Client()
collection = client.create_collection("my_docs")
collection.add(
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
results = collection.query(
query_texts=["What is the refund policy?"],
n_results=5
)
O Chroma lida com o embedding internamente (ele usa all-MiniLM-L6-v2 por padrão) and stores the vectors in a local database. Mesmo padrão, encanamento diferente. (Wait! In this last block, the comment inside code or the text after it? In code block 3, there's no comment, but after it: "O Chroma lida com o embedding internamente (ele usa all-MiniLM-L6-v2 por padrão) e armazena os vetores em um banco de dados local. Mesmo padrão, encanamento diferente." Oh, wait! I noticed "and stores the vectors in a local database. Mesmo padrão, encanamento diferente." had some untranslated English in my draft. Let's fix that to: "O Chroma lida com o embedding internamente (ele usa all-MiniLM-L6-v2 por padrão) e armazena os vetores em um banco de dados local. Mesmo padrão, encanamento diferente." Yes, I will write it correctly.)
Envie (Ship It)
Esta lição produz:
outputs/prompt-rag-architect.md— um prompt para projetar sistemas de RAG para casos de uso específicosoutputs/skill-rag-pipeline.md— uma habilidade (skill) que ensina agentes a construir e depurar pipelines de RAG
Exercícios
Substitua os embeddings TF-IDF por uma abordagem simples de saco de palavras (bag-of-words) (binária: 1 se a palavra estiver presente, 0 caso contrário). Compare a qualidade da recuperação nos documentos de amostra. O TF-IDF deve apresentar desempenho superior porque atribui maior peso às palavras raras.
Experimente com tamanhos de blocos: tente 50, 100, 200 e 500 palavras no mesmo conjunto de documentos. Para cada tamanho, execute as mesmas 5 consultas e conte quantas retornam um bloco relevante no top-3. Encontre o ponto ideal onde a qualidade da recuperação atinge o pico.
Adicione metadados a cada bloco (nome do documento de origem, posição do bloco). Modifique o modelo de prompt para incluir a atribuição de origem para que o LLM cite suas fontes.
Implemente uma avaliação simples: dados 10 pares de perguntas e respostas, execute cada pergunta pelo pipeline de RAG e meça qual porcentagem dos blocos recuperados contém a resposta. Isso é a revocação de recuperação em k (retrieval recall at k).
Construa um pipeline de RAG ciente de conversação: mantenha um histórico das últimas 3 interações e inclua-as no prompt junto com os blocos recuperados. Teste com perguntas subsequentes como "E quanto ao corporativo?" após perguntar sobre preços.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| RAG | "IA que lê seus documentos" | Recuperar documentos relevantes, colá-los no prompt e gerar uma resposta baseada nesses documentos |
| Embedding | "Converter texto em números" | Uma representação vetorial densa de texto onde significados semelhantes produzem vetores semelhantes |
| Banco de dados vetorial | "Mecanismo de busca para IA" | Um armazenamento de dados otimizado para guardar vetores e encontrar os vizinhos mais próximos por similaridade |
| Divisão em blocos (Chunking) | "Dividir documentos em pedaços" | Divisão de documentos em segmentos menores (normalmente de 256 a 512 tokens) para que cada um possa ser incorporado e recuperado de forma independente |
| Similaridade de cosseno | "Quão semelhantes são dois vetores" | O cosseno do ângulo entre dos vetores; 1 = direção idêntica, 0 = ortogonal, -1 = oposto |
| Recuperação top-k | "Obter as k melhores correspondências" | Retornar os k blocos mais semelhantes à consulta a partir do armazenamento vetorial |
| Janela de contexto | "Quanto texto o LLM consegue ver" | O número máximo de tokens que o LLM pode processar em uma única solicitação; os blocos recuperados devem caber dentro desse limite |
| Geração aumentada | "Responder usando o contexto fornecido" | Gerar uma resposta usando documentos recuperados como contexto, em vez de depender apenas do conhecimento adquirido no treinamento |
| TF-IDF | "Pontuação de importância de palavras" | Frequência do Termo multiplicada pelo Inverso da Frequência nos Documentos; pondera as palavras com base em quão distintivas elas são dentro de um corpus |
| Indexação | "Preparar documentos para busca" | O processo offline de divisão em blocos, incorporação e armazenamento de documentos para que possam ser pesquisados no momento da consulta |
Leituras Adicionais
- Lewis et al., "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (2020) — o artigo original sobre RAG da Facebook AI Research que formalizou o padrão de recuperar-depois-gerar
- Anthropic's RAG documentation (docs.anthropic.com) — diretrizes práticas para tamanhos de blocos, construção de prompt e avaliação
- Pinecone Learning Center, "What is RAG?" — explicações visuais claras do pipeline de RAG com considerações de produção
- Sentence-BERT: Reimers & Gurevych (2019) — o artigo por trás dos modelos de embedding all-MiniLM, demonstrando como treinar bi-encoders para similaridade semântica
- Karpukhin et al., "Dense Passage Retrieval for Open-Domain Question Answering" (EMNLP 2020) — o artigo sobre DPR que provou que a recuperação por bi-encoder denso supera o BM25 em perguntas e respostas de domínio aberto e estabeleceu o padrão para recuperadores de RAG modernos.
- Conceitos de Alto Nível do LlamaIndex — os principais conceitos a serem conhecidos ao construir pipelines de RAG: carregadores de dados, analisadores de nós, índices, recuperadores, sintetizadores de resposta.
- Tutorial de RAG do LangChain — o orquestrador com uma abordagem alternativa; visão do mesmo padrão de recuperar-depois-gerar baseada em cadeias de executáveis (chain-of-runnables).