Como montar um RAG do zero?
Um RAG tem sete peças: carregar o documento, cortar em pedaços, virar cada pedaço em vetor, guardar os vetores, buscar, montar o prompt e gerar. Nenhuma delas exige framework. O núcleo cabe em umas quarenta linhas de Python com um arquivo em disco no lugar do banco. Quatro dessas sete decidem a qualidade da resposta.
Este artigo é sobre a montagem. Se a pergunta ainda é o que a sigla significa e quando ela compensa, o lugar é o que é RAG. Se a pergunta é onde cortar o texto, chunking trata só disso — é a peça com mais consequência e mais folclore.
A arquitetura que você vai montar não é a do paper de 2020. O trabalho original ajustava o gerador junto com o recuperador1; o que se monta hoje é um pipeline de chamadas de API costurado por código seu. Herdou o nome e trocou a receita. E a parte que quase ninguém antecipa na primeira montagem é quanto do resultado depende do metadado que viaja junto com o vetor.
As sete peças
1. Carregar é extrair texto do arquivo. Em Markdown é ler o arquivo; em PDF é um problema inteiro, com ordem de leitura de coluna, tabela e cabeçalho de página que se repete em todo chunk. Aqui é onde mais gente subestima o trabalho.
2. Cortar transforma um documento em uma lista de pedaços. Cada pedaço vai virar uma unidade de recuperação: é ele que a busca devolve, e é ele que o modelo lê. Um corte que separa uma cláusula da exceção dela produz uma resposta que está tecnicamente no texto e mesmo assim errada.
3. Embutir passa cada pedaço por um modelo de embedding e recebe um vetor de algumas centenas ou milhares de números. Essa chamada é a única do pipeline que você paga por volume de corpus e não por volume de pergunta.
4. Indexar é guardar o vetor ao lado do texto original e do metadado: arquivo de origem, seção, data, permissão. Guardar só o vetor funciona até a primeira vez que alguém pergunta “de onde saiu isso?”.
5. Buscar embute a pergunta pelo mesmo modelo e procura os vetores mais próximos. “Mesmo modelo” não é detalhe: vetores de modelos diferentes não são comparáveis, mesmo com a mesma dimensão.
6. Montar concatena os trechos recuperados, a pergunta e a instrução em um prompt. É a peça mais barata de mudar e a mais ignorada.
7. Gerar é a chamada ao modelo. É a única etapa que a maioria dos times observa, e é a que menos explica o resultado.
O núcleo, em quarenta linhas
Duas funções ficam de fora porque dependem do seu provedor: embutir, que
devolve um vetor para um texto, e gerar, que devolve texto para um prompt. O
resto é isto:
import glob, json
import numpy as np
def cortar(texto, alvo=1200, overlap=200):
partes, i = [], 0
while i < len(texto):
partes.append(texto[i:i + alvo])
i += alvo - overlap
return partes
docs = []
for caminho in glob.glob("base/**/*.md", recursive=True):
for n, pedaco in enumerate(cortar(open(caminho).read())):
docs.append({"arquivo": caminho, "n": n, "texto": pedaco})
V = np.array([embutir(d["texto"]) for d in docs], dtype="float32")
V /= np.linalg.norm(V, axis=1, keepdims=True)
np.save("indice.npy", V)
json.dump(docs, open("indice.json", "w"))
def buscar(pergunta, k=5):
q = np.array(embutir(pergunta), dtype="float32")
q /= np.linalg.norm(q)
return [docs[i] for i in np.argsort(-(V @ q))[:k]]
def responder(pergunta):
trechos = buscar(pergunta)
contexto = "\n\n".join(f"[{d['arquivo']}#{d['n']}]\n{d['texto']}" for d in trechos)
return gerar(
"Responda usando apenas os trechos abaixo. Cite o identificador entre "
"colchetes ao lado de cada afirmação. Se os trechos não responderem, "
"diga que não encontrou.\n\n"
f"{contexto}\n\nPergunta: {pergunta}"
)
Os 1.200 caracteres do corte equivalem a algo perto de 300 tokens em português.
A normalização antes de np.save é o que permite trocar cosseno por um produto
escalar: com vetores de norma 1, os dois dão o mesmo número, e V @ q compara a
pergunta com o corpus inteiro numa multiplicação de matriz só.
Isso é um RAG completo. Falta tudo que produção exige — lote nas chamadas de
embedding, retentativa, reindexação incremental do que mudou, filtro de permissão
antes do argsort — e nada disso muda a arquitetura. São camadas em volta das
mesmas sete peças.
As quatro decisões que definem a qualidade
Onde o texto é cortado. O corte por número de caracteres do exemplo acima é o pior possível e o certo para começar: ele é previsível e serve de linha de base para você medir se cortar por seção melhora. Corte por estrutura sempre que houver estrutura — título, cláusula, função — e por tamanho só no que sobrar.
Qual modelo de embedding. É a decisão mais cara de reverter, porque trocar o modelo invalida o índice inteiro. Vale gastar meia hora comparando dois ou três antes de indexar 200 mil chunks, e vale registrar o nome e a versão do modelo dentro do arquivo de índice. Índice sem essa etiqueta é uma bomba-relógio: um dia alguém troca o modelo, o pipeline não reclama e a busca passa a devolver ruído.
Quantos trechos entram e em que ordem. Cinco é um começo razoável. Subir para vinte “por segurança” costuma piorar, porque o que entra a mais é o quase-relevante — o trecho que fala do assunto certo sem conter a resposta. Um estudo de 2024 que varreu combinações de etapas do pipeline mediu justamente essa troca entre desempenho e eficiência, e concluiu que existe um ponto em que acrescentar recuperação para de pagar2.
A ordem é a metade esquecida dessa decisão. O argsort devolve os trechos do
mais próximo ao mais distante e a maioria dos pipelines concatena nessa ordem,
sem pensar. Só que o modelo não lê o contexto de forma uniforme: um trabalho de
2023 mediu o desempenho movendo a informação relevante pelas posições do prompt e
encontrou queda significativa quando ela fica no meio, inclusive em modelos
anunciados como de contexto longo3. Com cinco trechos isso é secundário. Com
vinte, o quinto melhor caiu exatamente onde o modelo menos olha.
O que o prompt manda fazer quando não há trecho bom. Se a instrução não diz o que fazer com uma busca vazia, o modelo preenche. É uma linha de texto e é a diferença entre “não encontrei essa informação na base” e um número inventado com a mesma cara de um número correto.
As duas primeiras obrigam a reprocessar o corpus. As duas últimas você muda entre dois cafés. Isso não significa que as baratas importem menos — significa que você deve esgotar as baratas antes de mexer nas caras.
Quando o arquivo de vetores deixa de servir
A busca linear do exemplo compara a pergunta com todos os vetores. Cinquenta mil chunks de 1.536 dimensões são 77 milhões de multiplicações por pergunta, o que em numpy fica na casa das dezenas de milissegundos num laptop — ordem de grandeza, não medição. Para uma base interna com dezenas de usuários, isso acabou.
O ponto de virada não costuma ser a latência. É a memória do processo, o filtro por metadado e a escrita concorrente. Uma matriz de 500 mil vetores em float32 ocupa cerca de 3 GB, e nesse ponto carregá-la em cada worker deixa de fazer sentido.
Quando esse dia chegar, o que entra no lugar é um índice aproximado. O HNSW, que é o algoritmo por trás de boa parte dos bancos vetoriais atuais, constrói um grafo de proximidade em camadas e busca do topo para baixo, com custo que escala de forma logarítmica em vez de linear4. “Aproximado” é literal: você troca recall por velocidade, e o parâmetro que controla essa troca precisa ser medido com as suas perguntas, não aceito no padrão.
Vale registrar que busca densa não é a única opção nem sempre a melhor sozinha. O DPR, o trabalho de 2020 que tornou razoável apostar em vetor, superou o BM25 em 9 a 19 pontos absolutos de acurácia no top-205 — mas isso foi medido em perguntas de domínio aberto, com linguagem natural. Em corpus cheio de código de peça, número de norma e nome próprio, a busca por palavra-chave costuma ganhar do vetor nos casos exatos, e combinar as duas é o que resolve.
Onde falha
Um relatório de experiência de 2024 acompanhou três sistemas de RAG em produção, em pesquisa, educação e área biomédica, e catalogou sete pontos de falha recorrentes. As duas conclusões que os autores destacaram valem mais que a lista: validar um RAG só é possível em operação, e a robustez do sistema evolui, não é projetada no começo6. Quem espera acertar a arquitetura no papel antes de ver perguntas reais está resolvendo o problema errado.
O que isso significa na prática é que as falhas do RAG não têm log. Elas chegam como resposta bem escrita:
O corte separou a cláusula da exceção. O trecho recuperado está correto e incompleto. A resposta afirma a regra e omite o “salvo quando”, e nada no sistema sabe que faltou algo.
A busca não trouxe o trecho certo. O modelo responde com o que veio, e responde bem. Esse é o modo de falha que faz um RAG continuar alucinando mesmo com a base correta indexada.
O índice envelheceu. O documento mudou, a reindexação falhou calada. A atualidade é o principal argumento do RAG e é a primeira coisa a quebrar, porque o job que a mantém é o que ninguém monitora.
A permissão sumiu. O índice achata a estrutura de acesso dos arquivos. Sem metadado de permissão e sem filtro antes do corte do top-k, você construiu um canal de vazamento com interface de chat.
O modelo ignorou o trecho. Nada obriga o modelo a usar o que você entregou. Quando o texto recuperado contradiz o que ele aprendeu no treino — uma política interna que diverge do padrão do mercado, um nome de campo que significa outra coisa na sua empresa — o resultado depende de qual dos dois sinais é mais forte. Não existe garantia de que seja o seu, e o sintoma é uma resposta que mistura as duas versões sem avisar.
Quanto custa
Ordem de grandeza, para uma base de suporte com 1.200 artigos, algo como 3 milhões de caracteres.
Indexar dá cerca de 3 mil chunks de 300 tokens, ou 900 mil tokens de embedding. É a chamada mais barata do catálogo, uma ordem de grandeza abaixo da geração, e você paga quando o documento muda — não quando alguém pergunta.
Guardar são 3 mil vetores de 1.536 dimensões em float32: cerca de 18 MB. Cabe num arquivo, cabe em memória, cabe em Postgres. Não é aqui que o projeto trava.
Consultar é o custo que se repete: cinco trechos de 300 tokens são 1.500 tokens de contexto por pergunta, mais a pergunta e a instrução. O embedding da pergunta é ruído na conta. O que decide a viabilidade é o número de perguntas por dia multiplicado pelo tamanho do contexto, e é por isso que subir o top-k de 5 para 20 aparece na fatura antes de aparecer na qualidade.
Por onde começar
- Escreva trinta perguntas reais antes de escrever código. Cada uma com a resposta esperada e o arquivo de onde ela deveria sair. Sem isso, toda mudança no pipeline é palpite.
- Corte por caractere, com overlap, e siga em frente. Otimizar o corte antes de ter a busca funcionando é otimizar no escuro.
- Guarde o texto e o metadado junto com o vetor, desde o primeiro dia. Reindexar para acrescentar um campo que você esqueceu é o retrabalho mais evitável dessa lista.
- Meça só a busca. Rode as trinta perguntas e conte em quantas o trecho certo apareceu no top-5. Esse número é o teto do seu sistema: nenhum prompt recupera o que a busca não trouxe.
- Escreva a instrução de abstenção e a de citação no mesmo prompt. As duas juntas, porque citar sem poder se abster empurra o modelo a citar o trecho errado.
- Só então considere reranker, busca híbrida ou banco vetorial. Cada um resolve um problema específico, e você precisa saber qual dos três você tem.
O passo 4 é o que separa quem melhora um RAG de quem troca de biblioteca a cada duas semanas.
Footnotes
-
Lewis et al. (2020) treinavam gerador e recuperador em conjunto e chamaram o índice de memória não paramétrica — a arquitetura que popularizou o termo não é a que a maioria monta hoje. ↩
-
Wang et al. (2024) varreram combinações de etapas do pipeline de RAG procurando o equilíbrio entre desempenho e eficiência, e mostraram que acrescentar recuperação tem um ponto em que deixa de compensar. ↩
-
Liu et al. (2023) mediram a queda de desempenho conforme a informação relevante muda de posição no contexto: melhor no começo e no fim, pior no meio, inclusive em modelos de contexto longo. ↩
-
Malkov e Yashunin (2016) descrevem o HNSW, um grafo de proximidade em camadas cuja busca escala de forma logarítmica com o tamanho do índice. ↩
-
Karpukhin et al. (2020) mostraram que recuperação densa treinada com pares de pergunta e passagem supera o BM25 em 9 a 19 pontos absolutos de acurácia no top-20, em perguntas de domínio aberto. ↩
-
Barnett et al. (2024) catalogaram sete pontos de falha a partir de três casos em produção e concluíram que validar um RAG só é viável em operação, e que a robustez evolui em vez de ser projetada no início. ↩
Perguntas frequentes
- Quais são os componentes de um RAG?
- Sete: um carregador que extrai texto do arquivo, um cortador que divide em chunks, um modelo de embedding, um lugar para guardar os vetores, uma busca por proximidade, um montador de prompt e o modelo que gera. Os quatro primeiros rodam offline; os três últimos rodam a cada pergunta.
- Dá para montar um RAG sem framework?
- Dá, e é a forma recomendada de começar. O núcleo são duas chamadas de API — uma de embedding, uma de geração — mais um produto escalar em numpy. Frameworks entram depois, quando você já sabe qual peça quer trocar e por quê. Antes disso eles escondem exatamente o que você precisa medir.
- Como fazer um RAG em Python passo a passo?
- Leia os arquivos, corte cada um em pedaços de mil e poucos caracteres, embede cada pedaço e guarde a matriz com np.save. Na pergunta, embede a pergunta, normalize os vetores, multiplique a matriz pelo vetor da pergunta, pegue os cinco maiores e cole o texto deles no prompt com a instrução de citar a origem.
- Preciso de um banco vetorial para começar?
- Não. Abaixo de algumas dezenas de milhares de chunks, um array numpy em disco responde em dezenas de milissegundos e não tem infraestrutura para manter. Banco vetorial passa a valer quando você precisa de filtro por metadado, escrita concorrente ou índice grande demais para caber na memória do processo.
- Qual a ordem certa de construir as peças?
- Comece pelo fim. Escreva primeiro trinta perguntas reais com a resposta esperada e o trecho que deveria ser recuperado. Depois monte a busca e meça se o trecho certo aparece no top-5. Só então cuide do prompt. Quem monta na ordem do pipeline descobre o problema no final, sem instrumento para localizá-lo.
- O que muda quando o corpus cresce?
- Três coisas, e não ao mesmo tempo. Primeiro a busca linear fica lenta e você troca por um índice aproximado. Depois a memória do processo não segura a matriz e o índice vai para fora. Por último a reindexação deixa de caber numa rodada só e vira job incremental por documento alterado.
Referências
- Lewis, P. et al.. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (2020)arXiv:2005.11401
- Karpukhin, V. et al.. Dense Passage Retrieval for Open-Domain Question Answering (2020)arXiv:2004.04906
- Malkov, Yu. A. e Yashunin, D. A.. Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs (2016)arXiv:1603.09320
- Barnett, S. et al.. Seven Failure Points When Engineering a Retrieval Augmented Generation System (2024)arXiv:2401.05856
- Wang, X. et al.. Searching for Best Practices in Retrieval-Augmented Generation (2024)arXiv:2407.01219
- Liu, N. F. et al.. Lost in the Middle: How Language Models Use Long Contexts (2023)arXiv:2307.03172