TechLead
Lección 18 de 18
5 min de lectura
LangChain

Construyendo una Aplicación RAG Completa

Construye una aplicación completa de Generación Aumentada por Recuperación desde cero con ingesta de documentos, búsqueda vectorial e IA conversacional

Proyecto: App RAG Full-Stack

En este proyecto práctico, construirás una aplicación RAG (Generación Aumentada por Recuperación) completa que puede responder preguntas sobre tus propios documentos. Combinaremos todo lo aprendido: carga de documentos, división de texto, embeddings, almacenamiento vectorial, cadenas y streaming.

🏗️ Lo Que Construiremos

  • 📥 Ingesta de Documentos: Sube PDFs y páginas web a una base de datos vectorial
  • 🔍 Búsqueda Semántica: Encuentra fragmentos relevantes basados en preguntas del usuario
  • 💬 IA Conversacional: Chatea con tus documentos con memoria
  • 📡 Streaming: Transmisión de tokens en tiempo real para una gran UX
  • 📚 Citas de Fuentes: Muestra de qué documentos proviene la respuesta

Estructura del Proyecto

rag-app/
├── src/
│   ├── lib/
│   │   ├── embeddings.ts      # Configuración de embeddings
│   │   ├── vectorStore.ts     # Setup del vector store
│   │   ├── ingest.ts          # Pipeline de ingesta
│   │   └── ragChain.ts        # Cadena RAG con memoria
│   ├── app/
│   │   ├── api/
│   │   │   ├── ingest/route.ts   # Endpoint de carga
│   │   │   └── chat/route.ts     # Endpoint de chat
│   │   └── page.tsx              # UI de chat
├── .env.local
└── package.json

Paso 1: Setup y Configuración

# Instalar dependencias
npm install langchain @langchain/openai @langchain/community
npm install @langchain/core pdf-parse cheerio
// src/lib/embeddings.ts
import { OpenAIEmbeddings } from "@langchain/openai";

export const embeddings = new OpenAIEmbeddings({
  modelName: "text-embedding-3-small",
});

Paso 2: Setup del Vector Store

// src/lib/vectorStore.ts
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { embeddings } from "./embeddings";
import { Document } from "@langchain/core/documents";

let vectorStore: MemoryVectorStore | null = null;

export async function getVectorStore() {
  if (!vectorStore) {
    vectorStore = new MemoryVectorStore(embeddings);
  }
  return vectorStore;
}

export async function agregarDocumentos(docs: Document[]) {
  const store = await getVectorStore();
  await store.addDocuments(docs);
  console.log(`Agregados ${docs.length} documentos al vector store`);
}

export async function buscarDocumentos(consulta: string, k = 4) {
  const store = await getVectorStore();
  return store.similaritySearchWithScore(consulta, k);
}

Paso 3: Pipeline de Ingesta de Documentos

// src/lib/ingest.ts
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { agregarDocumentos } from "./vectorStore";

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
});

export async function ingerirPDF(rutaArchivo: string) {
  console.log(`Ingiriendo PDF: ${rutaArchivo}`);
  
  const loader = new PDFLoader(rutaArchivo, { splitPages: true });
  const docsRaw = await loader.load();
  const docsDivididos = await splitter.splitDocuments(docsRaw);
  
  const docsConMeta = docsDivididos.map((doc) => ({
    ...doc,
    metadata: {
      ...doc.metadata,
      source: rutaArchivo,
      type: "pdf",
      ingestedAt: new Date().toISOString(),
    },
  }));
  
  await agregarDocumentos(docsConMeta);
  return { fragmentos: docsConMeta.length, paginas: docsRaw.length };
}

export async function ingerirURL(url: string) {
  console.log(`Ingiriendo URL: ${url}`);
  
  const loader = new CheerioWebBaseLoader(url);
  const docsRaw = await loader.load();
  const docsDivididos = await splitter.splitDocuments(docsRaw);
  
  const docsConMeta = docsDivididos.map((doc) => ({
    ...doc,
    metadata: { ...doc.metadata, source: url, type: "web" },
  }));
  
  await agregarDocumentos(docsConMeta);
  return { fragmentos: docsConMeta.length };
}

