Blog

Chunking de documento para RAG sem perder contexto

Todo pipeline de RAG começa com uma decisão que quase ninguém debate e que decide a qualidade de tudo que vem depois: como cortar o documento em pedaços. O reflexo é o mais simples possível, partir o texto a cada N caracteres. Funciona no protótipo e engana na demo, mas em produção ele corta uma definição no meio, separa a pergunta da resposta, tira a tabela do seu cabeçalho e recupera um trecho que, isolado, não quer dizer nada. O modelo então recebe um contexto mutilado e responde com a confiança de sempre, só que sobre um fragmento sem sentido. O problema quase nunca está no modelo nem no embedding; está no chunk. Recuperação boa depende de o pedaço recuperado ser uma unidade que se entende sozinha, e isso não sai de um corte cego por tamanho. Este artigo mostra como cortar respeitando a estrutura do documento, por que a sobreposição entre pedaços existe, qual o tamanho certo de chunk, o metadado que todo pedaço precisa carregar e como medir se o chunking está ajudando ou sabotando o retrieval.

2026-07-16 / IA Aplicada / 13 min

01

Por que o corte por tamanho fixo quebra o contexto

Cortar o documento a cada N caracteres ignora tudo que dá sentido ao texto. O caractere na posição 500 pode estar no meio de uma frase, no meio de uma palavra, entre um número e sua unidade, ou separando o rótulo de uma lista do seu item. O resultado é um chunk que começa e termina no vazio: metade de uma ideia grudada na metade de outra. Na hora do retrieval, esse pedaço tem um embedding confuso, porque representa dois assuntos pela metade, e quando é recuperado chega ao modelo como um trecho que ninguém entenderia lendo isolado. O modelo, que só vê o que foi recuperado, responde a partir de um fragmento truncado.

O caso mais traiçoeiro é a separação entre pergunta e resposta, ou entre afirmação e sua condição. Um manual diz "o reembolso é liberado em 5 dias úteis, exceto quando o pagamento foi por boleto, caso em que o prazo é de 10 dias". Um corte por tamanho no lugar errado guarda a primeira metade num chunk e a exceção em outro. O retrieval traz o primeiro, o modelo responde "5 dias úteis" com toda a certeza, e a exceção que invertia a resposta ficou num pedaço que ninguém recuperou. Não é alucinação do modelo, é amputação na indexação.

SituaçãoCorte por tamanho fixoCorte por estrutura
Fim do chunkNo meio de frase ou palavraNo fim de parágrafo ou seção
Definição e sua exceçãoPodem cair em chunks diferentesFicam juntas na mesma unidade
Tabela e cabeçalhoCabeçalho separado das linhasTabela preservada como bloco
Embedding do chunkMistura dois assuntos pela metadeRepresenta um assunto coeso

A lição é que o tamanho não é a unidade certa de corte; o significado é. Um bom chunk termina onde uma ideia termina, não onde um contador de caracteres estoura. Todo o resto do artigo é sobre como cortar seguindo a estrutura do texto em vez de brigar contra ela.

02

Cortar seguindo a estrutura do documento

A estratégia que resolve a maior parte dos casos é o chunking recursivo por estrutura: em vez de partir por caractere, você parte pelos separadores naturais do documento, do maior para o menor. Primeiro tenta quebrar por seção ou título, depois por parágrafo, depois por frase, e só recorre ao corte bruto se um único parágrafo já for maior que o teto. Assim o corte cai sempre na maior fronteira semântica disponível, e um chunk raramente termina no meio de uma frase. O documento manda no corte, não o contador.

// chunk/recursive.js
// Chunking recursivo por estrutura: quebra pelos separadores naturais
// do documento, do maior para o menor, ate cada pedaco caber no teto.
// So corta "no bruto" quando um bloco unico ja excede o limite.

const SEPARATORS = ['\n\n\n', '\n\n', '\n', '. ', ' '];

export function chunkByStructure(text, { maxChars = 1200, sepIndex = 0 } = {}) {
  // Cabe inteiro: e um chunk coeso, nao precisa dividir mais.
  if (text.length <= maxChars) return [text];

  // Sem mais separadores: bloco unico gigante, corta no bruto como ultimo recurso.
  if (sepIndex >= SEPARATORS.length) {
    const cut = [];
    for (let i = 0; i < text.length; i += maxChars) cut.push(text.slice(i, i + maxChars));
    return cut;
  }

  const parts = text.split(SEPARATORS[sepIndex]).filter(Boolean);

  // Esse separador nao dividiu nada: tenta o proximo, mais fino.
  if (parts.length === 1) return chunkByStructure(text, { maxChars, sepIndex: sepIndex + 1 });

  // Junta os pedacos ate encher um chunk, respeitando a fronteira do separador.
  const chunks = [];
  let buffer = '';
  for (const part of parts) {
    if ((buffer + part).length > maxChars && buffer) {
      chunks.push(buffer.trim());
      buffer = '';
    }
    // Parte sozinha ja estoura o teto: desce um nivel de separador nela.
    if (part.length > maxChars) {
      chunks.push(...chunkByStructure(part, { maxChars, sepIndex: sepIndex + 1 }));
    } else {
      buffer += (buffer ? SEPARATORS[sepIndex] : '') + part;
    }
  }
  if (buffer.trim()) chunks.push(buffer.trim());
  return chunks;
}

Repare no formato do documento antes de escolher os separadores. Markdown tem títulos (#) e listas que são fronteiras óbvias; um PDF exportado pode ter quebras de linha artificiais no meio de parágrafos que enganam o separador; código quer ser cortado por função, não por linha em branco qualquer. Um chunking que ignora o formato de origem reintroduz pelo lado do parser o problema que estava tentando resolver. A regra geral é: identifique as fronteiras reais daquele tipo de documento e corte nelas, em vez de aplicar um separador único para tudo.

03

Sobreposição: costurar a fronteira entre chunks

Mesmo cortando por estrutura, sempre existe uma fronteira: o fim de um chunk e o começo do próximo. Uma informação relevante pode ficar exatamente em cima dessa linha, com a primeira parte no fim de um chunk e a continuação no início do seguinte. A sobreposição (overlap) resolve isso repetindo um pedaço do fim de cada chunk no começo do próximo. Assim, uma frase que atravessa a fronteira aparece inteira em pelo menos um dos dois chunks, e o retrieval consegue recuperá-la como unidade. É uma apólice de seguro barata contra o corte cair no pior lugar possível.

Sobreposição entre chunks costura a fronteira

  documento original
  [....A....][....B....][....C....]  <- fronteiras podem cortar uma ideia

  sem overlap:
    chunk 1: ....A....
    chunk 2: ....B....   <- ideia que estava entre A e B se perde no corte
    chunk 3: ....C....

  com overlap (repete o fim no proximo):
    chunk 1: ....A..[a]
    chunk 2: [a]..B..[b]   <- [a] traz o fim de A junto do comeco de B
    chunk 3: [b]..C....

  [a],[b] = trecho repetido na fronteira
  overlap grande = mais seguranca, mais custo de armazenamento e ruido
  overlap zero   = fronteiras viram pontos cegos do retrieval

A sobreposição tem um custo: cada trecho repetido é armazenado e embedado duas vezes, o índice cresce e há mais chunks parecidos competindo no retrieval. Overlap grande demais infla o custo e traz ruído; overlap zero transforma toda fronteira num ponto cego. Um valor prático fica entre 10% e 20% do tamanho do chunk, e o ajuste fino depende de quão auto-contido o conteúdo já é: texto muito fragmentado se beneficia de mais overlap, prosa corrida de menos. O ponto é que overlap não é enfeite, é o que impede que a informação certa desapareça justo na emenda entre dois pedaços.

04

O tamanho certo do chunk: o trade-off central

O tamanho do chunk governa um trade-off que aparece nas duas pontas do pipeline. Chunk grande carrega mais contexto por pedaço, mas seu embedding fica difuso: ele representa vários assuntos ao mesmo tempo, e na busca por similaridade um chunk que fala de dez coisas casa mal com uma pergunta sobre uma delas. Chunk pequeno tem embedding preciso e casa bem com a pergunta, mas pode ser específico demais e chegar ao modelo sem o contexto ao redor que dava sentido à resposta. Nenhum extremo é bom: o chunk gigante recupera com baixa precisão, o chunk minúsculo recupera preciso mas incompleto.

  • Chunk grande demais: embedding difuso, baixa precisão de retrieval, e ainda ocupa mais espaço da janela de contexto por pedaço recuperado.
  • Chunk pequeno demais: embedding preciso, mas recupera fragmentos sem contexto; o modelo recebe a frase certa sem o parágrafo que a explicava.
  • Ajuste por tipo de conteúdo: FAQ e trechos curtos toleram chunk pequeno; documentação técnica densa e contratos pedem chunk maior para não separar cláusula de condição.
  • Deixe o número calibrável, não hardcoded: o tamanho ideal muda com o corpus e com o modelo de embedding; se está fixo no código, você não consegue medir e ajustar.

Não existe um número mágico universal, mas há uma faixa de trabalho: chunks na ordem de algumas centenas de tokens costumam equilibrar precisão de embedding e contexto suficiente para a maioria dos textos. O caminho certo não é adivinhar e sim tratar o tamanho como parâmetro: começar numa faixa razoável, medir a qualidade do retrieval e ajustar por tipo de documento. Um corpus de FAQ curto e um corpus de contratos jurídicos não deveriam usar o mesmo tamanho de chunk, pela mesma razão que não usam a mesma linguagem.

05

Metadado: o chunk precisa saber de onde veio

Um chunk não é só o texto; é o texto mais a informação de onde ele veio. Sem metadado, um pedaço recuperado é um trecho órfão: o modelo não sabe de qual documento saiu, de qual seção, de qual versão, e você não consegue citar a fonte nem filtrar a busca. Anexar metadado a cada chunk transforma o retrieval de uma busca cega numa busca com procedência. Com o documento de origem e a seção, o modelo pode citar a fonte na resposta; com a data ou versão, você evita recuperar a política antiga; com o tenant ou o idioma, você filtra a busca antes mesmo da similaridade e não vaza dado de um cliente para outro.

// chunk/withMeta.js
// Cada chunk carrega o texto E a procedencia. Sem metadado, o pedaco
// recuperado e orfao: nao da para citar fonte, filtrar nem versionar.

export function attachMetadata(chunks, doc) {
  return chunks.map((text, index) => ({
    text,
    metadata: {
      docId: doc.id,          // de qual documento veio (permite citar a fonte)
      title: doc.title,       // titulo/secao para dar contexto na resposta
      source: doc.source,     // url ou caminho de origem
      version: doc.version,   // evita recuperar a versao antiga do conteudo
      tenant: doc.tenant,     // filtra a busca por cliente antes da similaridade
      lang: doc.lang,         // nao mistura idiomas no retrieval
      position: index,        // ordem do chunk dentro do documento
    },
  }));
}

// No retrieval: filtre por metadado (tenant, lang, version) ANTES de rankear
// por similaridade. Filtrar por metadado e barato e evita casar chunk errado.

O filtro por metadado antes do ranqueamento por similaridade é uma das melhorias de retrieval mais baratas que existem. Restringir a busca ao tenant certo, ao idioma certo e à versão vigente elimina de saída chunks que jamais deveriam casar, e o modelo passa a escolher entre candidatos que ao menos pertencem ao contexto da pergunta. Metadado também é o que viabiliza a citação de fonte na resposta, que é muitas vezes a diferença entre um bot que os usuários confiam e um que ninguém audita. O chunk sem procedência economiza cinco minutos na indexação e cobra caro em toda consulta.

06

Medir se o chunking está ajudando o retrieval

Chunking é fácil de mudar e difícil de avaliar no olho, porque o efeito só aparece lá na frente, na qualidade da resposta. Para não ajustar no escuro, você precisa de um conjunto de perguntas com o trecho de resposta esperado e de métricas de retrieval que digam se o chunk certo foi recuperado. A ideia é isolar a camada de chunking: fixando o modelo e o embedding, você troca só a estratégia de corte e mede o impacto no retrieval, sem confundir problema de chunk com problema de modelo.

MétricaO que revelaQuando agir
Recall@kSe o chunk certo apareceu entre os k recuperadosBaixo: chunk grande demais ou fronteira cortando a resposta
Precision@kQuantos dos k recuperados eram relevantesBaixa: chunk pequeno demais gerando fragmentos ruidosos
Fronteira partidaRespostas divididas entre dois chunksFrequente: aumente o overlap ou corte por estrutura
Chunk auto-contidoSe o pedaço faz sentido lido isoladoMuitos sem sentido: cortar por estrutura, não por tamanho

A métrica que mais importa é a recuperação: o chunk que contém a resposta esperada apareceu entre os k primeiros? Se não apareceu, nenhum reranking ou prompt melhora a resposta, porque a informação nem chegou ao modelo. Recall baixo aponta para chunk grande demais (embedding difuso) ou para a resposta partida na fronteira (overlap insuficiente). Antes de trocar de modelo de embedding ou de investir em reranking, vale medir se o problema não está simplesmente no corte, que é a alavanca mais barata do pipeline inteiro.

07

Montar a camada de chunking na ordem certa

Chunking é a fundação do RAG: ele é barato de implementar, difícil de perceber quando está errado e responsável por uma fatia enorme da qualidade final. Errar aqui condena tudo que vem depois, porque nenhum modelo responde bem sobre um trecho que foi recuperado pela metade. A ordem de implantação segue do estrutural para o fino.

  1. Comece cortando por estrutura, não por tamanho: respeite título, parágrafo e lista do documento antes de pensar em qualquer número de caracteres.
  2. Adapte os separadores ao formato de origem: markdown, PDF e código têm fronteiras diferentes; um separador único para tudo reintroduz o corte cego.
  3. Adicione overlap moderado (10% a 20%): costure as fronteiras para que informação na emenda não vire ponto cego, sem inflar o índice.
  4. Anexe metadado a todo chunk: docId, seção, versão, tenant e idioma; sem procedência não há citação de fonte nem filtro barato antes da similaridade.
  5. Meça o recall com um conjunto de perguntas rotuladas: só assim você ajusta tamanho e overlap com dado, em vez de trocar de modelo achando que o problema é o embedding.

A diferença entre um RAG que responde com precisão e um que alucina com confiança começa muito antes do modelo, no jeito de cortar o documento. Corte por estrutura, overlap na medida, metadado em cada pedaço e uma métrica de recall que fecha o ciclo: é isso que transforma um monte de texto num índice de onde o retrieval consegue tirar a resposta certa inteira. O embedding e o modelo são commodities; o chunking bem feito é a parte barata que decide se o resto do pipeline tem chance.

FAQ

Perguntas frequentes

Por que não posso simplesmente cortar o documento a cada N caracteres?

Porque o caractere N quase nunca cai numa fronteira de significado. Ele parte frases no meio, separa uma definição da sua exceção, tira uma tabela do cabeçalho e gera chunks que, lidos isolados, não querem dizer nada. Esse pedaço mutilado tem um embedding confuso e, quando recuperado, chega ao modelo como um trecho truncado. O resultado parece alucinação do modelo, mas é amputação feita na indexação. Cortar por estrutura (título, parágrafo, frase) faz o chunk terminar onde uma ideia termina, e é isso que preserva o contexto que o retrieval precisa recuperar.

Para que serve a sobreposição (overlap) entre chunks?

A sobreposição repete um pedaço do fim de cada chunk no começo do próximo, para que uma informação que cai exatamente na fronteira entre dois pedaços apareça inteira em pelo menos um deles. Sem overlap, toda fronteira vira um ponto cego: uma frase que atravessa a linha de corte fica pela metade nos dois chunks e o retrieval não consegue recuperá-la como unidade. O custo é armazenar e embedar o trecho repetido duas vezes, então o valor prático fica entre 10% e 20% do tamanho do chunk: o suficiente para costurar a fronteira sem inflar o índice nem encher o retrieval de chunks quase iguais.

Qual o tamanho ideal de chunk?

Não há número universal, porque o tamanho governa um trade-off. Chunk grande carrega mais contexto mas tem embedding difuso, que casa mal com perguntas específicas e baixa a precisão do retrieval. Chunk pequeno tem embedding preciso mas recupera fragmentos sem o contexto ao redor. Uma faixa de algumas centenas de tokens costuma equilibrar os dois para textos gerais, mas o certo é tratar o tamanho como parâmetro calibrável: começar numa faixa razoável, medir o recall e ajustar por tipo de documento. FAQ curto e contrato jurídico não deveriam usar o mesmo tamanho, pela mesma razão que não usam a mesma linguagem.

Chunking bem feito é a fundação barata de um RAG que não alucina

A qualidade do RAG começa no corte do documento: cortar por estrutura em vez de por tamanho, overlap moderado para costurar fronteiras, metadado de procedência em cada chunk e uma métrica de recall que fecha o ciclo. Posso desenhar essa camada de ingestão no seu sistema de IA, do parser ao índice, para que o retrieval recupere a resposta certa inteira em vez de fragmentos truncados.