Chunking e embeddings na prática: do documento ao vetor sem alucinar em português
Como fatiar documentos em PT, estimar custo de embedding em R$ e preparar metadados dept/classification para RAG com ACL no case Núcleo Desk.
Em junho de 2026, a OpenAI cobra cerca de US$ 0,02 por milhão de tokens no modelo text-embedding-3-small. Para ter ordem de grandeza em reais, use R$ 5 por US$ 1 como referência ilustrativa (não é cotação do dia; ajuste na planilha com o câmbio da sua fatura). Nessa base, dá ~R$ 0,10 por milhão de tokens. Parece barato até você multiplicar por dezenas de políticas internas, runbooks e FAQs que mudam todo trimestre. Uma PME com 200 PDFs de média 8 páginas (~1,6 milhão de tokens na primeira indexação; texto extraído, variável conforme densidade da página) gasta centavos na API de embedding, uma vez. O problema não é o centavo do vetor: é fatiar errado em português, perder o parágrafo que cita férias ou reembolso, e o LLM (Large Language Model, modelo de linguagem) inventar a política que não entrou no contexto.
No context engineering vimos o que entra na janela antes da inferência: system prompt, papel ACL (Access Control List, lista de controle de acesso), chunks RAG. Hoje descemos um degrau: como o PDF vira chunk, como o chunk vira vetor, e quanto isso custa em reais no Brasil. A tese: embedding barato não salva chunking preguiçoso; e chunking bom sem metadado dept/classification não salva o estagiário que não deveria ver runbook de deploy.
O problema no Núcleo Desk
No Núcleo Desk, portal de suporte interno de um SaaS B2B com cerca de 800 funcionários, o time de People mantém três documentos que voltam em quase todo ticket:
Fixture (nucleo-desk/fixtures/) |
dept |
classification |
Pergunta típica |
|---|---|---|---|
politica-ferias.md |
hr |
internal |
“Quantos dias de férias posso tirar?” |
faq-billing.md |
product |
internal |
“Como peço reembolso de despesa?” |
runbook-deploy.md |
sre |
restricted |
“Como faço deploy em staging?” |
Se você jogar o PDF inteiro no prompt, estoura orçamento de tokens e mistura RH com runbook de infra. Se cortar em pedaços de 50 caracteres, o modelo perde a frase “30 dias corridos” no meio de um chunk e responde com CLT genérica da internet. Se indexar tudo sem metadado, o estagiário (intern) recupera trecho de deploy restrito.
RAG (Retrieval-Augmented Generation, geração aumentada por recuperação) começa aqui: unidade mínima citável, vetorizada, com metadado que o filtro ACL usa antes de injetar no contexto.
Do Markdown ao chunk: regras que funcionam em português
Português brasileiro infla token count em relação ao inglês técnico: acentos, crases, palavras compostas (“reembolso”, “antecedência”). Na stack escolhida para a trilha (Supabase + pgvector no episódio de RAG v1), o chunk ideal não é “512 tokens” copiado de tutorial em inglês sem pensar.
Tamanho e overlap
| Parâmetro | Valor inicial Núcleo Desk | Por quê |
|---|---|---|
| Chunk size | 400–600 caracteres (~100–150 tokens) | Preserva parágrafo de política ou passo de runbook |
| Overlap | 10–20% do size (ex.: 80 em 480) | Frase cortada no limite ainda aparece no chunk vizinho |
| Quebra | Parágrafo (\n\n) antes de caractere fixo |
Lista numerada de deploy não vira dois pedaços órfãos |
Cortar só por contagem de caracteres no meio de “é possível converter até 1/3 dos dias” gera chunk sem o número. Por isso o preview da trilha prioriza parágrafo e só depois fatia blocos longos.
Metadados obrigatórios por chunk
Cada chunk herda do documento fonte:
source: politica-ferias.md
dept: hr
classification: internal
Quando o RAG governado entrar na trilha, o filtro ACL usa dept + classification + user_role na query ao índice vetorial. Indexar sem isso é como ter permissão só no menu do front: o RAG vaza pelo backend.
Custo de embedding em R$ (ordem de grandeza)
Números ilustrativos para planejamento; confira preço vigente do provedor. Valores em R$ usam referência R$ 5 por US$ 1 (mesma base do gancho).
| Cenário | Tokens (estimativa) | Custo embedding (1x) | Reindexação trimestral (4x/ano) |
|---|---|---|---|
| 3 fixtures Núcleo (~12 chunks) | ~4 mil | < R$ 0,01 | desprezível |
| PME 50 docs internos | ~400 mil | ~R$ 0,04 | ~R$ 0,16/ano |
| Empresa 500 docs + changelog mensal | ~5 milhões/ano | ~R$ 0,55/indexação cheia | R$ 3–15/ano se delta pequeno |
Compare com R$ 8 mil a R$ 40 mil de SaaS mal integrado (do episódio de context engineering): o embedding em si não é a linha que quebra o orçamento. Quebra é reprocessar tudo porque o chunk mudou sem versionamento, ou pagar LLM forte para ler PDF inteiro a cada pergunta.
Dica de produção: hash do conteúdo por chunk (content_sha256). Só re-embeda o que mudou. O monorepo nucleo-desk formaliza isso no episódio de RAG v1; hoje o script abaixo já estima tokensEstimate na saída.
CLI ingest-preview: chunks + scores sem API key
O código e as fixtures vivem no repositório público nucleo-desk. Este episódio congela o estado em episode/03. Use essa branch para reproduzir exatamente o que o artigo descreve; main segue evoluindo nos episódios seguintes. Paths abaixo são relativos à raiz do repo. Clone e rode:
git clone https://github.com/iwilldotdev/nucleo-desk.git
cd nucleo-desk
git checkout episode/03
pnpm install
pnpm ingest-preview -- --question "Quantos dias de ferias posso tirar este ano?"
Saída (trecho real):
{
"question": "Quantos dias de ferias posso tirar este ano?",
"chunks": [
{
"id": "politica-ferias#0",
"source": "politica-ferias.md",
"dept": "hr",
"classification": "internal",
"score": 0.248,
"text": "Todo colaborador com mais de 12 meses..."
}
],
"stats": {
"totalChunks": 11,
"tokensEstimate": 2847
}
}
O JSON acima não é mágica: é o resultado de um pipeline curto em packages/rag-core. Abaixo, o que cada peça faz no snapshot episode/03.
Por dentro do ingest-preview
Quatro passos, na ordem em que o CLI executa:
1. Ler fixture e herdar metadado
Cada .md em fixtures/ traz frontmatter YAML. O parser separa cabeçalho e corpo; só entram documentos com dept e classification definidos (README e arquivos sem metadado são ignorados):
// packages/rag-core/src/ingest.ts: loadDocuments (trecho)
const { meta, body } = parseFrontmatter(raw);
if (!meta.dept || !meta.classification) continue;
docs.push({
source: meta.source ?? file,
dept: asDept(meta.dept),
classification: asClassification(meta.classification),
body,
});
Isso materializa a regra da seção anterior: metadado vive no chunk, não só no PDF de origem. No RAG governado, o filtro ACL consulta esses campos antes de injetar texto no LLM.
2. Fatiar por parágrafo, depois por tamanho
chunk.ts implementa a tabela de parâmetros (480 caracteres, overlap 80). Primeiro agrupa por \n\n; só fatia no meio do parágrafo se ele estourar o limite. Depois repete o final do chunk anterior no início do próximo (overlap):
// packages/rag-core/src/chunk.ts: chunkText (trecho)
const paragraphs = text.split(/\n\n+/);
// ... acumula parágrafos curtos no buffer ...
const prev = i > 0 ? chunks[i - 1].slice(-overlap) : "";
const merged = prev ? `${prev}\n${chunks[i]}` : chunks[i];
Por isso a pergunta sobre “30 dias de férias” costuma recuperar trecho de politica-ferias.md mesmo quando o corte cai no meio de “14 dias corridos”: o overlap carrega a frase para o chunk vizinho.
buildChunks numera cada pedaço (politica-ferias#0, #1, …) e já anexa dept / classification do documento pai.
3. Rankear sem chamar API (preview lexical)
Neste episódio não há embedding de produção. lexical.ts tokeniza em PT (remove acento, stopwords básicas), monta vetor esparso por frequência de termos (TF, term frequency) e compara pergunta vs chunk com similaridade de cosseno:
// packages/rag-core/src/lexical.ts: tokenize (trecho)
return text
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
// ...
.filter((t) => t.length > 2 && !PT_STOPWORDS.has(t));
// packages/rag-core/src/ingest.ts: searchChunks (trecho)
const qVec = textToVector(question);
return chunks
.map((c) => ({ ...c, score: cosineSimilarity(qVec, c.vector) }))
.sort((a, b) => b.score - a.score)
.slice(0, topK);
O score no JSON é esse cosseno (0 a 1), não similaridade vetorial de embedding. Serve para validar chunk size, golden questions e filtro ACL antes de gastar token na API. Se “férias” sem acento e “férias” com acento divergirem no ranking, você descobre cedo; no episódio de RAG v1 o embedding multilíngue reduz isso, mas o eval continua obrigatório.
4. Filtro ACL na linha de comando
Flags --dept e --classification não são decoração: ingestPreview filtra a lista de chunks antes do ranking:
// packages/rag-core/src/ingest.ts: ingestPreview (trecho)
if (filters.dept) {
chunks = chunks.filter((c) => c.dept === filters.dept);
}
if (filters.classification) {
chunks = chunks.filter((c) => c.classification === filters.classification);
}
const ranked = searchChunks(question, chunks, topK);
É o mesmo contrato que o portal usará no índice pgvector: papel do usuário restringe o subconjunto recuperável, não só o menu do front.
O que muda no próximo episódio
Hoje (episode/03) |
RAG v1 (próximo episódio) |
|---|---|
| TF + cosseno local | text-embedding-3-small (ou equivalente) |
| JSON no terminal | vetores em Supabase pgvector |
ingest-preview isolado |
docs-ingest + retrieve no grafo LangGraph |
O pacote rag-core permanece; troca-se a função de similaridade e o destino do vetor. Os testes golden deste episódio viram regressão no CI quando as evals entrarem.
Golden questions do Núcleo Desk
| Pergunta | Fonte esperada (fixtures/) |
Filtro ACL relevante |
|---|---|---|
| “Quantos dias de férias?” | politica-ferias.md |
employee, hr_admin |
| “Reembolso de despesa?” | faq-billing.md |
employee (sem salário) |
| “Deploy em staging?” | runbook-deploy.md |
só sre |
Lista completa em fixtures/golden-questions.json no snapshot episode/03.
Teste com filtro ACL no preview:
pnpm ingest-preview -- --question "Como faco deploy em staging?" \
--dept sre --classification restricted
Se --dept product retornar runbook, o metadado ou o filtro está errado. Isso vira eval automatizado quando as golden suites entrarem no CI.
Por que português exige cuidado extra
- Normalização: busca híbrida precisa decidir se “férias” e “ferias” colidem; no embedding, modelos multilíngues costumam alinhar, mas não confie sem eval.
- Listas e tabelas: markdown de FAQ com tabela de limites em R$ deve ficar no mesmo chunk ou com overlap generoso.
- Siglas internas:
kubectl,staging-us-east-1em runbook precisam de chunk que não separe comando do contexto.
Para founders
- Risco de negócio: comprar SaaS que “sobe seus PDFs” sem chunking auditável nem metadado por departamento replica a alucinação com selo enterprise. Resposta errada em férias ou vazamento de runbook restrito continua sendo passivo trabalhista e LGPD (Lei Geral de Proteção de Dados).
- Custo típico: embedding em si costuma ficar entre R$ 5 e R$ 150/mês para base interna de PME (depende de volume e reindexação). O gasto que dói é integração sem ACL (R$ 8 mil–R$ 40 mil) ou LLM caro lendo documento inteiro a cada pergunta.
- Quando RAG manual basta: até ~100 documentos estáveis, poucos papéis de acesso, e equipe técnica que aceita rodar script de ingestão trimestral. Quando auditoria pede “qual chunk sustentou a resposta” ou papéis multiplicam, o caminho genérico de upload em massa sem metadado deixa de fechar.
Para devs
O que colocar no GitHub e no LinkedIn depois deste episódio:
- Script de ingestão em
nucleo-desk/packages/rag-core(episode/03) com chunk size, overlap e frontmatterdept/classification. - Tabela de golden questions com fonte esperada e score mínimo aceitável.
- README em PT explicando por que 480 caracteres em PT ≠ 512 tokens em tutorial gringo.
- Estimativa de custo em R$ por indexação (planilha ou JSON no CI).
- Diagrama ingestão → pgvector (Mermaid acima) ligado ao mapa da pilha.
Frase para headline: “RAG em português: chunking, metadados ACL e custo de embedding em R$” comunica mais que “fiz um chatbot genérico sem eval nem ACL”.
Próximo episódio
Esta é a Parte 3 da trilha Núcleo Desk. Episódio anterior: Context engineering: a skill fundamental do AI engineer no Brasil em 2026. Começo da trilha: LLM, RAG, agentes e MCP: o mapa que o mercado brasileiro precisa em 2026.
No próximo artigo: RAG v1 com pgvector, React Router 7, CopilotKit e LangGraph (TS): índice vetorial real, ingestão das fixtures, registry de modelos (fast / strong) e primeiro grafo de chat no monorepo nucleo-desk.
Estou aprendendo a colocar IA em produção e documentando o case Núcleo Desk em público. Me acompanhe no LinkedIn para seguir a trilha, trocar ideias e discutir o que estou construindo.
Bibliografia
Preços e modelos de embedding
- OpenAI API pricing:
text-embedding-3-small(~US$ 0,02 / 1M tokens input, jun/2026). - CopilotKit + LangGraph (TypeScript): orquestração do agente e UI do portal a partir do RAG v1.
- OpenAI Embeddings API: modelo
text-embedding-3-smallusado empackages/rag-corena trilha.
Chunking e RAG em produção
- Chunking strategies for LLM applications (Pinecone Learn): overlap, tamanho por domínio, trade-offs recuperação vs custo.
- Lost in the Middle: How Language Models Use Long Contexts (Liu et al., 2023): motivação para chunks focados e top-K enxuto.
Português e busca
- ONIX-PB-751: Normalização de texto: referência de normalização para índices em PT-BR (relevante para Season 2 híbrida).
Repositório público
- nucleo-desk @
episode/03: snapshot deste episódio (fixtures/,packages/rag-core, CLIingest-preview).
Trilha Núcleo Desk
- Context engineering: system prompt, ACL no contexto, métricas.
- Mapa LLM, RAG, agentes e MCP: pilha completa e persona Núcleo Desk.
LGPD e dado em índice vetorial
- ANPD: Lei Geral de Proteção de Dados: tratamento de dado pessoal em corpus interno indexado; metadado e minimização.