Phase 11 - Lesson 06

RAG (Retrieval-Augmented Generation)

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Su LLM sabe todo hasta su límite de fecha de entrenamiento. No sabe nada sobre los documentos de su empresa, su base de código o las notas de la reunión de la semana pasada. RAG resuelve esto recuperando documentos relevantes e introduciéndolos en el prompt. Es el patrón más implementado en la IA de producción. Si construye una sola cosa de este curso, construya un pipeline de RAG.

Type: Build Languages: Python Prerequisites: Phase 10 (LLMs from Scratch), Phase 11 Lessons 01-05 Time: ~90 minutes Related: Phase 5 · 23 (Chunking Strategies for RAG) para los seis algoritmos de fragmentación y cuándo gana cada uno. Phase 5 · 22 (Embedding Models Deep Dive) para elegir el generador de embeddings. Phase 11 · 07 (Advanced RAG) para búsqueda híbrida, reordenación y transformación de consultas.

Objetivos de Aprendizaje

  • Construir un pipeline de RAG completo: carga de documentos, fragmentación (chunking), incorporación (embedding), almacenamiento vectorial, recuperación y generación
  • Implementar búsqueda semántica utilizando una base de datos vectorial (ChromaDB, FAISS o Pinecone) con indexación adecuada
  • Explicar por qué se prefiere RAG sobre el ajuste fino (fine-tuning) para aplicaciones basadas en conocimiento (costo, actualización, atribución)
  • Evaluar la calidad de RAG utilizando métricas de recuperación (precisión, recall) y métricas de generación (fidelidad, relevancia)

El Problema

Usted construye un chatbot para su empresa. Un cliente pregunta: "¿Cuál es la política de reembolso para los planes corporativos (enterprise)?" El LLM responde con una respuesta genérica sobre políticas típicas de reembolso de SaaS. La política real, oculta en una wiki interna de 200 páginas, dice que los clientes corporativos obtienen una ventana de 60 días con reembolsos prorrateados. El LLM nunca ha visto este documento. No puede saber lo que no se le entrenó para saber.

El ajuste fino (fine-tuning) es una solución. Tomar el LLM, entrenarlo con sus documentos internos y desplegar el modelo actualizado. Esto funciona pero presenta problemas graves. El ajuste fino cuesta miles de dólares en cómputo. El modelo queda desactualizado en el momento en que cambia un documento. No tiene forma de saber de qué fuente se extrajo la respuesta. Y si la empresa adquiere otra línea de productos el próximo mes, tendrá que realizar el ajuste fino de nuevo.

RAG es la otra solución. Dejar el modelo intacto. Cuando llega una pregunta, buscar pasajes relevantes en su almacenamiento de documentos, pegarlos en el prompt antes de la pregunta y dejar que el modelo responda utilizando esos pasajes como contexto. El almacenamiento de documentos se puede actualizar en minutos. Puede ver exactamente qué documentos se recuperaron. El modelo en sí nunca cambia. Es por eso que RAG es el patrón dominante en producción: es más barato, más actualizado, más auditable y funciona con cualquier LLM.

El Concepto

El Patrón RAG

Todo el patrón cabe en cuatro pasos:

graph LR
    Q["Consulta del Usuario"] --> R["Recuperar"]
    R --> A["Aumentar Prompt"]
    A --> G["Generar"]
    G --> Ans["Respuesta"]

    subgraph "Recuperar"
        R --> Embed["Incorporar consulta (Embed)"]
        Embed --> Search["Buscar en base de datos vectorial"]
        Search --> TopK["Retornar fragmentos top-k"]
    end

    subgraph "Aumentar"
        TopK --> Format["Formatear fragmentos en el prompt"]
        Format --> Combine["Combinar con la pregunta del usuario"]
    end

    subgraph "Generar"
        Combine --> LLM["LLM genera respuesta"]
        LLM --> Cite["Respuesta fundamentada en docs recuperados"]
    end

Consulta -> Recuperar -> Aumentar prompt -> Generar. Cada sistema RAG sigue este patrón. Las diferencias entre los sistemas RAG en producción radican en los detalles de cada paso: cómo se fragmenta, cómo se hace la incorporación (embedding), cómo se busca y cómo se construye el prompt.

Por qué RAG Supera al Ajuste Fino (Fine-Tuning)

Preocupación Ajuste Fino (Fine-Tuning) RAG
Costo
,000-
00,000+ por ejecución de entrenamiento
$0.01-$0.10 por consulta (embedding + LLM)
Actualización Desactualizado hasta que se vuelva a entrenar Actualizado en minutos al reindexar los docs
Auditabilidad No se puede rastrear la respuesta hasta la fuente Puede mostrar los pasajes exactos recuperados
Alucinación Aún alucina libremente Fundamentado en los documentos recuperados
Privacidad de datos Datos de entrenamiento integrados en los pesos Los documentos permanecen en su almacenamiento vectorial

El ajuste fino cambia los pesos del modelo de forma permanente. RAG cambia el contexto del modelo de forma temporal. Para la mayoría de las aplicaciones, el contexto temporal es lo que se desea.

El único caso donde gana el ajuste fino es cuando se necesita que el modelo adopte un estilo, tono o patrón de razonamiento específico que no se puede lograr solo mediante la ingeniería de prompts. Para la recuperación de conocimiento factual, RAG gana siempre.

Modelos de Incorporación (Embedding Models)

Un modelo de incorporación (embedding) convierte el texto en un vector denso. Textos similares producen vectores que están cerca en este espacio de alta dimensión. "¿Cómo restablezco mi contraseña?" y "Necesito cambiar mi contraseña" producen vectores casi idénticos a pesar de compartir pocas palabras. "El gato se sentó en la alfombra" produce un vector muy diferente.

Modelos de embedding comunes (alineación de 2026 — ver Phase 5 · 22 para el análisis completo):

Modelo Dimensiones Proveedor Notas
text-embedding-3-small 1536 (Matryoshka) OpenAI Mejor relación precio/rendimiento para la mayoría de los casos de uso
text-embedding-3-large 3072 (Matryoshka) OpenAI Mayor precisión, truncable a 256/512/1024
Gemini Embedding 2 3072 (Matryoshka) Google Principal recuperación MTEB; contexto de 8K
voyage-4 1024/2048 (Matryoshka) Voyage AI Variantes de dominio (código, finanzas, legal)
Cohere embed-v4 1024 (Matryoshka) Cohere Fuerte soporte multilingüe, contexto de 128K
BGE-M3 1024 (dense + sparse + ColBERT) BAAI (open-weight) Tres perspectivas a partir de un solo modelo
Qwen3-Embedding 4096 (Matryoshka) Alibaba (open-weight) Mayor puntuación de recuperación en open-weight
all-MiniLM-L6-v2 384 Open-weight (Sentence Transformers) Línea de base para prototipado

Para esta lección, construiremos nuestra propia incorporación simple utilizando TF-IDF. No porque TF-IDF sea lo que usan los sistemas de producción, sino porque hace que el concepto sea concreto: entra texto, sale un vector, textos similares producen vectores similares.

Similitud Vectorial (Vector Similarity)

Dados dos vectores, ¿cómo se mide la similitud? Tres opciones:

Similitud de coseno: el coseno del ángulo entre dos vectores. Varía de -1 (opuesto) a 1 (idéntico). Ignora la magnitud, solo le importa la dirección. Este es el valor predeterminado para RAG.

cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)

Producto escalar (Dot product): el producto interno bruto. Los vectores más grandes obtienen puntuaciones más altas. Útil cuando la magnitud contiene información (los documentos más largos podrían ser más relevantes).

dot(a, b) = sum(a_i * b_i)

Distancia L2 (Euclidiana): distancia en línea recta en el espacio vectorial. Menor distancia = más similar. Sensible a las diferencias de magnitud.

L2(a, b) = sqrt(sum((a_i - b_i)^2))

La similitud de coseno es el estándar. Maneja documentos de diferentes longitudes de manera elegante porque normaliza por magnitud. Cuando alguien dice "búsqueda vectorial", casi siempre se refiere a la similitud de coseno.

Estrategias de Fragmentación (Chunking Strategies)

Los documentos son demasiado largos para incorporarse como vectores únicos. Un PDF de 50 páginas podría producir una incorporación deficiente porque contiene docenas de temas. En su lugar, se dividen los documentos en fragmentos (chunks) y se incorpora cada fragmento por separado.

Fragmentación de tamaño fijo (Fixed-size chunking): se divide cada N tokens. Simple y predecible. Un fragmento de 512 tokens con una sobreposición (overlap) de 50 tokens significa que el fragmento 1 contiene los tokens 0-511, el fragmento 2 contiene los tokens 462-973, y así sucesivamente. La sobreposición asegura que no se divida una oración en un límite desafortunado.

Fragmentación semántica (Semantic chunking): se divide en límites naturales. Párrafos, secciones o encabezados de markdown. Cada fragmento es una unidad coherente de significado. Es más complejo de implementar pero produce una mejor recuperación.

Fragmentación recursiva (Recursive chunking): intenta dividir primero en el límite más grande (encabezados de sección). Si una sección sigue siendo demasiado grande, se divide en los límites de párrafo. Si un párrafo sigue siendo demasiado grande, se divide en los límites de oración. Este es el enfoque de RecursiveCharacterTextSplitter de LangChain y funciona bien en la práctica.

El tamaño del fragmento importa más de lo que la gente piensa:

  • Demasiado pequeño (64-128 tokens): cada fragmento carece de contexto. "Aumentó un 15% el trimestre pasado" no significa nada sin saber a qué se refiere "ello".
  • Demasiado grande (2048+ tokens): cada fragmento cubre múltiples temas, lo que diluye la relevancia. Cuando busca datos de ingresos, obtiene un fragmento que contiene un 10% sobre ingresos y un 90% sobre cantidad de empleados.
  • Punto ideal (256-512 tokens): suficiente contexto para ser autosuficiente, lo suficientemente enfocado para ser relevante.

La mayoría de los sistemas RAG en producción utilizan fragmentos de 256-512 tokens con una sobreposición de 50 tokens. Las pautas de RAG de Anthropic recomendan este rango.

Bases de Datos Vectoriales (Vector Databases)

Una vez que tiene las incorporaciones (embeddings), necesita un lugar para almacenarlas y buscarlas. Opciones:

Base de Datos Tipo Mejor para
FAISS Biblioteca (en proceso) Prototipado, conjuntos de datos pequeños a medianos
Chroma Base de datos ligera Desarrollo local, implementaciones pequeñas
Pinecone Servicio gestionado Producción sin sobrecarga de operaciones
Weaviate Base de datos de código abierto Producción auto-hospedada
pgvector Extensión de Postgres Si ya se está usando Postgres
Qdrant Base de datos de código abierto Auto-hospedado de alto rendimiento

Para esta lección, construiremos un almacenamiento vectorial simple en memoria. Almacena vectores en una lista y realiza una búsqueda de similitud de coseno por fuerza bruta. Esto equivale a FAISS con un índice plano. Escala a unos 100,000 vectores antes de volverse lento. Los sistemas de producción utilizan algoritmos de vecino más cercano aproximado (ANN) como HNSW para buscar millones de vectores en milisegundos.

El Pipeline Completo

graph TD
    subgraph "Indexación (offline)"
        D["Documentos"] --> C["Fragmentar (Chunk)"]
        C --> E["Incorporar cada fragmento"]
        E --> S["Almacenar vectores + texto"]
    end

    subgraph "Consulta (online)"
        Q["Consulta del usuario"] --> QE["Incorporar consulta"]
        QE --> VS["Búsqueda vectorial (top-k)"]
        VS --> P["Construir prompt con fragmentos"]
        P --> LLM["LLM genera respuesta"]
    end

    S -.->|"mismo espacio vectorial"| VS

La fase de indexación se ejecuta una vez por documento (or cuando los documentos se actualizan). La fase de consulta se ejecuta en cada solicitud de usuario. En producción, la indexación podría procesar millones de documentos durante horas. La consulta debe responder en menos de un segundo.

Números Reales

La mayoría de los sistemas RAG en producción utilizan estos parámetros:

  • k = 5 a 10 fragmentos recuperados por consulta
  • Tamaño del fragmento = 256 a 512 tokens con una sobreposición de 50 tokens
  • Presupuesto de contexto: 2,500-5,000 tokens de contenido recuperado por consulta
  • Prompt total: ~8,000-16,000 tokens (prompt del sistema + fragmentos recuperados + historial de conversación + consulta del usuario)
  • Dimensión de incorporación: 384-3072 dependiendo del modelo
  • Rendimiento de indexación: 100-1,000 documentos por segundo con embeddings de API
  • Latência de consulta: 50-200 ms para recuperación, 500-3,000 ms para generación

Constrúyalo

Paso 1: Fragmentación de Documentos (Document Chunking)

def chunk_text(text, chunk_size=200, overlap=50):
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunk = " ".join(words[start:end])
        chunks.append(chunk)
        start += chunk_size - overlap
    return chunks

Paso 2: Incorporaciones TF-IDF (TF-IDF Embeddings)

Construimos una función de incorporación simple. TF-IDF (Term Frequency-Inverse Document Frequency) no es una incorporación neuronal, pero convierte el texto en vectores de una manera que captura la importancia de las palabras. Las palabras frecuentes en un documento obtienen un TF más alto. Las palabras raras en todo el corpus obtienen un IDF más alto. El producto proporciona un vector donde las palabras importantes y distintivas tienen valores altos.

import math
from collections import Counter

def build_vocabulary(documents):
    vocab = set()
    for doc in documents:
        vocab.update(doc.lower().split())
    return sorted(vocab)

def compute_tf(text, vocab):
    words = text.lower().split()
    count = Counter(words)
    total = len(words)
    return [count.get(word, 0) / total for word in vocab]

def compute_idf(documents, vocab):
    n = len(documents)
    idf = []
    for word in vocab:
        doc_count = sum(1 for doc in documents if word in doc.lower().split())
        idf.append(math.log((n + 1) / (doc_count + 1)) + 1)
    return idf

def tfidf_embed(text, vocab, idf):
    tf = compute_tf(text, vocab)
    return [t * i for t, i in zip(tf, idf)]

Paso 3: Búsqueda de Similitud de Coseno (Cosine Similarity Search)

def cosine_similarity(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(x * x for x in b))
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return dot / (norm_a * norm_b)

def search(query_embedding, stored_embeddings, top_k=5):
    scores = []
    for i, emb in enumerate(stored_embeddings):
        sim = cosine_similarity(query_embedding, emb)
        scores.append((i, sim))
    scores.sort(key=lambda x: x[1], reverse=True)
    return scores[:top_k]

Paso 4: Construcción del Prompt (Prompt Construction)

Aquí es donde ocurre el "aumento" en RAG. Tome los fragmentos recuperados, formatéelos en un prompt y pídale al LLM que responda basándose en el contexto proporcionado.

def build_rag_prompt(query, retrieved_chunks):
    context = "\n\n---\n\n".join(
        f"[Source {i+1}]\n{chunk}"
        for i, chunk in enumerate(retrieved_chunks)
    )
    return f"""Answer the question based ONLY on the following context.
If the context doesn't contain enough information, say "I don't have enough information to answer that."

Context:
{context}

Question: {query}

Answer:"""

Paso 5: El Pipeline de RAG Completo (The Complete RAG Pipeline)

class RAGPipeline:
    def __init__(self):
        self.chunks = []
        self.embeddings = []
        self.vocab = []
        self.idf = []

    def index(self, documents):
        all_chunks = []
        for doc in documents:
            all_chunks.extend(chunk_text(doc))
        self.chunks = all_chunks
        self.vocab = build_vocabulary(all_chunks)
        self.idf = compute_idf(all_chunks, self.vocab)
        self.embeddings = [
            tfidf_embed(chunk, self.vocab, self.idf)
            for chunk in all_chunks
        ]

    def query(self, question, top_k=5):
        query_emb = tfidf_embed(question, self.vocab, self.idf)
        results = search(query_emb, self.embeddings, top_k)
        retrieved = [(self.chunks[i], score) for i, score in results]
        prompt = build_rag_prompt(
            question, [chunk for chunk, _ in retrieved]
        )
        return prompt, retrieved

Paso 6: Generación (simulada) (Generation (simulated))

En producción, aquí es donde se llama a la API del LLM. Para esta lección, simulamos la generación extrayendo la oración más relevante del contexto recuperado.

def simple_generate(prompt, retrieved_chunks):
    query_words = set(prompt.lower().split("question:")[-1].split())
    best_sentence = ""
    best_score = 0
    for chunk in retrieved_chunks:
        for sentence in chunk.split("."):
            sentence = sentence.strip()
            if not sentence:
                continue
            words = set(sentence.lower().split())
            overlap = len(query_words & words)
            if overlap > best_score:
                best_score = overlap
                best_sentence = sentence
    return best_sentence if best_sentence else "I don't have enough information."

Úselo

Con un modelo de incorporación real y un LLM, el código apenas cambia:

from openai import OpenAI

client = OpenAI()

def embed(text):
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

def generate(prompt):
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    return response.choices[0].message.content

O con Anthropic:

import anthropic

client = anthropic.Anthropic()

def generate(prompt):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text

El pipeline es el mismo. Intercambie la función de incorporación. Intercambie la función de generación. La lógica de recuperación, la fragmentación, la construcción del prompt: todo es idéntico independientemente de qué modelos use.

Para el almacenamiento de vectores a escala, reemplace la búsqueda por fuerza bruta con una base de datos vectorial adecuada:

import chromadb

client = chromadb.Client()
collection = client.create_collection("my_docs")

collection.add(
    documents=chunks,
    ids=[f"chunk_{i}" for i in range(len(chunks))]
)

results = collection.query(
    query_texts=["What is the refund policy?"],
    n_results=5
)

Chroma maneja la incorporación internamente (usa all-MiniLM-L6-v2 por defecto) y almacena los vectores en una base de datos local. Mismo patrón, plomería diferente.

Despliéguelo (Ship It)

Esta lección produce:

  • outputs/prompt-rag-architect.md — un prompt para diseñar sistemas RAG para casos de uso específicos
  • outputs/skill-rag-pipeline.md — una habilidad (skill) que enseña a los agentes cómo construir y depurar pipelines de RAG

Ejercicios

  1. Reemplace las incorporaciones TF-IDF con un enfoque simple de bolsa de palabras (bag-of-words) (binario: 1 si la palabra está presente, 0 si no). Compare la calidad de la recuperación en los documentos de muestra. TF-IDF debería superar en rendimiento porque pondera más alto las palabras raras.

  2. Experimente con tamaños de fragmentos: pruebe con 50, 100, 200 y 500 palabras en el mismo conjunto de documentos. Para cada tamaño, ejecute las mismas 5 consultas y cuente cuántas devuelven un fragmento relevante en el top-3. Encuentre el punto ideal donde la calidad de la recuperación alcanza su punto máximo.

  3. Agregue metadatos a cada fragmento (nombre del documento de origen, posición del fragmento). Modifique la plantilla del prompt para incluir la atribución de origen para que el LLM cite sus fuentes.

  4. Implemente una evaluación simple: dados 10 pares de preguntas y respuestas, ejecute cada pregunta a través del pipeline de RAG y mida qué porcentaje de los fragmentos recuperados contiene la respuesta. Esto es la recuperación recall en k (retrieval recall at k).

  5. Construya un pipeline de RAG consciente de la la conversación: mantenga un historial de los últimos 3 intercambios e inclúyalos en el prompt junto con los fragmentos recuperados. Pruebe con preguntas de seguimiento como "¿Qué pasa con enterprise?" después de preguntar sobre precios.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
RAG "IA que lee tus documentos" Recuperar documentos relevantes, pegarlos en el prompt y generar una respuesta fundamentada en esos documentos
Embedding (Incorporación) "Convertir texto a números" Una representación vectorial densa de texto donde significados similares producen vectores similares
Base de datos vectorial "Motor de búsqueda para IA" Un almacenamiento de datos optimizado para almacenar vectores y encontrar los vecinos más cercanos por similitud
Fragmentación (Chunking) "Dividir documentos en partes" Dividir documentos en segmentos más pequeños (típicamente de 256 a 512 tokens) para que cada uno pueda ser incorporado y recuperado de forma independiente
Similitud de coseno "Qué tan similares son dos vectores" El coseno del ángulo entre dos vectores; 1 = dirección idéntica, 0 = ortogonal, -1 = opuesta
Recuperación top-k "Obtener las k mejores coincidencias" Devolver los k fragmentos más similares a la consulta desde el almacenamiento vectorial
Ventana de contexto "Cuánto texto puede ver el LLM" El número máximo de tokens que el LLM puede procesar en una sola solicitud; los fragmentos recuperados deben caber dentro de este límite
Generación aumentada "Responder usando el contexto dado" Generar una respuesta utilizando los documentos recuperados como contexto en lugar de depender únicamente del conocimiento del entrenamiento
TF-IDF "Puntuación de importancia de palabras" Frecuencia del término multiplicada por la frecuencia inversa del documento; pondera las palabras según qué tan distintivas son dentro de un corpus
Indexación "Preparar documentos para búsqueda" El proceso offline de fragmentar, incorporar y almacenar documentos para que puedan ser buscados en el momento de la consulta

Lecturas Adicionales

  • Lewis et al., "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (2020) — el artículo original de RAG de Facebook AI Research que formalizó el patrón de recuperar-y-luego-generar
  • Anthropic's RAG documentation (docs.anthropic.com) — pautas prácticas para tamaños de fragmentos, construcción de prompts y evaluación
  • Pinecone Learning Center, "What is RAG?" — explicaciones visuales claras del pipeline de RAG con consideraciones de producción
  • Sentence-BERT: Reimers & Gurevych (2019) — el artículo detrás de los modelos de embedding all-MiniLM, que muestra cómo entrenar bi-encoders para similitud semántica
  • Karpukhin et al., "Dense Passage Retrieval for Open-Domain Question Answering" (EMNLP 2020) — el artículo de DPR que demostró que la recuperación con bi-encoders densos supera a BM25 en preguntas y respuestas de dominio abierto y estableció el patrón para los recuperadores de RAG modernos.
  • Conceitos de Alto Nivel de LlamaIndex — los conceptos principales a conocer al construir pipelines de RAG: cargadores de datos, analizadores de nodos, índices, recuperadores, sintetizadores de respuestas.
  • Tutorial de RAG de LangChain — el orquestador con un enfoque alternativo; visión del mismo patrón de recuperar-y-luego-generar basada en cadenas de ejecutables (chain-of-runnables).
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).