A implementação de sistemas de Retrieval-Augmented Generation (RAG) representa uma das abordagens mais promissoras para resolver as limitações fundamentais dos Large Language Models modernos. Este artigo apresenta uma jornada completa na construção de um sistema RAG robusto e escalável, utilizando TypeScript como base de desenvolvimento, Docker para orquestração de infraestrutura, Google Gemini para inteligência artificial e LangChain.js como framework de integração.
Nossa solução permite que usuários façam perguntas em linguagem natural sobre documentos PDF, combinando busca semântica avançada com geração de respostas contextuais precisas. O sistema demonstra como integrar tecnologias de ponta para criar aplicações de IA práticas e escaláveis, abordando desde a extração e processamento de documentos até a geração de respostas contextualmente relevantes.
As tecnologias principais que formam o backbone desta implementação incluem Node.js versão 22 ou superior para runtime JavaScript moderno, TypeScript 5.9 ou superior para tipagem estática robusta, LangChain.js 0.3 ou superior como framework de orquestração de IA, Google Gemini API para embeddings e geração de texto, PostgreSQL 15 ou superior com a extensão pgVector para armazenamento e busca vetorial, e Docker para containerização e implantação simplificada.
observação: como muitos já sabem, estou fazendo o MBA em Engenheria de Software em A.I na FullCycle, e este artigo é baseado em um dos projetos práticos do curso. Não estou fazendo jabá, apenas compartilhando o conhecimento aprendido e para que outros possam se beneficiar também. Mas, caso queira saber mais sobre o MBA, clique no link anterior.
Compreendendo RAG e sua importância fundamental
O Desafio dos LLMs Tradicionais
Large Language Models como GPT, Claude e Gemini revolucionaram o processamento de linguagem natural, mas enfrentam limitações que impedem sua aplicação direta em cenários empresariais e especializados. O conhecimento destes modelos permanece estático, sendo limitado aos dados de treinamento até uma data específica, criando uma lacuna temporal que pode ser crítica em domínios onde informações atualizadas são essenciais.
Além disso, estes modelos tendem a produzir alucinações, inventando informações quando não possuem conhecimento suficiente sobre um tópico. Esta característica pode ser particularmente problemática em aplicações que exigem precisão factual. Os LLMs também carecem de contexto específico sobre dados internos de empresas ou documentos especializados, limitando sua utilidade em cenários onde conhecimento especializado é necessário.
A impossibilidade de atualização pós-treinamento representa outro obstáculo significativo. Uma vez treinado, um modelo não pode aprender novos fatos ou incorporar informações atualizadas sem um processo completo de retreinamento, que é custoso e complexo.
RAG como solução arquitetural elegante
Retrieval-Augmented Generation emerge como uma arquitetura que resolve elegantemente essas limitações através da combinação de dois componentes fundamentais.
-
O componente de Retrieval (Recuperação): funciona como um sistema de busca inteligente que encontra informações relevantes em uma base de conhecimento externa.
-
O componente de Generation (Geração): utiliza um LLM para gerar respostas baseadas exclusivamente no contexto recuperado, garantindo que as respostas sejam fundamentadas em informações verificáveis.
O fluxo de processamento segue uma sequência lógica onde uma consulta do usuário é convertida em embedding vetorial, que é então usado para busca por similaridade no banco vetorial. Os documentos mais relevantes são recuperados e concatenados em um contexto, que é fornecido ao LLM junto com a pergunta original para geração da resposta final.
Vantagens técnicas transformadoras
A arquitetura RAG oferece factualidade através de respostas baseadas em fontes verificáveis, eliminando a necessidade de confiar exclusivamente no conhecimento interno do modelo. A atualização é garantida pois a base de conhecimento pode ser atualizada sem necessidade de retreinar o modelo, permitindo incorporação de novos documentos e informações em tempo real.
A transparência é uma característica fundamental, pois permite rastrear as fontes das informações utilizadas na geração das respostas. A custo-efetividade é significativa, pois evita a necessidade de fine-tuning de modelos, que requer recursos computacionais massivos e expertise técnica especializada.
Arquitetura do sistema: visão técnica abragente
Arquitetura de alto nível detalhada
A arquitetura do sistema RAG pode ser visualizada como um pipeline de processamento que transforma documentos PDF em uma base de conhecimento pesquisável e utiliza essa base para responder perguntas em linguagem natural. O processo começa com um documento PDF que passa por extração de texto, seguida por segmentação inteligente usando LangChain.js. Os segmentos resultantes são convertidos em embeddings vetoriais através do modelo Gemini.
observação: embora o artigo enfoque em arquivos PDF, numa aplicação RAG, poderíamos utilizar qualquer fonte de dados, como: bancos de dados relacionais, NoSQL, APIs, documentos Word, planilhas Excel, entre outros.
Estes embeddings são armazenados em PostgreSQL com a extensão pgVector, criando uma base de conhecimento pesquisável. Quando um usuário faz uma pergunta, ela é convertida em embedding e usada para busca por similaridade no banco vetorial. Os documentos mais relevantes são recuperados e montados em contexto, que é então enviado para o Google Gemini junto com a pergunta para geração da resposta final.
Afinal, o que são embeddings?
Embeddings são representações numéricas de dados, como texto ou imagens, em um espaço vetorial de alta dimensão. Eles capturam o significado semântico dos dados, permitindo que máquinas compreendam e processem informações de maneira mais eficaz. No contexto de RAG, embeddings são usados para transformar consultas e documentos em vetores que podem ser comparados para encontrar similaridades.
- Exemplo:
"gato" -> [0.1, 0.3, 0.5, ...]
"cachorro" -> [0.2, 0.4, 0.6, ...]
Deixo a recomendação da documentação oficial do Gemini que explica com mais detalhes sobre embeddings: Embeddings
Componentes tecnológicos em profundidade
Para deixar a aplicação simples e fácil de executar, utilizei de interface que utilizam Node.js com TypeScript para runtime e tipagem estática robusta. A Readline Interface fornece uma CLI interativa para testes e demonstrações, permitindo interação natural com o sistema.
Para processamento de documentos, usamos as seguintes bibliotecas:
-
LangChain.js: serve como framework principal para aplicações LLM, oferecendo abstrações de alto nível para tarefas comuns.
-
RecursiveCharacterTextSplitter: implementa algoritmo inteligente de chunking que preserva contexto semântico.
-
PDF-Parse: realiza extração limpa de texto de documentos PDF.
Os embeddings e IA são gerenciados através da Google Gemini API, utilizando o modelo embedding-001 para geração de embeddings de 768 dimensões e gemini-2.0-flash para geração de respostas otimizadas.
O banco de dados vetorial combina PostgreSQL 15 ou superior como banco relacional robusto com pgVector como extensão para busca vetorial eficiente. HNSW Indexing implementa algoritmo de busca aproximada que oferece performance para buscas em milissegundos mesmo em grandes volumes de dados.
A infraestrutura utiliza Docker Compose para orquestração de containers, simplificando deployment e gerenciamento de dependências. Environment Variables proporcionam configuração flexível e segura.
O que é HNSW Indexing?
HNSW Indexing significa Hierarchical Navigable Small World Graph Indexing. É uma técnica muito usada em busca aproximada por vizinhos mais próximos (Approximate Nearest Neighbor Search – ANN) em bases vetoriais, como quando você precisa recuperar embeddings de texto, imagens ou áudio de forma rápida.
Como funciona?
-
Ele organiza os vetores em uma estrutura de grafo hierárquico.
-
Nos níveis superiores, você tem conexões mais gerais entre vetores, que permitem "pulos longos" pelo espaço de busca.
-
Conforme vai descendo nos níveis, os grafos ficam mais densos e conectados, permitindo buscas mais precisas e locais.
-
Isso cria um equilíbrio entre velocidade (saltos rápidos entre regiões) e precisão (ajuste fino nos níveis inferiores).
Por que é importante?
-
Alta performance: consegue buscar vizinhos em milhões de vetores com baixa latência.
-
Escalabilidade: é eficiente tanto em memória quanto em tempo, mesmo em bases grandes.
-
Uso comum: é o índice padrão em várias bibliotecas de bases vetoriais, como FAISS (Facebook AI Similarity Search), Milvus, Weaviate e Pinecone.
Exemplo prático
Imagine que você tem 10 milhões de embeddings de documentos. Se fosse comparar cada consulta com todos, seria inviável.
Com HNSW, você consegue encontrar os documentos semanticamente mais próximos em milissegundos, sem percorrer todos os vetores.
Não estarei entrando em detalhes sobre o HNSW Indexing, mas caso queira dar uma olhada numa implementação prática usando TypeScript, deixo o link do repositório do projeto que criei: HNSW + Gemini + LangChain.js - Clean Architecture. Num outro artigo, posso detalhar mais sobre o HNSW Indexing e quebrar em partes a essa implementação para que fique mais fácil de entender.
Pipeline RAG Detalhado
O pipeline de ingestão segue a sequência:
PDF → Text Extraction → Chunking → Embeddings → Vector Storage.
Cada etapa é otimizada para preservar máxima informação semântica enquanto prepara os dados para busca eficiente.
O pipeline de consulta executa:
User Query → Query Embedding → Similarity Search → Context Assembly → LLM Generation → Response.
Este processo garante que cada resposta seja fundamentada em evidências específicas dos documentos processados.
Configuração do Ambiente de Desenvolvimento
Pré-requisitos Técnicos Essenciais
O ambiente de desenvolvimento requer as seguintes versões mínimas:
- Node.js versão 22.0.0 ou superior - Para suporte às funcionalidades mais recentes do JavaScript e performance otimizada
- NPM versão 10.0.0 ou superior - Necessário para gerenciamento de dependências moderno
- Docker versão 24.0.0 ou superior - Garante compatibilidade com recursos de containerização avançados
- Git versão 2.40.0 ou superior - Essencial para controle de versão
ara verificar as versões instaladas, execute os seguintes comandos em seu terminal:
node --version # v22.0.0+
npm --version # 10.0.0+
docker --version # 24.0.0+
git --version # 2.40.0+
Inicialização Completa do Projeto
A estrutura do projeto começa com a criação de um diretório principal e subdiretório para código fonte:
mkdir rag-system-typescript && cd rag-system-typescript
mkdir src
A inicialização do Node.js é feita através do comando:
npm init -y
Este comando cria o arquivo package.json
com configurações padrão.
As dependências de produção incluem pacotes essenciais para funcionalidade do sistema:
npm install @google/generative-ai @langchain/core @langchain/community @langchain/textsplitters dotenv pg uuid
Estas bibliotecas fornecem integração com Google AI, framework LangChain, manipulação de variáveis de ambiente, conexão PostgreSQL e geração de identificadores únicos.
As dependências de desenvolvimento garantem experiência de desenvolvimento robusta:
npm install -D @types/node @types/pg @types/pdf-parse tsx typescript
Estas incluem definições de tipos TypeScript, compilador TypeScript e executor de desenvolvimento tsx.
Configuração TypeScript Avançada
O arquivo tsconfig.json
define configurações de compilação que otimizam para desenvolvimento moderno e performance.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"],
"lib": ["ES2022", "DOM"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
],
"ts-node": {
"esm": true
}
}
Scripts de Automação Inteligentes
Os scripts no package.json
automatizam tarefas comuns:
"scripts": {
"build": "tsc",
"start": "npm run build && node dist/chat.js",
"ingest": "npm run build && node dist/ingest.js",
"dev:chat": "tsx src/chat.ts",
"dev:ingest": "tsx src/ingest.ts"
},
Infraestrutura: PostgreSQL + pgVector
Fundamentos Teóricos dos Bancos Vetoriais
Embeddings matemáticos representam uma revolução na forma como computadores processam e compreendem linguagem natural. Textos são convertidos em vetores de alta dimensionalidade, onde cada dimensão captura aspectos específicos do significado semântico. Para o modelo Gemini embedding-001, cada texto é representado por 768 números de ponto flutuante.
A proximidade no espaço vetorial representa similaridade semântica, permitindo que algoritmos matemáticos encontrem textos relacionados através de cálculos de distância. Por exemplo, as frases "empresa faturamento" e "receita corporativa" produziriam vetores próximos no espaço multidimensional.
O pgVector adiciona capacidades vetoriais nativas ao PostgreSQL, incluindo tipo de dados vector para armazenamento eficiente, índices HNSW (Hierarchical Navigable Small World) para busca rápida, e operações de similaridade como distância coseno, euclidiana e produto interno.
Configuração Docker Avançada
O arquivo docker-compose.yml
define infraestrutura completa para o sistema RAG. O serviço PostgreSQL utiliza imagem pgvector/pgvector:pg17 que inclui PostgreSQL 17 com extensão pgVector pré-instalada.
services:
# Main service: PostgreSQL with pgVector extension
postgres:
image: pgvector/pgvector:pg17
container_name: postgres_rag_ts
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: rag
ports:
- "5432:5432"
volumes:
# Data persistence
- postgres_data:/var/lib/postgresql/data
healthcheck:
# Checks if the database is ready
test: ["CMD-SHELL", "pg_isready -U postgres -d rag"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# Auxiliary service: Initializes pgVector extension
bootstrap_vector_ext:
image: pgvector/pgvector:pg17
depends_on:
postgres:
condition: service_healthy
entrypoint: ["/bin/sh", "-c"]
command: >
PGPASSWORD=postgres
psql "postgresql://postgres@postgres:5432/rag" -v ON_ERROR_STOP=1
-c "CREATE EXTENSION IF NOT EXISTS vector;"
restart: "no"
volumes:
postgres_data:
O serviço bootstrap_vector_ext
garante que a extensão pgVector seja criada automaticamente após PostgreSQL estar operacional. O healthcheck monitora disponibilidade do banco antes de inicializar dependências.
Inicialização e Verificação da Infraestrutura
A inicialização da infraestrutura é feita através do comando:
docker-compose up -d
Este comando inicia containers em modo daemon. A verificação do status é realizada com:
docker ps
Este comando lista containers ativos. Os logs podem ser monitorados com:
docker logs postgres_rag_ts
Este comando permite identificar problemas de inicialização.
Integração Google Gemini: Cliente de IA Avançado
Teoria Aprofundada dos Embeddings
Embeddings representam uma das inovações mais significativas em processamento de linguagem natural, convertendo representações discretas de texto em vetores contínuos de números reais. Estes vetores capturam relações semânticas complexas, permitindo operações matemáticas sobre conceitos linguísticos.
A dimensionalidade de 768 números para o modelo embedding-001 oferece espaço suficiente para representar nuances semânticas sutis enquanto mantém eficiência computacional. Vetores próximos no espaço multidimensional correspondem a textos semanticamente similares, permitindo busca por similaridade matemática.
Operações vetoriais permitem manipulação conceitual, onde diferenças e somas de vetores podem revelar relações analógicas. O exemplo clássico "rei" - "homem" + "mulher" ≈ "rainha" demonstra como embeddings capturam estruturas relacionais abstratas.
Implementação Robusta do Cliente Google
A implementação do cliente Google encapsula toda comunicação com APIs Gemini, oferecendo interface limpa e tratamento de erros robusto.
import { config } from 'dotenv';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { Embeddings } from '@langchain/core/embeddings';
config();
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export class GoogleClient {
private googleApiKey: string;
private embeddingModel: string;
private chatModel: string;
private genAI: GoogleGenerativeAI;
constructor() {
this.googleApiKey = process.env.GOOGLE_API_KEY || '';
this.embeddingModel = process.env.GOOGLE_EMBEDDING_MODEL || '';
this.chatModel = process.env.GOOGLE_CHAT_MODEL || '';
if (!this.googleApiKey) {
throw new Error('Google API key is not set in environment variables.');
}
this.genAI = new GoogleGenerativeAI(this.googleApiKey);
}
async getEmbeddings(texts: string[]): Promise<number[][]> {
const embeddings: number[][] = [];
for(const text of texts) {
try {
const model = this.genAI.getGenerativeModel({ model: 'embedding-001' });
const result = await model.embedContent(text);
if (result.embedding && result.embedding.values) {
embeddings.push(result.embedding.values);
} else {
console.log(`No embedding returned for text: ${text}`);
const dummySize = 768;
embeddings.push(new Array(dummySize).fill(0));
}
} catch (error) {
console.log(`Error generating embedding: ${error}`);
const dummySize = 768;
embeddings.push(new Array(dummySize).fill(0));
}
}
return embeddings;
}
async chatCompletions(messages: ChatMessage[], temperature: number = 0.1): Promise<string> {
try {
const model = this.genAI.getGenerativeModel({
model: this.chatModel,
generationConfig: {
temperature,
maxOutputTokens: 1000,
}
});
let prompt = '';
for (const message of messages) {
const { role, content } = message;
if (role === 'system') {
prompt += `Instructions: ${content}\n\n`;
} else if (role === 'user') {
prompt += `${content}\n`;
} else if (role === 'assistant') {
prompt += `Assistant: ${content}\n`;
}
}
const result = await model.generateContent(prompt);
return result.response.text();
} catch (error) {
console.log(`Error generating chat completion: ${error}`);
return 'Sorry, an error occurred while generating the response.';
}
}
}
A classe GoogleClient
gerencia configuração e comunicação com APIs Gemini. O método getEmbeddings
processa textos em lotes, implementando tratamento de erros gracioso e fallback para casos de falha. chatCompletions
converte mensagens estruturadas em prompts otimizados para Gemini.
A classe GoogleEmbeddings
estende abstrações LangChain.js para integração seamless com frameworks existentes.
export class GoogleEmbeddings extends Embeddings {
private client: GoogleClient;
constructor() {
super({});
this.client = new GoogleClient();
}
async embedDocuments(texts: string[]): Promise<number[][]> {
console.log(`Generating embeddings for ${texts.length} documents...`);
const batchSize = 10; // Processing 10 texts at a time for a better optimization
const allEmbeddings: number[][] = [];
for(let i = 0; i < texts.length; i += batchSize) {
const batchTexts = texts.slice(i, i + batchSize);
const batchEmbeddings = await this.client.getEmbeddings(batchTexts);
allEmbeddings.push(...batchEmbeddings);
console.log(`Lot ${Math.floor(i / batchSize) + 1}: ${batchTexts.length} processed texts`);
}
return allEmbeddings;
}
// Method for embedding a single query
async embedQuery(text: string): Promise<number[]> {
const embeddings = await this.client.getEmbeddings([text]);
return embeddings[0];
}
}
// Factory function to create a GoogleClient instances
export function getGoogleClient(): GoogleClient {
return new GoogleClient();
}
Configuração de Ambiente Segura
O arquivo .env
centraliza configuração sensível, separando credenciais do código fonte para segurança e flexibilidade de deployment.
GOOGLE_API_KEY=sua_google_api_key_aqui
GOOGLE_EMBEDDING_MODEL=models/embedding-001
GOOGLE_CHAT_MODEL=gemini-2.0-flash
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/rag
PG_VECTOR_COLLECTION_NAME=pdf_documents
PDF_PATH=./document.pdf
observação: para criar uma API Key do Google Gemini, siga os passos descritos na documentação oficial: AI Studio - Google e clique em:
Create API Key
.
Sistema de Ingestão: PDF para Vetores Inteligentes
Teoria Avançada do Chunking
O chunking representa um dos aspectos mais críticos em sistemas RAG, determinando qualidade e relevância das respostas. O desafio fundamental é que LLMs possuem janelas de contexto limitadas, enquanto documentos podem ser extensos, criando necessidade de segmentação inteligente.
A estratégia de chunking deve balançar tamanho de contexto com especificidade de informação. Chunks muito grandes podem conter informações irrelevantes que diluem a relevância. Chunks muito pequenos podem carecer de contexto suficiente para compreensão completa.
O RecursiveCharacterTextSplitter
(do LangChain.js) é muito útil em documentos textuais, já que preserva a estrutura natural de parágrafos e frases. Nesse caso, parâmetros como chunk_size
em torno de 1.000 caracteres e chunk_overlap
de 150–200 funcionam como um bom ponto de partida, mantendo equilíbrio entre contexto e especificidade.
No entanto, como este projeto trabalha com PDF tabular, essa estratégia não é a mais eficaz. Para tabelas, preferimos quebrar o documento linha a linha, garantindo que cada registro seja um chunk independente. Além disso, incluímos o cabeçalho da tabela em cada fragmento para manter clareza semântica. Dessa forma, o overlap é desnecessário (mantido em 0) e os separadores são adaptados para priorizar quebras de linha.
Essa abordagem garante que cada entrada tabular seja preservada integralmente e melhora a precisão na hora de recuperar informações via RAG.
Algoritmo RecursiveCharacterTextSplitter
detalhado
O algoritmo segue estratégia de fallback inteligente que tenta quebrar por separadores naturais antes de recorrer a quebras artificiais. Primeiro, tenta quebrar por parágrafos usando quebras duplas de linha. Se chunks resultantes ainda excedem tamanho máximo, então quebra por linhas simples. Para chunks ainda grandes, quebra por espaços entre palavras. Como último recurso, quebra caractere por caractere.
Esta abordagem garante que a informação relacionada permaneça junta sempre que possível, preservando coerência semântica necessária para recuperação eficaz.
Implementação Completa da Ingestão
A implementação da ingestão combina extração de PDF, segmentação inteligente, geração de embeddings e armazenamento vetorial em pipeline integrado.
import { config } from 'dotenv';
import { Document } from '@langchain/core/documents';
import { PGVectorStore } from '@langchain/community/vectorstores/pgvector';
import { GoogleEmbeddings } from './google-client';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import { PDFLoader as LangChainPDFLoader } from '@langchain/community/document_loaders/fs/pdf';
config();
class PDFLoader {
constructor(private filePath: string) {}
async load(): Promise<Document[]> {
try {
console.log(`Reading PDF file: ${this.filePath}`);
const langChainLoader = new LangChainPDFLoader(this.filePath);
const documents = await langChainLoader.load();
console.log(`PDF loaded successfully! Found ${documents.length} pages`);
return documents;
} catch (error) {
console.error('Error loading PDF:', error);
throw error;
}
}
async ingestToVectorStore(): Promise<void> {
try {
console.log('Starting PDF ingestion process...');
const rawDocuments = await this.load();
console.log(`PDF loaded: ${rawDocuments.length} sections`);
console.log('Splitting documents into chunks...');
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400,
chunkOverlap: 0,
});
const splitDocuments = await textSplitter.splitDocuments(rawDocuments);
console.log(`Documents split into ${splitDocuments.length} chunks`);
console.log('Initializing Google embeddings...');
const embeddings = new GoogleEmbeddings();
console.log('Connecting to PostgreSQL vector store...');
const vectorStore = await PGVectorStore.initialize(embeddings, {
postgresConnectionOptions: {
connectionString: process.env.DATABASE_URL,
},
tableName: process.env.PG_VECTOR_COLLECTION_NAME || 'pdf_documents',
columns: {
idColumnName: 'id',
vectorColumnName: 'vector',
contentColumnName: 'content',
metadataColumnName: 'metadata',
},
});
console.log('Adding documents to vector store...');
await vectorStore.addDocuments(splitDocuments);
console.log('PDF ingestion completed successfully!');
console.log(`Total chunks processed: ${splitDocuments.length}`);
await vectorStore.end();
} catch (error) {
console.error('Error during PDF ingestion:', error);
process.exit(1);
}
}
}
async function main() {
const pdfPath = './document.pdf';
const loader = new PDFLoader(pdfPath);
await loader.ingestToVectorStore();
}
// Run ingestion
main();
A classe PDFLoader
encapsula todo processo de ingestão, desde carregamento do arquivo até armazenamento no banco vetorial. O método load
utiliza LangChain.js PDFLoader para extração robusta de texto. ingestToVectorStore
coordena pipeline completo de processamento.
Schema PostgreSQL Automático
O PGVectorStore
cria automaticamente schema otimizado para armazenamento e busca vetorial. A tabela pdf_documents inclui:
- id - Chave primária UUID para identificação única
- content - Texto original do chunk extraído do PDF
- vector - Embeddings de 768 dimensões gerados pelo Gemini
- metadata - Informações estruturais como página, fonte e contexto
CREATE TABLE pdf_documents (
id UUID PRIMARY KEY,
content TEXT,
vector VECTOR(768),
metadata JSONB
);
CREATE INDEX ON pdf_documents USING hnsw (vector vector_cosine_ops);
O índice HNSW
otimiza busca vetorial, oferecendo complexidade logarítmica versus busca linear tradicional.
Sistema de Busca RAG: Retrieval + Generation Inteligente
Teoria da Busca Semântica Avançada
O pipeline de busca semântica representa transformação fundamental na forma como sistemas computacionais encontram informação relevante. Diferentemente de busca por palavras-chave tradicional, busca semântica utiliza representações vetoriais para capturar significado conceitual.
O processo inicia com conversão da pergunta do usuário em embedding vetorial usando mesmo modelo utilizado durante a ingestão. Este query embedding é então comparado com todos embeddings armazenados usando métricas de similaridade matemática. O algoritmo HNSW acelera esta comparação, reduzindo complexidade de O(n) para O(log n).
Resultados são classificados por score de similaridade, onde valores menores indicam maior similaridade no espaço coseno. Context assembly concatena chunks mais relevantes, criando contexto rico para geração da resposta.
Sistema de Busca RAG: Retrieval + Generation Inteligente
Teoria da Busca Semântica Avançada
O pipeline de busca semântica representa transformação fundamental na forma como sistemas computacionais encontram informação relevante. Diferentemente de busca por palavras-chave tradicional, busca semântica utiliza representações vetoriais para capturar significado conceitual.
O processo inicia com conversão da pergunta do usuário em embedding vetorial usando mesmo modelo utilizado durante ingestão. Este query embedding é então comparado com todos embeddings armazenados usando métricas de similaridade matemática. O algoritmo HNSW acelera esta comparação, reduzindo complexidade de O(n) para O(log n).
Resultados são classificados por score de similaridade, onde valores menores indicam maior similaridade no espaço coseno. Context assembly concatena chunks mais relevantes, criando contexto rico para geração da resposta.
Prompt Engineering Anti-Alucinação
O template de prompt implementa estratégias sofisticadas para prevenir alucinações e garantir factualidade das respostas. Instruções explícitas enfatizam uso exclusivo do contexto fornecido. Fallback response fornece resposta padrão para casos onde informação não está disponível. Temperature baixa de 0.1 reduz criatividade e aumenta determinismo. Exemplos negativos demonstram casos onde resposta correta é "não sei".
Esta abordagem garante que sistema sempre reconheça limitações do conhecimento disponível, preferindo admitir ignorância a inventar informações.
Interface CLI: Experiência do Usuário Excepcional
Design Centrado no Usuário
A interface CLI foi projetada considerando princípios de experiência do usuário aplicados a sistemas de IA. Feedback imediato através de indicadores de progresso mantém usuários informados sobre operações em andamento. Comandos especiais como help, status, clear e exit
oferecem controle intuitivo. Error handling graceful apresenta mensagens informativas que guiam usuários na resolução de problemas. Interface assíncrona não-bloqueante mantém responsividade mesmo durante operações computacionalmente intensivas.
Implementação da Interface Interativa
A implementação combina readline nativo do Node.js com lógica de comando avançada para criar experiência fluida e intuitiva.
import { createInterface } from "readline";
import { searchPrompt, RAGSearch } from "./search";
// Function to print initial banner with system informations
function printBanner(): void {
console.log('='.repeat(60));
console.log('RAG CHAT - PDF Question and Answer System');
console.log('Powered by Google Gemini + LangChain + pgVector');
console.log('⚡ TypeScript + Node.js Implementation');
console.log('='.repeat(60));
console.log("Special commands:");
console.log(" • 'exit, quit, exit' - Closes the program");
console.log(" • 'help' - Shows available commands");
console.log(" • 'clear' - Clears the screen");
console.log(" • 'status' - Checks system status");
console.log('='.repeat(60));
}
// Function to print help instructions
function printHelp(): void {
console.log('\n AVAILABLE COMMANDS:');
console.log(' exit, quit, exit - Closes the program');
console.log(' help - Shows available commands');
console.log(' clear - Clears the screen');
console.log(' status - Checks system status');
console.log(' [any text] - Asks a question about the PDF');
console.log('\n TIPS FOR USE:');
console.log(' • Ask specific questions about the PDF content');
console.log(' • The system responds only based on the document');
console.log(' • Out-of-context questions return "I don\'t have information"');
console.log();
}
// Function to clear the console screen
function clearScreen(): void {
console.clear();
}
async function checkStatus(searchSystem: RAGSearch | null): Promise<void> {
console.log('\n RAG SYSTEM STATUS:');
console.log('='.repeat(40));
if (!searchSystem) {
console.log('System: NOT INITIALIZED');
console.log('\n TROUBLESHOOTING CHECKLIST:');
console.log(' 1. Is PostgreSQL running?');
console.log(' → Command: docker compose up -d');
console.log(' 2. Has ingestion been executed?');
console.log(' → Command: npm run ingest');
console.log(' 3. Is the API Key configured?');
console.log(' → File: .env (GOOGLE_API_KEY)');
console.log(' 4. Are dependencies installed?');
console.log(' → Command: npm install');
return;
}
try {
const systemStatus = await searchSystem.getSystemStatus();
console.log('RAG System: OPERATIONAL');
console.log('PostgreSQL Connection: OK');
console.log('pgVector Extension: OK');
console.log('Google Gemini API: OK');
console.log(`Vector Database: ${systemStatus.isReady ? 'READY' : 'NOT READY'}`);
if (systemStatus.chunksCount > 0) {
console.log(`Available chunks: ${systemStatus.chunksCount}`);
}
console.log('\n System ready to answer questions!');
} catch (error) {
console.log('Status: PARTIALLY OPERATIONAL');
console.log(`Error checking system status: ${error}`);
}
console.log('='.repeat(40));
}
// Main function to initialize RAG system and handle user input
async function main(): Promise<void> {
console.log('STEP 6: Initializing the RAG Chat CLI Interface');
printBanner();
console.log('\n PHASE 1: INITIALIZING RAG SYSTEM');
const searchSystem = await searchPrompt();
if (!searchSystem) {
console.log('\n CRITICAL ERROR: RAG system could not be initialized!');
console.log('\n POSSIBLE CAUSES AND SOLUTIONS:');
console.log(' 1. PostgreSQL is not running');
console.log(' → Solution: docker compose up -d');
console.log(' 2. Ingestion process has not been executed');
console.log(' → Solution: npm run ingest');
console.log(' 3. GOOGLE_API_KEY is not configured or invalid');
console.log(' → Solution: Configure in the .env file');
console.log(' 4. Node.js dependencies are not installed');
console.log(' → Solution: npm install');
console.log(' 5. pgVector extension has not been created');
console.log(' → Solution: Check Docker logs');
process.exit(1);
}
console.log('PHASE 1: RAG system initialized successfully!\n');
// PHASE 2: SETUP COMMAND LINE INTERFACE
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: '\n Make a question: '
});
// Helper function to capture user input asynchronously
const askQuestion = (prompt: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
console.log('System ready! Type your question or “help” to see commands.');
// PHASE 3: MAIN CHAT LOOP
while(true) {
try {
// Capture user input
const userInput = (await askQuestion('\n Make a question: ')).trim();
// PROCESSING COMMAND: Analyze whether it is a special command or a question
const command = userInput.toLowerCase();
// Output commands
if (['exit', 'quit', 'sair', 'q'].includes(command)) {
console.log('\n Thank you for using RAG Chat. Goodbye!\n');
console.log('System shutting down...');
break;
}
// Help command
if (['ajuda', 'help', 'h', '?'].includes(command)) {
printHelp();
continue;
}
// Clear screen command
if (['limpar', 'clear', 'cls'].includes(command)) {
clearScreen();
printBanner();
continue;
}
// Status command
if (['status', 'info', 's'].includes(command)) {
await checkStatus(searchSystem);
continue;
}
// Validate empty input
if (!userInput) {
console.log('Empty input. Type a question or “help” to see commands.');
continue;
}
// PROCESSING QUESTION: Forward the question to the RAG system
console.log('\n Processing your question...');
console.log('Searching PDF knowledge...');
const startTime = Date.now();
// Call the complete RAG pipeline
const answer = await searchSystem.generateAnswer(userInput);
const endTime = Date.now();
const responseTime = ((endTime - startTime) / 1000).toFixed(2);
// FORMATTED DISPLAY OF THE RESPONSE
console.log('\n' + '='.repeat(80));
console.log(`ASK: ${userInput}`);
console.log('='.repeat(80));
console.log(`🤖 RESPONSE:`);
console.log(answer);
console.log('='.repeat(80));
console.log(`⚡ Response time: ${responseTime}s`);
} catch (error) {
// TRATAMENTO DE ERROS
if (error instanceof Error && error.message.includes('SIGINT')) {
// Ctrl+C foi pressionado
console.log('\n\n Interruption detected (Ctrl+C)');
console.log('👋 Chat closed by user. See you next time!');
break;
} else {
// Outros erros
console.log(`\n Unexpected error during processing:`);
console.log(` ${error}`);
console.log('\n You can:');
console.log(' • Try again with another question');
console.log(' • Type "status" to check the system');
console.log(' • Type "exit" to quit');
}
}
}
rl.close();
}
// EVENT HANDLERS: Operating system signal management
// Handler for Ctrl+C (SIGINT)
process.on('SIGINT', () => {
console.log('\n\n Interrupt signal received (Ctrl+C)');
console.log('Cleaning up resources...');
console.log('RAG Chat closed. See you later!');
process.exit(0);
});
// Handler for uncaught errors
process.on('uncaughtException', (error) => {
console.error('\n Uncaught FATAL ERROR:', error);
console.error('Restart the application: npm run start');
process.exit(1);
});
// Handler for rejected promises
process.on('unhandledRejection', (reason, promise) => {
console.error('\n Unhandled rejected promise:', reason);
console.error('Promise:', promise);
});
// ENTRY POINT: Run the main function
main().catch((error) => {
console.error('\n FATAL ERROR in main application:', error);
console.error('Try restarting: npm run start');
process.exit(1);
});
A classe RAGSearch
encapsula funcionalidade completa de busca e geração. searchDocuments
executa busca vetorial e retorna resultados formatados com scores. generateAnswer
orquestra pipeline completo de RAG.
A função printBanner
apresenta informações essenciais sobre sistema e comandos disponíveis. checkStatus
oferece diagnóstico detalhado de componentes, facilitando troubleshooting. O loop principal processa comandos e perguntas com tratamento robusto de erros.
Execução e Validação Comprehensive
Sequência de Execução Otimizada
A execução segue sequência lógica que garante inicialização correta de todos componentes. Primeiro, inicialize infraestrutura:
docker-compose up -d
Este comando sobe PostgreSQL com pgVector. Verifique status dos containers:
docker ps
Este comando confirma operação correta. Execute ingestão para processar documentos PDF:
npm run dev:ingest
Finalmente, inicie chat interativo para interação com sistema:
npm run dev:chat
Cenários de Teste Abrangentes
O sistema suporta diversos cenários de teste que validam funcionalidade completa. Perguntas dentro do contexto do PDF devem retornar respostas baseadas exclusivamente no conteúdo processado. Perguntas fora do contexto devem resultar na resposta padrão "Não tenho informações necessárias para responder sua pergunta." Comandos especiais como status, help e clear devem funcionar corretamente.
Troubleshooting Sistemático
Problemas comuns possuem soluções bem definidas que podem ser identificadas através de mensagens de erro específicas:
-
Erro: "Google API key is not set": Este erro indica necessidade de configurar a variável de ambiente GOOGLE_API_KEY no arquivo .env. Verifique se o arquivo contém a chave API válida obtida no Google AI Studio.
-
Erro: "Vector store not initialized": Esta mensagem sugere que PostgreSQL não está operacional ou o processo de ingestão não foi executado. Confirme que os containers Docker estão rodando e execute a ingestão de documentos.
-
Erro: "No documents found": Este problema indica que o processo de ingestão precisa ser executado para popular o banco vetorial com chunks do PDF processado.
-
Erro: "Connection refused": Esta falha aponta para PostgreSQL offline, resolvível verificando status dos containers Docker e reinicializando a infraestrutura se necessário.
Considerações de Produção Avançadas
Performance e Escalabilidade Otimizada
As otimizações implementadas garantem performance adequada para uso produtivo. Batch processing durante ingestão implementa rate limiting para APIs externas, evitando throttling. Connection pooling no PostgreSQL permite múltiplas conexões simultâneas. HNSW indexing oferece busca sub-segundo mesmo com milhões de vetores. Operações assíncronas mantêm responsividade da aplicação.
Métricas de performance demonstram eficiência do sistema. Ingestão processa PDF de 50 páginas em aproximadamente 30 segundos. Busca retorna resultados em 2-3 segundos por pergunta. Throughput suporta mais de 100 perguntas por minuto em hardware modesto.
Segurança e Confiabilidade Robusta
Implementações de segurança seguem best practices para aplicações produtivas. Environment variables isolam secrets do código fonte. Input validation e sanitization previnem ataques de injeção. Error handling robusto previne vazamento de informações sensíveis. Graceful shutdown handling garante limpeza adequada de recursos.
Monitoramento recomendado inclui logs estruturados usando bibliotecas como Winston
ou Pino
. Métricas de performance podem ser coletadas com Prometheus
. Health checks automáticos monitoram disponibilidade de componentes. Rate limiting por usuário previne abuso de recursos. Fica a dica para futuras melhorias.
Roadmap de Melhorias Futuras
O roadmap técnico identifica oportunidades de evolução. Migração da CLI para API REST facilitará integração com aplicações web. Interface React
ou Next.js
oferecerá experiência visual moderna. Suporte multi-tenancy permitirá múltiplos usuários e documentos. Cache Redis
para respostas frequentes reduzirá latência. Integração OpenTelemetry
proporcionará observabilidade completa.
Referências e Recursos para Aprofundamento
Documentação e Repositório do Projeto
O código completo deste sistema RAG está disponível no repositório oficial rag-search-ingestion-langchainjs-gemini, onde você encontrará implementação funcional, instruções detalhadas de instalação, exemplos de uso, e documentação completa de todos os componentes desenvolvidos. O repositório inclui arquivos de configuração Docker prontos para produção, scripts de automação para desenvolvimento, e casos de teste específicos que demonstram a aplicação prática dos conceitos apresentados neste artigo.
Fundamentos Teóricos de RAG
Para compreensão aprofundada dos fundamentos teóricos, o paper original "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" por Lewis et al. na conferência NeurIPS 2020 estabelece os princípios fundamentais da arquitetura RAG. A pesquisa "Dense Passage Retrieval for Open-Domain Question Answering" por Karpukhin et al. explora técnicas avançadas de recuperação densa que fundamentam sistemas de busca semântica modernos. O trabalho "In-Context Retrieval-Augmented Language Models" apresenta evoluções recentes na integração de contexto dinânico em modelos de linguagem.
Tecnologias e Frameworks
A documentação oficial do LangChain.js em https://js.langchain.com/ oferece guias completos sobre implementação de pipelines de IA, incluindo tutoriais específicos sobre integração com diferentes provedores de embeddings e modelos de linguagem. O Google AI Developer Documentation em https://ai.google.dev/docs fornece especificações técnicas detalhadas sobre APIs Gemini, incluindo rate limits, melhores práticas de prompt engineering, e otimizações de performance. Para PostgreSQL e pgVector, a documentação oficial em https://github.com/pgvector/pgvector contém especificações técnicas sobre implementação de índices HNSW, configurações de performance, e estratégias de escalonamento para grandes volumes de dados vetoriais. O PostgreSQL Documentation em https://www.postgresql.org/docs/ oferece fundamentos sobre administração de banco de dados, otimização de queries, e configurações avançadas para aplicações de alta performance.
Embedding Models e Busca Vetorial
A compreensão profunda de embeddings pode ser expandida através da pesquisa "Attention Is All You Need" que introduz arquitetura Transformer fundamental para modelos de embedding modernos. O paper "Efficient Estimation of Word Representations in Vector Space" por Mikolov et al. estabelece fundamentos matemáticos de representações vetoriais semânticas. Para algoritmos de busca vetorial, "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs" detalha implementação e otimizações do algoritmo HNSW utilizado pelo pgVector.
Prompt Engineering e Controle de Alucinações
A pesquisa "Constitutional AI: Harmlessness from AI Feedback" explora técnicas avançadas para controle de comportamento em modelos de linguagem. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models" demonstra estratégias de estruturação de prompts para raciocínio complexo. "Instruction Following with Large Language Models" oferece insights sobre design de instruções eficazes para sistemas RAG.
Recursos Práticos e Tutoriais
LangChain Cookbook em https://github.com/langchain-ai/langchain/tree/master/cookbook contém exemplos práticos de implementação de diferentes padrões RAG. Pinecone Learning Center em https://www.pinecone.io/learn/ oferece tutoriais sobre bancos de dados vetoriais e aplicações de busca semântica. Weaviate Documentation em https://weaviate.io/developers/weaviate/ apresenta alternativas para armazenamento vetorial e suas especificidades técnicas.
Autora e Contribuições
Este projeto foi desenvolvido por Glaucia Lemos, A.I Developer Specialist, que compartilha conhecimento através de múltiplas plataformas. Seus perfis nas redes sociais incluem Twitter em https://twitter.com/glaucia86 para atualizações técnicas e insights sobre desenvolvimento, LinkedIn em https://www.linkedin.com/in/glaucialemos/ para networking profissional e artigos técnicos, e YouTube em https://www.youtube.com/@GlauciaLemos para tutoriais em vídeo e palestras técnicas sobre desenvolvimento moderno.