Quando o RAG erra, quase nunca é o LLM: 4 falhas de recuperação e como logar cada uma
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. 1. Score baixo, e o pipeline responde assim mesmo 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. 2. Chunk vizinho "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. 3. Alucinação com o contexto certo na mão 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. 4. Chunk cortado no meio Tabela partida, bloco de código sem fechamento. Chunking por contagem de token faz isso o tempo todo: 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. O que eu faria diferente 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 Quem já tem isso em produção: o ganho maior veio de mexer no chunking ou de botar um reranker na frente? Top comments (0)
Comments
No comments yet. Start the discussion.