Módulo 2: Chunking Strategies
Recursive Chunking con LangChain
Descripción de la cápsula
Recursive chunking es el balance óptimo entre simplicidad y calidad: respeta estructura del documento (párrafos, oraciones) sin el costo de semantic chunking. LangChain implementa RecursiveCharacterTextSplitter que intenta dividir por separators jerárquicos (paragraphs → sentences → words → chars) hasta encontrar chunks del tamaño deseado.
Esta strategy logra +10-15% precision vs fixed-size con cero costo adicional (no API calls, instantáneo). Es el default recomendado para production en 90% de casos: simple de configurar, respeta estructura, y usa chunk overlap para preservar contexto entre chunks.
Esta cápsula te enseña cómo funciona recursive splitting, implementación con LangChain, tuning de separators custom para diferentes document types, y benchmarking vs fixed-size para cuantificar mejora.
🔄 Cómo Funciona Recursive Chunking
Concepto: Separators jerárquicos
Recursive chunking intenta dividir documento usando separators en orden de prioridad:
separators = [
"\n\n", # 1. Intenta dividir por párrafos primero (más coherente)
"\n", # 2. Si párrafo es muy grande, divide por líneas
". ", # 3. Si línea es muy grande, divide por oraciones
" ", # 4. Si oración es muy grande, divide por palabras
"" # 5. Si todo falla, divide por caracteres (last resort)
]
Proceso:
- Intenta dividir documento por
\n\n(párrafos) - Si chunk resultante es < chunk_size → ✅ Keep it
- Si chunk resultante es > chunk_size → Intenta siguiente separator (
\n) - Repite recursivamente hasta encontrar chunks de tamaño adecuado
Ejemplo visual:
Documento original:
┌────────────────────────────────────────┐
│ Párrafo 1: FastAPI es un framework web│
│ moderno y rápido. │ ← 50 chars
│ │
│ Párrafo 2: Características: │
│ - Alto rendimiento │
│ - Validación automática │ ← 120 chars
│ - Documentación automática │
│ │
│ Párrafo 3: Historia de FastAPI. │ ← 30 chars
└────────────────────────────────────────┘
↓ Recursive chunking (chunk_size=100, separators=["\n\n", "\n", ". "])
Paso 1: Dividir por "\n\n" (párrafos)
├─ Chunk A: "Párrafo 1..." (50 chars) ✅ < 100 chars → Keep
├─ Chunk B: "Párrafo 2..." (120 chars) ❌ > 100 chars → Divide más
│ ↓ Dividir por "\n" (líneas)
│ ├─ "Características:" (16 chars)
│ ├─ "- Alto rendimiento" (20 chars)
│ ├─ "- Validación automática" (25 chars)
│ ├─ "- Documentación automática" (28 chars)
│ ↓ Agrupar hasta llegar a ~100 chars
│ ├─ Chunk B1: "Características:\n- Alto rendimiento\n- Validación automática" (61 chars) ✅
│ └─ Chunk B2: "- Documentación automática" (28 chars) ✅
└─ Chunk C: "Párrafo 3..." (30 chars) ✅ < 100 chars → Keep
Resultado:
├─ Chunk 1: Párrafo 1 completo
├─ Chunk 2: Características (parte 1)
├─ Chunk 3: Características (parte 2)
└─ Chunk 4: Párrafo 3 completo
Beneficio: Cada chunk respeta estructura lógica del documento
💻 Implementación con LangChain
Paso 1: Instalación
pip install langchain-text-splitters==0.0.1
Paso 2: Uso básico
from langchain.text_splitters import RecursiveCharacterTextSplitter
# Documento de ejemplo
document = """
FastAPI es un framework web moderno y rápido (alto rendimiento) para construir APIs con Python 3.8+.
Características principales:
- Rápido: Muy alto rendimiento, comparable a NodeJS y Go
- Fácil: Diseñado para ser fácil de usar y aprender
- Robusto: Código listo para producción con validación automática
- Basado en estándares: OpenAPI y JSON Schema
FastAPI fue creado por Sebastián Ramírez en 2018. Ha sido adoptado por empresas como Microsoft, Netflix, y Uber.
"""
# Configurar splitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # Tamaño máximo del chunk
chunk_overlap=20, # Overlap entre chunks (preserva contexto)
separators=["\n\n", "\n", ". ", " ", ""], # Separators en orden de prioridad
length_function=len # Función para medir tamaño (len() o tiktoken)
)
# Dividir documento
chunks = splitter.split_text(document)
# Mostrar resultados
for i, chunk in enumerate(chunks, 1):
print(f"Chunk {i} ({len(chunk)} chars):")
print(f"{chunk}")
print("=" * 60)
Output:
Chunk 1 (113 chars):
FastAPI es un framework web moderno y rápido (alto rendimiento) para construir APIs con Python 3.8+.
============================================================
Chunk 2 (178 chars):
Características principales:
- Rápido: Muy alto rendimiento, comparable a NodeJS y Go
- Fácil: Diseñado para ser fácil de usar y aprender
============================================================
Chunk 3 (145 chars):
- Robusto: Código listo para producción con validación automática
- Basado en estándares: OpenAPI y JSON Schema
============================================================
Chunk 4 (138 chars):
FastAPI fue creado por Sebastián Ramírez en 2018. Ha sido adoptado por empresas como Microsoft, Netflix, y Uber.
============================================================
Análisis:
- ✅ Chunk 1: Párrafo completo (intro coherente)
- ✅ Chunk 2: Lista de características (parte 1, coherente)
- ✅ Chunk 3: Lista de características (parte 2, completa la lista)
- ✅ Chunk 4: Párrafo completo (historia coherente)
Comparación con fixed-size:
# Fixed-size (chunk_size=200)
fixed_chunks = [document[i:i+200] for i in range(0, len(document), 200)]
# Chunk 1 (fixed): "FastAPI es un framework... (alto rendimiento) para construir APIs con Python 3.8+.\n\nCaracterísticas principales:\n- Rápido: Muy alto rendimiento, comparable a NodeJ" ← Cortado mid-sentence
# Chunk 1 (recursive): "FastAPI es un framework web moderno y rápido (alto rendimiento) para construir APIs con Python 3.8+." ← Párrafo completo
Mejora: Recursive respeta estructura → Chunks coherentes → +10-15% precision
🎛️ Configuración Avanzada
Parámetro 1: chunk_size
# Chunk size pequeño (100 chars)
splitter_small = RecursiveCharacterTextSplitter(chunk_size=100)
chunks_small = splitter_small.split_text(document)
print(f"Chunks pequeños: {len(chunks_small)}") # 8 chunks
# Chunk size grande (500 chars)
splitter_large = RecursiveCharacterTextSplitter(chunk_size=500)
chunks_large = splitter_large.split_text(document)
print(f"Chunks grandes: {len(chunks_large)}") # 2 chunks
Trade-off:
| Chunk Size | Pros | Cons | Cuándo usar |
|---|---|---|---|
| Pequeño (100-200) | ✅ Precision alta (match específico) | ❌ Puede perder contexto | Queries específicas |
| Medio (300-500) | ✅ Balance precision/contexto | ⚠️ Neutral | Default recomendado |
| Grande (700-1000) | ✅ Mucho contexto | ❌ Precision baja (mucho ruido) | Narrativo largo |
Recomendación: 400-600 chars (balance óptimo)
Parámetro 2: chunk_overlap
# Sin overlap
splitter_no_overlap = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=0 # No overlap
)
# Con overlap
splitter_with_overlap = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=50 # 25% overlap (50/200)
)
¿Por qué overlap importa?
document = """
FastAPI es un framework web. Usa Pydantic para validación.
Pydantic es una librería de Python. Valida tipos automáticamente.
"""
# Sin overlap (chunk_size=60)
chunks_no_overlap = splitter_no_overlap.split_text(document)
# Chunk 1: "FastAPI es un framework web. Usa Pydantic para validación."
# Chunk 2: "Pydantic es una librería de Python. Valida tipos..."
# Query: "How does FastAPI use Pydantic for validation?"
# Problema: Concepto "FastAPI + Pydantic + validation" está entre Chunk 1 y 2
# Chunk 1: Tiene "FastAPI" + "Pydantic" + "validación"
# Chunk 2: Tiene "Pydantic" + "validación" pero no "FastAPI"
# Resultado: Ningún chunk tiene contexto completo → Partial match
# Con overlap (chunk_size=60, overlap=20)
chunks_with_overlap = splitter_with_overlap.split_text(document)
# Chunk 1: "FastAPI es un framework web. Usa Pydantic para validación."
# Chunk 2: "Usa Pydantic para validación. Pydantic es una librería..." ← Overlap preserva contexto
# Query: "How does FastAPI use Pydantic for validation?"
# Chunk 2: Tiene "Pydantic para validación" (de overlap) + explicación de Pydantic
# Resultado: Chunk tiene contexto completo → Full match
Trade-off:
| Overlap | Pros | Cons |
|---|---|---|
| 0% (no overlap) | ✅ Menos storage | ❌ Pierde contexto entre chunks |
| 10-20% overlap | ✅ Balance contexto/storage | ⚠️ +10-15% storage |
| 30-50% overlap | ✅ Máximo contexto | ❌ +30-50% storage, duplicación |
Recomendación: 10-20% overlap (50-100 chars para chunk_size=500)
Parámetro 3: separators (Custom)
# Default separators (documentos genéricos)
default_separators = ["\n\n", "\n", ". ", " ", ""]
# Custom separators para código Python
code_separators = [
"\nclass ", # Dividir por clases primero
"\ndef ", # Luego por funciones
"\n ", # Luego por indentación
"\n", # Luego por líneas
" ", # Palabras
"" # Chars (last resort)
]
# Custom separators para Markdown
markdown_separators = [
"\n## ", # Dividir por headers H2 primero
"\n### ", # Luego por headers H3
"\n\n", # Luego por párrafos
"\n", # Líneas
". ", # Oraciones
" ", # Palabras
"" # Chars
]
# Ejemplo con código Python
python_code = """
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
"""
splitter_code = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=10,
separators=code_separators
)
chunks = splitter_code.split_text(python_code)
# Output:
# Chunk 1: class User completa
# Chunk 2: class Product completa
# Beneficio: Respeta estructura de código (no corta mid-class)
📊 Benchmarking: Fixed-Size vs Recursive
Experimento:
# benchmark_chunking.py
from langchain.text_splitters import RecursiveCharacterTextSplitter
import time
import statistics
def benchmark_chunking_strategies(document: str, num_iterations: int = 100):
"""Comparar fixed-size vs recursive chunking"""
# Strategy 1: Fixed-size
def fixed_size_chunking(text, size=500):
return [text[i:i+size] for i in range(0, len(text), size)]
# Strategy 2: Recursive
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""]
)
# Medir latency
fixed_times = []
for _ in range(num_iterations):
start = time.time()
fixed_chunks = fixed_size_chunking(document, size=500)
fixed_times.append((time.time() - start) * 1000) # ms
recursive_times = []
for _ in range(num_iterations):
start = time.time()
recursive_chunks = recursive_splitter.split_text(document)
recursive_times.append((time.time() - start) * 1000) # ms
# Medir coherence (proxy: avg chunk ends with punctuation)
fixed_final = fixed_size_chunking(document)
recursive_final = recursive_splitter.split_text(document)
fixed_coherence = sum(1 for c in fixed_final if c.rstrip()[-1] in '.!?') / len(fixed_final)
recursive_coherence = sum(1 for c in recursive_final if c.rstrip()[-1] in '.!?') / len(recursive_final)
return {
"fixed": {
"num_chunks": len(fixed_final),
"avg_latency_ms": statistics.mean(fixed_times),
"coherence": fixed_coherence
},
"recursive": {
"num_chunks": len(recursive_final),
"avg_latency_ms": statistics.mean(recursive_times),
"coherence": recursive_coherence
}
}
# Ejecutar benchmark
with open("data/fastapi_docs.txt", "r") as f:
document = f.read()
results = benchmark_chunking_strategies(document)
print("Benchmark Results:")
print(f"\nFixed-Size:")
print(f" - Chunks: {results['fixed']['num_chunks']}")
print(f" - Latency: {results['fixed']['avg_latency_ms']:.2f}ms")
print(f" - Coherence: {results['fixed']['coherence']:.2%}")
print(f"\nRecursive:")
print(f" - Chunks: {results['recursive']['num_chunks']}")
print(f" - Latency: {results['recursive']['avg_latency_ms']:.2f}ms")
print(f" - Coherence: {results['recursive']['coherence']:.2%}")
Output esperado:
Benchmark Results:
Fixed-Size:
- Chunks: 250
- Latency: 0.08ms
- Coherence: 45% ← Solo 45% de chunks terminan con puntuación (cortados mid-sentence)
Recursive:
- Chunks: 268
- Latency: 1.2ms
- Coherence: 89% ← 89% de chunks terminan con puntuación (oraciones completas)
Análisis:
| Métrica | Fixed-Size | Recursive | Delta |
|---|---|---|---|
| Num chunks | 250 | 268 | +7% (overlap) |
| Latency | 0.08ms | 1.2ms | +1.12ms (negligible) |
| Coherence | 45% | 89% | +44% |
Conclusión: Recursive tiene coherence 2x mejor con latency negligible (+1ms)
🎯 Resumen
Conceptos clave:
- ✅ Recursive chunking: Divide por separators jerárquicos (\n\n → \n → . → espacio → char)
- ✅ Respeta estructura: Prioriza párrafos y oraciones completas vs corte arbitrario
- ✅ Chunk overlap: Preserva contexto entre chunks (+10-15% recall)
- ✅ Trade-offs: +1-2ms latency (negligible), +10-15% storage (overlap), +44% coherence
- ✅ Mejora vs fixed-size: +10-15% precision, +5-8% recall
- ✅ Default recomendado: chunk_size=500, overlap=50, separators default
- ✅ Custom separators: Para código, HTML, Markdown (respeta estructura específica)
Qué sigue:
Cápsula 04 te enseña semantic chunking: agrupar por tema semántico usando embeddings para máxima coherencia (+15-20% precision), con trade-off de costo y latency.
📚 Recursos Adicionales
- LangChain RecursiveCharacterTextSplitter Docs - Documentación oficial
- Chunking Strategies Deep Dive - Pinecone guide
- Optimal Chunk Size Research - Academic paper
- Chunk Overlap Best Practices - Community discussion
- Custom Separators Examples - LangChain cookbook
Creado: Febrero 6, 2026
Versión: 1.0