RAG (Retrieval-Augmented Generation) es el patrón más usado para dar acceso a un LLM a información que no está en sus datos de entrenamiento. Este tutorial cubre la implementación completa desde cero.

Instalación

pip install langchain langchain-openai langchain-community chromadb pypdf

Paso 1: Cargar documentos

from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Cargar PDFs de un directorio
loader = DirectoryLoader('./documentos', glob="**/*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()

print(f"Cargados {len(documents)} documentos")

# Dividir en chunks
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ".", " "]
)

chunks = text_splitter.split_documents(documents)
print(f"Generados {len(chunks)} chunks")

Paso 2: Generar embeddings e indexar

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Crear el índice vectorial (persiste en disco)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

print("Indexación completada")

Paso 3: Construir el retriever

retriever = vectorstore.as_retriever(
    search_type="mmr",  # Maximal Marginal Relevance: diversifica los resultados
    search_kwargs={
        "k": 5,           # Número de documentos a recuperar
        "fetch_k": 20,    # Candidatos a evaluar antes de seleccionar k
    }
)

Paso 4: La cadena RAG completa

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)

prompt_template = """Usa el siguiente contexto para responder la pregunta. 
Si no encuentras la respuesta en el contexto, di explícitamente que no tienes esa información.

Contexto:
{context}

Pregunta: {question}

Respuesta:"""

PROMPT = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": PROMPT}
)

# Hacer una consulta
result = qa_chain.invoke({"query": "¿Cuál es la política de devoluciones?"})

print("Respuesta:", result["result"])
print("\nFuentes:")
for doc in result["source_documents"]:
    print(f"  - {doc.metadata.get('source', 'desconocido')}, página {doc.metadata.get('page', '?')}")

Versión con LangChain Expression Language (LCEL)

from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

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()
)

respuesta = rag_chain.invoke("¿Qué garantías ofrecéis?")
print(respuesta)

Cargar el índice existente (sin re-indexar)

# En el próximo arranque, carga sin recalcular embeddings
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings
)

Para producción: pasar a Qdrant o Pinecone

from langchain_community.vectorstores import Qdrant

# Reemplazar Chroma por Qdrant sin cambiar nada más
vectorstore = Qdrant.from_documents(
    documents=chunks,
    embedding=embeddings,
    url="http://localhost:6333",
    collection_name="mi_coleccion"
)

Fuentes: Documentación oficial de LangChain, guías de integración de Chroma y Qdrant.