LLMs & RAG
RAG
LangChain
Python
LLMs

Cómo construir un sistema RAG con LangChain y Python

Construye un sistema RAG con LangChain y Python: estrategias de chunking, recuperación con MMR, evaluación con RAGAS y errores comunes en producción.

8 de enero de 2026·8 min read

Pregúntale a un LLM por la política de vacaciones de tu empresa y te inventará una con total confianza. El modelo nunca ha visto tus documentos: su conocimiento quedó congelado en el momento del entrenamiento, y rellena los vacíos con ficción plausible.

Retrieval-Augmented Generation (RAG) resuelve esto buscando en tus propios documentos los fragmentos relevantes para cada pregunta e inyectándolos en el prompt, de modo que el modelo responde con evidencia en vez de memoria. En esta guía construirás un sistema RAG completo con LangChain y Python — y, más importante, verás las partes que la mayoría de los tutoriales se salta: cómo elegir la estrategia de chunking, cómo construir el dataset de evaluación y los errores que aparecen en producción.

Cómo funciona un sistema RAG

La arquitectura tiene dos fases:

  1. Indexación (offline): dividir los documentos en fragmentos, convertir cada fragmento en un embedding — un vector que captura su significado — y almacenar los vectores en una base de datos vectorial.
  2. Recuperación y generación (por pregunta): convertir la pregunta en embedding, buscar los fragmentos más similares y pasarlos al LLM como contexto junto con la pregunta.

Todo lo que sigue pertenece a una de estas dos fases — y casi todos los problemas de calidad también: las malas respuestas suelen rastrearse hasta una mala recuperación, y la mala recuperación hasta un mal chunking.

Instalación

bash
pip install langchain langchain-community langchain-openai langchain-chroma tiktoken

Indexar documentos en una base vectorial

python
from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

# Cargar documentos
loader = DirectoryLoader("./docs", glob="**/*.pdf")
documents = loader.load()

# Dividir en fragmentos de 1000 caracteres con 200 de solapamiento
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
)
chunks = splitter.split_documents(documents)

# Crear la base vectorial
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./chroma_db")

Aquí importan dos detalles. Primero, chunk_size se mide en caracteres, no en tokens — si quieres tamaños basados en tokens, usa RecursiveCharacterTextSplitter.from_tiktoken_encoder(). Segundo, el solapamiento (chunk_overlap) garantiza que una idea cortada en el borde de un fragmento sobreviva completa en al menos uno de los dos fragmentos vecinos.

Estrategias de chunking: donde se decide la calidad de la recuperación

chunk_size=1000 es un valor inicial razonable, no una ley. El chunking es la decisión con más impacto en la calidad de la recuperación, así que vale la pena entender el trade-off:

  • Fragmentos pequeños (200–400 tokens) producen embeddings precisos — cada vector representa una sola idea — pero suelen carecer del contexto que el LLM necesita para responder. La recuperación acierta, la generación falla.
  • Fragmentos grandes (más de 1,500 tokens) cargan mucho contexto pero diluyen el embedding: un fragmento que mezcla tres temas coincide mal con consultas sobre cualquiera de los tres.
  • Un solapamiento del 10–20% del tamaño del fragmento es un seguro suficiente contra los cortes en los bordes; mucho más que eso solo duplica tu índice y tu factura de embeddings.

Igual de importante: ajusta el divisor a la estructura del documento.

  1. Prosa (políticas, manuales, artículos): RecursiveCharacterTextSplitter funciona bien porque intenta cortar por párrafos antes que por oraciones, y por oraciones antes que por palabras.
  2. Markdown o HTML con encabezados: divide primero por encabezado (MarkdownHeaderTextSplitter) y adjunta la ruta de encabezados como metadatos. Un fragmento que solo dice "se pueden acumular hasta 5 días" es inútil si no sabe que vive bajo "Vacaciones → Acumulación".
  3. Tablas: nunca dejes que el divisor corte filas por la mitad. Extrae las tablas por separado y convierte cada fila (o grupos pequeños de filas) en una oración antes de generar el embedding.
  4. Código: divide por función o clase con un divisor que entienda el lenguaje, no por número de caracteres.

Mi regla práctica: no discutas tamaños de fragmento en abstracto. Indexa el mismo corpus con dos o tres configuraciones, corre tus preguntas de evaluación (más abajo) contra cada una y deja que las métricas decidan.

Crear el pipeline de recuperación

python
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQAWithSourcesChain

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

retriever = vectorstore.as_retriever(
    search_type="mmr",  # Maximum Marginal Relevance para diversidad
    search_kwargs={"k": 5, "fetch_k": 20},
)

chain = RetrievalQAWithSourcesChain.from_chain_type(
    llm=llm,
    retriever=retriever,
    return_source_documents=True,
)

Usamos MMR (Maximum Marginal Relevance) en lugar de similitud coseno pura: trae fetch_k=20 candidatos y luego selecciona k=5 maximizando relevancia y diversidad, lo que evita recuperar cinco fragmentos casi idénticos del mismo documento.

Consultar el sistema

python
result = chain.invoke({"question": "¿Cuál es la política de vacaciones?"})

print(result["answer"])
print("Fuentes:", result["sources"])

Devolver las fuentes no es cosmético: les permite a los usuarios verificar las respuestas, y a ti depurar la recuperación cuando una respuesta sale mal.

Evaluación: el paso que todos se saltan

Un sistema RAG sin evaluación es un sistema que no sabes si funciona. Usa RAGAS para medir, como mínimo:

  • Faithfulness: ¿La respuesta está respaldada por los documentos recuperados?
  • Answer Relevancy: ¿La respuesta responde realmente la pregunta?
  • Context Recall: ¿Se recuperaron los fragmentos correctos?
python
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall

results = evaluate(
    dataset=test_dataset,
    metrics=[faithfulness, answer_relevancy, context_recall],
)

Cómo construir el dataset de evaluación

RAGAS necesita un conjunto de preguntas de prueba, idealmente con respuestas de referencia. Tres fuentes, en orden de valor:

  1. Preguntas reales de usuarios. Si el sistema reemplaza un FAQ o un flujo de soporte, extrae 30–50 preguntas de tickets o logs de chat. Superan a cualquier cosa que inventes porque traen fraseo real, typos y ambigüedad.
  2. Preguntas sintéticas para cobertura. Usa un LLM para generar pares pregunta–respuesta a partir de fragmentos aleatorios del corpus (RAGAS incluye un TestsetGenerator para esto). Revísalas a mano: las preguntas sintéticas tienden a estar sospechosamente bien alineadas con el texto que las produjo, lo que infla tus métricas.
  3. Preguntas sin respuesta. Haz que el 10–15% del conjunto sean preguntas que tu corpus no puede responder. El comportamiento correcto es "no lo sé" — mide si realmente lo obtienes.

Entre 50 y 100 preguntas curadas bastan para detectar regresiones. Versiona este dataset junto a tu código y vuélvelo a correr después de cada cambio de chunking, embeddings o prompts. Sin ese hábito, cada "mejora" es un redespliegue a ciegas.

Errores comunes de RAG en producción

Estos son los problemas que más veo cuando un prototipo se encuentra con usuarios reales, junto con sus soluciones estándar:

  1. Índice desactualizado. Los documentos cambian; el índice no. Programa la reindexación disparada desde la fuente de los documentos y guarda la fecha de última modificación de cada documento como metadato, para poder auditar qué tan fresco está el índice.
  2. Lost in the middle. Subir k a 20 fragmentos empeora las respuestas en vez de mejorarlas: los modelos prestan menos atención a la mitad de un contexto largo. Recupera menos fragmentos pero mejores — y agrega un paso de re-ranking (un cross-encoder que reordena los candidatos del retriever) si la precisión es el cuello de botella.
  3. Responder lo irrespondible. La búsqueda vectorial siempre devuelve algo — el vecino más cercano existe aunque sea irrelevante. Define un umbral de similitud e instruye explícitamente al modelo para que diga que no sabe cuando el contexto no respalda una respuesta.
  4. Sin filtrado por metadatos. "¿Cuál es la política de 2025?" recupera sin problema la versión de 2019, porque ambas son semánticamente idénticas. Filtra por metadatos (año, departamento, tipo de documento) antes de la búsqueda por similitud.
  5. Sorpresas de costo. Generar embeddings es barato; generar respuestas no. El caché semántico (reutilizar respuestas a preguntas similares) y enrutar las preguntas fáciles a un modelo más pequeño reducen la factura de forma significativa.

Cuándo NO usar RAG

RAG no es la respuesta a todo. Considera alternativas si:

  • Tienes unas pocas decenas de documentos cortos: con las ventanas de contexto modernas muchas veces puedes pasarlo todo directamente en el prompt — sin búsqueda vectorial.
  • El conocimiento es extremadamente dinámico (datos en tiempo real): usa function calling + API en lugar de reindexar constantemente.
  • Las preguntas requieren razonamiento multi-paso o cómputo: un agente con herramientas funciona mejor — muestro cómo construir uno en Agentes de IA con LangGraph para análisis de datos.
  • Necesitas extraer campos específicos de documentos en lugar de responder preguntas abiertas: eso es extracción de datos estructurados con LLMs, un patrón más simple y confiable.

Conclusión

RAG sigue siendo la forma más pragmática de conectar un LLM a conocimiento privado, y con LangChain tienes un prototipo funcional en una tarde. Lo que separa un demo de un sistema de producción es todo lo que rodea al pipeline: un chunking elegido según la estructura de tus documentos, un dataset de evaluación construido con preguntas reales y métricas que vuelves a correr antes de cada cambio. Invierte ahí primero — la calidad de la recuperación, no la redacción del prompt, es donde la mayoría de los sistemas RAG gana o pierde.

¿Estás implementando RAG en tu empresa? Hablemos — puedo ayudarte a evitar los errores más comunes.


Sobre el autor

Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas