O problema real
Você tem 50 documentos de 100 páginas cada para indexar num sistema RAG. Tira o código de exemplo que viu online, coloca tudo num chunk de 1.000 tokens, e acaba com 250 chunks gigantes que misturam introdução com apêndice. Ou faz o oposto: chunks de 128 tokens que cortam frases no meio. Quando o retriever volta com esses pedaços, o LLM fica confuso — o contexto ou é demais ou é de menos. Resultado: alucinações, respostas genéricas, custos altos de embedding.
A decisão sobre como dividir o texto é tão crítica quanto o modelo de embedding que você escolhe. Cada estratégia tem trade-offs reais em qualidade, latência e custo. Vamos mostrar as quatro que funcionam na prática, com código rodando em cada uma.
Qual estratégia de chunking usar para seu caso?
Chunking é dividir um documento em pedaços indexáveis. Tem quatro estratégias principais:
- Tamanho fixo: 512 tokens com 20% sobreposição. Simples, previsível, padrão industrial.
- Por parágrafo: Respeita a estrutura do documento. Bom para prosa, ruim para código e tabelas.
- Recursiva: Começa com chunks grandes, quebra recursivamente se for necessário. Encaixa melhor.
- Semântica: Agrupa por tópico usando um embedding model. Caro, mas mais relevante.
Não existe "a melhor". Depende do tipo de conteúdo e do seu SLA (acordo de nível de serviço).
Mão na massa
Vamos implementar as quatro estratégias com código real. Use-as em seus dados e meça qual devolve chunks mais relevantes.
1. Tamanho fixo (baseline)
Simples e previsível. A maioria das pessoas deveria começar aqui.
import tiktoken
from typing import List
def chunk_fixed_size(text: str, chunk_size: int = 512, overlap: int = 0.2) -> List[str]:
"""Divide texto em chunks de tamanho fixo em tokens.
Args:
text: Texto bruto
chunk_size: Tamanho em tokens (típico: 512)
overlap: Sobreposição como decimal (0.2 = 20%)
Returns:
Lista de chunks
"""
encoding = tiktoken.get_encoding("cl100k_base")
tokens = encoding.encode(text)
overlap_tokens = int(chunk_size * overlap)
stride = chunk_size - overlap_tokens
chunks = []
for i in range(0, len(tokens), stride):
chunk_tokens = tokens[i : i + chunk_size]
chunk_text = encoding.decode(chunk_tokens)
chunks.append(chunk_text)
if i + chunk_size >= len(tokens):
break
return chunks
# Teste
sample_doc = """A economia brasileira cresceu 2,9% em 2023, impulsionada pelo setor de
serviços. A inflação encerrou o ano em 4,6%, dentro da meta do Banco Central.
No câmbio, o real fechou em R$ 4,99 por dólar. O mercado de trabalho manteve
a força com criação de 900 mil empregos formais. Mas a dívida pública atingiu
56% do PIB, preocupando investidores."""
chunks = chunk_fixed_size(sample_doc, chunk_size=100, overlap=0.2)
print(f"Total de chunks: {len(chunks)}")
for i, c in enumerate(chunks):
print(f"Chunk {i} ({len(c)} chars):\n{c}\n---")
Pros: Rápido, determinístico, fácil debugar. Contras: Pode cortar no meio de uma frase. Ineficiente para documentos curtos. Quando usar: Sempre comece aqui. Qualidade baseline.
2. Por parágrafo
Respeita a estrutura do documento original. Bom para prosa, relatórios.
import re
def chunk_by_paragraph(text: str, min_size: int = 100) -> List[str]:
"""Divide texto por parágrafos, mergeando os muito pequenos.
Args:
text: Texto bruto
min_size: Tamanho mínimo de um chunk em caracteres
Returns:
Lista de chunks (parágrafos)
"""
# Separa por quebra de linha dupla (parágrafo)
paragraphs = re.split(r'\n\n+', text.strip())
# Merge de parágrafos pequenos
chunks = []
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) < min_size:
current_chunk += "\n\n" + para if current_chunk else para
else:
if current_chunk:
chunks.append(current_chunk.strip())
current_chunk = para
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
# Teste
sample_doc = """Primeira frase do parágrafo 1. Continuação da frase.
Segundo parágrafo. Mais contexto aqui. Terminando o pensamento.
Terceira seção. Informação importante."""
chunks = chunk_by_paragraph(sample_doc, min_size=50)
for i, c in enumerate(chunks):
print(f"Parágrafo {i}:\n{c}\n---")
Pros: Preserva contexto, sem cortes arbitrários em meio a frases. Contras: Parágrafos muito longos viram chunks gigantes. Péssimo para código/tabelas. Quando usar: Prosa, artigos, relatórios bem estruturados.
3. Recursiva
Começa com chunks grandes, quebra se necessário. Bom para documentos mistos.
def chunk_recursive(text: str, chunk_sizes: List[int] = None, overlap: int = 0.2) -> List[str]:
"""Chunking recursivo: tenta quebrar de forma inteligente em cascata.
Args:
text: Texto bruto
chunk_sizes: Lista de tamanhos a tentar em ordem [512, 256, 128]
overlap: Sobreposição
Returns:
Lista de chunks
"""
if chunk_sizes is None:
chunk_sizes = [512, 256, 128]
encoding = tiktoken.get_encoding("cl100k_base")
tokens = encoding.encode(text)
def _chunk_recursive(token_list, sizes_to_try, current_overlap):
if len(token_list) <= sizes_to_try[0]:
return [encoding.decode(token_list)]
size = sizes_to_try[0]
overlap_tokens = int(size * current_overlap)
stride = size - overlap_tokens
chunks = []
for i in range(0, len(token_list), stride):
chunk = token_list[i : i + size]
if len(chunk) < size and len(sizes_to_try) > 1:
# Chunk pequeno demais, tenta tamanho menor
chunks.extend(_chunk_recursive(chunk, sizes_to_try[1:], current_overlap))
else:
chunks.append(encoding.decode(chunk))
return chunks
return _chunk_recursive(tokens, chunk_sizes, overlap)
# Teste
chunks = chunk_recursive(sample_doc, chunk_sizes=[100, 50, 25])
print(f"Total: {len(chunks)} chunks")
Pros: Adapta-se melhor a documentos heterogêneos. Contras: Mais lento, mais complexo debugar. Quando usar: Documentos que misturam prosa, código e estruturas.
4. Semântica (com LLM)
Agrupa por tópico usando embeddings. Mais caro, mas mais preciso.
from openai import OpenAI
import numpy as np
def chunk_semantic(text: str, target_chunk_size: int = 512) -> List[str]:
"""Chunking semântico: agrupa sentenças similares usando embeddings.
Args:
text: Texto bruto
target_chunk_size: Tokens alvo por chunk
Returns:
Lista de chunks agrupados por semântica
"""
client = OpenAI()
# Separa em sentenças
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
sentences = [s.strip() for s in sentences if s.strip()]
# Gera embedding de cada sentença
embeddings = []
for sent in sentences:
resp = client.embeddings.create(model="text-embedding-3-small", input=[sent])
embeddings.append(resp.data[0].embedding)
embeddings = np.array(embeddings)
# Agrupa sentenças similares
chunks = []
current_chunk = []
current_tokens = 0
encoding = tiktoken.get_encoding("cl100k_base")
for i, sent in enumerate(sentences):
sent_tokens = len(encoding.encode(sent))
# Se vai estourar o target, fecha o chunk
if current_tokens + sent_tokens > target_chunk_size and current_chunk:
chunks.append(" ".join(current_chunk))
current_chunk = [sent]
current_tokens = sent_tokens
else:
current_chunk.append(sent)
current_tokens += sent_tokens
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
# Nota: usar com cuidado — cada sentença gera um embedding (custo!)
# Para teste local, descomente a função quando tiver OPENAI_API_KEY
Pros: Semanticamente relevante, agrupa ideias relacionadas. Contras: Caro (custo de embedding × número de sentenças). Lento. Overkill para a maioria. Quando usar: Corpus técnico complexo ou qualidade crítica onde preço não importa.
Armadilhas comuns
Armadilha 1: Overlap insuficiente
Se usar 0% de sobreposição, conceitos que caem na borda desaparecem. Use pelo menos 10-20%. No exemplo de parágrafo anterior, a palavra "contexto" estava na borda — com 0% overlap, um chunk terminaria em "preserva" e o outro começaria em "contexto", quebrando a sentença.
Armadilha 2: Não respeitar estruturas
Cortar um arquivo Markdown no meio de um título ou de uma tabela deixa o chunk inutilizável. Se indexar código, não corte funções. Considere usar langchain.text_splitter.RecursiveCharacterTextSplitter que entende essa semântica.
Armadilha 3: Medir apenas recency
Chunks podem parecer bons isolados, mas quando vão num embedding, perdem contexto. Sempre meça: qual é a taxa de acurácia de relevância do seu retriever? (Quantos dos top-3 resultados são realmente relevantes?)
Armadilha 4: Tamanho fixo demais
512 tokens é padrão, mas pode ser errado. Um parágrafo de documentação técnica tem ~200 tokens. Um parágrafo de marketing tem ~100. Mude o tamanho conforme seu domínio.
Quando não usar isso
- Você tem < 1.000 palavras de conteúdo: Não precisa chunking, jogue tudo num embedding.
- Chunks sempre precisam de contexto externo: Se cada chunk só faz sentido com o anterior, o RAG não vai funcionar bem. Melhor fazer fine-tuning ou expander chunks.
- Latência < 50ms é crítico: Semântica é tão cara que não consegue bater esse SLA.
- Você quer preservar 100% da estrutura original: Chunking destrói estrutura. Use busca por palavra-chave para esse caso.
Próximos passos
- Teste as quatro estratégias com seus dados reais usando simulador de chunking — mede taxa de recuperação.
- Integre com RAG na prática usando a estratégia que tiver melhor taxa.
- Combine com avaliação de LLM com evals para medir qualidade ponta a ponta.
Resumo prático
| Estratégia | Tempo | Qualidade | Preço | Caso de uso |
|---|---|---|---|---|
| Tamanho fixo | < 1s | ⭐⭐⭐ | Grátis | Baseline sempre |
| Parágrafo | < 1s | ⭐⭐⭐⭐ | Grátis | Prosa estruturada |
| Recursiva | 2-5s | ⭐⭐⭐⭐ | Grátis | Documentos mistos |
| Semântica | 30-60s | ⭐⭐⭐⭐⭐ | USD 0,01+ | Qualidade crítica |
Comece com tamanho fixo (512 tokens, 20% overlap). Se a qualidade não passar no seu eval, escale para recursiva ou parágrafo. Só considere semântica se tiver orçamento e seus evals mostrem que vale a pena — em 90% dos casos, tamanho fixo bem calibrado bate estratégia cara.