Pular para o conteúdo
IAIntermediário

RAG na prática: monte seu primeiro retriever em Python

Aprenda RAG na prática em Python: chunking, embeddings, busca no Chroma e prompt final, mais os erros que fazem o retriever devolver lixo.

Por Equipe IAUAI Estudos · 1 de agosto de 2026 · 11 min de leitura

Nesta página

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

Aprenda jogando

Caça-Tokens

Combine tokens para liberar espaço antes que a janela de contexto estoure.

Jogar Caça-Tokens

Teste seu conhecimento

Teste o que você aprendeu sobre RAG

Pergunta 1 de 5

Seu retriever devolve um trecho que corta no meio de uma frase importante, perdendo contexto. Qual é a causa mais provável?

Um conteúdo prático por quinzena

Deixe o seu e-mail para receber um conteúdo prático de IA e cloud a cada quinzena. Sem spam, e você pede a exclusão quando quiser.

Continue lendo