Se você já usou o ChatGPT, o Claude ou qualquer assistente de IA para responder perguntas sobre documentos internos da sua empresa, provavelmente já usou RAG sem saber. Neste artigo vamos entender o que é RAG, por que ele existe, como funciona na prática — com código Python — e quais são as tendências mais recentes que estão redefinindo essa arquitetura em 2026.
O problema que o RAG resolve
LLMs como GPT, Claude ou Gemini são treinados com um corpus de dados até uma certa data (o knowledge cutoff). Isso gera dois problemas:
- Conhecimento desatualizado: o modelo não sabe o que aconteceu depois do treinamento.
- Conhecimento genérico: o modelo não conhece os documentos internos da sua empresa, seu banco de dados de produtos, ou seus contratos.
A saída "óbvia" seria fazer fine-tuning do modelo com seus dados. Mas isso é caro, lento, e precisa ser refeito toda vez que os dados mudam.
O RAG (Retrieval-Augmented Generation) resolve isso de outra forma: em vez de "ensinar" o modelo, você busca a informação relevante em tempo real e a entrega como contexto na hora da pergunta.
Pergunta do usuário
│
▼
[Retriever] ──busca──▶ [Base de conhecimento]
│
▼
Contexto relevante + Pergunta
│
▼
[LLM] ──▶ Resposta grounded (fundamentada)
Anatomia de um pipeline RAG básico
Vamos construir um RAG simples do zero, sem frameworks, só para entender os conceitos. Depois falamos de ferramentas prontas.
1. Chunking — quebrando documentos em pedaços
Documentos grandes não cabem inteiros no contexto do LLM, e mesmo se coubessem, buscar em um texto inteiro é menos preciso do que buscar em pedaços menores e mais específicos.
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""Divide o texto em chunks com sobreposição para não perder contexto nas bordas."""
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
documento = """
RAG combina recuperação de informação com geração de texto...
(imagine um documento de várias páginas aqui)
"""
chunks = chunk_text(documento, chunk_size=100, overlap=20)
print(f"Documento dividido em {len(chunks)} chunks")
Dica prática: o overlap evita que uma frase importante seja cortada exatamente na fronteira entre dois chunks e perca sentido.
2. Embeddings — transformando texto em vetores
Um embedding é uma representação numérica do significado de um texto. Textos com significados parecidos geram vetores próximos no espaço.
from openai import OpenAI
client = OpenAI()
def gerar_embedding(texto: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=texto
)
return response.data[0].embedding
# Exemplo
vetor = gerar_embedding("Como cancelo minha assinatura?")
print(len(vetor)) # ex: 1536 dimensões
Frases como "Como cancelo minha assinatura?" e "Quero encerrar meu plano" terão vetores próximos, mesmo sem compartilhar nenhuma palavra — isso é a mágica da busca semântica.
3. Indexação — armazenando os vetores
Para buscar rapidamente entre milhares (ou milhões) de vetores, usamos um banco vetorial. Aqui vai um exemplo simples com ChromaDB, que roda localmente:
import chromadb
chroma_client = chromadb.Client()
colecao = chroma_client.create_collection(name="documentos")
# Indexando os chunks
for i, chunk in enumerate(chunks):
colecao.add(
documents=[chunk],
ids=[f"chunk_{i}"]
)
O ChromaDB gera os embeddings automaticamente por baixo dos panos (ou você pode passar os seus próprios).
4. Retrieval — buscando o que é relevante
def buscar_contexto(pergunta: str, top_k: int = 3) -> list[str]:
resultados = colecao.query(
query_texts=[pergunta],
n_results=top_k
)
return resultados["documents"][0]
pergunta = "Como funciona o overlap no chunking?"
contexto = buscar_contexto(pergunta)
print(contexto)
5. Geração — juntando tudo
def responder_com_rag(pergunta: str) -> str:
contexto = buscar_contexto(pergunta)
contexto_formatado = "\n\n".join(contexto)
prompt = f"""Responda à pergunta usando APENAS o contexto abaixo.
Se a resposta não estiver no contexto, diga que não sabe.
Contexto:
{contexto_formatado}
Pergunta: {pergunta}
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
print(responder_com_rag("Como funciona o overlap no chunking?"))
Pronto — esse é um RAG funcional, ainda que simples. Ele já resolve boa parte dos casos de uso. Mas, na prática, esse pipeline "ingênuo" falha bastante. Vamos entender por quê.
Por que RAG simples falha (e como consertar)
Estudos de 2026 mostram que quando um sistema RAG erra, a causa é o retrieval na maioria esmagadora dos casos — não a geração. Ou seja: o LLM está sendo alimentado com o contexto errado e, mesmo assim, gera uma resposta fluente e convincente. Isso é perigoso porque parece certo.
Os problemas mais comuns:
- Gap semântico: a pergunta do usuário usa vocabulário diferente do documento (ex: "cancelar assinatura" vs. "encerramento de conta").
- Poluição de contexto: buscar 10 chunks quando só 2 são relevantes dilui o sinal e piora a resposta.
- Chunks quebrados: um chunk de tamanho fixo pode cortar uma tabela no meio ou uma frase pela metade.
Solução 1: Busca híbrida (vetorial + lexical)
Nem tudo é semântica. Se o usuário busca por um código de erro específico (ERR_504) ou um nome próprio, a busca vetorial pode falhar — mas uma busca por palavra-chave (BM25) acerta na hora. A busca híbrida combina os dois:
from rank_bm25 import BM25Okapi
# Índice lexical (palavra-chave)
tokenized_chunks = [c.split() for c in chunks]
bm25 = BM25Okapi(tokenized_chunks)
def busca_hibrida(pergunta: str, top_k: int = 3, peso_vetorial: float = 0.6):
# Busca vetorial (semântica)
resultados_vetoriais = colecao.query(query_texts=[pergunta], n_results=top_k)
# Busca lexical (BM25)
scores_bm25 = bm25.get_scores(pergunta.split())
# Aqui você combinaria os rankings (ex: Reciprocal Rank Fusion)
# Simplificado para fins didáticos
return resultados_vetoriais, scores_bm25
Solução 2: Reranking
Depois de recuperar, por exemplo, 20 candidatos com busca rápida (barata), usa-se um modelo mais caro e preciso — um cross-encoder — para reordenar e escolher só os 3-5 melhores.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def rerank(pergunta: str, candidatos: list[str], top_k: int = 3) -> list[str]:
pares = [(pergunta, c) for c in candidatos]
scores = reranker.predict(pares)
ranqueados = sorted(zip(candidatos, scores), key=lambda x: x[1], reverse=True)
return [chunk for chunk, score in ranqueados[:top_k]]
Essa é considerada uma das melhorias de maior custo-benefício em RAG: retrieval barato traz muitos candidatos, reranking caro escolhe os melhores.
Solução 3: HyDE (Hypothetical Document Embeddings)
Uma técnica elegante: em vez de buscar diretamente pela pergunta do usuário, pedimos ao LLM para gerar uma resposta hipotética primeiro, e buscamos pelo embedding dessa resposta. Por quê funciona? Porque a resposta hipotética usa o vocabulário do domínio, mais parecido com o dos documentos, do que a pergunta original do usuário.
def hyde_retrieval(pergunta: str) -> list[str]:
resposta_hipotetica = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Responda hipoteticamente: {pergunta}"}]
).choices[0].message.content
return buscar_contexto(resposta_hipotetica)
As tendências de 2026
Com essa base, dá pra entender melhor para onde o RAG está indo:
RAG Agêntico
O padrão dominante hoje trata a busca como um processo iterativo de decisão, não uma etapa única. Um agente pode decidir buscar, avaliar se a evidência é suficiente, refinar a busca, ou até consultar múltiplas fontes (SQL, grafo, busca vetorial) antes de responder.
def rag_agentico(pergunta: str, max_tentativas: int = 3) -> str:
contexto_acumulado = []
for tentativa in range(max_tentativas):
contexto = buscar_contexto(pergunta, top_k=3)
contexto_acumulado.extend(contexto)
# O próprio LLM avalia se já tem evidência suficiente
avaliacao = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Contexto: {contexto_acumulado}\n\n"
f"Pergunta: {pergunta}\n\n"
f"O contexto é suficiente para responder? Responda SIM ou NAO."
}]
).choices[0].message.content
if "SIM" in avaliacao.upper():
break
# Se não, reformula a busca (ex: gera sub-perguntas) e tenta de novo
return responder_com_rag(pergunta)
GraphRAG
Em vez de recuperar apenas chunks de texto isolados, o GraphRAG recupera subgrafos — entidades e as relações entre elas. Isso é poderoso quando a resposta depende de conectar várias informações espalhadas (ex: "quais projetos o funcionário X liderou que envolveram o cliente Y?"), algo que a busca vetorial pura tem dificuldade de capturar.
RAG auto-corretivo
O sistema avalia sua própria resposta antes de entregá-la ao usuário e refaz a busca se a confiança for baixa — reduzindo bastante as alucinações em domínios de alto risco (saúde, jurídico, financeiro).
Segurança e governança
Com RAG virando peça central de sistemas empresariais, surgiram preocupações novas: envenenamento de corpus (alguém injeta documentos maliciosos na base para manipular respostas) e a necessidade de controle de acesso granular — para que o RAG não vire uma forma de vazar dados que o usuário não deveria ver.
E para documentos gigantes? (livros, relatórios, corpus enormes)
Tudo que vimos até aqui parte de um pressuposto: chunks tratados como uma lista "achatada" (flat), todos no mesmo nível, competindo entre si na busca. Isso funciona bem para bases pequenas, mas quebra em dois cenários comuns:
- Documentos muito longos (um livro, um relatório de 300 páginas): a resposta certa pode exigir juntar informação de capítulos diferentes.
- Corpus muito grande (milhares de documentos): comparar a pergunta com todos os chunks de todos os documentos fica caro e impreciso.
Para isso, surgiram técnicas que adicionam estrutura hierárquica ao índice, em vez de tratar tudo como uma lista plana.
RAPTOR — árvore de resumos recursivos
A ideia do RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval) é simples de entender, mas poderosa: em vez de indexar só os chunks originais, ele constrói uma árvore.
Nível 2: [Resumo geral do documento]
/ \
Nível 1: [Resumo seção A] [Resumo seção B]
/ \ / \
Nível 0: chunk1 chunk2 chunk3 chunk4 (texto original)
Como isso é construído:
- Gera embeddings de todos os chunks originais (nível 0).
- Agrupa (clustering) chunks semanticamente parecidos.
- Para cada grupo, pede ao LLM um resumo abstrativo.
- Repete o processo sobre os resumos, criando o próximo nível da árvore, até sobrar só um resumo geral no topo.
Na hora da busca, o sistema compara a pergunta com nós de todos os níveis simultaneamente — tanto chunks bem específicos quanto resumos de alto nível — e retorna a melhor mistura. Isso é o que permite responder perguntas de síntese ("do que trata esse relatório de forma geral?") e perguntas pontuais ("qual foi o valor mencionado na seção 4.2?") com o mesmo índice.
Implementação simplificada:
from sklearn.cluster import KMeans
import numpy as np
def gerar_resumo(textos: list[str]) -> str:
"""Pede ao LLM um resumo abstrativo de um grupo de chunks."""
texto_unido = "\n\n".join(textos)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Resuma o texto abaixo de forma concisa, preservando os fatos-chave:\n\n{texto_unido}"
}]
)
return response.choices[0].message.content
def construir_arvore_raptor(chunks: list[str], n_clusters: int = 3, niveis: int = 2) -> list[list[str]]:
"""Constrói uma árvore RAPTOR simplificada: retorna uma lista de níveis,
cada um contendo os textos (chunks originais ou resumos) daquele nível."""
arvore = [chunks] # nível 0 = chunks originais
nivel_atual = chunks
for _ in range(niveis):
if len(nivel_atual) <= n_clusters:
break # já convergiu para poucos nós, não vale a pena agrupar mais
# Embeddings do nível atual
embeddings = np.array([gerar_embedding(t) for t in nivel_atual])
# Clustering
k = min(n_clusters, len(nivel_atual))
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10).fit(embeddings)
# Gera um resumo por cluster
proximo_nivel = []
for cluster_id in range(k):
membros = [t for t, label in zip(nivel_atual, kmeans.labels_) if label == cluster_id]
proximo_nivel.append(gerar_resumo(membros))
arvore.append(proximo_nivel)
nivel_atual = proximo_nivel
return arvore
def indexar_arvore(arvore: list[list[str]]):
"""Indexa todos os níveis da árvore na mesma coleção vetorial."""
for nivel_idx, nivel in enumerate(arvore):
for i, texto in enumerate(nivel):
colecao.add(
documents=[texto],
metadatas=[{"nivel": nivel_idx}],
ids=[f"n{nivel_idx}_c{i}"]
)
# Uso
arvore = construir_arvore_raptor(chunks, n_clusters=3, niveis=2)
indexar_arvore(arvore)
# A busca (colecao.query) agora retorna candidatos de qualquer nível —
# tanto chunks originais quanto resumos — misturados por relevância.
Quando usar: ótimo para um único documento longo (livro, relatório) onde perguntas de síntese são comuns. Limitação importante: pesquisas recentes mostram que essa abordagem em árvore perde precisão quando o espaço de busca cresce para nível de corpus inteiro, com milhões de tokens espalhados por muitos documentos — RAPTOR foi pensado para profundidade (um documento longo), não para largura (muitos documentos).
RAG hierárquico em 2 estágios — documento primeiro, chunk depois
Uma alternativa mais simples e barata, ótima quando você tem muitos documentos (não necessariamente cada um gigante): em vez de comparar a pergunta com todos os chunks de todos os documentos de uma vez, faça em duas etapas.
def busca_hierarquica_2_estagios(pergunta: str, top_documentos: int = 3, top_chunks: int = 3):
# Estágio 1: busca em nível de DOCUMENTO (usando resumo/embedding de cada doc inteiro)
docs_relevantes = colecao_documentos.query(
query_texts=[pergunta],
n_results=top_documentos
)
# Estágio 2: busca só DENTRO dos documentos encontrados no estágio 1
resultados_finais = []
for doc_id in docs_relevantes["ids"][0]:
chunks_do_doc = colecao_chunks.query(
query_texts=[pergunta],
n_results=top_chunks,
where={"documento_id": doc_id} # filtra só chunks desse documento
)
resultados_finais.extend(chunks_do_doc["documents"][0])
return resultados_finais
Isso funciona porque reduz drasticamente o espaço de busca: em vez de vasculhar milhares de chunks, você primeiro elimina os documentos irrelevantes e só depois refina. Um corpus de 100 documentos com 20 chunks cada (2.000 chunks) pode cair para ~60 comparações de similaridade — cerca de 33x menos custo — mantendo o mesmo recall, desde que o estágio 1 não erre o documento certo.
Parent-Child Retrieval — busca precisa, contexto generoso
Outra técnica simples e muito usada na prática: você indexa chunks pequenos (uma ou duas frases) para ter a busca mais precisa possível, mas guarda a referência de qual "chunk pai" (um parágrafo maior, ou o documento inteiro) cada um pertence. Quando um chunk-filho é encontrado na busca, você recupera o pai para dar ao LLM.
def indexar_com_hierarquia_pai_filho(documento: str, doc_id: str):
paragrafos = documento.split("\n\n") # os "pais"
for p_idx, paragrafo in enumerate(paragrafos):
parent_id = f"{doc_id}_p{p_idx}"
frases = paragrafo.split(". ") # os "filhos", granularidade menor
for f_idx, frase in enumerate(frases):
colecao.add(
documents=[frase],
metadatas=[{"parent_id": parent_id, "parent_text": paragrafo}],
ids=[f"{parent_id}_f{f_idx}"]
)
def buscar_com_contexto_do_pai(pergunta: str, top_k: int = 3) -> list[str]:
resultados = colecao.query(query_texts=[pergunta], n_results=top_k)
# Retorna o texto do PAI, não do filho — contexto mais rico pro LLM
return [meta["parent_text"] for meta in resultados["metadatas"][0]]
A vantagem: a busca compara a pergunta com unidades pequenas e específicas (mais fácil de "casar" semanticamente), mas o LLM recebe um contexto maior e mais coerente, em vez de uma frase solta fora de contexto.
GraphRAG e MegaRAG — quando a resposta exige conectar pontos
Para perguntas que exigem raciocínio multi-hop — tipo "quais projetos o funcionário X liderou que envolveram o cliente Y?" — nenhuma das técnicas acima resolve bem, porque a resposta não está em um único chunk, está na relação entre entidades espalhadas pelo documento.
O GraphRAG ataca isso construindo um grafo de conhecimento a partir dos documentos (entidades + relações), depois agrupa entidades relacionadas em "comunidades" e gera resumos hierárquicos desses grupos. Na busca, em vez de retornar chunks, ele retorna subgrafos — o que permite ao LLM seguir cadeias de relação que a busca por similaridade pura nunca capturaria.
# Pseudocódigo conceitual — bibliotecas como Neo4j + LLM, ou frameworks
# como Microsoft GraphRAG cuidam da parte pesada na prática
def construir_grafo(documento: str):
entidades_e_relacoes = extrair_entidades_relacoes_com_llm(documento)
grafo = montar_grafo(entidades_e_relacoes) # ex: Neo4j
comunidades = detectar_comunidades(grafo) # ex: algoritmo de Louvain
resumos_comunidades = {
c: gerar_resumo(nos_da_comunidade(grafo, c)) for c in comunidades
}
return grafo, resumos_comunidades
def busca_graphrag(pergunta: str, grafo, resumos_comunidades):
comunidades_relevantes = buscar_comunidades_relevantes(pergunta, resumos_comunidades)
subgrafo = extrair_subgrafo(grafo, comunidades_relevantes)
return subgrafo # entregue ao LLM como contexto estruturado
A variante MegaRAG estende essa ideia para documentos multimodais — livros e relatórios com diagramas, figuras e tabelas — extraindo entidades tanto do texto quanto do conteúdo visual.
Qual técnica escolher?
| Cenário | Técnica recomendada |
|---|---|
| Um documento longo, perguntas de síntese | RAPTOR |
| Muitos documentos, corpus grande | Hierárquico em 2 estágios |
| Precisão na busca + contexto rico na resposta | Parent-Child Retrieval |
| Perguntas que conectam entidades/relações (multi-hop) | GraphRAG |
| Documentos com tabelas, figuras, diagramas | MegaRAG |
Na prática, essas técnicas não são mutuamente exclusivas — é comum combinar, por exemplo, parent-child retrieval com busca hierárquica em 2 estágios, ou usar RAPTOR dentro de cada documento de um corpus que também tem busca em 2 estágios no nível macro.
Conclusão
RAG começou como "busque uns chunks e jogue no prompt", mas evoluiu para uma camada de raciocínio, memória e governança em torno dos LLMs. Se você está começando, o pipeline básico (chunking → embeddings → índice vetorial → retrieval → geração) já resolve muita coisa. Mas conforme sua aplicação cresce — seja em complexidade das perguntas ou em tamanho dos documentos — vale a pena investir em busca híbrida, reranking, estruturas hierárquicas (RAPTOR, busca em 2 estágios, parent-child) e, dependendo do caso, em padrões agênticos ou baseados em grafo.
A régua de qualidade em 2026 não é mais "o RAG funciona?", é "o RAG erra graciosamente, sabe quando não sabe, e protege os dados que não deveria expor?".
Gostou do artigo? Deixe um comentário contando qual parte do seu pipeline RAG te dá mais dor de cabeça — retrieval, chunking ou avaliação de qualidade.
Top comments (0)