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:

  1. Intenta dividir documento por \n\n (párrafos)
  2. Si chunk resultante es < chunk_size → ✅ Keep it
  3. Si chunk resultante es > chunk_size → Intenta siguiente separator (\n)
  4. 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 SizeProsConsCuándo usar
Pequeño (100-200)✅ Precision alta (match específico)❌ Puede perder contextoQueries específicas
Medio (300-500)✅ Balance precision/contexto⚠️ NeutralDefault 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:

OverlapProsCons
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étricaFixed-SizeRecursiveDelta
Num chunks250268+7% (overlap)
Latency0.08ms1.2ms+1.12ms (negligible)
Coherence45%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

  1. LangChain RecursiveCharacterTextSplitter Docs - Documentación oficial
  2. Chunking Strategies Deep Dive - Pinecone guide
  3. Optimal Chunk Size Research - Academic paper
  4. Chunk Overlap Best Practices - Community discussion
  5. Custom Separators Examples - LangChain cookbook

Creado: Febrero 6, 2026
Versión: 1.0