September 16th, 2025

Sistema RAG Completo - Zero to Hero com TypeScript, Docker, Google Gemini e LangChain.js

#RAG
#TypeScript
#Docker

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.

alt text

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

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

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?

Por que é importante?

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:

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:

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:

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.