Easy-RAG: Un Template NestJS para Orquestar Pipelines RAG

RAG (Retrieval-Augmented Generation) es la columna vertebral de las aplicaciones de IA modernas — pero configurarlo correctamente requiere unir embeddings, vector stores, text splitters, providers de LLM y respuestas en streaming. La mayoría de los tutoriales muestran cómo hacerlo en un Jupyter notebook o un script de Python de 50 líneas.

Yo quería un template listo para producción, modular y TypeScript-first que pudiera usar en cualquier proyecto NestJS. Así que construí easy-rag.


La Investigación: Vercel AI SDK vs LangChain.js

Antes de escribir una sola línea de código, pasé una fase completa de SDD Explore comparando las dos bibliotecas de orquestación principales:

AspectoVercel AI SDKLangChain.js
GitHub Stars24,550 ⭐17,734 ⭐
Descargas npm/semana12.9M4.2M
Providers~30+ oficiales~15 oficiales + comunidad
StreamingExcelenteFuncional pero verboso
Vector stores❌ No nativo✅ 20+ integraciones
Document loaders❌ No nativo✅ PDF, CSV, HTML, Markdown…
Text splitters❌ No nativo✅ RecursiveCharacter, Token…
Retrievers❌ No nativo✅ MultiQuery, ParentDocument…

La Decisión Híbrida

Ninguna biblioteca ganó rotundamente. En su lugar, elegí una arquitectura híbrida:

Vercel AI SDK (orquestación principal)
    ├── generateText / streamText / embed
    ├── @ai-sdk/openai → DeepSeek (API compatible OpenAI)
    ├── @ai-sdk/anthropic, @ai-sdk/google (extensible)
    └── @ai-sdk/langchain (puente oficial)
          └── LangChain text splitters + document loaders

¿Por qué? AI SDK tiene 3x más descargas, mejor streaming y mejor DX. LangChain tiene el ecosistema RAG maduro (text splitters, document loaders, retrievers). El adapter oficial @ai-sdk/langchain permite usar ambos sin fricción.


Arquitectura

Decisiones de Diseño (6 ADRs)

ADRDecisiónRazón
001Vercel AI SDK como coreStreaming nativo, 30+ providers, DX limpio
002LangChain solo para RAGVia adapter @ai-sdk/langchain
003pgvector sin ORMpg Pool nativo, control total de queries vectoriales
004DeepSeek via API compatible OpenAI@ai-sdk/openai con baseURL custom
005Módulos por dominio, no por capasClean Architecture, paquetes extraíbles
006Zod para validación de configPeer dependency compartida con AI SDK

Diseño de Módulos

src/
├── config/          # Env vars validadas con Zod (13 vars)
├── ai/              # AiService (generateText, streamText, embed)
├── embeddings/      # EmbeddingsService (abstracto + implementación)
├── vector-store/    # VectorStoreService (abstracto + pgvector)
└── rag/             # RagService + IngestionService + RagController
    ├── ingestion/   # Pipeline: chunk → embed → store
    └── query/       # Pipeline: embed → search → context → stream

Flujo de Datos

Ingestión:

POST /rag/ingest { content: "texto..." }
  → RecursiveCharacterTextSplitter → [chunks]
  → EmbeddingsService.embed(chunk) → [vectors]
  → VectorStoreService.storeChunks() → { chunks: N, ids: [...] }

Query (streaming):

POST /rag/query { question: "¿Qué es X?" }
  → embed(question) → [vector]
  → similaritySearch(vector, topK) → docs de contexto
  → buildPrompt(question, context)
  → AiService.streamText(prompt) → stream SSE

El stream SSE se envía como text/event-stream con formato data: {chunk}\n\n. El cliente recibe tokens en tiempo real mientras el LLM los genera.


Detalles de Implementación

Esquema Vectorial (pgvector)

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE embeddings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content TEXT NOT NULL,
  source TEXT,
  metadata JSONB DEFAULT '{}',
  embedding vector(1536),
  created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_embeddings_vector
  ON embeddings USING ivfflat (embedding vector_cosine_ops);

Abstracción de Providers

AiService detecta automáticamente qué provider usar:

// DeepSeek (default, via API compatible OpenAI)
const deepseek = createOpenAI({
  baseURL: 'https://api.deepseek.com/v1',
  apiKey: process.env.DEEPSEEK_API_KEY,
});

// O OpenAI (setear OPENAI_API_KEY)
const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

El cliente puede cambiar de provider simplemente cambiando variables de entorno.


Stats & Testing

  • Suites de test: 6 (config, ai, embeddings, vector-store, ingestion + e2e)
  • Tests: 42 pasando (32 unit + 10 e2e via supertest)
  • Cobertura E2E: POST /rag/ingest, POST /rag/query (SSE), GET /api/health, manejo 404
  • CI: GitHub Actions — build y tests en push/PR a main
  • Archivos fuente: 28 archivos TypeScript
  • Dependencias: 13 producción + 13 desarrollo
  • Build: tsup ESM

Probarlo

git clone https://github.com/fxckcode/easy-rag.git
cd easy-rag
pnpm install
docker compose up -d    # inicia pgvector
cp .env.example .env    # configurar DEEPSEEK_API_KEY
pnpm start:dev
# Ingestionar un documento
curl -X POST http://localhost:3000/rag/ingest \
  -H "Content-Type: application/json" \
  -d '{"content": "NestJS es un framework para construir aplicaciones Node.js del lado del servidor.", "source": "docs"}'

# Query con streaming
curl -X POST http://localhost:3000/rag/query \
  -H "Content-Type: application/json" \
  -d '{"question": "¿Qué es NestJS?"}'

El proyecto tiene licencia MIT y está disponible en GitHub.

Compartir
D
Diego Duran
@fxckcode

Ingeniero backend que construye herramientas CLI agentivas. NestJS, Go, Django, AWS.