Pular para o conteúdo
Categoria: Desenvolvendo com IA14 min de leitura

"RAG na prática: dê memória e contexto ao seu LLM"

Por Lucas Andrade ·

Implemente Retrieval-Augmented Generation para responder com base nos seus próprios documentos, reduzir alucinações, manter o modelo atualizado e medir a qualidade do pipeline.

"RAG na prática: dê memória e contexto ao seu LLM"

Um modelo de linguagem sozinho só sabe o que viu durante o treinamento — ele não conhece os seus documentos, sua política interna ou o produto que você lançou ontem. RAG (Retrieval-Augmented Generation) resolve isso conectando o modelo a uma base de conhecimento que você controla. Neste guia prático você vai entender o fluxo completo de um sistema RAG, desde a indexação dos documentos até a geração da resposta, com as decisões, o código e as armadilhas que aparecem na vida real.

O problema que o RAG resolve

LLMs têm duas limitações incômodas. Primeiro, o conhecimento congelado: o que não estava nos dados de treino, o modelo não sabe. Segundo, a alucinação: quando não sabe, o modelo frequentemente inventa uma resposta confiante e errada.

RAG ataca os dois de uma vez. Em vez de confiar só na "memória" interna do modelo, você busca trechos relevantes de uma base externa e os injeta no prompt antes de pedir a resposta. O modelo passa a responder com base em fatos que você forneceu. A técnica foi formalizada por Lewis et al. (2020), que combinaram um recuperador de documentos com um gerador e demonstraram ganhos consistentes em tarefas que dependem de conhecimento específico. Se você ainda não tem o conceito firme, vale ler antes o que é RAG (Retrieval-Augmented Generation)?.

Há um terceiro ganho que costuma decidir a adoção em empresas: rastreabilidade. Como a resposta vem de trechos concretos, você pode citar a fonte de cada afirmação. Num suporte técnico ou num assistente jurídico, poder apontar "isto veio do documento X, seção Y" é a diferença entre uma ferramenta confiável e um gerador de texto bonito.

A anatomia de um sistema RAG

Todo sistema RAG tem duas fases. A fase de indexação acontece offline, uma vez (e a cada atualização dos documentos). A fase de consulta acontece a cada pergunta do usuário.

INDEXAÇÃO (offline)
documentos → divisão em pedaços → embeddings → banco vetorial

CONSULTA (a cada pergunta)
pergunta → embedding → busca por similaridade → trechos relevantes
        → prompt (pergunta + trechos) → LLM → resposta

Vamos destrinchar cada parte.

Passo 1: divida os documentos em pedaços (chunking)

Você não joga documentos inteiros na busca — divide em pedaços menores, os chunks. Por quê? Porque você quer recuperar só os trechos relevantes, não páginas inteiras de ruído. O tamanho do chunk é uma decisão delicada:

  • Chunks muito pequenos perdem contexto e fragmentam ideias.
  • Chunks muito grandes trazem ruído e gastam tokens.

Um ponto de partida comum é dividir por parágrafos ou em blocos de algumas centenas de tokens, com uma pequena sobreposição entre chunks vizinhos para não cortar uma ideia no meio. Ajuste conforme o seu conteúdo.

Estratégias de chunking

Nem todo documento se divide bem do mesmo jeito. Vale conhecer as abordagens:

  • Tamanho fixo com sobreposição: simples e robusto. Corte a cada N tokens com uma sobreposição de 10–20%. É o padrão para começar.
  • Por estrutura: respeite a hierarquia natural do documento (títulos, seções, parágrafos). Funciona muito bem para manuais e bases técnicas com Markdown ou HTML.
  • Semântico: quebre onde o assunto muda, detectando saltos de similaridade entre frases. Mais caro de computar, mas produz chunks mais coerentes.

Um cuidado prático: preserve metadados junto de cada chunk (título do documento, seção, URL, data). Eles servem tanto para filtrar a busca quanto para citar a fonte na resposta.

def dividir_em_chunks(texto: str, tamanho: int = 500, sobreposicao: int = 75) -> list[str]:
    palavras = texto.split()
    chunks = []
    passo = tamanho - sobreposicao
    for inicio in range(0, len(palavras), passo):
        pedaco = palavras[inicio:inicio + tamanho]
        if pedaco:
            chunks.append(" ".join(pedaco))
    return chunks

Passo 2: transforme texto em embeddings

Cada chunk vira um vetor numérico que representa seu significado — um embedding. Textos com significado parecido geram vetores próximos no espaço, o que permite busca por similaridade semântica em vez de busca por palavra exata. Esse é o coração do RAG. Se a ideia de "significado virar número" ainda é nebulosa, leia o que são embeddings? Representando significado em vetores.

from openai import OpenAI

client = OpenAI()

def gerar_embedding(texto: str) -> list[float]:
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=texto,
    )
    return resp.data[0].embedding

Você gera embeddings para todos os chunks na indexação e, depois, para cada pergunta na consulta.

Duas regras de ouro aqui. Primeiro, use o mesmo modelo de embedding na indexação e na consulta — vetores de modelos diferentes não são comparáveis. Segundo, se você trocar o modelo de embedding, reindexe tudo: os vetores antigos viram lixo no novo espaço. Por isso a escolha do modelo de embedding não é trivial e vale testar alguns antes de comprometer a base inteira.

Passo 3: guarde os vetores em um banco vetorial

Com milhares de embeddings, você precisa de uma estrutura que faça busca por similaridade rápido. É o papel do banco de dados vetorial, que indexa os vetores e responde "quais são os k mais parecidos com este?" em milissegundos. Entenda as opções em o que é um banco de dados vetorial?.

A busca por similaridade no RAG é um caso de recuperação densa (dense retrieval): em vez de casar palavras, você compara vetores. Karpukhin et al. (2020) mostraram que essa abordagem supera a busca por palavras-chave tradicional em perguntas de domínio aberto, porque captura significado e não só termos literais.

Vale entender o trade-off do índice. Bancos vetoriais usam algoritmos de busca aproximada de vizinhos (ANN, como HNSW) para serem rápidos em escala — o preço é abrir mão de uma fração da precisão em troca de muita velocidade. Para a maioria das aplicações, esse trade é excelente; só em bases pequenas faz sentido uma busca exata.

Passo 4: recupere os trechos relevantes

Na hora da pergunta, o fluxo é:

  1. Gere o embedding da pergunta.
  2. Busque os k chunks mais similares no banco vetorial (top-k).
  3. Reúna esses trechos como contexto.

O valor de k é um ajuste: poucos trechos podem deixar de fora a resposta; trechos demais diluem o contexto e elevam o custo. Comece com algo entre 3 e 5 e calibre.

def recuperar(pergunta: str, k: int = 4) -> list[str]:
    emb = gerar_embedding(pergunta)
    resultados = banco_vetorial.buscar(vetor=emb, top_k=k)
    return [r.texto for r in resultados]

Indo além do top-k simples: busca híbrida e reranking

Quando a recuperação por embedding falha, costuma ser por dois motivos: a pergunta usa um termo exato (um código de produto, um nome próprio, uma sigla) que a busca semântica não prioriza, ou os top-k trazem trechos parecidos mas não os mais úteis. Duas técnicas atacam isso:

  • Busca híbrida: combine a recuperação densa (vetores) com uma busca esparsa por palavra-chave (como BM25). A densa pega significado; a esparsa garante o casamento de termos literais. Some os scores das duas.
  • Reranking: recupere um conjunto maior (digamos, top-20) e passe-o por um modelo cross-encoder que pontua cada par pergunta-trecho com mais precisão, ficando só com os melhores. Custa mais por consulta, mas melhora muito a relevância final.

Essas duas adições são, na prática, o que mais diferencia um RAG "de demo" de um RAG que aguenta perguntas reais.

Passo 5: monte o prompt e gere a resposta

Agora você combina os trechos recuperados com a pergunta num prompt bem estruturado. A instrução-chave é ancorar o modelo no contexto e permitir que ele admita ignorância.

def responder(pergunta: str) -> str:
    trechos = recuperar(pergunta)
    contexto = "\n\n".join(trechos)
    prompt = (
        "Use APENAS o contexto abaixo para responder. "
        "Se a resposta não estiver no contexto, diga que não sabe.\n\n"
        f"Contexto:\n{contexto}\n\n"
        f"Pergunta: {pergunta}"
    )
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

Essa instrução de "use apenas o contexto" é o que mais reduz alucinação — assunto que aprofundo em o que é alucinação em IA e como reduzi-la.

Para tornar a resposta rastreável, vale incluir um identificador em cada trecho e pedir que o modelo cite a fonte:

contexto = "\n\n".join(f"[Fonte {i+1}] {t}" for i, t in enumerate(trechos))
# e na instrução: "Cite a fonte entre colchetes ao usar uma informação."

Assim a resposta vem com "[Fonte 2]" ao lado de cada afirmação, e o usuário consegue verificar.

Reescrevendo a pergunta antes de buscar

Um ganho de qualidade que muita gente esquece está antes da busca: a pergunta do usuário nem sempre é o melhor texto para gerar o embedding de consulta. Perguntas reais são curtas, cheias de pronomes e dependentes do histórico da conversa. "E o segundo caso?" não tem como recuperar nada útil sozinho.

Duas técnicas resolvem isso:

  • Reescrita de consulta (query rewriting): use um LLM para transformar a pergunta crua, junto com o histórico, em uma consulta autossuficiente. "E o segundo caso?" vira "Quais são os requisitos do segundo caso de uso do produto X?", que recupera muito melhor.
  • Expansão de consulta: gere algumas variações da pergunta e busque por todas, unindo os resultados. Isso aumenta a chance de casar com a forma como a informação foi escrita nos documentos.
def reescrever_consulta(pergunta: str, historico: str) -> str:
    prompt = (
        "Reescreva a pergunta abaixo como uma consulta de busca "
        "autossuficiente, incorporando o contexto do histórico.\n\n"
        f"Histórico:\n{historico}\n\nPergunta: {pergunta}\n\nConsulta:"
    )
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

Essa etapa custa uma chamada extra ao modelo, mas em sistemas conversacionais costuma ser a diferença entre uma recuperação que funciona e uma que falha silenciosamente.

Por que o RAG vence o fine-tuning em muitos casos

Uma dúvida frequente: por que não treinar o modelo nos meus dados? RAG costuma ser melhor quando:

  • O conhecimento muda com frequência. Basta atualizar a base; nada de retreinar.
  • Você precisa de rastreabilidade. RAG mostra a fonte de cada resposta.
  • O orçamento é limitado. Indexar documentos é muito mais barato que fine-tuning.
  • Os dados são sensíveis. Você controla o acesso à base em tempo de consulta.

Fine-tuning ainda tem seu lugar (ensinar formato, tom, tarefas específicas), mas para "responder com base nos meus documentos", RAG quase sempre é o caminho. Os dois também não são excludentes: você pode usar fine-tuning para fixar o estilo das respostas e RAG para alimentar os fatos atualizados.

Onde os sistemas RAG falham

Conhecer as falhas comuns economiza semanas de frustração:

  • Recuperação ruim. Se os trechos certos não são encontrados, a resposta vem errada — e o modelo nem percebe. Qualidade do RAG = qualidade da recuperação.
  • Chunking malfeito. Pedaços que cortam ideias no meio quebram a busca.
  • Contexto excessivo. Enfiar trechos demais dilui a relevância e encarece.
  • Documentos desatualizados. A base precisa de manutenção; lixo entra, lixo sai.
  • Pergunta ambígua. Se o usuário pergunta mal, o embedding da pergunta busca a coisa errada.

Como depurar quando a resposta vem errada

Quando um RAG erra, o reflexo de muitos é mexer no modelo gerador — quase sempre o lugar errado. Depure na ordem do pipeline:

  1. Olhe os trechos recuperados. Os chunks certos estavam no top-k? Se não, o problema é recuperação (chunking, embedding ou k), não geração.
  2. Se os trechos estavam certos mas a resposta não, o problema é o prompt ou o modelo: a instrução de ancoragem está clara? O modelo está "vazando" conhecimento próprio em vez de usar o contexto?
  3. Se nem existem chunks certos na base, o problema é a ingestão: o documento foi indexado? O chunking preservou a informação?

Separar "falha de recuperação" de "falha de geração" é a habilidade de depuração mais importante em RAG.

Medindo a qualidade

Não confie no "parece que está bom". Avalie em duas frentes:

  • Recuperação: dado um conjunto de perguntas, os trechos certos aparecem no top-k? (métricas como recall@k).
  • Geração: a resposta final é correta, fundamentada e sem invenções?

Monte um conjunto de perguntas de teste com respostas conhecidas e rode-o sempre que mudar chunking, modelo de embedding ou k.

Na avaliação da geração, três dimensões são úteis de medir separadamente:

  • Fidelidade (faithfulness): a resposta se sustenta nos trechos recuperados, sem inventar?
  • Relevância da resposta: ela de fato responde à pergunta feita?
  • Relevância do contexto: os trechos recuperados eram pertinentes, ou trouxeram ruído?

Você pode automatizar parte dessa avaliação usando um LLM como juiz, comparando resposta e contexto — desde que valide o juiz contra um conjunto rotulado por humanos antes de confiar nele.

Mantendo a base saudável ao longo do tempo

Um RAG não é um projeto que você entrega e esquece — é um sistema vivo que degrada se a base não for cuidada. Alguns hábitos de manutenção evitam o apodrecimento silencioso:

  • Versione e date os documentos. Guarde metadados de origem e data em cada chunk. Quando uma política muda, você consegue remover ou substituir só os chunks afetados, em vez de reindexar tudo às cegas.
  • Trate duplicatas e contradições. Se duas versões de um mesmo documento convivem na base, a recuperação pode trazer informação contraditória, e o modelo escolhe uma ao acaso. Deduplique na ingestão.
  • Monitore as perguntas sem resposta. Logue os casos em que o modelo disse "não sei" ou em que a recuperação trouxe trechos irrelevantes. Esse log é um mapa direto das lacunas da sua base.
  • Reavalie periodicamente. Rode o conjunto de testes de tempos em tempos, não só quando muda código. A própria base mudando pode degradar métricas que antes estavam boas.

A frase "lixo entra, lixo sai" vale em dose dupla para RAG: a qualidade da resposta nunca supera a qualidade e a organização dos documentos que você indexou.

Segurança e privacidade no pipeline

RAG levanta questões de segurança que valem atenção desde o início. Como você injeta conteúdo recuperado no prompt, sua aplicação fica exposta a prompt injection indireto: um documento malicioso na base pode conter instruções ocultas que o modelo segue ao processá-lo. Trate o conteúdo recuperado como dado não confiável e delimite-o claramente no prompt.

Há também a dimensão de controle de acesso. Se diferentes usuários têm permissão para ver diferentes documentos, a busca precisa respeitar isso. O padrão é filtrar por permissão no momento da recuperação — usando metadados de cada chunk — para que um usuário nunca receba, no contexto, um trecho que ele não poderia ler. Aplicar o filtro só na resposta final é tarde demais: o dado sensível já passou pelo modelo.

Perguntas frequentes

Qual o melhor tamanho de chunk? Não existe número universal. Para texto corrido, blocos de algumas centenas de tokens com 10–20% de sobreposição funcionam bem como ponto de partida. Documentos muito estruturados pedem chunking por seção. Meça com o seu conteúdo e ajuste.

Posso usar RAG sem banco vetorial? Em bases pequenas (poucas centenas de chunks), uma busca exata em memória, calculando a similaridade contra todos os vetores, resolve. O banco vetorial passa a valer a pena quando o volume cresce e a latência importa.

RAG elimina alucinação? Reduz drasticamente, mas não elimina. Se a recuperação falha ou o prompt não ancora bem o modelo, ele ainda pode inventar. Por isso a instrução de "use apenas o contexto" e a medição de fidelidade são essenciais.

Preciso reindexar quando adiciono um documento novo? Só o documento novo: gere os chunks e os embeddings dele e insira no banco. Reindexação completa só é necessária quando você troca o modelo de embedding ou a estratégia de chunking.

Conclusão

RAG é a forma mais prática e econômica de dar a um LLM conhecimento próprio, atualizado e rastreável. O segredo é entender que a qualidade do sistema depende sobretudo da recuperação: chunking bem-feito, bons embeddings, um banco vetorial eficiente, técnicas como busca híbrida e reranking, e um k calibrado. Com a recuperação certa e um prompt que ancora o modelo no contexto, você reduz alucinações drasticamente e ganha um assistente que responde sobre o que importa para você. Depure na ordem do pipeline, meça com rigor as dimensões de fidelidade e relevância, e refine o pipeline antes de culpar o modelo — é nele, não no gerador, que mora o ganho.

Referências

  • Karpukhin, V., Oğuz, B., Min, S., Lewis, P., Wu, L., Edunov, S., Chen, D., & Yih, W. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP.
  • Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly