O problema real
Você descobriu RAG e achou a solução perfeita: em vez de treinar um modelo com mil documentos, recupera os trechos certos na hora e coloca no prompt. Na primeira semana em produção, o retriever começa a devolver coisas sem relação. O usuário pergunta "qual é a política de devolução?" e o sistema responde com um guia de política fiscal. Quase sempre a causa está no corte dos documentos, no modelo de embedding ou na métrica de distância, e ninguém olhou para nenhum dos três.
RAG (Retrieval-Augmented Generation) é simples na teoria. Você indexa os documentos, busca os mais parecidos com a pergunta, monta um prompt com eles e envia ao modelo. Ao terminar este artigo você terá um pipeline completo rodando e saberá onde ele costuma quebrar.
Como montar um pipeline RAG que recupera documentos relevantes
O pipeline tem cinco etapas: chunking (dividir o texto), embeddings, armazenamento vetorial, busca e montagem do prompt. Para rodar os exemplos, instale pip install tiktoken openai chromadb sentence-transformers e defina OPENAI_API_KEY. Os trechos abaixo formam um único script, na ordem em que aparecem.
Etapa 1: chunking
Documento inteiro num único vetor dilui o significado. Por isso dividimos em pedaços, os chunks. Não existe tamanho universal. Algo entre 200 e 800 tokens com sobreposição de 10% a 20% é um ponto de partida comum, mas você precisa validar com as suas perguntas. A sobreposição garante que uma frase cortada na fronteira apareça inteira em pelo menos um chunk. O artigo Chunking: as estratégias que funcionam compara as alternativas.
import tiktoken
def chunk_text(text: str, chunk_size: int = 512, overlap: float = 0.2) -> list[str]:
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(text)
stride = chunk_size - int(chunk_size * overlap)
chunks = []
for i in range(0, len(tokens), stride):
chunks.append(enc.decode(tokens[i : i + chunk_size]))
if i + chunk_size >= len(tokens):
break
return chunks
Para o restante do exemplo, usaremos chunks curtos escritos à mão, assim você vê o resultado sem precisar de um corpus.
Etapa 2: embeddings
Um embedding é um vetor que representa o significado do texto. "Qual é a política de devolução?" e "como faço para devolver?" ficam próximos no espaço vetorial. Você tem três caminhos comuns.
- Uma API de embeddings, como a da OpenAI (
text-embedding-3-small, 1.536 dimensões por padrão). É a opção mais simples para começar. - Um modelo local via Ollama, como o
nomic-embed-text. Os dados não saem da sua máquina. Veja Rodando um LLM local com Ollama. - Um modelo aberto do Hugging Face, de preferência multilíngue, porque o seu conteúdo está em português.
Embeddings custam pouco perto da geração de texto, mas confira a tabela de preços do provedor na hora de planejar, porque ela muda.
from openai import OpenAI
client_ai = OpenAI()
def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
resp = client_ai.embeddings.create(model=model, input=texts)
return [item.embedding for item in resp.data]
Etapa 3: armazenamento vetorial
Você precisa de um lugar que busque por proximidade de vetores. Para aprender, o Chroma roda em memória sem configuração. Para produção, veja Banco vetorial na prática, onde usamos o pgvector. Um detalhe importante: o Chroma usa distância L2 por padrão. Como embeddings de texto costumam ser comparados por cosseno, configure isso na criação da coleção.
import chromadb
chroma = chromadb.Client()
colecao = chroma.create_collection(
name="documentos",
configuration={"hnsw": {"space": "cosine"}},
)
docs = [
"A política de devolução permite devolver itens em até 30 dias após a compra.",
"O reembolso é integral quando o produto está em bom estado e na embalagem.",
"Produtos usados ou danificados pelo cliente não são aceitos para devolução.",
"Para iniciar uma devolução, abra um chamado pelo formulário do site.",
"Nosso horário de atendimento é de segunda a sexta, das 9h às 18h.",
]
colecao.add(
ids=[f"doc_{i}" for i in range(len(docs))],
embeddings=embed(docs),
documents=docs,
metadatas=[{"fonte": "politica"} for _ in docs],
)
Etapa 4: busca
A busca embeda a pergunta com o mesmo modelo usado nos documentos e recupera os K vizinhos mais próximos. K entre 3 e 5 é um começo razoável.
def buscar(pergunta: str, k: int = 3) -> list[str]:
res = colecao.query(
query_embeddings=embed([pergunta]),
n_results=k,
include=["documents", "distances"],
)
for doc, dist in zip(res["documents"][0], res["distances"][0]):
print(f"distância {dist:.3f} | {doc[:70]}")
return res["documents"][0]
Com cosseno, distância menor significa mais parecido. Olhar esses números ajuda a descobrir onde cortar resultados fracos.
Etapa 5: montagem do prompt
Agora juntamos os trechos e a pergunta, deixando claro que o modelo deve se limitar ao contexto.
def montar_prompt(pergunta: str, trechos: list[str]) -> str:
contexto = "\n".join(f"- {t}" for t in trechos)
return f"""Você é um assistente de atendimento ao cliente.
Responda usando apenas o contexto abaixo. Se a resposta não estiver nele, diga "não encontrei essa informação".
CONTEXTO
{contexto}
PERGUNTA
{pergunta}"""
pergunta = "Como faço para devolver um produto?"
print(montar_prompt(pergunta, buscar(pergunta)))
Envie esse prompt para o modelo que você usa na geração. O RAG termina aqui, porque o retriever não depende de qual modelo gera a resposta.
Armadilhas comuns
Chunking mal feito
Se o corte cai no meio de uma ideia, o chunk não faz sentido sozinho. Use sobreposição e respeite parágrafos e títulos. Em código, corte por função.
A pergunta não se parece com o documento
O documento diz "política de devolução" e a pessoa escreve "como mando de volta?". Duas saídas ajudam bastante, reescrever a pergunta antes da busca e reordenar os candidatos com um reranker. O padrão é buscar mais (K=20) e deixar o reranker escolher os melhores.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1")
def reordenar(pergunta: str, trechos: list[str], top: int = 3) -> list[str]:
scores = reranker.predict([(pergunta, t) for t in trechos])
ranqueado = sorted(zip(trechos, scores), key=lambda x: x[1], reverse=True)
return [t for t, _ in ranqueado[:top]]
melhores = reordenar(pergunta, buscar(pergunta, k=5))
Não medir a qualidade
Recuperar o trecho certo não garante que o modelo o use. Monte um conjunto de 30 a 50 perguntas reais com a resposta esperada e meça duas coisas. A primeira é com que frequência o trecho correto aparece no top-3 da busca. A segunda é se a resposta final está certa quando ele aparece. Sem isso, qualquer ajuste é palpite. O artigo Avaliação de LLM com evals mostra como estruturar essa medição.
Reembedar a cada consulta
Calcule os embeddings dos documentos uma vez, guarde e só recalcule o que mudou. Embeddar a pergunta a cada busca é inevitável, mas os documentos não.
Misturar modelos de embedding
Vetores gerados por modelos diferentes não são comparáveis. Se trocar o modelo, reindexe tudo.
Quando RAG não é a melhor escolha
- Seu conteúdo cabe inteiro no contexto do modelo e muda pouco. Colocar tudo no prompt é mais simples, e o cache de prompt reduz o custo.
- Você quer mudar o estilo ou o formato das respostas. Isso é trabalho de prompt ou fine-tuning, não de recuperação.
- A pergunta exige raciocínio sobre o corpus inteiro, como "quais são os cinco temas mais citados?". Uma busca por similaridade devolve só alguns trechos.
- A latência é muito apertada. A busca soma uma ou mais chamadas de rede ao fluxo, então meça no seu ambiente.
Se estiver em dúvida, o guia de decisão entre RAG, fine-tuning e prompt ajuda a escolher.
Resumo prático
| Etapa | O que fazer | Cuidado principal |
|---|---|---|
| Chunking | Pedaços coerentes com sobreposição | Não cortar ideias ao meio |
| Embeddings | Um modelo só, multilíngue | Reindexar se trocar de modelo |
| Armazenamento | Chroma para aprender, pgvector ou similar em produção | Configurar a métrica (cosseno) |
| Busca | K de 3 a 5, ou mais com reranker | Olhar as distâncias |
| Prompt | Restringir ao contexto | Admitir quando não sabe |
Teste com perguntas reais do seu domínio antes de ir para produção. Cada corpus traz surpresas, e a única forma de achá-las é medir. Para ver como os tokens afetam o custo, jogue Caça-Tokens. Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria