← todos os papers
rag / eval / chunking12 mar 2026

Chunking recursivo vs. semântico: 6 estratégias no mesmo corpus

Todo tutorial de RAG chega no chunking e diz a mesma coisa: "experimente valores diferentes". Ninguém mostra os valores. Então dividimos o mesmo corpus de seis jeitos, indexamos os seis, e rodamos as mesmas 300 perguntas contra cada um.

O corpus é o Vantek: 1.240 documentos internos de uma empresa fictícia que a gente montou pra ter algo com a bagunça de um corpus corporativo real — políticas de RH, atas de reunião, tickets de suporte, PDFs de contrato exportados torto. As 300 perguntas foram escritas à mão com o trecho-resposta marcado no documento de origem.

As seis estratégias

estratégiatamanhooverlapchunkstempo de indexação
fixo 256256 tok08.41241 s
fixo 512512 tok04.19038 s
recursivo 512512 tok04.37744 s
recursivo 512/15%512 tok77 tok5.03147 s
recursivo 1024/15%1024 tok154 tok2.58843 s
semântico (bge-m3)variável3.90431 min

O semântico usa o próprio modelo de embedding pra achar as quebras: embeda frase a frase e corta onde a similaridade entre frases consecutivas cai abaixo de um percentil. É a estratégia que soa mais inteligente e é a que custa 40x mais pra indexar.

Como medimos

A métrica principal é recall@k — a fração de perguntas em que o trecho-resposta anotado apareceu entre os kk chunks recuperados:

recall@k=1QqQ1 ⁣[cRk(q):answer(q)c]\text{recall@}k = \frac{1}{|Q|}\sum_{q \in Q} \mathbb{1}\!\left[\, \exists\, c \in R_k(q) : \text{answer}(q) \subseteq c \,\right]

Recall@5 porque cinco chunks é o que cabe confortavelmente na janela junto com o prompt e o histórico, no tamanho de modelo que a gente consegue rodar local.

eval/recall.py
def recall_at_k(retriever, questions, k=5):
    hits = 0
    for q in questions:
        chunks = retriever.search(q.text, k=k)
        # O trecho anotado precisa estar contido inteiro em algum chunk.
        # Match parcial não conta: se a resposta foi cortada ao meio,
        # o gerador não tem como respondê-la, e é isso que importa.
        if any(q.answer_span in c.text for c in chunks):
            hits += 1
    return hits / len(questions)

Resultados

Recall@5 por estratégia de chunking

recall@5 · corpus Vantek · n=300 perguntas

fixo 256
0.61
fixo 512
0.68
recursivo 512
0.72
recursivo 512/15%
0.79
recursivo 1024/15%
0.74
semântico
0.69
ver como tabela
recall@5recall@5
fixo 2560.61
fixo 5120.68
recursivo 5120.72
recursivo 512/15%0.79
recursivo 1024/15%0.74
semântico0.69

O recursivo com 15% de overlap ganhou de todos, inclusive do semântico, por 10 pontos. E a explicação é chata: a maioria das respostas perdidas estava cortada na fronteira do chunk. Overlap não é uma otimização elegante, é um curativo — e o curativo funciona.

O semântico corta em fronteiras que fazem sentido pro modelo de embedding, que não é a mesma coisa que fazer sentido pra pergunta. Ele produziu chunks lindos de ler e perdeu exatamente onde a resposta cruzava dois assuntos.

O overlap tem um teto

Subir o overlap continua ajudando até parar:

Recall@5 conforme o overlap, em chunk de 512 tokens

overlap em % do chunk

ver como tabela
overlap em % do chunkrecall@5
0%0.72
10%0.77
15%0.79
25%0.79
40%0.78

Depois de 15% o ganho some e o índice só engorda. A 40% de overlap você paga 1,6x mais chunks pra ter o mesmo recall de 15%.

O que isso custa

estratégiarecall@5chunkstokens/consultacusto relativo de indexação
recursivo 512/15%0.795.0312.5601.0x
recursivo 1024/15%0.742.5885.1200.9x
semântico0.693.9043.14040.2x

Chunk de 1024 recupera menos e gasta o dobro de token por consulta. Chunk grande parece seguro porque "cabe mais contexto", mas cada chunk grande que entra no top-5 desloca um chunk que talvez tivesse a resposta.

O que a gente ainda não sabe

Isso é um corpus, num idioma, com um modelo de embedding. O bge-m3 é multilíngue e forte em português; um modelo só de inglês provavelmente muda a ordem das estratégias, e principalmente muda o semântico, que depende inteiramente da qualidade do embedding pra achar as quebras.

O próximo é rodar o mesmo protocolo com chunking guiado por estrutura — cortar em cabeçalho de markdown, cláusula de contrato, linha de ata. A intuição é que o ganho do overlap some quando as fronteiras são de verdade, e aí o número interessante passa a ser quanto do corpus tem estrutura recuperável.

Se você rodar isso no seu corpus e der diferente, escreve pra gente. Resultado que não replica é o dado mais útil que existe.