Quando o RAG erra, quase nunca é o LLM: 4 falhas de recuperação e como logar cada uma A developer explains that most RAG failures are not caused by the LLM but by retrieval issues, and shares four common failure types along with logging strategies. The developer recommends hybrid search and rerankers over embedding swaps, and emphasizes logging retrieval scores before tuning the pipeline. Todo problema de RAG parece problema do modelo. Quase nunca é. Demorei pra aceitar isso. Ficava trocando modelo de embedding, mexendo no prompt, achando que o Claude tava inventando coisa. O que estava quebrado era o que eu entregava pra ele, e só ficou óbvio quando comecei a logar o que voltava da recuperação. Depois que instrumentei, os erros se separaram em quatro tipos bem diferentes. A pergunta simplesmente não tem resposta na base. Quase ninguém checa. Um piso resolve: q emb = vo.embed question , model="voyage-3.5", input type="query" .embeddings 0 cur.execute "SELECT content, 1 - embedding <= %s AS score " "FROM documents ORDER BY embedding <= %s LIMIT %s", q emb, q emb, top k rows = cur.fetchall if not rows or rows 0 1 < 0.7: return "Não tenho essa informação na base." Feio, mas corta a maior parte das respostas inventadas com cara de certeza. O 0.7 não é sagrado: é o ponto onde, na minha base, o chunk deixava de ter relação com a pergunta. Loga o score por uma semana e você acha o teu. "como resetar senha" trazendo "como resetar produto". Vetorialmente colado, na prática inútil. Esse foi o que mais me irritou, porque parece bug do modelo e não é: busca vetorial pura não separa as duas coisas. Os dois chunks falam de resetar algo, com estrutura de frase quase idêntica, e a diferença inteira mora numa palavra que o vetor dilui. Hybrid search vetor + BM25 rendeu mais aqui do que qualquer troca de embedding que eu tinha tentado antes, porque o BM25 dá peso ao token literal "senha" — justamente o que o vetor perdeu. Reranker com cross-encoder na frente ataca o mesmo problema por outro lado. O retrieval acertou e o modelo extrapolou mesmo assim. Isso é instrução, não recuperação: o system prompt precisa proibir na cara dura e pedir citação do trecho. SYSTEM = "Responda APENAS com base no contexto fornecido. " "Cite o trecho que embasa cada afirmação. " "Se a resposta não estiver no contexto, diga que não sabe." Óbvio depois de escrito. Passei semanas culpando o retrieval por isso. Tabela partida, bloco de código sem fechamento. Chunking por contagem de token faz isso o tempo todo: python def chunk text text, size=500, overlap=50 : words = text.split return " ".join words i:i + size for i in range 0, len words , size - overlap Funciona pra prosa corrida e destrói documentação técnica. E o estrago é silencioso: o modelo responde bonito em cima de um pedaço mutilado, sem sinal nenhum de que faltou metade da tabela. Respeitar a estrutura do documento header de markdown, seção do PDF dá mais trabalho pra escrever e se paga na primeira semana. Se fosse começar de novo, a primeira coisa que eu escreveria não era o pipeline, era o log. Score de cada chunk, quais entraram, tamanho de cada um. Sem isso você fica trocando peça no escuro, e trocar modelo de embedding é caro, demorado e quase nunca era o problema. Escrevi o passo a passo completo no blog, com o pipeline em Python usando pgvector, Voyage e Claude, as 4 estratégias de chunking e a comparação com fine-tuning: https://www.techknow.com.br/post/o-que-e-rag https://www.techknow.com.br/post/o-que-e-rag Quem já tem isso em produção: o ganho maior veio de mexer no chunking ou de botar um reranker na frente?