Modelos de Linguagem de Grande Porte (LLMs) demonstram capacidades notáveis de síntese linguística e raciocínio generalista. Contudo, ao serem implantados em ambientes corporativos de produção, os modelos de base esbarram em dois limites estruturais: a data de corte de seus dados de treino (knowledge cutoff) e a propensão a gerar afirmações verossímeis, porém factualmente falsas, conhecidas como alucinações (confabulation).
A arquitetura de Geração Aumentada por Recuperação (RAG - Retrieval-Augmented Generation) resolve esse dilema desacoplando a memória de armazenamento de conhecimento da memória paramétrica da rede neural. Em vez de exigir que o LLM memorize toda a base documental interna da organização, o sistema RAG extrai dinamicamente trechos textuais relevantes de um mecanismo de busca especializado (tipicamente um banco de dados vetorial) e os injeta na janela de contexto no momento da inferência.
Entretanto, elevar um pipeline RAG de um protótipo experimental para um serviço corporativo de alta confiabilidade exige rigor de engenharia. Divisões ingênuas de texto a cada 1.000 caracteres combinadas com buscas vetoriais simplórias provocam quebras de contexto, tabelas corrompidas, ruído excessivo e alucinações derivadas de dados conflitantes.
Neste tutorial técnico, detalhamos as camadas de engenharia de um pipeline RAG de nível de produção: ingestão estruturada, estratégias de particionamento (chunking), métricas de similaridade vetorial, reranking com cross-encoders e síntese de respostas com citações auditáveis de proveniência.
1. Arquitetura RAG de Ponta a Ponta
Um sistema RAG de produção opera através de dois ciclos assíncronos e desacoplados: o Pipeline de Ingestão e Indexação Offline e o Ciclo de Recuperação e Geração Online.
[ PIPELINE DE INGESTÃO OFFLINE ]
Documentos Brutos (PDF, Markdown, HTML, Bases Confluence)
│
▼
Extração de Texto e Limpeza
│
▼
Particionamento Sensível à Estrutura (400–800 tokens, 10–15% sobreposição)
│
▼
Geração de Embeddings Densos (text-embedding-3-small, 1536 dim)
│
▼
Indexação em Banco Vetorial (Grafo HNSW / Carga de Metadados)
─────────────────────────────────────────────────────────────────
[ CICLO DE RECUPERAÇÃO E GERAÇÃO ONLINE ]
Consulta do Usuário
│
▼
Pré-processamento e Expansão de Consulta (HyDE)
│
▼
Busca Híbrida (Similaridade de Cosseno Densa + Frequência de Palavras BM25)
│
▼
Fusão por Classificação Recíproca (RRF) ──> Top 50 Candidatos
│
▼
Reranker com Cross-Encoder ──> Top 4 Trechos Mais Relevantes
│
▼
Montagem de Contexto e Tags de Citação
│
▼
Geração Fundamentada pelo LLM (Exigência de [Fonte-N] e Recusa se Ausente)
2. Ingestão Documental e Estratégias de Chunking
A qualidade final da recuperação da informação é delimitada pela precisão da estratégia de particionamento (chunking). Se o trecho for pequeno demais, o contexto envolvente se perde; se for excessivamente longo, o embedding denso é diluído por ruídos irrelevantes, reduzindo a similaridade em consultas pontuais.
Comparação de Abordagens de Particionamento
| Estratégia | Alvo de Tokens | Sobreposição | Vantagens | Desvantagens e Riscos |
|---|---|---|---|---|
| Janela Deslizante Fixa | 256–512 tokens | 10–20% | Implementação trivial, custo previsível | Corta frases, parágrafos e linhas de tabelas |
| Divisor Recurrente de Caracteres | 400–800 tokens | 50–100 tokens | Respeita parágrafos (\n\n) e pontuações |
Pode dividir listas aninhadas e trechos de código |
| Particionamento AST Markdown | Dinâmico | Hierarquia de títulos | Mantém títulos, blocos de código e tabelas intactos | Requer agrupamento para documentos com muitas seções |
| Particionamento Semântico | Dinâmico | Nenhuma | Baseia-se no desvio de embeddings entre frases | Custo de inferência alto durante a ingestão inicial |
Implementação de Chunker Estruturado em Markdown (TypeScript)
Para documentação de engenharia e bases de conhecimento, a preservação da árvore semântica do Markdown assegura o maior índice de acerto na recuperação:
// src/lib/rag/chunker.ts
export interface DocumentSection {
title: string;
sourceUrl: string;
category: string;
content: string;
}
export interface TextChunk {
chunkId: string;
sourceUrl: string;
headingPath: string;
content: string;
tokenEstimate: number;
}
/**
* Divide o texto respeitando os títulos Markdown (#, ##, ###) e as quebras de parágrafo
*/
export function chunkMarkdownDocument(
doc: DocumentSection,
maxTokens: number = 600,
overlapTokens: number = 60
): TextChunk[] {
const sections = doc.content.split(/\n(?=#{1,3}\s)/);
const chunks: TextChunk[] = [];
let chunkCounter = 0;
for (const section of sections) {
const lines = section.trim().split("\n");
const heading = lines[0].startsWith("#") ? lines[0].replace(/^#+\s*/, "") : "General";
const body = lines[0].startsWith("#") ? lines.slice(1).join("\n").trim() : section.trim();
if (!body) continue;
const estimatedTokens = Math.ceil(body.length / 4);
if (estimatedTokens <= maxTokens) {
chunkCounter++;
chunks.push({
chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
sourceUrl: doc.sourceUrl,
headingPath: `${doc.title} > ${heading}`,
content: `### ${heading}\n${body}`,
tokenEstimate: estimatedTokens
});
} else {
const paragraphs = body.split(/\n\n+/);
let currentBuffer: string[] = [];
let currentTokenCount = 0;
for (const paragraph of paragraphs) {
const pTokens = Math.ceil(paragraph.length / 4);
if (currentTokenCount + pTokens > maxTokens && currentBuffer.length > 0) {
chunkCounter++;
chunks.push({
chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
sourceUrl: doc.sourceUrl,
headingPath: `${doc.title} > ${heading}`,
content: `### ${heading}\n${currentBuffer.join("\n\n")}`,
tokenEstimate: currentTokenCount
});
const overlap = currentBuffer.slice(-1);
currentBuffer = [...overlap, paragraph];
currentTokenCount = Math.ceil(currentBuffer.join("\n\n").length / 4);
} else {
currentBuffer.push(paragraph);
currentTokenCount += pTokens;
}
}
if (currentBuffer.length > 0) {
chunkCounter++;
chunks.push({
chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
sourceUrl: doc.sourceUrl,
headingPath: `${doc.title} > ${heading}`,
content: `### ${heading}\n${currentBuffer.join("\n\n")}`,
tokenEstimate: currentTokenCount
});
}
}
}
return chunks;
}
3. Métricas de Similaridade Vetorial e Indexação HNSW
Modelos de embedding projetam passagens de texto em um espaço contínuo multidimensional $\mathbb{R}^d$.
Similaridade de Cosseno vs Produto Escalar (Dot Product)
- Similaridade de Cosseno: Avalia o cosseno do ângulo entre dois vetores, desconsiderando magnitudes absolutas, o que a torna ideal para trechos textuais de tamanhos heterogêneos.
- Produto Escalar: Ao normalizar previamente os vetores com norma unitária L2 ($|\mathbf{u}|_2 = 1$), o produto escalar se torna matematicamente idêntico à similaridade de cosseno, sendo computado com altíssima eficiência em hardware moderno via instruções vetorizadas (AVX-512).
Busca Aproximada de Vizinhos Mais Próximos (ANN) e Parâmetros HNSW
Percorrer exaustivamente coleções de milhões de vetores possui complexidade de $O(N \cdot d)$, inviabilizando tempos de resposta em milissegundos. Os bancos vetoriais empregam a estrutura de grafos HNSW (Hierarchical Navigable Small World):
M: Número máximo de conexões bidirecionais por nó.efConstruction: Tamanho da fila dinâmica de avaliação durante a geração do índice.efSearch: Profundidade da fila durante a consulta em tempo real, permitindo calibrar o balanço entre velocidade e precisão de recuperação.
4. Busca Híbrida e Reranking com Cross-Encoder
Confiar exclusivamente em vetores densos introduz vulnerabilidades conhecidas: baixa sensibilidade a termos exatos, números de protocolo, códigos de erro e identificadores específicos.
A arquitetura de produção implementa a Busca Híbrida (Hybrid Search): disparando simultaneamente a busca semântica por vetores densos e a busca léxica BM25 por palavras-chave, unificando os resultados por meio de Reciprocal Rank Fusion (RRF).
Em seguida, o conjunto consolidado de candidatos (Top 40 a 50) é submetido a um modelo Cross-Encoder Reranker, que avalia a atenção direta entre a pergunta e cada passagem candidata, eliminando até 90% do ruído documental.
5. Implementação Completa do Pipeline RAG (TypeScript)
Apresentamos a implementação de um serviço RAG pronto para produção em TypeScript com Pinecone e OpenAI:
// src/lib/rag/pipeline.ts
import { Pinecone } from '@pinecone-database/pinecone';
export interface RetrievedChunk {
chunkId: string;
sourceUrl: string;
headingPath: string;
content: string;
score: number;
}
export interface GroundedRAGResponse {
answer: string;
citations: Array<{ citationIndex: number; sourceUrl: string; headingPath: string }>;
tokenUsage: { promptTokens: number; completionTokens: number };
refusal: boolean;
}
export async function executeGroundedRAG(
query: string,
pineconeClient: Pinecone,
pineconeIndexName: string,
embeddingApiKey: string,
llmApiKey: string
): Promise<GroundedRAGResponse> {
// 1. Geração de embedding normalizado da consulta
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${embeddingApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
input: query,
model: 'text-embedding-3-small'
})
});
if (!embeddingResponse.ok) {
throw new Error(`Erro na API de Embeddings: ${embeddingResponse.statusText}`);
}
const embeddingData = await embeddingResponse.json();
const queryVector: number[] = embeddingData.data[0].embedding;
// 2. Consulta de candidatos no banco vetorial
const index = pineconeClient.index(pineconeIndexName);
const queryResult = await index.query({
vector: queryVector,
topK: 10,
includeMetadata: true
});
const candidates: RetrievedChunk[] = (queryResult.matches || []).map((match) => ({
chunkId: match.id,
sourceUrl: (match.metadata?.sourceUrl as string) || 'Unknown Source',
headingPath: (match.metadata?.headingPath as string) || 'General',
content: (match.metadata?.content as string) || '',
score: match.score || 0
}));
// 3. Filtro de similaridade mínima
const MIN_SIMILARITY_SCORE = 0.65;
const filteredChunks = candidates.filter(c => c.score >= MIN_SIMILARITY_SCORE);
// Recusa explícita caso não haja evidências seguras para prevenir alucinação
if (filteredChunks.length === 0) {
return {
answer: "A documentação fornecida não contém evidências suficientes para responder à sua solicitação com precisão.",
citations: [],
tokenUsage: { promptTokens: 0, completionTokens: 0 },
refusal: true
};
}
const selectedContext = filteredChunks.slice(0, 4);
// 4. Montagem de prompt com rigor de citações
const contextBlock = selectedContext
.map((chunk, idx) => `[Fonte-${idx + 1}] (${chunk.headingPath})\n${chunk.content}`)
.join("\n\n---\n\n");
const systemPrompt = `Você é um assistente técnico documental rigoroso.
Responda à pergunta do usuário utilizando EXCLUSIVAMENTE os trechos verificados abaixo.
Regras inegociáveis:
1. Toda afirmação factual deve ser sucedida de uma citação correspondente no formato [Fonte-N].
2. Se as fontes fornecidas não contiverem evidências claras, responda: "A documentação fornecida não possui informações suficientes para responder."
3. Jamais especule ou traga conhecimentos externos à margem dos trechos disponibilizados.
FONTES VERIFICADAS:
${contextBlock}`;
// 5. Síntese do LLM
const llmResponse = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${llmApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: query }
],
temperature: 0.1,
max_tokens: 500
})
});
if (!llmResponse.ok) {
throw new Error(`Erro na síntese do LLM: ${llmResponse.statusText}`);
}
const completionData = await llmResponse.json();
const answerText = completionData.choices[0].message.content;
const citations = selectedContext.map((chunk, idx) => ({
citationIndex: idx + 1,
sourceUrl: chunk.sourceUrl,
headingPath: chunk.headingPath
}));
return {
answer: answerText,
citations,
tokenUsage: {
promptTokens: completionData.usage.prompt_tokens,
completionTokens: completionData.usage.completion_tokens
},
refusal: answerText.includes("não possui informações suficientes")
};
}
6. Checagem de Fatos: RAG Elimina Totalmente as Alucinações?
A afirmação comercial de que “bancos de dados vetoriais e RAG eliminam 100% das alucinações de modelos de linguagem” é tecnicamente inverídica.
Embora o RAG ancore a geração em evidências factuais, ele introduz modos de falha próprios:
- Falhas de Recuperação (Retrieval Miss): Quando há divergência de vocabulário, o trecho correto não é resgatado. Sem regras de recusa, o modelo tenta adivinhar a resposta.
- Efeito ‘Lost in the Middle’: Modelos de atenção focam mais no início e no final de contextos extensos. Fornecer muitos trechos faz com que fatos essenciais localizados no meio passem despercebidos.
- Contaminação por Documentos Antigos: Caso a base guarde especificações de uma versão legada e da versão atual, o modelo pode fundir conceitos e inventar parâmetros inexistentes.
- Viés de Pré-treino (Faithfulness Failure): Quando uma evidência textual contraria uma convicção firmada durante o pré-treinamento, o modelo pode desconsiderar o trecho recuperado.
7. Conclusão
Bancos de dados vetoriais e pipelines RAG são elementos vitais para a construção de inteligências artificiais corporativas seguras. Ao associar chunking consciente de estrutura, busca híbrida, reranking e instruções de citação inegociáveis, engenheiros de software conseguem entregar sistemas com alto grau de auditabilidade e confiança.
Para explorar padrões de isolamento de dados entre clientes, leia nosso artigo sobre Padrões de Arquitetura SaaS Multi-Tenant, ou veja como especializar modelos de linguagem no guia sobre Fine-Tuning de LLMs para Tarefas de Domínio Específico.