O problema real
Você tem dezenas de documentos longos para indexar num sistema RAG. Pega um exemplo da internet e joga tudo em chunks gigantes, que misturam introdução com apêndice. Ou faz o oposto e gera pedaços minúsculos que cortam frases no meio. Nos dois casos o retriever devolve trechos que atrapalham o modelo, e você vê respostas genéricas ou inventadas.
A forma de dividir o texto pesa tanto quanto o modelo de embedding. Neste artigo você vai implementar quatro estratégias de chunking, entender o que cada uma ganha e perde, e montar um jeito simples de decidir com dados. Se ainda não montou um pipeline, comece por RAG na prática.
Qual estratégia de chunking usar?
Chunking é dividir um documento em pedaços que serão indexados e recuperados. As quatro estratégias mais usadas são estas.
- Tamanho fixo, cortando por quantidade de tokens, com sobreposição entre pedaços.
- Por parágrafo, respeitando a estrutura do texto.
- Recursiva, que tenta cortar em separadores grandes (seções, parágrafos) e desce para separadores menores só quando o pedaço ainda é grande demais.
- Semântica, que corta onde o assunto muda, usando embeddings das frases.
Não existe uma melhor. Depende do tipo de conteúdo e do que você mede no seu conjunto de perguntas. Os trechos abaixo usam tiktoken para contar tokens (pip install tiktoken). Contar caracteres também funciona como aproximação, mas tokens refletem o que o modelo de embedding enxerga.
Mão na massa
1. Tamanho fixo (a linha de base)
Simples, previsível e fácil de depurar. Comece por aqui para ter um número de referência.
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
def chunk_tamanho_fixo(texto: str, tamanho: int = 512, sobreposicao: float = 0.2) -> list[str]:
tokens = enc.encode(texto)
passo = tamanho - int(tamanho * sobreposicao)
chunks = []
for i in range(0, len(tokens), passo):
chunks.append(enc.decode(tokens[i : i + tamanho]))
if i + tamanho >= len(tokens):
break
return chunks
doc = (
"A política de devolução permite devolver itens em até 30 dias. "
"O reembolso é integral se o produto estiver em bom estado. "
"Itens usados ou danificados pelo cliente não são aceitos. "
"Para iniciar a devolução, abra um chamado pelo formulário do site."
)
for i, c in enumerate(chunk_tamanho_fixo(doc, tamanho=40, sobreposicao=0.25)):
print(f"[{i}] {c}\n")
O ponto fraco é cortar frases ao meio. A sobreposição reduz o problema, mas não o elimina.
2. Por parágrafo
Respeita a estrutura natural do texto. Funciona bem em artigos, relatórios e manuais bem escritos. O código junta parágrafos curtos até atingir um tamanho mínimo.
import re
def chunk_por_paragrafo(texto: str, minimo_tokens: int = 80) -> list[str]:
paragrafos = [p.strip() for p in re.split(r"\n\s*\n", texto) if p.strip()]
chunks, atual = [], ""
for p in paragrafos:
atual = f"{atual}\n\n{p}" if atual else p
if len(enc.encode(atual)) >= minimo_tokens:
chunks.append(atual)
atual = ""
if atual:
chunks.append(atual)
return chunks
Parágrafos muito longos continuam longos, e tabelas ou código perdem o sentido quando separados. Para esses casos, use a próxima estratégia.
3. Recursiva
A ideia é tentar separadores em ordem, do mais "grosso" ao mais "fino". Se um pedaço já cabe no limite, fica como está. Se não cabe, divide com o próximo separador. Por isso ela se adapta bem a documentos mistos.
SEPARADORES = ["\n## ", "\n\n", "\n", ". ", " "]
def chunk_recursivo(texto: str, limite: int = 300, seps: list[str] = SEPARADORES) -> list[str]:
if len(enc.encode(texto)) <= limite or not seps:
return [texto]
sep, resto = seps[0], seps[1:]
partes = texto.split(sep)
if len(partes) == 1:
return chunk_recursivo(texto, limite, resto)
chunks, atual = [], ""
for parte in partes:
candidato = f"{atual}{sep}{parte}" if atual else parte
if len(enc.encode(candidato)) <= limite:
atual = candidato
else:
if atual:
chunks.append(atual)
atual = ""
chunks.extend(chunk_recursivo(parte, limite, resto))
if atual:
chunks.append(atual)
return chunks
Na prática, muita gente usa a implementação pronta RecursiveCharacterTextSplitter, do pacote langchain-text-splitters. Ela já traz sobreposição e separadores para Markdown e vários tipos de código. Escreva a sua só para entender o mecanismo.
4. Semântica
Em vez de cortar por tamanho, cortamos onde o assunto muda. Calculamos o embedding de cada frase e, quando a similaridade entre frases vizinhas cai abaixo de um limiar, abrimos um novo chunk. A chamada é feita em lote, uma requisição para todas as frases, e não uma por frase.
import re
import numpy as np
from openai import OpenAI
client = OpenAI()
def chunk_semantico(texto: str, limiar: float = 0.6) -> list[str]:
frases = [f.strip() for f in re.split(r"(?<=[.!?])\s+", texto.strip()) if f.strip()]
if len(frases) < 2:
return frases
resp = client.embeddings.create(model="text-embedding-3-small", input=frases)
vecs = np.array([d.embedding for d in resp.data])
vecs = vecs / np.linalg.norm(vecs, axis=1, keepdims=True)
chunks, atual = [], [frases[0]]
for i in range(1, len(frases)):
similaridade = float(vecs[i - 1] @ vecs[i])
if similaridade < limiar:
chunks.append(" ".join(atual))
atual = []
atual.append(frases[i])
chunks.append(" ".join(atual))
return chunks
O limiar depende do modelo de embedding e do idioma, então ajuste olhando exemplos reais. O custo extra é gerar embeddings de todo o corpus antes da indexação, e o ganho nem sempre aparece. Estudos de comparação publicados mostram resultados mistos, então só adote se o seu teste mostrar melhora.
Como escolher com dados
Monte de 30 a 50 perguntas reais, cada uma ligada ao trecho do documento que responde. Para cada estratégia, indexe, rode as perguntas e calcule em quantas o trecho correto aparece entre os 3 primeiros resultados (recall@3). Compare também o tamanho médio dos chunks e o número total, que afeta custo de armazenamento. Quem ganha no seu corpus é a estratégia a usar. O simulador de chunking ajuda a visualizar os cortes.
Armadilhas comuns
Sobreposição zero
Sem sobreposição, uma ideia na fronteira fica partida entre dois chunks e nenhum deles a contém completa. Experimente 10% a 20%.
Ignorar a estrutura do documento
Cortar um Markdown no meio de uma tabela ou de um bloco de código deixa o chunk inútil. Separe por títulos primeiro e guarde o título como metadado ou prefixo do chunk, assim o trecho carrega o contexto de onde veio.
Tamanho fixo para todo tipo de conteúdo
Documentação técnica, contratos e FAQs têm densidades diferentes. Teste tamanhos distintos por tipo de documento em vez de adotar um valor único.
Avaliar o chunk isolado
Um chunk pode parecer bom ao ler, mas ser ruim na busca. Quem decide é a métrica de recuperação, não a leitura.
Quando chunking não resolve
- Seu conteúdo total é pequeno e cabe no contexto do modelo. Coloque tudo no prompt.
- Cada chunk só faz sentido com o anterior. Aumente o tamanho, ou recupere o chunk vizinho junto com o encontrado.
- Você precisa de uma citação literal e exata. Combine a busca vetorial com busca por palavra-chave.
Próximos passos
Comece com tamanho fixo de 512 tokens e 20% de sobreposição, meça o recall e só então teste recursiva ou por parágrafo. Considere a semântica apenas se os dados justificarem. Para medir ponta a ponta, veja Avaliação de LLM com evals, e para guardar os chunks, Banco vetorial na prática.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria