Pular para o conteúdo
mnzes

Como montar um RAG do zero?

PorDiógenes MenezesAprendendo IA em público

13 min de leitura

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.

indexação · uma vez, e de novo quando o documento muda1 · documento bruto2 · chunks de ~300 tokens3 · um vetor por chunk4 · índice + metadadoconsulta · a cada pergunta5 · pergunta virada em vetor6 · top-5 trechos7 · resposta com citaçãovizinhos mais próximos
Figura 1As quatro primeiras peças rodam antes de existir pergunta; as três últimas rodam a cada uma. O índice é o único ponto em que as duas tiras se tocam.

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 é cortadoe de que tamanhoqual modelode embeddingquantos trechos entrame em que ordemo que o prompt mandafazer sem trecho bomqualidadeda respostareindexa tudoreindexa tudomuda um númeromuda um textoas quatro escolhas, e o que custa desfazer cada uma
Figura 2As quatro escolhas pesam igual na resposta e nada igual no bolso: duas obrigam a reprocessar o corpus inteiro, duas você desfaz editando uma linha.

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

onde quebrao que aparece na respostao corte separou a cláusulada exceção delaa resposta cita metadeda regra e soa completaa busca não trouxeo trecho certotexto fluente, fato errado,nenhum avisotop-k alto demaisresposta genérica: o trechobom ficou no meioo prompt não mandaabstero modelo preencheo que faltou
Figura 3Nenhuma dessas falhas aparece como erro. Todas aparecem como uma resposta bem escrita, e é por isso que o log de exceção não ajuda a achar a causa.

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

  1. 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.
  2. Corte por caractere, com overlap, e siga em frente. Otimizar o corte antes de ter a busca funcionando é otimizar no escuro.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. Lewis, P. et al.. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (2020)arXiv:2005.11401
  2. Karpukhin, V. et al.. Dense Passage Retrieval for Open-Domain Question Answering (2020)arXiv:2004.04906
  3. Malkov, Yu. A. e Yashunin, D. A.. Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs (2016)arXiv:1603.09320
  4. Barnett, S. et al.. Seven Failure Points When Engineering a Retrieval Augmented Generation System (2024)arXiv:2401.05856
  5. Wang, X. et al.. Searching for Best Practices in Retrieval-Augmented Generation (2024)arXiv:2407.01219
  6. Liu, N. F. et al.. Lost in the Middle: How Language Models Use Long Contexts (2023)arXiv:2307.03172