🧠FFVAcademy
🔪

Chunking e Embeddings: as decisões que fazem ou quebram seu RAG

17 min de leitura·+85 XP
Pré-requisitos (0/1)0%

Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.

Se 80% do resultado do RAG vem do ingest, então 80% do ingest vem de duas decisões: como você corta (chunking) e como você codifica (embedding). Este módulo é o mapa das opções reais, os trade-offs e o que a comunidade convergiu em 2024-2026 como default.

Estratégias de chunking, lado a lado

EstratégiaComo cortaQuando usa
Fixed-sizeN tokens ou caracteres, ignora estruturaTexto homogêneo, log streams — quase nunca o certo
RecursiveTenta \n\n → \n → . → palavraDefault para markdown, artigos, docs — 90% dos casos
Document-awareRespeita headings, listas, tabelas (markdown/HTML parsers)Documentação técnica, manuais, KB estruturada
SemanticEmbeda frases, quebra em saltos de similaridadeTexto narrativo longo, transcrições
Proposition-basedLLM extrai proposições atômicasBases caras/críticas onde precisão > custo
Contextual (Anthropic)Recursive + prefixo gerado por LLM com contexto do docBases grandes onde naive retrieval falha muito

Anatomia do chunk certo

ParâmetroRange comumPor que
Tamanho (tokens)256 – 1024Pequeno perde contexto, grande dilui sinal. 512 é o sweet spot para maioria.
Overlap10% – 20%Frase cortada em dois chunks sobrevive. Acima de 25%, duplica demais.
Metadata embutidasource, title, section, created_at, typeFiltragem por metadata no retrieval é barata e potente.
Boundary preservationrespeitar \n\n, listas, code fencesCortar no meio de código é catástrofe.
💡
Regra prática: 512 tokens / 15% overlap / recursive splitter respeitando markdown. Isso é 95% do que se precisa para começar. Otimize depois, com eval harness na mão.

Contextual Retrieval: o truque de 2024 da Anthropic

Chunk isolado perde contexto. A margem operacional caiu 8% no trimestre. Qual empresa? Qual trimestre? Sem o documento original, ninguém sabe — nem o retrieval. Solução: pré-processar.

python
# Para cada chunk, gerar um contexto curto usando LLM barato
from anthropic import Anthropic

client = Anthropic()

def contextualize(doc: str, chunk: str) -> str:
    prompt = f"""<document>{doc}</document>
Here is a chunk from the document:
<chunk>{chunk}</chunk>
Provide a short (50-100 tokens) context situating this chunk within the document.
Output only the context, nothing else."""
    r = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=120,
        messages=[{"role": "user", "content": prompt}],
    )
    return r.content[0].text.strip()

# Ingest
for chunk in chunks:
    ctx = contextualize(full_doc, chunk)
    embedded_text = f"{ctx}\n\n{chunk}"  # embeda o combinado
    vector = embedder.encode(embedded_text)
    db.insert(chunk=chunk, ctx=ctx, vector=vector)
# Prompt caching no doc full faz o custo cair ~10x
Resultado no paper: redução de 35% em failures de retrieval (49% quando combinado com hybrid search + reranking). Custo é pago uma vez no ingest, não por query. Use prompt caching do Anthropic para baratear em 10×.

Escolha de embedding em 2026

ModeloDimContextoForte em
OpenAI text-embedding-3-small1536 (ajustável)8191Default barato, multilíngue decente
OpenAI text-embedding-3-large3072 (ajustável)8191Precisão alta; use Matryoshka para comprimir dim
Voyage voyage-3-large102432000State-of-art em benchmarks independentes
Cohere embed-multilingual-v31024512Forte em PT, ES, AR
BAAI/bge-m3 (open)10248192Open-source, multilíngue, multi-funcional (dense+sparse)
intfloat/multilingual-e5-large1024512Open-source, boa base pt-BR

📋 Começar um projeto sem muito budget e base em pt-BR

OpenAI text-embedding-3-small OU bge-m3 (self-host)

text-embedding-3-small é barato (US$0.02/1M tokens), 1536-dim configurável via Matryoshka, performance sólida. bge-m3 é opção open-source no mesmo patamar, com bônus de rodar local.

Alt: Voyage-3-largeganha em qualidade, mas mais caro e menos conhecido

Alt: Cohere v3multilíngue bom, mas ecossistema menor

Matryoshka: reduzir dimensão sem perder precisão

Modelos Matryoshka (OpenAI text-embedding-3, Voyage) foram treinados de forma que os primeiros N dims já capturam o essencial. Truncar para 512 ou 768 dims mantém ~95% da precisão, com storage e search 3-6× mais baratos.

python
# OpenAI: pedir dimensão menor na API
embedding = client.embeddings.create(
    model="text-embedding-3-large",
    input="texto",
    dimensions=768,  # default 3072, mas 768 mantém ~95% da quality
).data[0].embedding

# Depois de obter, re-normalizar se for cosine
import numpy as np
v = np.array(embedding)
v_norm = v / np.linalg.norm(v)

Métricas de similaridade: cosine, dot, L2

MétricaFórmulaQuando usar
Cosine similaritydot(a,b) / (|a|·|b|)Default para texto. Invariante a magnitude.
Dot productΣ a_i · b_iSe vetores já normalizados (= cosine, mais rápido).
Euclidean (L2)√Σ (a_i - b_i)²Raro em texto. Mais comum em imagens/visão.
⚠️
Pegadinha: ao migrar entre vector DBs, confirme a métrica. pgvector aceita <=> (cosine), <#> (dot negativo), <-> (L2). Usar a errada degrada silenciosamente a qualidade.

Código: pipeline de ingest com metadata + chunks + pgvector

python
from langchain_text_splitters import RecursiveCharacterTextSplitter
import psycopg
from openai import OpenAI

oai = OpenAI()
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,            # em tokens aproximados
    chunk_overlap=75,
    separators=["\n\n", "\n", ". ", " ", ""],
)

def embed(text: str) -> list[float]:
    return oai.embeddings.create(
        model="text-embedding-3-small",
        input=text,
        dimensions=768,
    ).data[0].embedding

conn = psycopg.connect("postgresql://...")
with conn.cursor() as cur:
    cur.execute("""
        CREATE TABLE IF NOT EXISTS chunks (
            id BIGSERIAL PRIMARY KEY,
            doc_id TEXT NOT NULL,
            section TEXT,
            chunk TEXT NOT NULL,
            embedding VECTOR(768) NOT NULL,
            created_at TIMESTAMPTZ DEFAULT now()
        );
        CREATE INDEX IF NOT EXISTS chunks_emb_idx
          ON chunks USING hnsw (embedding vector_cosine_ops);
    """)

def ingest(doc_id: str, section: str, text: str) -> None:
    for piece in splitter.split_text(text):
        vec = embed(piece)
        with conn.cursor() as cur:
            cur.execute(
                "INSERT INTO chunks(doc_id,section,chunk,embedding) VALUES (%s,%s,%s,%s)",
                (doc_id, section, piece, vec),
            )
    conn.commit()

Perguntas típicas

Chunk de 1000 tokens é sempre pior que 512?

Não. Em bases de texto longo e narrativo (ex: transcrições), chunks maiores preservam coesão e melhoram faithfulness. Em bases fragmentadas (FAQ, KB), 256-512 é melhor. Meça com seu golden set.

Devo guardar o texto original ou só o embedding?

Sempre ambos. Embedding é só para busca; o texto vai para o prompt do LLM. Guardar só embedding é erro comum — você não consegue reconstruir o chunk para citar fonte.

Como lido com PDFs com tabelas e layout complexo?

Parser importa. Usar pdfplumber ou unstructured.io ou VLMs (Claude) para extração estruturada. PyPDF básico perde tabelas e colunas. Para PDFs críticos, OCR + VLM é o caminho.

Re-indexar toda base quando trocar embedder é obrigatório?

Sim. Embeddings de modelos diferentes não são comparáveis — vivem em espaços vetoriais distintos. Migrar embedder = re-embedar tudo. Por isso a escolha inicial importa (e por isso existe Matryoshka: reduzir dim sem trocar modelo).
Take-aways. Recursive chunking de 512/75 overlap é o default correto. Contextual Retrieval reduz failures em 35%+ e vale o custo no ingest. Embedding: OpenAI text-embedding-3-small ou bge-m3 cobrem 95% dos casos — meça antes de trocar. Matryoshka corta dimensão sem perder qualidade. Sempre guarde texto original + embedding + metadata. Próximo: hybrid search e reranking para elevar precisão de 60% para 85%+.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo