Los Large Language Models (LLMs) como GPT-4 y Claude son extraordinariamente potentes, pero sufren de una limitación fundamental: su conocimiento está congelado en el momento del entrenamiento. No pueden acceder a tus documentos internos, tu base de datos o información en tiempo real. La Retrieval-Augmented Generation (RAG) resuelve exactamente este problema, combinando el poder generativo de los LLMs con la capacidad de recuperar información de fuentes externas.
El Problema: Las Limitaciones de los LLMs
Antes de hablar de RAG, es importante entender por qué la necesitamos.
- Conocimiento estático: Un LLM solo sabe lo que vio durante el entrenamiento. Si le preguntas sobre un evento posterior a su fecha de corte, no puede responder.
- Alucinaciones: Cuando un LLM no conoce la respuesta, tiende a inventarla, generando información plausible pero completamente falsa.
- Sin acceso a datos privados: Un LLM genérico no tiene acceso a la documentación interna de tu empresa, tickets o tu base de código.
RAG aborda estos tres problemas proporcionando al modelo un contexto relevante recuperado de fuentes externas en el momento de la consulta.
¿Qué es RAG?
La Retrieval-Augmented Generation es una arquitectura que enriquece el prompt enviado a un LLM con información recuperada de una base de conocimiento externa. En lugar de depender únicamente del conocimiento paramétrico del modelo, RAG busca primero la información relevante y luego la inyecta en el prompt, permitiendo al modelo generar respuestas precisas y fundamentadas.
Cómo Funciona RAG en Detalle
La arquitectura RAG se compone de dos fases principales: Indexación (offline) y Recuperación + Generación (online).
Fase 1: Indexación (Ingestión de Documentos)
La fase de indexación prepara tus documentos para la búsqueda semántica. Se compone de cuatro pasos.
1. Carga de Documentos
Los documentos pueden provenir de cualquier fuente: archivos PDF, páginas web, bases de datos, archivos Markdown, APIs. El Document Loader lee estos documentos y los convierte en texto estructurado.
2. División del Texto (Chunking)
Los LLMs tienen una ventana de contexto limitada, y los documentos pueden ser muy largos. El Text Splitter divide los documentos en fragmentos más pequeños llamados chunks. La calidad del chunking es crítica: chunks demasiado pequeños pierden contexto, chunks demasiado grandes diluyen la relevancia.
Las estrategias más comunes son:
- Recursive Character Splitting: Divide el texto recursivamente usando separadores como
\n\n,\n,., respetando la estructura del documento. - Semantic Splitting: Usa los embeddings para encontrar los puntos de ruptura naturales en el texto.
- Chunk Overlap: Incluye un solapamiento entre chunks consecutivos para preservar el contexto en los límites.
3. Embedding
Cada chunk se transforma en un vector numérico (embedding) mediante un modelo de embedding (como text-embedding-3-small de OpenAI). Estos vectores capturan el significado semántico del texto: frases con significados similares tendrán vectores cercanos en el espacio multidimensional.
4. Vector Store
Los vectores se almacenan en un Vector Store (o base de datos vectorial), como ChromaDB, Pinecone, Weaviate o FAISS. Esta base de datos está optimizada para la búsqueda por similitud: dada una consulta, encuentra los vectores (y por tanto los chunks de texto) más similares.
Fase 2: Recuperación + Generación
Cuando el usuario hace una pregunta:
- La pregunta se transforma en un embedding usando el mismo modelo de embedding.
- El Vector Store encuentra los chunks más similares mediante búsqueda por similitud (típicamente similitud coseno o distancia euclidiana).
- Los chunks recuperados se insertan en el prompt como contexto.
- El LLM genera la respuesta basándose en el contexto proporcionado.
Construir una Pipeline RAG con LangChain
LangChain es el framework Python (y JavaScript) más popular para construir aplicaciones basadas en LLMs. Ofrece abstracciones de alto nivel para cada componente de la pipeline RAG.
Instalación
pip install langchain langchain-openai langchain-community chromadb
Paso 1: Cargar Documentos
LangChain proporciona decenas de Document Loaders para diferentes fuentes de datos.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Cargar un PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Cargar una página web web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Cargar todos los archivos .md de un directorio dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
Paso 2: Dividir Documentos en Chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Documentos originales: {len(all_docs)}, Chunks: {len(chunks)}")
El parámetro chunk_overlap es fundamental: crea un solapamiento entre chunks consecutivos para que no se pierda contexto en los límites.
Paso 3: Crear los Embeddings y el Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
Paso 4: Crear el Retriever
El retriever es el componente que, dada una consulta, recupera los chunks más relevantes del vector store.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("¿Cómo funciona la autenticación?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
Paso 5: Construir la Chain RAG
Ahora juntamos todo con un LLM y un template de prompt.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Responde a la pregunta basándote exclusivamente en el contexto proporcionado. Si el contexto no contiene información suficiente, di que no lo sabes. Contexto: {context} Pregunta: {question} Respuesta: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("¿Cómo funciona la autenticación en el sistema?") print(response)
Técnicas RAG Avanzadas
La pipeline básica funciona bien, pero existen varias técnicas para mejorar significativamente la calidad de las respuestas.
Multi-Query Retrieval
A veces la consulta del usuario es ambigua o no está alineada con el lenguaje usado en los documentos. El Multi-Query Retriever genera automáticamente variantes de la pregunta original para capturar múltiples perspectivas.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("¿Cuáles son las mejores prácticas de seguridad?")
Contextual Compression
No todo el contenido de un chunk es relevante para la consulta. El Contextual Compression Retriever usa un LLM para extraer solo las partes pertinentes de cada chunk recuperado.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
Hybrid Search
La búsqueda puramente semántica no siempre es óptima. La Hybrid Search combina la búsqueda semántica (embeddings) con la búsqueda léxica (BM25, coincidencia de palabras clave) para obtener mejores resultados.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
RAG Conversacional (con Memoria)
Para construir un chatbot RAG que recuerde el contexto de la conversación, es necesario añadir una memoria que reformule las preguntas del usuario teniendo en cuenta el historial.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Dado el historial del chat y la última pregunta del usuario, " "reformula la pregunta para que sea comprensible sin el historial."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
Mejores Prácticas
- Elige el tamaño correcto de chunk: Experimenta con diferentes tamaños (500-1500 tokens). Chunks más pequeños para respuestas precisas, más grandes para contexto amplio.
- Usa metadatos en los documentos: Añade fuente, fecha y categoría como metadatos a los chunks. Esto permite filtrar los resultados durante la recuperación.
- Evalúa la calidad: Usa frameworks como RAGAS para medir métricas como faithfulness, relevancy y context precision.
- Gestiona las actualizaciones de documentos: Implementa una pipeline de re-ingestión para mantener el vector store sincronizado con tus fuentes de datos.
- Añade un re-ranker: Después de la recuperación inicial, usa un modelo de re-ranking (como Cohere Rerank) para reordenar los resultados según la relevancia real.
Conclusión
RAG se ha convertido en la arquitectura estándar para construir aplicaciones de IA que necesitan acceso a conocimientos específicos y actualizados. LangChain simplifica enormemente la implementación, proporcionando abstracciones para cada componente de la pipeline.
Próximos pasos:
- Experimenta en local: Comienza con ChromaDB y pocos documentos para familiarizarte con la pipeline.
- Explora LangSmith: Usa LangSmith para monitorear y depurar tus chains en producción.
- Prueba diferentes modelos de embedding: Compara modelos como
text-embedding-3-small,text-embedding-3-largey modelos open-source de Sentence Transformers. - Consulta la documentación: La documentación de LangChain es un recurso excelente y en constante actualización.