Paso 4: Cadena RAG con Memoria de Conversación

// src/lib/ragChain.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { createHistoryAwareRetriever } from "langchain/chains/history_aware_retriever";
import { getVectorStore } from "./vectorStore";

const llm = new ChatOpenAI({
  modelName: "gpt-4",
  streaming: true,
  temperature: 0.3,
});

export async function crearCadenaRAG() {
  const vectorStore = await getVectorStore();
  const retriever = vectorStore.asRetriever({ k: 4 });

  const promptContextualizar = ChatPromptTemplate.fromMessages([
    ["system", `Dado el historial de chat y la última pregunta, reformula 
    la pregunta para que sea independiente. NO respondas la pregunta.`],
    new MessagesPlaceholder("chat_history"),
    ["human", "{input}"],
  ]);

  const retrieverConHistorial = await createHistoryAwareRetriever({
    llm,
    retriever,
    rephrasePrompt: promptContextualizar,
  });

  const promptRespuesta = ChatPromptTemplate.fromMessages([
    ["system", `Eres un asistente útil que responde preguntas basándose en 
    el contexto proporcionado. Si el contexto no contiene información relevante, 
    dilo honestamente. Siempre cita qué documento fuente usaste.
    
    Contexto: {context}`],
    new MessagesPlaceholder("chat_history"),
    ["human", "{input}"],
  ]);

  const documentChain = await createStuffDocumentsChain({
    llm,
    prompt: promptRespuesta,
  });

  return await createRetrievalChain({
    retriever: retrieverConHistorial,
    combineDocsChain: documentChain,
  });
}

Paso 5: Ruta API con Streaming

// app/api/chat/route.ts
import { crearCadenaRAG } from "@/lib/ragChain";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

export async function POST(req: Request) {
  const { message, chatHistory = [] } = await req.json();
  const chain = await crearCadenaRAG();

  const history = chatHistory.map((msg: any) =>
    msg.role === "user"
      ? new HumanMessage(msg.content)
      : new AIMessage(msg.content)
  );

  const stream = await chain.stream({
    input: message,
    chat_history: history,
  });

  const encoder = new TextEncoder();
  let sources: any[] = [];

  const readable = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        if (chunk.answer) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ 
              type: "token", content: chunk.answer 
            })}\n\n`)
          );
        }
        if (chunk.context) {
          sources = chunk.context.map((doc: any) => ({
            contenido: doc.pageContent.slice(0, 200),
            fuente: doc.metadata.source,
          }));
        }
      }
      controller.enqueue(
        encoder.encode(`data: ${JSON.stringify({ type: "sources", sources })}\n\n`)
      );
      controller.enqueue(encoder.encode("data: [DONE]\n\n"));
      controller.close();
    },
  });

  return new Response(readable, {
    headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
  });
}

🚀 Extendiendo Este Proyecto

  • • Cambia a Pinecone o Supabase pgvector para almacenamiento persistente
  • • Agrega UI de carga de archivos con drag-and-drop para PDFs
  • • Implementa autenticación para que cada usuario tenga sus propios documentos
  • • Agrega filtrado por metadatos (buscar solo ciertos tipos de documentos)
  • • Despliega en Vercel con Edge Functions para baja latencia

💡 Puntos Clave

  • • Una app RAG completa combina: Carga → División → Embedding → Almacenamiento → Recuperación → Generación
  • • Los retrievers con historial hacen las conversaciones naturales al contextualizar preguntas de seguimiento
  • • El streaming con SSE proporciona entrega de tokens en tiempo real para la mejor UX
  • • Siempre proporciona citas de fuentes para que los usuarios puedan verificar las respuestas
  • • Empieza con MemoryVectorStore, luego actualiza a una BD vectorial persistente para producción

Continuar aprendiendo