Módulo 4: ChromaDB Setup y Configuración
Cápsula 11: Pipeline RAG end-to-end con ChromaDB
Descripción de la cápsula
Hasta aquí construiste piezas. ChromaDB con metadata filtering (cápsulas 04-08), embeddings con OpenAI (cápsula 09), chunking con RecursiveCharacterTextSplitter (cápsula 10). Cada pieza la probaste por separado.
Ahora juntas todo en el pipeline RAG completo: tomas un documento, lo procesas con chunking, lo embebes con OpenAI, lo almacenas en ChromaDB con metadata, recuperas los chunks relevantes para una pregunta, y generas la respuesta con GPT pasándole esos chunks como contexto. Es la versión mínima funcional — sin API REST, sin Docker, sin testing — pero es la primera vez en esta guía que ves el sistema completo de punta a punta.
Esta cápsula tiene un propósito específico: que el "modelo mental de RAG" deje de ser abstracto. Cuando termines, vas a haber ejecutado un sistema que recibe una pregunta en español y devuelve una respuesta fundamentada en tus documentos, citando las fuentes. A partir de ahí, todo lo que construyes en el resto de la guía (Landscape, Decision Matrix, Production, y el proyecto del Módulo 8) tiene un sistema concreto al que referirse.
Al finalizar esta cápsula serás capaz de:
- ✅ Explicar el flujo completo de RAG (ingestion → retrieval → generation) con cada paso ejecutable
- ✅ Construir un pipeline mínimo de RAG con ChromaDB + OpenAI en ~150 líneas de código
- ✅ Diseñar un prompt RAG que evite alucinaciones y exija citar fuentes
- ✅ Identificar los cinco puntos de falla más comunes en un pipeline RAG
- ✅ Diferenciar qué responsabilidad pertenece a retrieval vs generation cuando una respuesta es mala
Tiempo estimado: 45-60 minutos
El flujo RAG completo en una sola imagen mental
Antes del código, el modelo mental:
INGESTION (una vez por documento, o cuando se actualiza):
Documento original
↓
Chunking (M4/10) → ["chunk1", "chunk2", "chunk3", ...]
↓
OpenAI Embeddings → [[v1...], [v2...], [v3...], ...]
↓
ChromaDB.add( → Almacenamiento + indexación HNSW
documents=chunks,
embeddings=vectors,
metadatas=[{doc_id, chunk_index, source}, ...]
)
──────────────────────────────────────────────────────────
RETRIEVAL + GENERATION (cada vez que el usuario pregunta):
Pregunta del usuario
↓
OpenAI embeddings de la query
↓
ChromaDB.query(top_k=5) → 5 chunks más similares + metadata
↓
Construir prompt: "Contexto: {chunks}\n\nPregunta: {query}"
↓
GPT-4 con ese prompt → Respuesta + cita de fuentes
↓
Respuesta al usuario
El insight crítico: RAG no es un sistema "inteligente". Es un sistema de dos pasos donde cada paso es relativamente simple:
- Retrieval es solo búsqueda de similaridad: "dame los 5 chunks más parecidos a esta pregunta".
- Generation es solo prompt engineering: "dado este contexto y esta pregunta, redacta una respuesta".
La magia no existe. Si el sistema falla, está fallando o el retrieval (los chunks correctos no se recuperaron) o el generation (los chunks correctos se recuperaron pero el LLM ignoró información, alucinó, o redactó mal). Saber distinguir cuál de los dos falla es la habilidad central para debuggear RAG en producción.
Implementación: pipeline mínimo viable
Vas a construir un sistema RAG sobre un dataset chico (3-4 documentos técnicos) para que cada paso sea rastreable. Los conceptos son los mismos al escalar a miles de documentos.
Paso 1: Setup del proyecto
# Estructura del proyecto
rag_pipeline/
├── .env # OPENAI_API_KEY=sk-...
├── .gitignore # incluye .env y chroma_db/
├── requirements.txt
├── data/
│ ├── chromadb_intro.md
│ ├── pinecone_overview.md
│ └── hnsw_explained.md
├── ingestion.py
├── rag.py
└── main.py
# requirements.txt
chromadb>=0.5.0
openai>=1.40.0
langchain-text-splitters>=0.3.0
python-dotenv>=1.0.0
pip install -r requirements.txt
Paso 2: Ingestion — del documento a ChromaDB
# ingestion.py
import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_text_splitters import RecursiveCharacterTextSplitter
import chromadb
from chromadb.utils import embedding_functions
load_dotenv()
# Configuración (en producción, esto va en config.py)
CHROMA_PATH = "./chroma_db"
COLLECTION_NAME = "rag_demo"
EMBEDDING_MODEL = "text-embedding-3-small"
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
DATA_DIR = Path("./data")
def get_collection():
"""Crea o recupera la collection con OpenAI embeddings."""
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name=EMBEDDING_MODEL
)
client = chromadb.PersistentClient(path=CHROMA_PATH)
return client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=openai_ef,
metadata={"hnsw:space": "cosine"}
)
def ingest_file(file_path: Path, collection):
"""Procesa un archivo: chunking + metadata + insert."""
content = file_path.read_text(encoding="utf-8")
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_text(content)
doc_id = file_path.stem # nombre sin extensión
chunk_ids = [f"{doc_id}_chunk_{i:03d}" for i in range(len(chunks))]
chunk_metadatas = [
{
"doc_id": doc_id,
"chunk_index": i,
"total_chunks": len(chunks),
"source": str(file_path),
"filename": file_path.name,
}
for i in range(len(chunks))
]
collection.add(
documents=chunks,
ids=chunk_ids,
metadatas=chunk_metadatas
)
return len(chunks)
def ingest_directory(directory: Path = DATA_DIR):
"""Procesa todos los .md del directorio."""
collection = get_collection()
total_chunks = 0
for file_path in directory.glob("*.md"):
n_chunks = ingest_file(file_path, collection)
print(f" {file_path.name}: {n_chunks} chunks")
total_chunks += n_chunks
print(f"\nTotal: {total_chunks} chunks en collection '{COLLECTION_NAME}'")
print(f"Collection size: {collection.count()} documentos")
if __name__ == "__main__":
ingest_directory()
Ejecución:
$ python ingestion.py
chromadb_intro.md: 8 chunks
pinecone_overview.md: 12 chunks
hnsw_explained.md: 14 chunks
Total: 34 chunks en collection 'rag_demo'
Collection size: 34 documentos
Paso 3: Retrieval — encontrar chunks relevantes
# rag.py
import os
from dataclasses import dataclass
from openai import OpenAI
import chromadb
from chromadb.utils import embedding_functions
CHROMA_PATH = "./chroma_db"
COLLECTION_NAME = "rag_demo"
EMBEDDING_MODEL = "text-embedding-3-small"
LLM_MODEL = "gpt-4o-mini"
TOP_K = 5
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
@dataclass
class RetrievedChunk:
text: str
source: str
doc_id: str
chunk_index: int
distance: float
def get_collection():
"""Recupera la collection ya poblada."""
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name=EMBEDDING_MODEL
)
client = chromadb.PersistentClient(path=CHROMA_PATH)
return client.get_collection(
name=COLLECTION_NAME,
embedding_function=openai_ef
)
def retrieve(query: str, top_k: int = TOP_K) -> list[RetrievedChunk]:
"""Encuentra los top_k chunks más similares a la query."""
collection = get_collection()
results = collection.query(
query_texts=[query],
n_results=top_k,
include=['documents', 'metadatas', 'distances']
)
chunks = []
for doc, meta, dist in zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
):
chunks.append(RetrievedChunk(
text=doc,
source=meta['source'],
doc_id=meta['doc_id'],
chunk_index=meta['chunk_index'],
distance=dist
))
return chunks
Paso 4: Generation — construir prompt y llamar a GPT
Aquí está la parte que la mayoría de tutoriales hace mal. El prompt RAG no es "aquí hay contexto, contesta". Tiene tres exigencias específicas:
- Anti-alucinación: instrucción explícita de no inventar información que no está en el contexto.
- Cita de fuentes: la respuesta debe identificar de qué chunk salió cada afirmación.
- Fallback explícito: si el contexto no contiene la respuesta, decirlo en lugar de inventar.
# rag.py (continuación)
SYSTEM_PROMPT = """Eres un asistente técnico que responde preguntas basándote ÚNICAMENTE en el contexto proporcionado.
REGLAS ESTRICTAS:
1. Si la respuesta no está en el contexto, responde: "No tengo información suficiente en los documentos para responder esto."
2. NO inventes información. NO uses tu conocimiento previo.
3. Cita las fuentes usando el formato [Fuente: doc_id, chunk N] al final de cada afirmación importante.
4. Si hay información contradictoria entre chunks, menciónalo explícitamente.
5. Sé conciso. La respuesta debe ser de 2-4 párrafos máximo."""
def build_user_prompt(query: str, chunks: list[RetrievedChunk]) -> str:
"""Construye el prompt con la query y los chunks recuperados."""
context_parts = []
for i, chunk in enumerate(chunks, 1):
context_parts.append(
f"[Chunk {i} | doc_id: {chunk.doc_id} | chunk_index: {chunk.chunk_index}]\n"
f"{chunk.text}\n"
)
context = "\n---\n".join(context_parts)
return f"""CONTEXTO:
{context}
PREGUNTA DEL USUARIO:
{query}
Responde basándote SOLO en el contexto. Cita las fuentes."""
@dataclass
class RagResponse:
answer: str
sources: list[dict]
chunks_used: list[RetrievedChunk]
fallback: bool
def generate(query: str, chunks: list[RetrievedChunk]) -> RagResponse:
"""Llama al LLM con el prompt construido y devuelve respuesta estructurada."""
user_prompt = build_user_prompt(query, chunks)
response = openai_client.chat.completions.create(
model=LLM_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
temperature=0.0, # Determinístico — RAG no debe ser "creativo"
max_tokens=600
)
answer = response.choices[0].message.content
# Detección de fallback (el LLM dijo que no sabe)
fallback = "no tengo información suficiente" in answer.lower()
sources = [
{
"doc_id": chunk.doc_id,
"source": chunk.source,
"chunk_index": chunk.chunk_index,
"distance": round(chunk.distance, 3)
}
for chunk in chunks
]
return RagResponse(
answer=answer,
sources=sources,
chunks_used=chunks,
fallback=fallback
)
def ask(query: str, top_k: int = TOP_K) -> RagResponse:
"""Pipeline completo: query → retrieve → generate → respuesta."""
chunks = retrieve(query, top_k=top_k)
return generate(query, chunks)
Paso 5: Probar el pipeline completo
# main.py
from rag import ask
queries = [
"What are the recommended HNSW parameters for production?",
"¿Cuál es la diferencia entre Pinecone y ChromaDB?",
"How do I configure HTTPS for ChromaDB?", # No está en docs → debería fallback
]
for query in queries:
print(f"\n{'='*70}")
print(f"Q: {query}")
print('='*70)
response = ask(query)
print(f"\nA: {response.answer}\n")
if response.fallback:
print("⚠️ FALLBACK: El sistema no encontró información suficiente.")
else:
print(f"Fuentes consultadas:")
for src in response.sources[:3]:
print(f" - {src['doc_id']} (chunk {src['chunk_index']}, dist {src['distance']})")
Output esperado:
======================================================================
Q: What are the recommended HNSW parameters for production?
======================================================================
A: For production deployments with ChromaDB, the recommended HNSW parameters
are M=32 and construction_ef=200 [Fuente: hnsw_explained, chunk 4]. The default
values (M=16, construction_ef=100) work for development and prototypes, but
production benefits from the higher accuracy that comes from increased graph
connectivity [Fuente: hnsw_explained, chunk 5].
Note that increasing M proportionally increases memory usage, so this is a
trade-off between recall and resources.
Fuentes consultadas:
- hnsw_explained (chunk 4, dist 0.198)
- hnsw_explained (chunk 5, dist 0.241)
- chromadb_intro (chunk 3, dist 0.456)
======================================================================
Q: ¿Cuál es la diferencia entre Pinecone y ChromaDB?
======================================================================
A: Pinecone es un servicio managed (Pinecone Inc. opera la infraestructura)
mientras que ChromaDB es open-source y self-hosted [Fuente: pinecone_overview, chunk 1].
Pinecone se usa típicamente para producción de gran escala con SLA garantizado,
mientras que ChromaDB es ideal para desarrollo local y proyectos de menor escala
[Fuente: chromadb_intro, chunk 2].
Fuentes consultadas:
- pinecone_overview (chunk 1, dist 0.234)
- chromadb_intro (chunk 2, dist 0.298)
- pinecone_overview (chunk 5, dist 0.412)
======================================================================
Q: How do I configure HTTPS for ChromaDB?
======================================================================
A: No tengo información suficiente en los documentos para responder esto.
⚠️ FALLBACK: El sistema no encontró información suficiente.
Lo que demuestra esta ejecución:
-
Cross-language retrieval funciona: la query 2 está en español, los documentos en inglés. OpenAI embeddings hacen el match correctamente (esto sería imposible con el default de ChromaDB — ver M4/09).
-
El sistema cita fuentes correctas: "[Fuente: hnsw_explained, chunk 4]" permite verificar la respuesta abriendo ese archivo y leyendo ese chunk.
-
Fallback funciona: la query 3 sobre HTTPS no está en los docs, el sistema dice "no sé" en vez de inventar. Esto es lo que separa un RAG profesional de uno que alucina.
Cómo distinguir si falla retrieval o generation
Cuando el sistema da una mala respuesta, hay solo dos sospechosos. Saber cuál es te dice qué arreglar.
Caso A: falla retrieval
Síntoma: la respuesta es genérica o incorrecta. Los chunks recuperados (response.sources) no contienen la información que la query necesita.
Cómo detectarlo: mira los chunks crudos:
response = ask("What's the difference between cosine and L2 distance?")
for chunk in response.chunks_used:
print(f"--- dist {chunk.distance:.3f} ---")
print(chunk.text[:200])
Si los chunks no hablan de cosine vs L2, retrieval falló. Posibles causas:
- chunk_size demasiado grande (información diluida)
- query muy genérica (no matchea conceptos específicos)
- el documento no contiene esa información (no es problema de retrieval, es de cobertura)
- embeddings inadecuados (default cuando deberías usar OpenAI)
Soluciones: ajustar chunk_size/overlap, mejorar query (query expansion), agregar documentos faltantes, cambiar modelo de embeddings.
Caso B: falla generation
Síntoma: los chunks recuperados SÍ contienen la respuesta correcta, pero la respuesta del LLM la ignora, alucina, o redacta mal.
Cómo detectarlo: lees los chunks y la respuesta debería estar ahí, pero no está.
Posibles causas:
- prompt mal diseñado (sin instrucción de no alucinar)
- temperature alta (LLM "creativo" cuando debería ser literal)
- modelo demasiado pequeño para el dominio (gpt-3.5 con texto técnico complejo)
- contexto demasiado largo (LLM pierde información en el medio — "lost in the middle")
Soluciones: prompt engineering más estricto, temperature=0, modelo más capaz, recortar contexto.
El test de los 30 segundos
Cuando el sistema falla, antes de reescribir prompt o ajustar chunking, hazte esta pregunta:
"Si yo, manualmente, leyera los 5 chunks recuperados y tuviera que responder la pregunta, ¿podría?"
- Sí, claramente → falla generation. Arregla el prompt.
- No, falta información → falla retrieval. Arregla la búsqueda.
- Tal vez, está sutilmente → ambos. Empieza por retrieval (es más fácil de arreglar y suele tener más impacto).
Trampas y errores comunes
Trampa 1: temperature mayor a 0 en RAG
El error:
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[...],
temperature=0.7 # ❌ "Para que las respuestas sean naturales"
)
Síntoma: las respuestas varían entre ejecuciones, el LLM agrega información que no está en el contexto, las citas de fuentes a veces son inventadas.
Por qué pasa: temperature controla la aleatoriedad del muestreo. En generación creativa (escribir un cuento), temperature alta es bueno. En RAG, donde queremos que el LLM se ciña al contexto, cualquier aleatoriedad introduce alucinaciones.
Cómo corregir: temperature=0.0 siempre en RAG. La "naturalidad" la das con prompt design, no con creatividad estocástica.
Trampa 2: contexto demasiado largo
El error:
chunks = retrieve(query, top_k=20) # ❌ "más contexto = mejor respuesta"
Síntoma: las respuestas se vuelven más vagas o ignoran información que está claramente en chunks 12-15. Latencia y costo aumentan.
Por qué pasa: los LLMs sufren de "lost in the middle" — la información en el medio del contexto recibe menos atención que la del principio o el final. Pasar 20 chunks de 500 chars cada uno = 10,000 chars de contexto, gran parte del cual el LLM va a tratar como ruido.
Cómo corregir:
top_k=3-5es el rango óptimo para la mayoría de casos.- Si necesitas más cobertura, usa re-ranking (cubierto en guía #8 Advanced RAG).
- Mide la calidad:
top_k=5con re-ranking suele superar atop_k=20sin re-ranking.
Trampa 3: prompt sin instrucción anti-alucinación
El error: prompt minimalista del estilo "Contexto: {chunks}. Pregunta: {query}. Responde."
Síntoma: el LLM responde con conocimiento general cuando el contexto no tiene la respuesta. No cita fuentes. Mezcla información del contexto con su training data.
Por qué pasa: sin instrucción explícita de ceñirse al contexto, GPT por default usa todo lo que sabe.
Cómo corregir: instrucción explícita en el system prompt — ver SYSTEM_PROMPT arriba con las 5 reglas.
Trampa 4: ignorar el feedback de fallback
El error: el sistema responde "no tengo información suficiente" y se trata como error en logs. El equipo agrega try/except para "recuperarse" del fallback.
Síntoma: se pierde la señal más valiosa del sistema — saber qué preguntas no puede responder.
Por qué importa: los fallbacks frecuentes en producción te dicen exactamente qué documentos te faltan. Es información de oro para mejorar el dataset.
Cómo corregir: loggea fallbacks como métrica positiva ("fallback rate"). Cuando supere un threshold (ej: >15%), revisa qué tipo de queries están fallando y agrega los documentos correspondientes.
Trampa 5: no medir antes de optimizar
El error: intuyes que el sistema podría ser mejor con re-ranking, hybrid search, query expansion, semantic chunking, modelo grande, etc. Empiezas a agregar todo simultáneamente.
Síntoma: no sabes qué mejoró qué. La latencia subió, el costo subió, la calidad mejoró marginalmente. Retrocedes algunas cosas pero no estás seguro cuáles.
Cómo corregir: construye un eval set fijo (20-50 queries con respuestas anotadas) ANTES de optimizar. Cada cambio se mide contra el eval set. Si no mejora, se descarta.
# Estructura de eval set
EVAL_SET = [
{
"query": "What are the recommended HNSW parameters for production?",
"expected_keywords": ["M=32", "construction_ef=200"],
"expected_doc": "hnsw_explained",
},
# ... 30+ entries
]
def evaluate(eval_set):
"""Calcula recall, precisión de fuentes, fallback rate."""
correct_retrieval = 0
fallback_count = 0
for item in eval_set:
response = ask(item["query"])
if response.fallback:
fallback_count += 1
elif item["expected_doc"] in [s["doc_id"] for s in response.sources]:
correct_retrieval += 1
return {
"retrieval_accuracy": correct_retrieval / len(eval_set),
"fallback_rate": fallback_count / len(eval_set),
}
Trampa 6: API keys gestionadas mal
El error: múltiples archivos cargan OPENAI_API_KEY de distintas maneras. Algunos hardcoded para debugging "temporal". Otros usando defaults.
Síntoma: errores intermitentes "Invalid API key", facturas inesperadas (alguien usó una key de producción para tests), keys leakeadas en commits.
Cómo corregir:
- Una sola fuente de verdad: variable de entorno cargada con
dotenven un punto del programa. .envsiempre en.gitignore.- Pre-commit hook (
gitleaksodetect-secrets) que detecte el patrónsk-. - Keys distintas para dev / staging / prod, rotación periódica.
Ejercicio aplicado
Escenario: el pipeline RAG que construiste funciona, pero un beta tester reporta lo siguiente:
"Le pregunté '¿cuál es el mejor M para HNSW si tengo 100K vectores?' y me respondió que el M óptimo es 16, citando como fuente
hnsw_explained chunk 4. Pero abrí ese chunk y dice claramente que para producción se recomienda M=32. El sistema está alucinando."
Tu trabajo: investigar si falla retrieval o generation, y proponer un fix. Tienes acceso al código y al ChromaDB pobladito.
Solución
Diagnóstico paso a paso:
Paso 1: revisar qué chunks devolvió retrieval.
response = ask("¿cuál es el mejor M para HNSW si tengo 100K vectores?")
for i, chunk in enumerate(response.chunks_used):
print(f"\n--- Chunk {i+1} | dist {chunk.distance:.3f} ---")
print(f"doc: {chunk.doc_id} chunk {chunk.chunk_index}")
print(chunk.text)
Hipótesis a verificar: ¿el chunk 4 de hnsw_explained realmente contiene la información sobre M=32?
Paso 2: leer el chunk crudo.
Si el chunk 4 dice exactamente: "Para producción con alta accuracy se recomienda M=32 y construction_ef=200..." → entonces retrieval funcionó correctamente. Falla generation.
Si el chunk 4 dice algo distinto (por ejemplo, "M (default 16) controla cuántas conexiones..." sin mencionar M=32) → entonces retrieval recuperó un chunk parecido pero no el que tenía la respuesta. Falla retrieval.
Es altamente probable que sea generation (porque la cita es específica al chunk 4 y el bug del usuario sí lo abrió y verificó). Procedamos asumiendo eso.
Paso 3: identificar la causa del fallo de generation.
Posibles causas:
- a)
temperaturemayor a 0 → revisarrag.py. Si está en 0, no es esto. - b) Prompt no es lo suficientemente estricto.
- c) El modelo eligió tomar el "M default es 16" del chunk como la respuesta a "cuál es el mejor M", ignorando la frase posterior sobre M=32 para producción. Esto es interpretación errónea, no alucinación pura.
La causa más probable es (c). El chunk contiene dos afirmaciones:
- "M (default 16) controla cuántas conexiones tiene cada nodo en el grafo HNSW"
- "Para producción con alta accuracy se recomienda M=32 y construction_ef=200"
El LLM, sin instrucción de priorizar el contexto de la query (producción / 100K vectores), tomó el primer dato (M=16 default) y lo presentó como respuesta.
Paso 4: el fix.
Fix corto: mejorar el system prompt para que pida razonamiento explícito antes de responder.
SYSTEM_PROMPT = """Eres un asistente técnico que responde preguntas basándote ÚNICAMENTE en el contexto proporcionado.
PROCESO DE RESPUESTA (sigue este orden):
1. Identifica todos los datos relevantes a la pregunta en el contexto.
2. Si hay múltiples valores posibles (ej: default vs producción), identifica cuál se aplica al caso preguntado.
3. Si la pregunta tiene contexto específico (ej: "para producción", "con 100K vectores"), prioriza la respuesta para ESE caso.
4. Solo entonces redacta la respuesta final, citando fuentes.
REGLAS ESTRICTAS:
1. Si la respuesta no está en el contexto, responde: "No tengo información suficiente en los documentos para responder esto."
2. NO inventes información. NO uses tu conocimiento previo.
3. Cita las fuentes usando [Fuente: doc_id, chunk N].
4. Si hay valores distintos según el escenario (default vs producción), menciona ambos y cuál aplica."""
Fix largo (más robusto): chain-of-thought explícito.
Modificar build_user_prompt para incluir un paso de razonamiento antes de la respuesta final:
def build_user_prompt(query: str, chunks: list[RetrievedChunk]) -> str:
context = "\n---\n".join([
f"[Chunk {i} | doc_id: {c.doc_id} | chunk_index: {c.chunk_index}]\n{c.text}"
for i, c in enumerate(chunks, 1)
])
return f"""CONTEXTO:
{context}
PREGUNTA DEL USUARIO:
{query}
Antes de responder, razona en voz alta:
1. ¿Qué datos del contexto son relevantes a esta pregunta?
2. ¿Hay datos distintos para distintos escenarios?
3. ¿Cuál escenario describe la pregunta?
Después de razonar, da la respuesta final con citas."""
Verificación post-fix: ejecutar la query del beta tester con el prompt modificado. Esperado: la respuesta menciona explícitamente que el default es 16 pero para producción (que aplica al caso de 100K vectores) se recomienda M=32, citando el chunk 4 correctamente.
Lección de proceso: siempre hacer el "test de los 30 segundos" — leer los chunks y preguntar si TÚ podrías responder. Si la respuesta está pero el LLM la perdió, no es problema de retrieval. Es prompt. Y prompt suele ser más rápido de arreglar que retrieval.
Resumen y siguiente paso
Lo que aprendiste:
- RAG es un sistema de dos pasos: retrieval (busca chunks similares) + generation (LLM redacta usando esos chunks como contexto). No hay magia.
- El pipeline mínimo viable son ~150 líneas de código: chunking → embeddings → ChromaDB → retrieve top-k → prompt con instrucciones estrictas → respuesta con citas.
- El prompt RAG profesional tiene tres exigencias: anti-alucinación, citación de fuentes, fallback explícito cuando no hay información.
temperature=0es no-negociable en RAG. La aleatoriedad introduce alucinaciones.- Cuando el sistema falla, distingue retrieval vs generation con el "test de los 30 segundos": ¿tú podrías responder leyendo los chunks recuperados?
Checkpoint: antes de cerrar el módulo, deberías poder:
- Dibujar el pipeline RAG completo desde memoria, indicando qué pasa en ingestion vs runtime.
- Explicar las tres exigencias de un prompt RAG profesional y por qué cada una existe.
- Diagnosticar una respuesta mala distinguiendo si falla retrieval o generation.
Cierre del Módulo 4.
Acabas de cerrar el ciclo completo de Vector Databases con ChromaDB. Sabes:
- Por qué necesitas vector databases (M1)
- Cómo funcionan internamente (M2)
- Qué features importan para RAG (M3)
- Cómo implementar ChromaDB con CRUD, metadata, batch ingestion, optimización (M4/01-08)
- Cómo generar embeddings de calidad con OpenAI (M4/09)
- Cómo dividir documentos largos sin perder resolución (M4/10)
- Cómo construir el pipeline RAG end-to-end mínimo (M4/11)
Siguiente módulo: 5 — Landscape de Vector Databases.
Implementaste todo con ChromaDB. La pregunta inevitable es: ¿es la elección correcta para tu próximo proyecto, o solo la primera que aprendiste? El Módulo 5 abre el panorama completo: Pinecone, Weaviate, Qdrant, Milvus y ChromaDB lado a lado. No vas a instalar cinco vector databases — vas a construir el criterio para elegir entre ellas con datos, no con preferencia.
Recursos
- OpenAI Chat Completions API — Referencia oficial de generación
- Anthropic — How to make a LLM RAG more accurate — Técnicas avanzadas de prompt para RAG
- Lost in the Middle: How Language Models Use Long Contexts (paper) — Por qué top_k=20 no siempre es mejor
- LangChain RAG documentation — Implementación con framework alternativo
- Ragas — RAG evaluation framework — Para construir eval sets sistemáticos (cubierto en guía #12 Evaluation Frameworks)
- Prompt Engineering Guide — RAG — Patrones de prompts efectivos para RAG
Tiempo estimado: 45-60 minutos Siguiente módulo: ../../module-05-vector-db-landscape/es/01-introduccion-modulo.md