Fundamentos de RAG: Arquitectura y Componentes
¿Qué es RAG?
RAG (Retrieval-Augmented Generation) es el patrón más importante para aplicaciones de IA generativa en producción. Combina la capacidad de generación de un LLM con búsqueda en bases de conocimiento propias.
¿Por Qué RAG?
Los LLMs tienen limitaciones críticas que RAG resuelve:
| Problema del LLM | Solución RAG |
|---|---|
| Conocimiento desactualizado | Recupera datos actuales |
| Alucinaciones | Contexto verificable con fuentes |
| Sin datos privados | Indexa documentos internos |
| Contexto limitado | Recupera solo lo relevante |
| Costo de fine-tuning | No requiere re-entrenamiento |
Arquitectura RAG Completa
┌─────────────────────────────────────────────────────────────┐
│ PIPELINE DE INDEXACIÓN │
│ │
│ Documentos ──► Loader ──► Splitter ──► Embedder ──► VectorDB │
│ (PDF, MD, (parse) (chunk) (vectorize) (store) │
│ HTML, DB) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PIPELINE DE CONSULTA │
│ │
│ Query ──► Embedder ──► VectorDB ──► Reranker ──► Prompt ──► LLM │
│ (user) (vectorize) (search) (refine) (augment) (gen) │
│ │
│ ◄────────────────── Respuesta con fuentes ──────────────────► │
└─────────────────────────────────────────────────────────────┘
Pipeline de Indexación
1. Document Loading
from langchain_community.document_loaders import (
PyPDFLoader,
UnstructuredMarkdownLoader,
WebBaseLoader,
CSVLoader,
)
# PDF
pdf_docs = PyPDFLoader("manual_tecnico.pdf").load()
# Markdown
md_docs = UnstructuredMarkdownLoader("docs/api.md").load()
# Web
web_docs = WebBaseLoader("https://docs.example.com/api").load()
# Cada documento tiene: page_content + metadata
print(pdf_docs[0].metadata)
# {'source': 'manual_tecnico.pdf', 'page': 0}
2. Text Splitting (Chunking)
El chunking es crítico para la calidad de RAG. Chunks muy grandes diluyen información, muy pequeños pierden contexto.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # ~1000 caracteres por chunk (ajustar según modelo de embedding)
chunk_overlap=200, # 200 chars de overlap: evita perder ideas que quedan en el borde
# entre dos chunks — asegura que aparezcan en ambos
separators=[ # Prioridad de corte (intenta el primero, si no cabe, el siguiente):
"\n\n", # 1. Párrafo doble (mejor: preserva secciones completas)
"\n", # 2. Salto de línea
". ", # 3. Fin de oración
" ", # 4. Espacio (entre palabras)
"" # 5. Carácter a carácter (último recurso)
],
length_function=len, # Cuenta caracteres; alternativa: tiktoken para contar tokens
)
chunks = splitter.split_documents(documents)
print(f"Documentos: {len(documents)} → Chunks: {len(chunks)}")
Estrategias de Chunking:
| Estrategia | Cuándo Usar | Pros | Contras |
|---|---|---|---|
| Fixed-size | Texto general | Simple, predecible | Corta en medio de ideas |
| Recursive | Texto estructurado | Respeta estructura | Tamaños variables |
| Semantic | Alta calidad | Chunks coherentes | Más lento, más costoso |
| Document-based | Docs cortos | Preserva contexto completo | Chunks grandes |
3. Embedding
from langchain_openai import OpenAIEmbeddings
# Los embeddings convierten texto a vectores numéricos donde textos semánticamente
# similares quedan cerca en el espacio vectorial — esto permite búsqueda semántica
embeddings = OpenAIEmbeddings(
model="text-embedding-3-large",
dimensions=1536, # Matryoshka embedding: el modelo soporta hasta 3072 dims
# Reducir dimensiones ahorra ~50% almacenamiento/costos,
# con pérdida mínima de calidad (~1-2% en benchmarks)
)
# embed_query: para la pregunta del usuario (puede usar prefijo interno optimizado)
vector = embeddings.embed_query("¿Cómo configuro autenticación?")
print(f"Dimensiones: {len(vector)}") # 1536
# embed_documents: para los chunks a indexar (prefijo diferente para búsqueda asimétrica)
vectors = embeddings.embed_documents([chunk.page_content for chunk in chunks])
Modelos de Embedding Populares:
| Modelo | Dimensiones | Contexto | Nota |
|---|---|---|---|
| text-embedding-3-large | 3072 (ajustable) | 8K | Mejor calidad OpenAI |
| text-embedding-3-small | 1536 | 8K | Económico |
| Cohere embed-v4 | 1024 | 512 | Multilingüe |
| voyage-3-large | 1024 | 32K | Código y texto |
| BGE-M3 | 1024 | 8K | Open-source, multilingüe |
4. Vector Storage
from langchain_community.vectorstores import Chroma
# Crear e indexar vectores en Chroma (base de datos vectorial ligera)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db", # Persiste a disco; sin esto, es solo in-memory
collection_metadata={
"hnsw:space": "cosine" # HNSW: algoritmo de indexación para búsqueda rápida
# "cosine": mide similitud por ángulo entre vectores
# Alternativas: "l2" (euclidiana), "ip" (producto interno)
},
)
# similarity_search: retorna los k documentos más similares semánticamente
results = vectorstore.similarity_search(
"autenticación JWT", k=5
)
# Con scores: score más bajo = más similar (distancia coseno)
# k=5: balance entre contexto suficiente y no agregar ruido/tokens innecesarios
results_with_scores = vectorstore.similarity_search_with_score(
"autenticación JWT", k=5
)
for doc, score in results_with_scores:
print(f"Score: {score:.4f} | {doc.page_content[:80]}...")
Pipeline de Consulta
5. Retrieval
retriever = vectorstore.as_retriever(
search_type="similarity", # o "mmr" para diversidad
search_kwargs={
"k": 5, # Número de resultados
"score_threshold": 0.7, # Mínimo de relevancia (0-1); documentos debajo se descartan
# Puede retornar menos de k si no hay docs suficientemente similares
},
)
# MMR (Maximum Marginal Relevance) — diversifica resultados
# ¿Por qué? Si los top-5 dicen lo mismo, desperdicias contexto del LLM
# MMR selecciona docs que son relevantes Y diferentes entre sí
retriever_mmr = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 5,
"fetch_k": 20, # Candidatos iniciales (más = mejor diversidad, más lento)
"lambda_mult": 0.7, # 1.0 = solo relevancia, 0.0 = solo diversidad
# 0.7: prioriza relevancia con algo de diversidad
},
)
6. Prompt Augmentation
from langchain_core.prompts import ChatPromptTemplate
rag_prompt = ChatPromptTemplate.from_template("""
Eres un asistente técnico. Responde SOLO con información del contexto proporcionado.
Si no encuentras la respuesta en el contexto, di "No tengo información suficiente".
Contexto:
{context}
Pregunta: {question}
Instrucciones:
- Responde de forma precisa y concisa
- Cita las fuentes cuando sea posible
- Si la información está incompleta, indícalo
""")
7. Chain Completo
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
# temperature=0: respuestas deterministas y fieles al contexto (no creativas)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Convierte la lista de Document objects del retriever en texto formateado con fuentes
def format_docs(docs):
return "\n\n---\n\n".join(
f"[Fuente: {d.metadata.get('source', 'N/A')}]\n{d.page_content}"
for d in docs
)
# LCEL (LangChain Expression Language) — el | encadena pasos secuencialmente
# Flujo: pregunta → [retriever + passthrough] → prompt → LLM → parser
rag_chain = (
{
# retriever busca docs relevantes → format_docs los convierte a texto
"context": retriever | format_docs,
# RunnablePassthrough pasa la pregunta del usuario sin modificar
"question": RunnablePassthrough()
}
| rag_prompt # Inserta context y question en el template
| llm # Envía al LLM, recibe AIMessage
| StrOutputParser() # Extrae el string de texto del AIMessage
)
# Uso
response = rag_chain.invoke("¿Cómo configuro autenticación JWT?")
print(response)
Métricas de Evaluación para RAG
| Métrica | Qué Mide | Rango |
|---|---|---|
| Context Precision | ¿Los docs recuperados son relevantes? | 0-1 |
| Context Recall | ¿Se recuperaron todos los docs necesarios? | 0-1 |
| Faithfulness | ¿La respuesta es fiel al contexto? | 0-1 |
| Answer Relevancy | ¿La respuesta es relevante a la pregunta? | 0-1 |
# RAGAS: framework de evaluación automática para pipelines RAG
# NOTA: usa un LLM internamente para evaluar (genera costo de API)
from ragas import evaluate
from ragas.metrics import (
faithfulness, # ¿Cada afirmación de la respuesta está respaldada por el contexto?
answer_relevancy, # ¿La respuesta realmente responde la pregunta?
context_precision, # ¿Los chunks relevantes están en las primeras posiciones?
context_recall, # ¿Se recuperaron TODOS los chunks necesarios?
)
# eval_dataset debe tener columnas: question, answer, contexts, ground_truth
result = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)
# {'faithfulness': 0.89, 'answer_relevancy': 0.92, ...}
Errores Comunes en RAG
| Error | Síntoma | Solución |
|---|---|---|
| Chunks muy grandes | Respuestas vagas | Reducir chunk_size |
| Chunks muy pequeños | Falta contexto | Aumentar overlap |
| Embedding incorrecto | Baja relevancia | Probar otro modelo |
| Sin metadata filtering | Ruido en resultados | Añadir filtros |
| Prompt débil | Alucinaciones | Instrucciones más estrictas |
| Sin reranking | Docs irrelevantes en top-k | Añadir reranker |
Resumen
RAG es el patrón fundamental para aplicaciones de IA generativa con datos propios. Los componentes clave son:
- Chunking inteligente que preserve semántica
- Embeddings de calidad adecuados al dominio
- Vector database eficiente y escalable
- Retrieval con reranking para máxima relevancia
- Evaluación continua con métricas específicas de RAG
🧠 Preguntas de Repaso
1. En RAG, ¿cuál es la función del parámetro lambda_mult en MMR (Maximum Marginal Relevance)?
- A) Controla el número máximo de documentos retornados
- B) Controla el balance entre relevancia y diversidad: 1.0 = solo relevancia, 0.0 = solo diversidad
- C) Define el tamaño mínimo de los chunks
- D) Establece el umbral de confianza para filtrar resultados
Respuesta: B) —
lambda_multen MMR controla el balance: con 1.0 se prioriza solo relevancia, con 0.0 solo diversidad. Un valor de 0.7 prioriza relevancia pero con suficiente diversidad para evitar redundancia.
2. ¿Por qué el modelo text-embedding-3-large soporta "Matryoshka embeddings"?
- A) Porque puede procesar múltiples idiomas simultáneamente
- B) Porque sus dimensiones son reducibles (ej: de 3072 a 1536) ahorrando ~50% de almacenamiento con solo 1-2% de pérdida de calidad
- C) Porque puede generar embeddings para imágenes y texto
- D) Porque permite entrenar sub-modelos dentro del modelo principal
Respuesta: B) — Matryoshka embeddings permiten reducir las dimensiones del vector (de 3072 a 1536 o menos) sin necesidad de re-entrenamiento, ahorrando almacenamiento con pérdida mínima de calidad.
3. ¿Cuáles son las 4 métricas principales de RAGAS para evaluar un sistema RAG?
- A) Accuracy, Precision, Recall, F1
- B) Context Precision, Context Recall, Faithfulness, Answer Relevancy
- C) BLEU, ROUGE, METEOR, BERTScore
- D) Latency, Throughput, Error Rate, Cost
Respuesta: B) — RAGAS evalúa con: Context Precision (relevancia del contexto recuperado), Context Recall (cobertura), Faithfulness (fidelidad al contexto) y Answer Relevancy (relevancia de la respuesta). Todas en rango 0-1.
4. ¿Cuál es un error común al configurar el chunking en RAG y su consecuencia?
- A) Chunks muy grandes producen respuestas más precisas
- B) Chunks muy pequeños pierden contexto necesario y chunks muy grandes producen respuestas vagas
- C) El overlap entre chunks no tiene ningún efecto en la calidad
- D) Usar separadores jerárquicos reduce la calidad de búsqueda
Respuesta: B) — Un error frecuente es no calibrar el tamaño: chunks muy grandes producen respuestas vagas porque incluyen información irrelevante, mientras que chunks muy pequeños pierden el contexto necesario para respuestas coherentes.