~ / tutoriais /rag-nao-funciona-causas-e-conserto $ _

Seu RAG não funciona? As causas reais (e o conserto de cada uma)

Lucas Souza Lucas Souza 12 min de leitura Tutoriais
Seu RAG não funciona? As causas reais (e o conserto de cada uma)

Você montou seu RAG, e ele responde qualquer coisa menos o que está nos documentos. Vamos achar onde está vazando.

Porque o sintoma é sempre o mesmo: o usuário pergunta algo que está no PDF, e o modelo responde com confiança total uma informação que não existe em lugar nenhum. Ou pior: responde meia verdade, misturando dois clientes diferentes na mesma frase. Quando seu RAG não funciona, a culpa quase nunca é do LLM. É de uma das quatro etapas antes dele.

Neste post a gente vai destrinchar as causas reais de um RAG retornando lixo — chunking, embedding, falta de reranker e prompt — e o conserto de cada uma. Não é teoria. É o roteiro de debug que você roda quando o agente está em produção mandando resposta errada e o cliente já reclamou.

TL;DR

  • O que é: diagnóstico das causas reais de um RAG que retorna resposta errada, com o conserto de cada uma.
  • As 4 causas: chunking que quebra o sentido, embedding errado pro domínio, ausência de reranker e prompt que ignora o contexto.
  • A regra de ouro: antes de consertar, descubra se o problema é de recuperação (o chunk certo não chegou) ou de geração (chegou e o modelo ignorou). São conserto diferente.
  • Pré-requisito: entender o pipeline básico. Se ainda não tem o mapa mental, comece por O que é RAG.

RAG não funciona? Separe recuperação de geração antes de tudo

Esse é o passo que 90% das pessoas pula, e é o que faz você perder uma tarde inteira mexendo na coisa errada.

Um RAG tem duas metades. A recuperação: pegar a pergunta, virar vetor, buscar os chunks parecidos e devolver os top-k. E a geração: enfiar esses chunks no prompt e pedir pro LLM responder. Quando a resposta sai errada, ela vazou em uma das duas. Você precisa saber em qual antes de tocar em qualquer código.

O teste é bobo e resolve tudo. Pega uma pergunta que você sabe que está no documento. Roda só a busca e olha os chunks que voltaram, sem passar pelo modelo.

# Só a recuperação. Sem LLM no meio.
results = vector_store.similarity_search("qual o prazo de garantia do produto X?", k=5)
for r in results:
    print(r.score, r.page_content[:120])

Duas saídas possíveis:

  • O chunk com a resposta NÃO está nos 5. Problema de recuperação. A culpa é de chunking, embedding ou falta de reranker. O LLM nunca teve chance.
  • O chunk com a resposta ESTÁ nos 5, mas a resposta final continua errada. Problema de geração. A culpa é do prompt.

Pronto. Você acabou de cortar o espaço de busca pela metade. Agora cada causa abaixo tem um endereço.

Causa 1: chunking que despedaça o sentido

Essa é a número um. É onde, na nossa régua, 80% dos RAGs nascem ruins.

O erro clássico é cortar o texto por contagem fixa de caracteres, sem dó. Você define chunk_size=500 e o splitter corta no caractere 500 — no meio de uma frase, no meio de uma tabela, separando a pergunta da resposta. O chunk que sobra é um pedaço sem sentido. Vira um vetor que não representa ideia nenhuma, e na hora da busca ele não casa com pergunta nenhuma.

O segundo erro é o oposto: chunk gigante. Você joga a página inteira num chunk só pra "não perder contexto". Aí o vetor fica diluído — uma página fala de cinco assuntos, o embedding vira uma média morna de tudo, e não se destaca em nada.

O conserto tem três camadas, da mais barata pra mais cara:

  1. Splitter recursivo com overlap. Em vez de cortar no caractere seco, corta respeitando parágrafo → frase → palavra, e deixa as fronteiras se sobreporem. O overlap garante que a frase cortada apareça inteira em pelo menos um dos dois chunks vizinhos.
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=120,          # ~15% de sobreposição
    separators=["\n\n", "\n", ". ", " ", ""],  # tenta a fronteira mais "limpa" primeiro
)
chunks = splitter.split_text(documento)
  1. Chunking semântico. Em vez de tamanho fixo, quebra onde o assunto muda — calcula a similaridade entre frases vizinhas e corta no vale. Mais caro de indexar, melhor pra documento heterogêneo.

  2. Contextualizar o chunk antes de indexar. Essa é a virada de chave que a Anthropic publicou no Contextual Retrieval: antes de gerar o embedding, você prepende a cada chunk uma frase curta que situa ele no documento ("Este trecho é da seção de garantia do contrato do cliente X"). Soa simples. O número não é: contextual embeddings sozinho reduziu a taxa de falha de recuperação em 35% (de 5,7% para 3,7% no top-20). Junto com BM25 contextual, 49%.

Não precisa das três de uma vez. Comece pelo overlap. Se a recuperação melhorar, você achou o vazamento.

Causa 2: embedding errado pro seu domínio

O chunk está bom, mas a busca ainda traz lixo. Hora de olhar o embedding.

O embedding é o que transforma seu texto em vetor. Se o modelo de embedding não entende o vocabulário do seu domínio, ele coloca termos relacionados longe um do outro no espaço vetorial — e a busca por similaridade vira loteria. Três armadilhas comuns:

Modelo genérico em domínio técnico. Um embedding treinado em texto geral da web não sabe que "NF-e" e "nota fiscal eletrônica" são a mesma coisa, nem que "PIX" tem peso num contexto financeiro. Pra domínio muito específico (jurídico, médico, fiscal), vale testar um modelo especializado ou um com dimensão maior.

Idioma. Indexou documento em português com um modelo otimizado pra inglês? A qualidade despenca e ninguém percebe, porque "funciona" — só funciona mal. Use um modelo multilíngue de verdade ou um treinado em PT.

Assimetria entre pergunta e documento — o erro silencioso. A pergunta do usuário ("qual a garantia?") é curta. O chunk é um parágrafo formal. São formatos diferentes, e alguns modelos pedem que você diga qual é qual. Modelos com instruction prefix (tipo os da família E5 ou os mais novos da OpenAI/Cohere) esperam um prefixo distinto pra query e pra documento:

# Query e documento NÃO entram do mesmo jeito quando o modelo é assimétrico
query_vec = embed("query: qual o prazo de garantia do produto X?")
doc_vec   = embed("passage: A garantia do produto X é de 12 meses a contar da nota fiscal.")

Esquecer esse prefixo é um dos bugs mais chatos de achar: tudo parece certo no código, e a recuperação simplesmente rende menos do que deveria. Leia a doc do seu modelo de embedding antes de assumir que query e documento são intercambiáveis. O conserto aqui é quase sempre trocar de modelo e re-indexar — embedding errado não se conserta no prompt.

Causa 3: sem reranker — "parecido" não é "relevante"

Esse é o mais subestimado, e o de melhor retorno por hora de trabalho.

A busca vetorial é rápida e burra. Ela traz os 20 chunks mais parecidos com a pergunta. Mas parecido não é relevante. "Qual o prazo de garantia?" e "a garantia não cobre dano por mau uso" são parecidíssimos no espaço vetorial — falam de garantia, têm as mesmas palavras — mas um responde a pergunta e o outro não. Se você manda os top-5 da busca vetorial direto pro modelo, metade pode ser ruído que parece sinal.

O reranker resolve isso. Ele é um modelo diferente — um cross-encoder — que olha a pergunta e cada candidato juntos e dá uma nota de relevância real. Você busca 20 ou 50 candidatos rápido na vetorial, e o reranker reordena e devolve os 5 que de fato respondem. É o passo que faz seu RAG parar de devolver lixo.

# Estágio 1: busca vetorial larga e barata (recall alto)
candidatos = vector_store.similarity_search(pergunta, k=30)

# Estágio 2: reranker decide quem é relevante de verdade (precisão alta)
reranked = reranker.rank(query=pergunta, documents=[c.page_content for c in candidatos])
contexto = [r.document for r in reranked[:5]]   # só os 5 melhores vão pro prompt

O número fecha a conta: no estudo da Anthropic, adicionar reranking sobre a recuperação contextual derrubou a taxa de falha em 67% (5,7% → 1,9%). É o componente que mais eleva qualidade por dólar gasto, antes de você sair trocando pro LLM mais caro. O custo é uma latência extra de runtime — mede o p95 antes de botar em produção.

Bônus: se a busca pura erra muito, considere busca híbrida (vetorial + BM25 com RRF) antes do reranker. Termo exato — código de produto, nome próprio, número de lei — o BM25 acerta e o vetorial erra.

Causa 4: o prompt joga o contexto fora

Aqui é o cenário do teste lá de cima: o chunk certo chegou no top-5, e a resposta ainda sai errada. A recuperação fez o trabalho. Quem vazou foi a geração.

Três causas, todas no prompt:

O modelo ignora o contexto e responde de cabeça. Se você não diz explicitamente "responda APENAS com base no contexto abaixo", o LLM completa com o que ele "sabe" — que pode ser desatualizado ou inventado. Pior: ele mistura. Pega metade do contexto e metade do treino. Aí vem a alucinação confiante.

Você não deu saída de emergência. Se o contexto não tem a resposta, o modelo precisa de permissão pra dizer isso. Sem ela, ele inventa pra agradar. Uma frase resolve: "se a resposta não estiver no contexto, diga que não encontrou".

Lost in the middle. Esse é técnico e quase ninguém conhece. O paper Lost in the Middle (Liu et al., 2023) mostrou que os LLMs usam muito melhor a informação que está no começo ou no fim do contexto, e degradam quando a informação relevante está no meio de um contexto longo. Tradução prática: se você empilha 20 chunks no prompt, o que está na posição 10 quase some. Por isso o reranker da causa 3 importa duas vezes — menos chunks, melhor ordenados, com o mais relevante perto da borda.

O conserto é um template explícito e enxuto:

Você responde com base EXCLUSIVAMENTE no contexto abaixo.
Se a resposta não estiver no contexto, responda "Não encontrei essa informação nos documentos."
Cite o trecho que usou.

Contexto:
{chunks_reordenados_pelo_reranker}

Pergunta: {pergunta}

Pedir a citação do trecho não é firula — é o que te deixa auditar a resposta e o que segura a alucinação, porque o modelo precisa apontar onde está o que ele afirmou.

Como saber que consertou: mede com número

Aqui está a parte que separa quem conserta de quem fica chutando: você não sabe se melhorou se não está medindo. "Achei que ficou melhor" não é métrica.

O jeito sério é montar um dataset pequeno — 30, 50 perguntas com a resposta certa conhecida — e rodar o pipeline contra ele a cada mudança. O framework RAGAS dá as métricas que mapeiam exatamente a divisão recuperação/geração do começo do post:

  • Context precision e context recall medem a recuperação: os chunks certos chegaram? Mexeu no chunking, embedding ou reranker → olha essas duas.
  • Faithfulness mede a geração: a resposta está ancorada no contexto que chegou, ou o modelo inventou? Faithfulness baixo com context recall alto = problema de prompt, não de busca.

Com esse dataset rodando, cada conserto deste post vira um número que sobe ou desce. É a diferença entre debug e adivinhação. Quem quer levar isso pro CI e barrar regressão em cada PR, o caminho é um pipeline de eval contínuo.

FAQ rápido

Troquei o modelo de LLM e piorou. Por quê? Provavelmente o prompt. Modelos diferentes seguem instrução de "responda só pelo contexto" com rigor diferente. Reescreva o template pro novo modelo e rode o eval — não confie na impressão.

Preciso re-indexar tudo quando mexo no chunking ou no embedding? No embedding, sempre — o vetor antigo foi gerado por outro modelo, não dá pra comparar. No chunking, também, porque os chunks mudaram. Reranker e prompt, não: são runtime, mudou e já valeu.

Reranker resolve chunking ruim? Não. O reranker só reordena o que a busca trouxe. Se o chunk está despedaçado e nem entrou nos candidatos, não tem o que reordenar. Conserte o chunking primeiro.

Como faço isso em Laravel/PHP? Mesmo pipeline, outra stack: pgvector no Postgres, busca híbrida e reranker como tool. Tem o caminho completo no guia de RAG para devs backend.

Conclusão

RAG retornando lixo quase nunca é um problema só. É um vazamento em uma das quatro etapas — e o erro mais caro é mexer na etapa errada. Por isso o roteiro: primeiro separe recuperação de geração com o teste de busca crua, depois ataque a causa certa — chunking, embedding, reranker ou prompt — e por último prove com número que consertou.

A real é que cada uma dessas decisões é arquitetura, não sorte. Onde cortar o chunk, qual embedding, rerankar ou não, o que vai no prompt — é o tipo de escolha que define se o produto de IA aguenta produção ou cai no primeiro cliente. Se você quer ver esse raciocínio aplicado de ponta a ponta, é exatamente o que a gente põe na mesa no Workshop Arquitetando Soluções de IA: como desenhar a arquitetura de uma solução com agentes de IA, decisão por decisão, sem slide motivacional.

O próximo passo é parar de tratar o RAG como caixa-preta. Ele tem quatro alavancas. Agora você sabe puxar cada uma.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando