Seu RAG é instável porque a recuperação não é determinística: um playbook para CTOs

Por Diogo Hudson Dias
Two engineers in a São Paulo office reviewing vector retrieval diagrams with a server rack behind them.

Se as respostas do seu RAG oscilam entre ótimas e péssimas para a mesma pergunta, pare de culpar o modelo. Em nove de cada dez casos, o culpado é a recuperação não determinística. Aquele post do dev.to que está circulando — “Your RAG Eval Isn’t Flaky. Your Retrieval Is Non-Deterministic.” — está certo. Seu top‑k não é estável, seu índice não é versionado e seus critérios de desempate no ranking não são determinísticos. Você está avaliando um alvo móvel.

Este post traz um playbook em nível de CTO para consertar isso. Vamos definir onde o não determinismo se infiltra, o que instrumentar e como blindar sua trilha de recuperação para que a mesma consulta sobre o mesmo corpus retorne o mesmo contexto — hoje, amanhã e após a sua próxima construção de índice.

Onde o não determinismo da recuperação se esconde (é em mais lugares do que você imagina)

Pipelines de RAG derivam porque muitos componentes “pequenos” são probabilísticos, paralelos ou dependentes do tempo. Alguns vilões comuns:

  • Turbulência no índice: Atualizações incrementais embaralham vizinhanças no grafo (HNSW), reagrupam centróides (IVF) ou alteram mapas de exclusão. Dois minutos depois, mesma consulta, vizinhos diferentes.
  • Variabilidade da busca aproximada: Buscas HNSW e IVF são dependentes do caminho. ef_search, nprobe, largura do beam, escalonamento de threads — tudo isso altera os conjuntos de candidatos.
  • Ordenação não estável: Se você ordena apenas por score, empates produzem ordem aleatória entre threads/hosts. Seu LLM vê 2–3 primeiros chunks diferentes e responde de modo distinto.
  • Deriva de tokenizador/segmentador: Atualizar o tokenizador ou o chunker muda as fronteiras dos chunks. A unidade semântica que ocupava o #3 agora se espalha por dois chunks e sai do top‑k.
  • Impulso de recência via now(): Se você aumenta a “atualidade” usando o relógio de parede, a mesma consulta, com horas de diferença, recebe rankings diferentes.
  • Ruído de ponto flutuante: Misturar esquemas de quantização (índice em FP32, consultas em INT8), diferentes back-ends BLAS e contagens de threads pode reordenar sutilmente scores no limite.
  • Roleta do pré-processamento: Casefolding, normalização Unicode, remoção de diacríticos (crítico para português/espanhol), listas de stopwords — pequenas mudanças alteram embeddings e candidatos.

O efeito a jusante não é sutil. No mundo real, já vimos a similaridade de Jaccard do top‑5 entre duas execuções da mesma consulta variar de 0,55 a 0,9, dependendo das configurações do índice. Em 0,6, seu harness de avaliação vai gritar “deriva de modelo”, enquanto o LLM é inocente.

O objetivo: estabilidade de recuperação que você pode medir

Você não governa o que não mede. Defina uma métrica e um SLO:

  • Retrieval Stability Index (RSI): Para um snapshot fixo do corpus, execute cada consulta N vezes e calcule a similaridade média de Jaccard dos conjuntos de resultados top‑k (ou uma sobreposição sensível à posição se você se importa com a ordem). Mire RSI ≥ 0,90 para Q&A em produção. Para fluxos sensíveis a conformidade, busque ≥ 0,95.
  • Stability SLO: 95% das consultas de produção devem ter RSI ≥ 0,9 em 3 execuções independentes contra o mesmo snapshot.

Depois, construa seu pipeline para realmente cumprir esse SLO.

Um playbook de CTO para tornar a recuperação determinística

1) Trate seu corpus como artefatos imutáveis e versionados

  • Endereçamento por conteúdo: Atribua a cada documento um hash de conteúdo (ex.: SHA‑256). Armazene um manifesto mapeando doc_id → content_hash, idioma e timestamps effective_from.
  • Atualizações append‑only: Atualizações criam uma nova versão do documento. Versões antigas permanecem consultáveis até que o próximo snapshot seja gerado e ativado. Isso evita deriva no meio da consulta.
  • Disciplina de snapshots: Todas as consultas atingem um snapshot nomeado do corpus (ex.: index_2026‑07‑12T19:00Z). Snapshots são imutáveis e fazem rollback de forma atômica.

Resultado: quando alguém reclamar “as respostas mudaram”, você pode correlacionar a uma troca de snapshot, não a um fantasma na máquina.

2) Fragmentação canônica e determinística

  • Fixe o tokenizador: Escolha um tokenizador e sua versão (ex.: cl100k_base vX) e não faça auto‑upgrade. Atualizações de tokenizador são migrações de índice, não hotfixes.
  • Stride fixo, política fixa: Use janelas de tokens fixas (ex.: 512 tokens com 64 de overlap) ou uma política com reconhecimento de idioma — mas congele isso. Mudar o overlap de 64 para 80 é uma nova versão do corpus.
  • Normalize antes do chunking: Unicode NFKC, casefold, tratamento padrão de pontuação e política consistente de diacríticos. Para português/espanhol, decida se você vai incorporar variantes com ou sem acentos. Seja consistente.

Dica de especialista: não gere chunks “inteligentes” com um LLM se você se importa com determinismo. Se for inevitável, armazene no manifesto o prompt do chunker, a versão do modelo e a semente de amostragem, e trate mudanças como migrações.

3) Embeddings determinísticos

  • Fixe o modelo de embeddings: Nome do modelo, provedor e checksum entram no manifesto do índice. Trocar de text‑embedding‑3‑small para text‑embedding‑3‑large é um novo snapshot de corpus — ponto.
  • Normalize de forma consistente: Sempre normalize por L2 ou na gravação ou na consulta — não em ambas. Documente a política.
  • Alinhamento de quantização: Se o índice usa quantização (ex.: INT8), aplique a mesma transformação aos vetores de consulta. Precisão mista é fonte comum de caos nos desempates.

Armazene um digest (ex.: xxh3) do vetor em ponto flutuante antes da quantização, junto do vetor armazenado. Isso fornece um sanity check de que você não alterou silenciosamente o pré-processamento.

4) Índices ANN com sementes, não com “achismo”

Métodos de vizinhos mais próximos aproximados são rápidos porque pulam trabalho. Eles são reproduzíveis se você assumir o controle.

  • HNSW (FAISS, Qdrant, Milvus): Fixe M e efConstruction, defina uma semente de build se a biblioteca suportar e evite mutação concorrente. No tempo de consulta, defina efSearch explicitamente. Não confie em modos “auto” do provedor.
  • IVF (FAISS, Milvus, pgvector ivfflat): Treine centróides no mesmo conjunto de amostra, com semente e parâmetros fixos (nlist). No pgvector, treine as listas uma vez por snapshot; não deixe o auto‑train rodar de forma oportunista.
  • Índices exatos para K pequeno: Se sua coleção tem ≤ 5M vetores e a vazão permite, use busca exata (ex.: FAISS IndexFlatIP/L2 ou modo exato no Qdrant) para workloads de alto risco. É 4–10x mais lento, porém maximamente estável.

Independentemente do engine escolhido, exporte os parâmetros do índice no manifesto do snapshot. Nunca “ajuste em produção” sem criar um novo snapshot.

5) Ranqueamento estável e critérios de desempate

  • Ordenação estável: Ranqueie por (score DESC, doc_id ASC, chunk_offset ASC). Documente e aplique isso em todo lugar (serviço, SDK, SQL).
  • Nada de aleatoriedade escondida: Desative modos “autocut”, “sampling” ou “hybrid blending” que descartam quase‑empates. Se precisar hibridizar (BM25 + vector), fixe alpha e pesos de termos.
  • Re‑ranqueadores determinísticos: Se usar cross‑encoders para re‑ranquear, use greedy decoding ou defina uma semente fixa com temperatura 0 e fixe a versão do modelo.

6) Tempo é parâmetro, não variável global

  • Consultas as‑of: Se você faz recency boosting, forneça um timestamp as_of com a consulta e inclua-o na chave de cache. Não chame now() dentro da função de ranking.
  • Corpora com janela temporal: Para dados de alta rotatividade, construa shards diários ou horários e direcione consultas por uma política determinística (ex.: últimos 30 dias + documentos de política). Limites de shards fazem parte do snapshot.

7) Arquitetura single‑writer, snapshot‑reader

  • Construa fora do hot path: Crie o índice em isolamento, valide-o e depois alterne os readers para o novo snapshot de forma atômica.
  • Zero writes intercalados: Não permita mesclas ou compactações em background que mudem topologias ao longo do dia. Se seu fornecedor impõe compactação, delimite janela e trate como rotação de snapshot.

8) Cache de recuperação em que você pode confiar

  • Chaves de cache que importam: Digest do texto da consulta, as_of, top‑k, versão do re‑ranqueador, id do snapshot do corpus. Se algo divergir, é miss.
  • Faça cache do conjunto, não do texto: Faça cache dos IDs e posições canônicos dos resultados; reidrate o conteúdo a partir do snapshot. Isso captura mutação acidental de texto.

9) Realidade bilíngue (Português e Espanhol importam)

Dados de suporte na América Latina não são apenas em inglês, e diacríticos importam. Se você opera em português ou espanhol:

  • Defina uma estratégia de normalização: Ou indexe variantes preservando acentos e variantes sem acentos, ou padronize em uma delas. Misturar regras ad hoc injeta deriva silenciosa.
  • Fragmentação com reconhecimento de idioma: A segmentação de sentenças em português/espanhol (ex.: lidar com abreviações como “Sr.”) precisa de um conjunto de regras fixado. Não herde regras de locale do sistema operacional hospedeiro.
  • Embeddings multilíngues: Se você usa embeddings multilíngues, fixe a versão, porque a cobertura e os vocabulários de subpalavras evoluem mais agressivamente do que em modelos apenas em inglês.

Instrumente: o que registrar e comparar

Você não melhora RSI sem visibilidade. No mínimo, registre isto para cada consulta:

  • Impressão digital da consulta: Digest do texto normalizado da consulta e código de idioma.
  • Id do snapshot do corpus: Nome e hash do manifesto.
  • Versões de embedding e índice: Id do modelo, id do tokenizador, parâmetros de ANN.
  • Conjunto de candidatos e scores: IDs do top‑k e scores brutos antes de qualquer re‑ranqueamento.
  • Versão do re‑ranqueador e ordem final: As últimas 10 posições da escada são onde vivem os empates; você precisa vê‑las.

Crie um job noturno que reproduza um conjunto canário de 1.000 consultas contra o snapshot ativo e calcule o RSI. Alerta se o RSI cair abaixo do seu SLO. Se tiver coragem, publique o RSI na sua página interna de status como um error budget.

Restrições específicas por ferramenta (sem culto ao fornecedor)

FAISS

  • Exato: IndexFlatIP/L2 é determinístico se seu back‑end BLAS e a contagem de threads estiverem fixos.
  • IVF: Treine uma vez por snapshot com semente e conjunto de treino fixos; fixe nlist e nprobe.
  • HNSW: Fixe M, efConstruction, efSearch. Construa o índice single‑thread para máxima reprodutibilidade, se couber.

Qdrant

  • Parâmetros de HNSW: Defina m, ef_construct e ef explicitamente. Desative auto‑tuning. Considere modo exato para coleções pequenas.
  • Quantização: Se habilitar quantização escalar, aplique o mesmo aos vetores de consulta e congele a configuração do codec por snapshot.

Milvus

  • Tipos de índice: IVF_FLAT, HNSW, IVF_PQ — em todos os casos, fixe parâmetros do índice e a versão do esquema da coleção.
  • Compactação de segmentos: Agende compactação fora do horário de pico e trate como troca de snapshot. Não deixe ad hoc.

pgvector

  • Treino do ivfflat: Treine as listas com semente fixa e amostra de treino fixada. Defina ivfflat.probes explicitamente. Re‑treine apenas na virada de snapshot.
  • Busca exata: Para ≤ 1M linhas ou workloads pouco sensíveis à latência, use exata para eliminar a variância do ANN.

Weaviate

  • Determinismo no híbrido: Fixe alpha para o híbrido BM25/vector; evite autoCut se você se importa com top‑k estável.
  • Versões de módulos: Fixe as versões do re‑ranqueador e do módulo text2vec. Upgrades equivalem a novos snapshots.

Trade‑offs que você deve reconhecer desde o início

  • Latência vs. estabilidade: Busca exata pode ser 4–10x mais lenta que ANN. Um compromisso comum é ANN até 50 candidatos + re‑ranqueamento com cross‑encoder determinístico até 10.
  • Atualidade vs. determinismo: Snapshots append‑only significam que novo conhecimento entra em um cronograma (de hora em hora/diário), não instantaneamente. Se “até o segundo” importa, direcione explicitamente um pequeno shard de atualidade e documente que essas respostas podem variar.
  • Custo da disciplina: Fixar modelos, tokenizadores e parâmetros de índice desacelera features. O retorno é depuração mais rápida e avaliações críveis. Se você não reproduz uma resposta ruim, você não a corrige.

