Retrieval-Augmented Generation (RAG)
Conectar un LLM a tus propios datos sin reentrenarlo
Un LLM tiene conocimiento congelado en el momento de su entrenamiento. RAG resuelve esto sin tocar los pesos del modelo: en lugar de reentrenarlo, le inyectas contexto relevante en cada llamada. El flujo es simple: cuando llega una pregunta, buscas los fragmentos de tus documentos más relacionados con esa pregunta y los añades al prompt. El modelo responde usando esos fragmentos como fuente de verdad.
El chunking es el primer paso crítico: dividir tus documentos en fragmentos del tamaño adecuado. Demasiado pequeños y pierdes contexto (un párrafo suelto puede no tener sentido); demasiado grandes y el chunk ocupa casi todo el contexto del modelo, dejando poco espacio para la respuesta. La heurística habitual es 512–1024 tokens con un solapamiento (overlap) de 10–20% para no cortar ideas en la frontera entre chunks.
Los embeddings convierten cada chunk en un vector numérico que captura su significado semántico. Dos chunks con significado similar tendrán vectores cercanos en el espacio vectorial, independientemente de que compartan palabras exactas. Esta es la clave que permite buscar por significado en lugar de por coincidencia de texto. Modelos como `text-embedding-3-small` (OpenAI) o `embed-english-v3.0` (Cohere) producen vectores de cientos a miles de dimensiones.
Una base de datos vectorial (vector store) almacena esos embeddings y permite búsqueda por similaridad coseno en milisegundos, incluso sobre millones de vectores. Opciones populares: Pinecone y Weaviate para producción cloud; pgvector si ya usas PostgreSQL; Chroma y FAISS para prototipos locales. La búsqueda retorna los K chunks más similares a la query — este paso se llama retrieval.
El contexto recuperado se inyecta en el prompt del LLM junto con la pregunta original. La calidad del retrieval determina la calidad de la respuesta: si el retrieval falla y trae chunks irrelevantes, el modelo alucina o responde con información errónea. Por eso el pipeline completo de RAG se evalúa en dos dimensiones independientes: calidad del retrieval y calidad de la generación. Un pipeline de OCR + LLM con structured output aplicado a documentos complejos que demuestra este principio está documentado en https://docs.benjacode.com.
Los errores más comunes en RAG son: (1) chunks demasiado grandes que saturan el contexto, (2) olvidar el paso de retrieval y pasar la pregunta directamente al LLM, (3) no filtrar chunks irrelevantes con un umbral de similaridad mínimo, y (4) no citar las fuentes en la respuesta, lo que hace imposible auditar de dónde vienen las afirmaciones del modelo.
# RAG pipeline (end to end)
# === INDEXING (offline, run once) ===
docs = load_documents('contracts/*.pdf')
chunks = chunk_text(
docs,
chunk_size=512, # tokens per chunk
chunk_overlap=64, # overlap to avoid cutting ideas
)
embeddings = embed_model.encode(chunks) # shape: (N, 1536)
vector_store.upsert(ids, embeddings, metadata={'source': ...})
# === RETRIEVAL + GENERATION (online, per query) ===
query = 'What is the termination clause?'
query_vec = embed_model.encode(query) # shape: (1536,)
results = vector_store.query(
vector=query_vec,
top_k=5,
min_score=0.75, # filter irrelevant chunks
)
context = '\n\n'.join(r.text for r in results)
prompt = f'''
You are a contract analyst. Answer using ONLY the context below.
If the answer is not in the context, say 'Not found'.
Context:
{context}
Question: {query}
'''
response = llm.complete(prompt, temperature=0.0, max_tokens=512)Debugging lab
Detecta y corrige el error en el código.
- 4.2.5.1
chunks = chunk_text(docs, chunk_size=8000, chunk_overlap=0) # chunks almost fill the entire context window
- 4.2.5.2
# User asks about a contract clause query = 'What is the penalty for late payment?' response = llm.complete(query, temperature=0.0) # no retrieval step
- 4.2.5.3
index_embeddings = openai_embed_model.encode(chunks) # ... later, at query time: query_vec = cohere_embed_model.encode(query) # different model!
- 4.2.5.4
results = vector_store.query(vector=query_vec, top_k=5) # no score filter context = '\n\n'.join(r.text for r in results) # includes irrelevant chunks
- 4.2.5.5
prompt = f'Answer this question: {query}\nContext: {context}'