Núcleo Desk: Engenharia de IA no BrasilParte3IA30minutos de leitura

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 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

  1. 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.
  2. Listas e tabelas: markdown de FAQ com tabela de limites em R$ deve ficar no mesmo chunk ou com overlap generoso.
  3. Siglas internas: kubectl, staging-us-east-1 em 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:

  1. Script de ingestão em nucleo-desk/packages/rag-core (episode/03) com chunk size, overlap e frontmatter dept/classification.
  2. Tabela de golden questions com fonte esperada e score mínimo aceitável.
  3. README em PT explicando por que 480 caracteres em PT ≠ 512 tokens em tutorial gringo.
  4. Estimativa de custo em R$ por indexação (planilha ou JSON no CI).
  5. 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

Chunking e RAG em produção

Português e busca

Repositório público

Trilha Núcleo Desk

LGPD e dado em índice vetorial