Chunking e Embeddings: as decisões que fazem ou quebram seu RAG
- ⬜🧩 RAG: por que "só jogar tudo no LLM" não funciona(Engenharia AI-Native)
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égia | Como corta | Quando usa |
|---|---|---|
| Fixed-size | N tokens ou caracteres, ignora estrutura | Texto homogêneo, log streams — quase nunca o certo |
| Recursive | Tenta \n\n → \n → . → palavra | Default para markdown, artigos, docs — 90% dos casos |
| Document-aware | Respeita headings, listas, tabelas (markdown/HTML parsers) | Documentação técnica, manuais, KB estruturada |
| Semantic | Embeda frases, quebra em saltos de similaridade | Texto narrativo longo, transcrições |
| Proposition-based | LLM extrai proposições atômicas | Bases caras/críticas onde precisão > custo |
| Contextual (Anthropic) | Recursive + prefixo gerado por LLM com contexto do doc | Bases grandes onde naive retrieval falha muito |
Anatomia do chunk certo
| Parâmetro | Range comum | Por que |
|---|---|---|
| Tamanho (tokens) | 256 – 1024 | Pequeno perde contexto, grande dilui sinal. 512 é o sweet spot para maioria. |
| Overlap | 10% – 20% | Frase cortada em dois chunks sobrevive. Acima de 25%, duplica demais. |
| Metadata embutida | source, title, section, created_at, type | Filtragem por metadata no retrieval é barata e potente. |
| Boundary preservation | respeitar \n\n, listas, code fences | Cortar no meio de código é catástrofe. |
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.
# 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 ~10xEscolha de embedding em 2026
| Modelo | Dim | Contexto | Forte em |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 (ajustável) | 8191 | Default barato, multilíngue decente |
| OpenAI text-embedding-3-large | 3072 (ajustável) | 8191 | Precisão alta; use Matryoshka para comprimir dim |
| Voyage voyage-3-large | 1024 | 32000 | State-of-art em benchmarks independentes |
| Cohere embed-multilingual-v3 | 1024 | 512 | Forte em PT, ES, AR |
| BAAI/bge-m3 (open) | 1024 | 8192 | Open-source, multilíngue, multi-funcional (dense+sparse) |
| intfloat/multilingual-e5-large | 1024 | 512 | Open-source, boa base pt-BR |
📋 Começar um projeto sem muito budget e base em pt-BR
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-large — ganha em qualidade, mas mais caro e menos conhecido
Alt: Cohere v3 — multilí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.
# 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étrica | Fórmula | Quando usar |
|---|---|---|
| Cosine similarity | dot(a,b) / (|a|·|b|) | Default para texto. Invariante a magnitude. |
| Dot product | Σ a_i · b_i | Se vetores já normalizados (= cosine, mais rápido). |
| Euclidean (L2) | √Σ (a_i - b_i)² | Raro em texto. Mais comum em imagens/visão. |
<=> (cosine), <#> (dot negativo), <-> (L2). Usar a errada degrada silenciosamente a qualidade.Código: pipeline de ingest com metadata + chunks + pgvector
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?
❓ Devo guardar o texto original ou só o embedding?
❓ Como lido com PDFs com tabelas e layout complexo?
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?
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito