Blog

Deduplicação de contexto em RAG: cortar o trecho repetido

Você monta o RAG, mede o recall, aumenta o top-k para garantir que a resposta está lá dentro, e a qualidade não sobe na proporção. Quando abre o prompt que foi mandado ao modelo, entende por quê: dos oito trechos recuperados, cinco dizem a mesma coisa. É a mesma política de reembolso que aparece no manual, no FAQ, no e-mail de treinamento e em duas versões do mesmo documento que ninguém arquivou. O retriever fez o trabalho dele, achou tudo que é relevante, e o problema é justamente esse: relevante não significa novo. Cada trecho repetido ocupa espaço na janela que um trecho distinto poderia ocupar, e o resultado é um contexto que parece cheio mas é raso, gastando tokens para reafirmar o que já estava dito enquanto a informação que faltava ficou de fora do corte. Este artigo trata da deduplicação de contexto: por que a repetição não é só desperdício mas também viés, os três níveis em que a duplicata aparece, como detectar cada um com o custo certo, por que fundir é melhor do que descartar, onde encaixar o passo no pipeline e como medir se a deduplicação está aumentando a densidade de informação ou cortando sinal.

2026-07-24 / IA Aplicada / 13 min

01

Repetição não é só desperdício, é viés

O primeiro custo da duplicata é óbvio: token gasto. Se cinco dos oito trechos dizem a mesma coisa, você pagou por oito e recebeu a informação de quatro, e os quatro trechos distintos que ficaram de fora do top-k eram justamente os que poderiam completar a resposta. Mas o custo mais grave é o segundo, e ele quase nunca é medido: a repetição enviesa o modelo. Um fato que aparece cinco vezes no contexto parece mais estabelecido, mais confirmado e mais importante do que um fato que aparece uma vez, e o modelo trata assim, porque é exatamente esse o sinal que a repetição carrega em texto natural. Só que aqui a repetição não vem de consenso, vem de indexação: o mesmo parágrafo foi copiado em quatro documentos, ou o mesmo documento foi ingerido duas vezes com nomes diferentes.

A consequência prática é uma distorção silenciosa da resposta. Quando o contexto tem uma versão antiga da política repetida quatro vezes e a versão nova uma vez só, o modelo tende a responder com a antiga, e não porque ela é melhor, mas porque ela é mais frequente naquele prompt. O retriever não errou, os dois documentos são relevantes, mas a densidade de cópias decidiu o resultado. É por isso que deduplicar não é uma otimização de custo que se faz quando sobra tempo: é uma correção de viés. Sem ela, a sua resposta reflete quantas vezes cada fato foi copiado no seu acervo, e não qual fato está certo.

Sintoma no promptCausa provávelEfeito na resposta
Mesmo parágrafo palavra por palavra em vários trechosDocumento ingerido duas vezes ou boilerplate copiadoGasta janela e infla a confiança do modelo naquele fato
Trechos quase iguais com pequenas diferenças de redaçãoVersões do mesmo documento coexistindo no índiceModelo mistura versões e pode responder com a desatualizada
Trechos consecutivos do mesmo documento com miolo repetidoChunking com janela sobrepostaRepete o meio e desperdiça o espaço das bordas novas
Aumentar o top-k não melhora a respostaOs trechos extras são cópias, não informação novaCusto sobe, qualidade fica igual ou piora

02

Os três níveis de duplicata

Duplicata não é uma coisa só, e tratar todas com a mesma técnica é o erro que faz a deduplicação parecer cara ou imprecisa. O primeiro nível é a duplicata exata: dois trechos com o mesmo texto, caractere por caractere. Isso acontece mais do que se imagina, porque o mesmo PDF entrou duas vezes no índice, ou porque o rodapé jurídico se repete em todo documento e virou chunk. Detectar isso é barato: normaliza o texto e compara um hash. O segundo nível é o quase idêntico: dois trechos que dizem exatamente a mesma coisa com diferenças de formatação, pontuação, uma frase a mais no começo ou uma versão levemente editada do mesmo parágrafo. Hash não pega, porque um caractere diferente muda o hash inteiro, e é aqui que a maioria das duplicatas reais mora.

O terceiro nível é a sobreposição de janelas, e ele é específico de RAG. Se o seu chunking usa janela deslizante com overlap, dois chunks vizinhos do mesmo documento compartilham um pedaço por construção, e quando os dois são recuperados juntos, o miolo comum aparece duas vezes no prompt. Esse caso não é bem uma duplicata para descartar, é uma sobreposição para costurar: os dois trechos têm bordas distintas que você quer manter, e só o meio se repete. E existe ainda a duplicata semântica, dois textos completamente diferentes na letra que afirmam o mesmo fato, que é a mais cara de detectar e a mais perigosa de tratar, porque a fronteira entre "diz o mesmo" e "diz algo parecido mas com uma exceção importante" é tênue.

NívelComo detectarCustoO que fazer
ExataHash do texto normalizadoMuito baixoDescartar a cópia, manter a de melhor score
Quase idênticaSimilaridade de shingles (Jaccard) ou MinHashBaixoManter uma, registrar as fontes das outras
Sobreposição de janelaSufixo/prefixo comum entre chunks do mesmo docBaixoFundir num trecho único costurado
SemânticaSimilaridade de embedding acima de um limiar altoMédioAgrupar e resumir, nunca descartar cegamente

03

Detectar barato antes de detectar caro

A ordem importa. Deduplicação bem feita é uma cascata que começa pelo detector mais barato e só usa o mais caro no que sobrou. Primeiro o hash, que elimina as cópias exatas com custo praticamente zero e já limpa uma fatia grande em acervos reais. Depois a similaridade léxica por shingles, que quebra o texto em n-gramas e compara os conjuntos: dois trechos com Jaccard acima de um limiar alto dizem a mesma coisa com outra pontuação. Depois a sobreposição de janela, que só precisa ser testada entre chunks do mesmo documento e é resolvida comparando sufixo com prefixo. E só no fim, se ainda fizer sentido, a comparação por embedding, que é a única que pega paráfrase de verdade e é também a única que custa vetor e tempo.

A tentação de pular direto para o embedding é grande porque ele parece resolver tudo de uma vez, e é justamente aí que a deduplicação vira um passo caro e impreciso. Caro porque você compara todo mundo com todo mundo num conjunto que o hash teria reduzido pela metade em um milissegundo. Impreciso porque similaridade de embedding alta não é sinônimo de conteúdo redundante: dois parágrafos sobre a mesma política, um dizendo que o reembolso vale por trinta dias e outro dizendo que não vale para produto usado, têm embedding altíssimo e são informação complementar, não duplicata. Descartar um deles pelo cosseno é perder exatamente a exceção que o cliente perguntou.

// rag/dedup.js
// Cascata de deduplicacao: do detector mais barato para o mais caro.
// Cada nivel roda apenas no que sobrou do nivel anterior.

export function dedupPassages(passages, { jaccardThreshold = 0.85 } = {}) {
  const merged = [];
  const report = [];

  // Ordena por score: a passagem que fica e sempre a de melhor score,
  // e as demais viram fontes adicionais dela, nao lixo silencioso.
  const ordered = [...passages].sort((a, b) => b.score - a.score);

  for (const candidate of ordered) {
    // Nivel 1: exata. Hash do texto normalizado, custo quase zero.
    const key = hashNormalized(candidate.text);
    const exact = merged.find((p) => p.hash === key);
    if (exact) {
      exact.sources.push(candidate.source);
      report.push({ level: 'exact', dropped: candidate.id, into: exact.id });
      continue;
    }

    // Nivel 2: quase identica. Shingles + Jaccard, ainda barato.
    const near = merged.find(
      (p) => jaccard(shingles(p.text), shingles(candidate.text)) >= jaccardThreshold,
    );
    if (near) {
      near.sources.push(candidate.source);
      report.push({ level: 'near', dropped: candidate.id, into: near.id });
      continue;
    }

    // Nivel 3: sobreposicao de janela. So entre chunks do MESMO documento,
    // e o tratamento e fundir (costurar bordas), nao descartar.
    const neighbor = merged.find(
      (p) => p.docId === candidate.docId && overlapLength(p.text, candidate.text) > 0,
    );
    if (neighbor) {
      neighbor.text = stitch(neighbor.text, candidate.text);
      neighbor.sources.push(candidate.source);
      report.push({ level: 'overlap', merged: candidate.id, into: neighbor.id });
      continue;
    }

    merged.push({ ...candidate, hash: key, sources: [candidate.source] });
  }

  return { passages: merged, report };
}

04

Fundir é melhor do que descartar

O reflexo natural ao encontrar duas passagens parecidas é jogar uma fora. Para a duplicata exata isso está certo, mas para os outros níveis é uma perda de informação disfarçada de limpeza. Dois chunks vizinhos com overlap não são duas cópias, são um trecho maior partido no meio: descartar um deles apaga uma borda que só ele tinha. Duas versões quase iguais de um parágrafo não são redundância pura: a diferença entre elas pode ser exatamente a cláusula que mudou na revisão. A regra que evita esse tipo de perda é simples: quanto menos exata a duplicata, mais a operação certa é fundir em vez de descartar, e a fusão precisa preservar as bordas distintas.

Deduplicacao em cascata: barato -> caro, e funde em vez de descartar

  top-k do retriever (8 trechos, muita repeticao)
     |
     v
  [1] hash do texto normalizado ......... corta copia exata
     |                                     (descarta, guarda a fonte)
     v
  [2] shingles + Jaccard ................ corta quase identica
     |                                     (mantem a de maior score)
     v
  [3] sufixo x prefixo (mesmo doc) ...... FUNDE janelas sobrepostas
     |     A: [....miolo]                  resultado: [....miolo....]
     |     B:     [miolo....]              (as duas bordas sobrevivem)
     v
  [4] embedding (opcional, caro) ........ agrupa parafrase
     |                                     (agrupa e resume, nao descarta)
     v
  contexto denso: trechos distintos, cada um com suas fontes
     |
     +-- espaco liberado -> puxa o proximo trecho da fila do retriever
     |
     v
  prompt final: mesma janela, mais informacao unica dentro dela

A fusão tem um efeito colateral valioso: ela deixa a citação mais honesta. Quando três passagens dizem o mesmo fato e você funde numa só, essa passagem passa a carregar três fontes, e o modelo pode citar o documento certo sem que o fato ocupe três blocos do prompt. Você separou a densidade de evidência da densidade de texto, que era exatamente o que a repetição bagunçava. E há um segundo ganho, esse direto na qualidade: o espaço liberado pela deduplicação não deve simplesmente sobrar, ele deve puxar o próximo trecho da fila do retriever. Deduplicar sem reabastecer economiza tokens; deduplicar reabastecendo aumenta a quantidade de informação distinta que cabe na mesma janela, que é o objetivo real.

05

Onde encaixar o passo no pipeline

Deduplicação pode acontecer em três momentos diferentes, e eles não são alternativas, são camadas complementares. A primeira é na ingestão: antes de indexar, você detecta que aquele documento já está no acervo e não o duplica. É a mais eficiente, porque resolve o problema na origem e nunca mais paga por ele, mas ela só pega o que é duplicata no momento da entrada e não protege contra o boilerplate que se repete legitimamente entre documentos diferentes. A segunda é na recuperação, depois do retriever e antes de montar o prompt, e é a camada indispensável: só ali você sabe quais trechos específicos foram selecionados juntos para esta pergunta e como eles se sobrepõem entre si.

  1. Ingestão: detecta documento já indexado e boilerplate recorrente antes de gerar chunks. Resolve na origem, mas não vê a combinação específica de trechos de cada consulta.
  2. Pós-retrieval, antes do rerank: reduz o conjunto que o reranker vai pontuar, o que barateia o rerank e evita que ele gaste posições com cópias.
  3. Pós-rerank, antes do prompt: a camada indispensável, porque é a última chance de garantir que a janela não carrega o mesmo fato várias vezes.
  4. Reabastecimento: para cada trecho removido, puxe o próximo da fila do retriever, para que a deduplicação aumente informação distinta em vez de só sobrar espaço.
  5. Registro: guarde o relatório do que foi fundido e com quais fontes, para auditar a citação e para depurar quando uma resposta perder um detalhe.

A relação com o reranking merece uma nota, porque os dois passos parecem resolver a mesma coisa e não resolvem. O reranker ordena por relevância, e ele não tem nenhuma obrigação de olhar para redundância: se as cinco cópias da política são todas muito relevantes, um reranker bem calibrado coloca as cinco no topo, e ele está certo pelo critério dele. Deduplicar é o passo ortogonal que otimiza diversidade em vez de relevância. Rodar a deduplicação antes do rerank barateia o rerank e evita que ele desperdice posições; rodar depois garante a limpeza final. Em pipelines exigentes vale rodar nos dois pontos, com limiar mais conservador antes e mais estrito depois.

06

O limiar que corta sinal

Toda deduplicação tem um limiar, e todo limiar erra para os dois lados. Frouxo demais, ele deixa passar quase duplicatas e você não ganha nada. Estrito demais, ele funde ou descarta trechos que eram complementares, e aí a deduplicação passa a destruir exatamente o que o retriever tinha achado. O caso clássico é o de dois parágrafos sobre a mesma regra em que um enuncia o caso geral e o outro a exceção: eles são textualmente parecidíssimos, têm embedding altíssimo, e são a informação mais importante que existe naquela consulta. Um limiar agressivo joga a exceção fora e o bot passa a responder a regra geral em todos os casos, inclusive nos que ela não vale.

A proteção contra esse erro tem três partes. A primeira é a assimetria de tratamento por nível: descarte só é seguro no nível exato, e quanto mais semântica a comparação, mais a operação certa é agrupar e resumir preservando as diferenças, nunca eliminar. A segunda é a proteção por negação e número: se dois trechos parecidos divergem numa negação, num valor, numa data ou numa condição, eles não são duplicatas por definição, e essa checagem barata bloqueia o pior tipo de falso positivo. A terceira é o registro: toda remoção fica no relatório, com o que foi removido e por qual regra, porque uma deduplicação que apaga em silêncio é indepurável quando a resposta sai errada.

// rag/dedup-guard.js
// Freio contra o falso positivo: trechos parecidos que DIVERGEM
// em negacao, numero, data ou condicao nao sao duplicatas.

const DIVERGENCE_PATTERNS = [
  /\b(nao|exceto|salvo|desde que|apenas se)\b/i, // condicao e negacao
  /\d+([.,]\d+)?\s*(dias|meses|%|reais)/i,       // valores e prazos
];

export function isSafeDuplicate(a, b) {
  // Se um dos trechos tem um marcador de divergencia que o outro nao tem,
  // eles podem ser regra geral x excecao. Nunca funda nesse caso.
  for (const pattern of DIVERGENCE_PATTERNS) {
    const inA = extractMatches(a.text, pattern);
    const inB = extractMatches(b.text, pattern);
    if (!sameSet(inA, inB)) return false;
  }
  return true;
}

export function dedupWithGuard(passages, options) {
  const { passages: candidates, report } = dedupPassages(passages, options);

  // Reintroduz o que o guard vetou: melhor gastar janela com uma
  // possivel duplicata do que perder a excecao que muda a resposta.
  const restored = report
    .filter((r) => r.level !== 'exact')
    .filter((r) => !isSafeDuplicate(byId(passages, r.dropped), byId(candidates, r.into)))
    .map((r) => byId(passages, r.dropped));

  return { passages: [...candidates, ...restored], report };
}

07

Medir densidade, não só economia

A métrica errada para avaliar deduplicação é a economia de tokens, porque ela sempre melhora: quanto mais agressivo o limiar, menos tokens, e o gráfico sobe bonito enquanto a qualidade despenca. A métrica certa é a densidade de informação única na janela: quantos fatos distintos e relevantes o contexto carrega depois da deduplicação em relação a antes. Se você deduplica e reabastece, a contagem de tokens fica praticamente igual e o número de fatos distintos sobe, e é exatamente esse o resultado que você quer ver. Se a economia de tokens sobe e a densidade fica igual, você só está gastando menos para entregar o mesmo; se a densidade cai, o limiar está cortando sinal.

  1. Fatos distintos por janela: conte quantas afirmações únicas e relevantes o contexto carrega antes e depois; deduplicar com reabastecimento tem que subir esse número.
  2. Taxa de falso positivo do limiar: em um conjunto rotulado com pares regra geral x exceção, meça quantas exceções a deduplicação removeu. O alvo prático é zero.
  3. Acerto de resposta com e sem o passo: rode o mesmo conjunto de perguntas com e sem deduplicação e compare a resposta final, que é a única métrica que importa de verdade.
  4. Cobertura por nível: acompanhe quanto cada nível remove; se o exato e o quase idêntico cortam muito, o problema real está na ingestão e deve ser resolvido lá.
  5. Fidelidade da citação: verifique que a passagem fundida cita todas as fontes que foram fundidas nela, para que a evidência não desapareça junto com a cópia.

Vale medir também um sinal de diagnóstico que aponta para fora do RAG: se a deduplicação está cortando uma fração enorme do contexto todo dia, o problema não é de recuperação, é de acervo. Documento ingerido em duplicata, versões antigas que ninguém arquivou, boilerplate que deveria ter sido removido no chunking. A deduplicação em tempo de consulta é uma rede de segurança boa e barata, mas ela paga o mesmo custo repetidamente em toda pergunta para consertar algo que uma limpeza única no índice resolveria de vez. Quando o relatório mostra que o mesmo par de documentos é fundido em toda consulta, a resposta certa não é ajustar o limiar, é arquivar a versão antiga.

FAQ

Perguntas frequentes

Por que aumentar o top-k do retriever não melhora a resposta?

Porque relevância não é novidade. O retriever busca os trechos mais parecidos com a pergunta, e se o seu acervo tem o mesmo fato repetido em quatro documentos, os quatro são igualmente relevantes e todos entram no top-k. Aumentar o k traz mais cópias antes de trazer informação nova, então você paga mais tokens e recebe a mesma informação. Pior: a repetição enviesa o modelo, porque um fato que aparece cinco vezes parece mais confirmado do que um que aparece uma vez, e a resposta passa a refletir quantas vezes cada fato foi copiado no acervo em vez de qual fato está certo. A deduplicação é o passo que separa relevância de novidade, liberando espaço na janela e puxando o próximo trecho distinto da fila.

Deduplicar por similaridade de embedding não corre o risco de cortar informação?

Corre, e é o erro mais caro da deduplicação. Similaridade alta de embedding não significa conteúdo redundante: dois parágrafos sobre a mesma política, um enunciando a regra geral e o outro a exceção, têm cosseno altíssimo e são informação complementar, muitas vezes a mais importante da consulta. Descartar um deles pelo limiar faz o bot responder a regra geral inclusive nos casos em que ela não vale. A proteção tem três partes: descarte só é seguro no nível exato, e quanto mais semântica a comparação mais a operação certa é agrupar e resumir preservando as diferenças; uma checagem barata bloqueia a fusão quando os trechos divergem em negação, número, data ou condição; e toda remoção fica registrada, porque deduplicação que apaga em silêncio é indepurável.

Deduplicar substitui o reranking?

Não, os dois passos otimizam critérios diferentes e são complementares. O reranker ordena por relevância e não tem obrigação nenhuma de olhar redundância: se as cinco cópias da mesma política são todas muito relevantes, ele coloca as cinco no topo e está certo pelo critério dele. A deduplicação é o passo ortogonal que otimiza diversidade, garantindo que a janela carregue trechos distintos. Rodar a deduplicação antes do rerank reduz o conjunto a pontuar, o que barateia o rerank e evita que ele gaste posições com cópias; rodar depois garante a limpeza final antes de montar o prompt. Em pipelines exigentes vale rodar nos dois pontos, com limiar mais conservador antes e mais estrito depois.

Contexto bom não é contexto cheio, é contexto denso

Uma janela lotada com o mesmo fato cinco vezes entrega menos do que uma janela menor com cinco fatos distintos, e ainda enviesa o modelo a favor do que foi mais copiado no acervo. Tratar a duplicata em cascata, do hash barato ao embedding caro, fundir em vez de descartar quando a duplicata não é exata, proteger a exceção que parece cópia mas muda a resposta, reabastecer o espaço liberado com o próximo trecho da fila e medir densidade em vez de economia transforma o RAG de um sistema que enche o prompt num que informa. Posso desenhar essa camada de deduplicação no seu pipeline de RAG, calibrando o limiar com o seu acervo, blindando o falso positivo e medindo o ganho na resposta final, para que cada token da sua janela carregue informação que ainda não estava lá.