Um modelo simples de governança que funciona

Trate seu índice como um artefato de build.

  • Manifesto do snapshot: Arquivo JSON com campos para digest do corpus, versão do tokenizador, id do modelo de embeddings, regras de pré‑processamento, tipo de índice + parâmetros, id do re‑ranqueador e política de tempo. Versione no Git.
  • Revisão de mudanças: Qualquer alteração nos campos do manifesto passa por revisão via PR com um diff de RSI e um diff de avaliação.
  • Gates de promoção: RSI do canário ≥ 0,9 e nenhuma regressão em um conjunto de QA específico do domínio. Se falhar, não sobe.

Como é o “bom” em números

Em corpora típicos de suporte SaaS (50k–500k chunks):

  • RSI: 0,93–0,97 em três execuções com ef_search do HNSW fixado em 128.
  • Latência: 35–70 ms p95 de busca ANN em CPU para top‑50 candidatos; 90–140 ms com busca exata em 500k vetores; some 30–60 ms para re‑ranqueamento com cross‑encoder p95 em uma T4/A10.
  • Cadência de atualização: Snapshots de hora em hora para changelogs; diários para docs; semanais para políticas/jurídico.

Seus números irão variar, mas se seu RSI está travado em 0,7, não é azar — é o seu pipeline.

Por que isso importa para você, como CTO

Recuperação determinística permite fazer engenharia de verdade em cima:

  • Avaliações confiáveis: Você consegue atribuir ganhos a mudanças, não a ruído. Um ganho de +3 pontos significa algo.
  • Menor risco de suporte: Quando as respostas oscilam, os tickets disparam. Estabilidade no top‑k reduz a variância na qualidade da resposta, mesmo quando o LLM é estocástico.
  • Postura regulatória: Você pode mostrar aos auditores exatamente qual corpus e versão do índice produziram uma resposta. É a diferença entre “achamos” e “sabemos”.
  • Alavancagem nearshore: Um pipeline disciplinado é portável. Você pode replicar toda a stack com um pod nearshore no Brasil e esperar o mesmo comportamento — mesmo manifesto, mesmo RSI, mesmos SLOs.

Começando neste mês

  1. Semana 1: Adicione IDs de snapshot ao seu serviço de recuperação e faça log dos IDs + scores do top‑k. Construa o job de RSI com o tráfego da semana passada.
  2. Semana 2: Congele tokenizador, modelo de embeddings e chunking. Gere seu primeiro snapshot imutável. Faça as consultas referenciá‑lo explicitamente.
  3. Semana 3: Fixe parâmetros de ANN e critérios de desempate. Desative qualquer ajuste “auto”. Gere novo snapshot e meça o RSI.
  4. Semana 4: Introduza um re‑ranqueador com cross‑encoder e configurações determinísticas. Defina seu SLO de RSI e conecte alertas.

Depois disso, mova os builds de snapshot para fora do hot path, adicione gates de promoção e pare de levar culpa de “deriva de modelo” por um problema de recuperação que você consegue controlar.

Principais aprendizados

  • A instabilidade no RAG geralmente é não determinismo na recuperação, não “humor” do LLM.
  • Defina e acompanhe a métrica RSI; mire estabilidade ≥ 0,9 para Q&A em produção.
  • Versione tudo: corpus, tokenizador, modelo de embeddings, tipo/parâmetros de índice, re‑ranqueador e política de tempo.
  • Use fragmentação determinística e desempates de ordenação estáveis; trate upgrades de tokenizador como migrações.
  • Fixe parâmetros e sementes de ANN; considere busca exata para fluxos de alto risco.
  • Torne o tempo explícito com consultas as_of; pare de chamar now() no ranking.
  • Adote arquitetura single‑writer, snapshot‑reader e faça cache de IDs canônicos, não de texto.
  • Para corpora em português/espanhol, fixe regras de normalização e segmentação para evitar deriva silenciosa.
  • Governe com um manifesto e gates de promoção; nenhum snapshot entra em produção sem diff de RSI/avaliação.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation