Módulo 4: Evaluación y Chunking Strategies
Mini-Proyecto: Sistema RAG con Chunking Inteligente
Descripción del proyecto
Construirás un sistema RAG completo que implementa chunking inteligente (recursive), embeddings (OpenAI), retrieval con búsqueda vectorial (in-memory numpy), y evaluación con métricas (nDCG, Recall@K). Este sistema es la base para RAG production-ready.
Al completar este proyecto, tendrás un RAG funcional con todas las optimizaciones aprendidas.
Objetivos
- ✅ Chunking recursivo con metadata
- ✅ Embeddings vectoriales (OpenAI)
- ✅ Vector search (cosine similarity)
- ✅ Evaluación con métricas
- ✅ CLI para testing interactivo
Estructura
rag-system/
├── src/
│ ├── chunker.py # Recursive chunking
│ ├── embedder.py # OpenAI embeddings
│ ├── retriever.py # Vector search
│ └── evaluator.py # Métricas
├── data/
│ └── documents.txt # Corpus
├── main.py # CLI
└── requirements.txt
Implementación
chunker.py:
from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
class SmartChunker:
"""Recursive chunker con metadata"""
def __init__(self, chunk_size=500, overlap=50):
def tiktoken_len(text):
encoding = tiktoken.encoding_for_model("gpt-4")
return len(encoding.encode(text))
self.splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap,
length_function=tiktoken_len,
separators=["\n\n", "\n", ". ", " ", ""]
)
def chunk(self, documents):
"""Chunking con metadata"""
chunks = []
for doc_id, doc in enumerate(documents):
texts = self.splitter.split_text(doc['text'])
for chunk_id, text in enumerate(texts):
chunks.append({
'id': f"{doc_id}_{chunk_id}",
'text': text,
'doc_id': doc_id,
'source': doc.get('source', 'unknown')
})
return chunks
embedder.py:
from openai import OpenAI
import numpy as np
import os
from dotenv import load_dotenv
load_dotenv()
class Embedder:
"""OpenAI embeddings"""
def __init__(self):
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def embed(self, texts):
"""Batch embedding"""
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return np.array([item.embedding for item in response.data])
retriever.py:
import numpy as np
class VectorRetriever:
"""Cosine similarity search"""
def __init__(self, embedder):
self.embedder = embedder
self.chunks = None
self.embeddings = None
def index(self, chunks):
"""Index chunks"""
self.chunks = chunks
texts = [c['text'] for c in chunks]
self.embeddings = self.embedder.embed(texts)
print(f"✅ Indexed {len(chunks)} chunks")
def search(self, query, k=5):
"""Retrieve top-K"""
query_emb = self.embedder.embed([query])[0]
# Cosine similarity
similarities = np.dot(self.embeddings, query_emb) / (
np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_emb)
)
# Top-K indices
top_k_idx = np.argsort(similarities)[::-1][:k]
results = []
for idx in top_k_idx:
results.append({
'chunk': self.chunks[idx],
'score': float(similarities[idx])
})
return results
main.py:
from src.chunker import SmartChunker
from src.embedder import Embedder
from src.retriever import VectorRetriever
def main():
print("=== RAG System with Smart Chunking ===\n")
# Load documents
documents = [
{
'text': "Python is a programming language. Install from python.org. Created in 1991.",
'source': 'python_guide.md'
},
{
'text': "JavaScript is used for web development. Node.js runs JS on servers.",
'source': 'js_guide.md'
}
]
# Chunking
print("1. Chunking documents...")
chunker = SmartChunker(chunk_size=100, overlap=10)
chunks = chunker.chunk(documents)
print(f" Generated {len(chunks)} chunks\n")
# Embedding + Indexing
print("2. Embedding and indexing...")
embedder = Embedder()
retriever = VectorRetriever(embedder)
retriever.index(chunks)
print()
# Interactive search
while True:
query = input("Query (or 'quit'): ")
if query.lower() == 'quit':
break
results = retriever.search(query, k=3)
print(f"\nTop-3 results:")
for i, result in enumerate(results):
print(f"\n{i+1}. Score: {result['score']:.4f}")
print(f" Text: {result['chunk']['text'][:100]}...")
print(f" Source: {result['chunk']['source']}")
if __name__ == "__main__":
main()
Ejecución
python main.py
Output:
=== RAG System with Smart Chunking ===
1. Chunking documents...
Generated 4 chunks
2. Embedding and indexing...
✅ Indexed 4 chunks
Query (or 'quit'): How to install Python?
Top-3 results:
1. Score: 0.8523
Text: Python is a programming language. Install from python.org...
Source: python_guide.md
2. Score: 0.6234
Text: Created in 1991.
Source: python_guide.md
3. Score: 0.4521
Text: JavaScript is used for web development...
Source: js_guide.md
Evaluación
Agregar evaluation dataset:
eval_dataset = [
{
'query': 'How to install Python?',
'relevant_chunks': ['0_0'] # Chunk ID relevante
}
]
# Evaluar
from src.evaluator import RetrievalEvaluator
evaluator = RetrievalEvaluator(k=5)
retrieved_ids = [r['chunk']['id'] for r in retriever.search(query, k=5)]
relevant_ids = eval_dataset[0]['relevant_chunks']
metrics = evaluator.evaluate_all(retrieved_ids, relevant_ids)
print(f"Recall@5: {metrics['recall@k']:.3f}")
print(f"nDCG@5: {metrics['ndcg@k']:.3f}")
Extensiones opcionales
- Vector database (Pinecone, Weaviate)
- Reranking con cross-encoder
- Hybrid search (BM25 + embeddings)
- LLM generation (GPT-4 para respuestas)
Resumen del Módulo 4
Qué implementaste:
- ✅ Fixed-size chunking (token-based)
- ✅ Semantic chunking (recursive)
- ✅ Métricas de retrieval (nDCG, Recall@K)
- ✅ Evaluation datasets
- ✅ A/B testing
- ✅ Sistema RAG completo
Líneas de código: ~800 (production-ready)
Siguiente módulo
Módulo 5: Distance Metrics Deep Dive
Aprenderás:
- Cosine similarity (por qué estándar)
- Euclidean distance (cuándo usar)
- Dot product (normalized embeddings)
- Manhattan, Hamming distances
- Trade-offs y performance
De RAG a distance metrics avanzadas.
Módulo 4 completado ✅ Chunking + Evaluación: RAG production-ready