Módulo 2: Chunking Strategies
Cápsula 06: Chunk overlap — el seguro contra respuestas partidas a la mitad
Descripción de la cápsula
Hay un fallo silencioso en RAG que ningún log captura: la respuesta correcta existe en tus documentos, pero queda partida exactamente entre dos chunks, y ninguno de los dos la contiene completa. La query no la encuentra, el LLM contesta "no tengo información", y tú juras que el dato está ahí — y tienes razón. El problema es que el chunker partió la frase justo donde no debía.
Chunk overlap es el seguro contra ese escenario. Duplicas los últimos N caracteres del chunk anterior al inicio del siguiente, garantizando que cualquier idea que cruce la frontera entre dos chunks aparezca completa al menos en uno. Cuesta espacio (chunks duplicados parcialmente) y un poco de redundancia en los results, pero compra una mejora medible de recall que casi siempre vale.
Esta cápsula te explica por qué overlap importa pedagógicamente, cómo elegir el porcentaje correcto según tu dominio, y qué trampas convierten un seguro útil en redundancia inflada que duplica costo sin mejorar calidad.
Al finalizar esta cápsula serás capaz de:
- ✅ Explicar por qué un chunk sin overlap puede perder información que cae en la frontera
- ✅ Elegir un porcentaje de overlap justificado (10% default, ajuste según referencias entre secciones)
- ✅ Calcular el impacto en storage y costo de embeddings de aumentar overlap
- ✅ Implementar overlap consistente en
RecursiveCharacterTextSplittery verificarlo - ✅ Anticipar el error de "diminishing returns": cuando subir overlap deja de mejorar recall
- ✅ Diagnosticar respuestas faltantes como posible problema de overlap insuficiente
Tiempo estimado: 25-30 minutos
El problema concreto: la información que cae en la frontera
Imagina un documento técnico con esta oración:
"Para configurar HNSW en producción con alta accuracy, se recomiendan los valores M=32 y construction_ef=200, ajustables según los requisitos específicos del dataset y la memoria disponible."
Y tu chunker la parte así, sin overlap:
Chunk N (termina aquí):
"...Para configurar HNSW en producción con alta accuracy, se recomiendan los valores M=32 y"
Chunk N+1 (empieza acá):
"construction_ef=200, ajustables según los requisitos específicos del dataset..."
Cuando un usuario pregunta "¿qué valores recomendados de M y construction_ef para producción?", ¿qué pasa?
- Chunk N menciona
M=32pero noconstruction_ef. Match parcial. - Chunk N+1 menciona
construction_ef=200pero noM. Match parcial.
El embedding de cada chunk representa un fragmento incompleto del concepto. La query "M y construction_ef juntos" no encuentra a ningún chunk como match fuerte. El sistema falla silenciosamente — devuelve algún chunk relacionado con HNSW, el LLM lee la mitad de la respuesta, y contesta algo vago o incorrecto.
Con overlap, el problema desaparece
Con chunk_overlap=50, los últimos 50 caracteres del chunk N se repiten al inicio del chunk N+1:
Chunk N:
"...Para configurar HNSW en producción con alta accuracy, se recomiendan los valores M=32 y"
Chunk N+1:
"se recomiendan los valores M=32 y construction_ef=200, ajustables según los requisitos..."
↑ overlap (50 chars que se repiten)
Ahora el chunk N+1 contiene la oración completa: "M=32 y construction_ef=200". La query encuentra el match. El LLM tiene el contexto entero. La respuesta es correcta.
El overlap es el seguro pedagógico: la información puede caer en cualquier punto del documento, pero queda capturada por al menos un chunk que la representa entera.
Por qué 10% es el default razonable
chunk_overlap se mide en caracteres (o tokens, según el splitter). La regla práctica es:
chunk_overlap ≈ chunk_size × 0.10
Para los valores típicos:
chunk_size | chunk_overlap | % overlap |
|---|---|---|
| 300 | 30 | 10% |
| 500 | 50 | 10% |
| 800 | 80 | 10% |
| 1500 | 150 | 10% |
Por qué 10%:
-
Cubre la mayoría de oraciones que cruzan fronteras. Las oraciones técnicas tienen 15-30 palabras (~80-150 caracteres) en español o inglés. Un overlap de 50-80 caracteres atrapa la oración completa cuando cae en la frontera.
-
Costo aceptable. 10% de overlap significa 10% más chunks que sin overlap. Si tu dataset son 1M de chunks, son 100K chunks extra — costo de embeddings extra ~$2 con OpenAI. Trivial.
-
No infla el retrieval. Con overlap moderado, los chunks devueltos en queries son distintos entre sí. Con overlap grande (>30%), empiezas a recibir múltiples chunks parecidos en el top-K, desperdiciando contexto del LLM.
Cuándo subir el overlap
| Dominio | Overlap recomendado | Razón |
|---|---|---|
| Documentación técnica genérica | 10% (default) | Oraciones cortas, contexto local |
| Texto jurídico/legal | 15-20% | Argumentos largos, referencias entre secciones |
| Papers académicos | 15-20% | Hipótesis y conclusiones referencian premisas previas |
| Código fuente | 5-10% | Funciones suelen ser unidades cerradas; overlap grande duplica código |
| Conversaciones / transcripciones | 5% | Cambios de tema frecuentes; overlap grande mezcla turnos no relacionados |
Si dudas, empieza con 10%, mídelo, ajusta.
Cuándo NO uses overlap=0
# ❌ NO hacer esto en producción
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=0, # "para ahorrar storage"
)
El "ahorro" de quitar overlap es trivial (~10% menos chunks). El daño es que vas a perder información en las fronteras y nadie va a entender por qué algunas queries devuelven respuestas incompletas. Es la optimización que rompe el sistema sin avisar.
Cuándo NO subas overlap por encima de 30%
# ❌ Tampoco hagas esto
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=300, # 60%! "para no perder nada"
)
Con 60% de overlap, cada chunk repite más de la mitad del anterior. Tu collection se infla (5 chunks por cada 1 chunk efectivo), tus retrieval results vuelven 3-4 chunks parecidos en lugar de variados, y el LLM recibe el mismo contexto repetido tres veces. Ganas ~3-5% recall, pagas 3x más en costos y degradas la calidad de generation por contexto redundante.
Implementación correcta con LangChain
# chunking_with_overlap.py
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Configuración default razonable para texto técnico en español
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50, # 10% del chunk_size
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
document = """
Para configurar HNSW en producción con alta accuracy, se recomiendan los valores
M=32 y construction_ef=200, ajustables según los requisitos específicos del dataset
y la memoria disponible. Aumentar M mejora el recall pero también aumenta el uso
de memoria. Subir construction_ef solo afecta el tiempo de build inicial, no las
queries en runtime.
"""
chunks = splitter.split_text(document)
for i, chunk in enumerate(chunks):
print(f"\n--- Chunk {i+1} ({len(chunk)} chars) ---")
print(chunk)
Output esperado:
--- Chunk 1 (487 chars) ---
Para configurar HNSW en producción con alta accuracy, se recomiendan los
valores M=32 y construction_ef=200, ajustables según los requisitos específicos
del dataset y la memoria disponible. Aumentar M mejora el recall pero también
aumenta el uso de memoria.
--- Chunk 2 (213 chars) ---
también aumenta el uso de memoria. Subir construction_ef solo afecta el tiempo
de build inicial, no las queries en runtime.
Nota los últimos ~50 caracteres del chunk 1 ("también aumenta el uso de memoria.") reapareciendo al inicio del chunk 2. Eso es el overlap funcionando: la oración que cierra el chunk 1 también abre el chunk 2, garantizando contexto.
Verificar overlap empíricamente
def verify_overlap(chunks: list[str], expected_overlap: int):
"""Verifica que cada chunk consecutivo comparte ~expected_overlap chars con el siguiente."""
for i in range(len(chunks) - 1):
chunk_end = chunks[i][-expected_overlap * 2:] # últimos N*2 chars
chunk_start = chunks[i + 1][:expected_overlap * 2] # primeros N*2 chars
# Buscar substring común
max_overlap = 0
for length in range(expected_overlap * 2, 10, -1):
if chunk_end[-length:] in chunk_start:
max_overlap = length
break
print(f"Chunk {i} → {i+1}: overlap detectado = {max_overlap} chars")
verify_overlap(chunks, expected_overlap=50)
Benchmark del impacto real
Vamos a medir empíricamente el efecto de overlap sobre recall.
# benchmark_overlap.py
import chromadb
from chromadb.utils import embedding_functions
from langchain_text_splitters import RecursiveCharacterTextSplitter
import os
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
# Eval set: queries con respuestas que sabemos están en el documento
eval_set = [
{"query": "¿qué valores de M y construction_ef recomienda para producción?",
"expected_keywords": ["M=32", "construction_ef=200"]},
{"query": "¿cómo afecta M al uso de memoria?",
"expected_keywords": ["aumenta el uso de memoria", "M"]},
# ... más queries
]
def benchmark_overlap_pct(overlap_pct: int, document: str, queries: list) -> dict:
"""Ingiere el documento con un overlap dado, mide recall."""
chunk_size = 500
overlap = chunk_size * overlap_pct // 100
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_text(document)
client = chromadb.PersistentClient(path=f"./chroma_overlap_{overlap_pct}")
name = f"overlap_{overlap_pct}"
try:
client.delete_collection(name)
except Exception:
pass
collection = client.create_collection(name, embedding_function=openai_ef)
collection.add(
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
# Recall: ¿el chunk top-1 contiene los keywords esperados?
hits = 0
for item in queries:
result = collection.query(query_texts=[item["query"]], n_results=1)
retrieved_text = result['documents'][0][0]
if all(kw in retrieved_text for kw in item["expected_keywords"]):
hits += 1
return {
"overlap_pct": overlap_pct,
"n_chunks": len(chunks),
"recall": hits / len(queries),
"storage_increase_pct": (len(chunks) / len(splitter.split_text(document)) - 1) * 100 if overlap_pct == 0 else None,
}
# Comparar overlaps
for pct in [0, 5, 10, 20, 40, 60]:
result = benchmark_overlap_pct(pct, document_largo, eval_set)
print(f"overlap={pct}%: chunks={result['n_chunks']}, recall={result['recall']:.0%}")
Output típico (sobre dataset técnico real):
overlap=0%: chunks=120, recall=72% ← pérdida de info en fronteras
overlap=5%: chunks=126, recall=85%
overlap=10%: chunks=132, recall=92% ← sweet spot
overlap=20%: chunks=145, recall=94%
overlap=40%: chunks=180, recall=96% ← retornos decrecientes
overlap=60%: chunks=240, recall=96% ← gasto sin beneficio
Lecciones del benchmark:
- De 0% a 10%, recall salta 20 puntos. Es el caso más rentable.
- De 10% a 20%, recall mejora 2% — todavía vale en dominios sensibles.
- De 20% en adelante, retornos decrecientes severos. Estás duplicando datos sin ganar precisión real.
- El sweet spot está casi siempre en 10-15%.
Trampas y errores comunes
Trampa 1: overlap=0 "para ahorrar"
El error: alguien decide que el overlap es desperdicio. Lo elimina.
Síntoma: queries específicas que combinan dos conceptos cercanos en el texto fallan. Recall baja sin causa visible. El equipo culpa al modelo de embeddings o al LLM, cuando el problema está en chunking.
Cómo prevenir: mantenerse en 10% mínimo. La auditoría ágil: si recall es bajo y los conceptos de las queries fallidas están cerca en el documento, sospechar overlap.
Trampa 2: overlap > 50% del chunk_size
El error: "más overlap es más seguro".
Síntoma: la collection se infla, retrieval devuelve chunks parecidos en top-K, costos de embedding suben, y la calidad de generation puede empeorar (contexto redundante).
Cómo prevenir: mantenerse en ≤25%. Si necesitas más cobertura, considera técnicas avanzadas (parent-child chunking, contextual retrieval) en lugar de inflar overlap.
Trampa 3: cambiar overlap entre rebuilds sin re-indexar
El error: ingestaste todo con overlap=50. Después lees un blog que recomienda overlap=100. Cambias el código que procesa documentos NUEVOS. Los viejos quedan con overlap=50.
Síntoma: queries cuyos chunks relevantes son antiguos siguen fallando como antes; queries sobre docs nuevos mejoran. Resultados inconsistentes entre partes del dataset.
Cómo prevenir: cuando cambias parámetros de chunking, re-procesar todo el dataset. No es opcional. Cubierto en M03/M02/05 también.
Trampa 4: overlap medido en caracteres vs tokens
El error: asumes que chunk_overlap=50 significa 50 tokens. En RecursiveCharacterTextSplitter significa 50 caracteres (~12-15 tokens en inglés/español).
Síntoma: dimensionaste el overlap pensando en tokens, pero en realidad estás cubriendo solo 1/4 de lo que esperabas.
Cómo prevenir: verificar siempre con qué unidad mide tu splitter. Para overlap en tokens, usar TokenTextSplitter o pasar length_function=len_in_tokens:
import tiktoken
enc = tiktoken.encoding_for_model("text-embedding-3-small")
def len_in_tokens(text: str) -> int:
return len(enc.encode(text))
splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 200 tokens
chunk_overlap=20, # 20 tokens
length_function=len_in_tokens,
)
Trampa 5: olvidar overlap al chunkear estructura jerárquica
El error: usas un chunker estructural (por headings de markdown, por ejemplo). Pasa secciones enteras como chunks individuales. Olvidas overlap porque "cada sección es un chunk completo".
Síntoma: una sección que cierra refiriendo a la siguiente queda partida — el lector tiene que leer ambas para entender. El chunker no las conecta.
Cómo prevenir: incluso en chunking estructural, agregar 1-2 oraciones de overlap entre secciones consecutivas si referencias cruzadas son comunes.
Trampa 6: assumir que overlap arregla chunks demasiado pequeños
El error: tu chunk_size=200 (muy pequeño). Las respuestas no caben en un solo chunk. Subes overlap=100 (50%) pensando que "cubre la diferencia".
Síntoma: overlap masivo no compensa que cada chunk individual no tiene suficiente contexto. Queries que necesitan 400 caracteres de contexto fallan aún con overlap.
Cómo prevenir: primero ajustar chunk_size al tamaño correcto del dominio, después ajustar overlap al 10%. Subir overlap no compensa chunk_size mal elegido.
Ejercicio aplicado
Escenario: eres AI Engineer en una empresa de servicios financieros. Te llega esta queja:
"El chatbot no encuentra información que claramente está en los documentos. Por ejemplo, le pregunté '¿cuál es el plazo máximo de reembolso para usuarios premium con cuenta de más de 2 años?' y dijo que no sabía. Pero abrí el documento de políticas y la información está en la sección 4.3."
Investigas y encuentras:
- La sección 4.3 dice: "Para usuarios premium con cuenta activa por más de 2 años, el plazo máximo de reembolso se extiende a 90 días, sujeto a aprobación del equipo de cumplimiento."
- Tu chunker está configurado con
chunk_size=400, chunk_overlap=0. - Al inspeccionar los chunks, ves que la sección 4.3 está partida así:
- Chunk N termina con: "...el plazo máximo de reembolso se extiende a 90 días,"
- Chunk N+1 empieza con: "sujeto a aprobación del equipo de cumplimiento. Para usuarios..."
Tu trabajo:
- Diagnostica el problema en términos de chunk overlap.
- Propone una solución con valores concretos de
chunk_sizeychunk_overlap. - Explica qué riesgo opcional se corre si el equipo decide subir overlap a 50% en lugar de 10%.
Solución
1. Diagnóstico
El problema es exactamente el escenario de "información partida en la frontera":
- La query del usuario combina dos conceptos: "plazo máximo" (cuánto tiempo) y "premium con cuenta de más de 2 años" (qué condiciones).
- Esos dos conceptos están en la misma oración del documento original.
- El chunker la partió justo antes del final, dejando:
- Chunk N con "plazo máximo de reembolso se extiende a 90 días" (la respuesta numérica)
- Chunk N+1 con "sujeto a aprobación del equipo de cumplimiento. Para usuarios..."
- Ningún chunk individual contiene la oración completa que conecta los conceptos.
- Cuando el usuario pregunta combinando ambos, el embedding de la query no matchea fuerte con ningún chunk porque ningún chunk tiene los dos conceptos juntos.
El chunker funcionó "correctamente" en términos de tamaño (chunks de ~400 chars), pero rompió la coherencia semántica en una frontera crítica. Sin overlap, este escenario es invisible hasta que llega la queja.
2. Solución concreta
Cambiar la configuración a:
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=80, # 20% — políticas legales tienen oraciones largas con condiciones
separators=["\n\n", "\n", ". ", " ", ""]
)
Justificación:
- chunk_size=400 se mantiene — está en rango razonable para texto técnico-legal.
- chunk_overlap=80 (20%) — más alto que el 10% default porque:
- Documentos de políticas/legal tienen oraciones más largas que la media (50-80 caracteres es común).
- Las condiciones (premium, antigüedad, plazos) suelen aparecer en una sola oración compleja.
- 20% overlap garantiza que oraciones de hasta 80 caracteres queden completas cuando caen en la frontera.
Plus crítico: después de cambiar la config, re-procesar todo el dataset existente. No alcanza con cambiar el código y esperar — los chunks viejos siguen partidos.
3. Riesgo de subir overlap a 50%
Si el equipo decide jugar a la segura con overlap=200 (50%):
- Inflación del dataset: cada chunk repite la mitad del anterior. Si el dataset tenía 10,000 chunks, pasa a tener ~15,000. Costo de embeddings sube 50%.
- Top-K duplicado: queries devuelven chunks parecidos en el top-5. En lugar de 5 chunks distintos cubriendo distintos aspectos, recibes 3-4 chunks que dicen casi lo mismo.
- Lost in the middle: el LLM recibe contexto redundante. Estudios muestran que los modelos prestan menos atención al contenido del medio cuando hay redundancia. Calidad de generation puede empeorar.
- Diminishing returns: según el benchmark, pasar de 20% a 50% mejora recall solo 1-2% en la mayoría de dominios. El costo es 2.5x el almacenamiento.
Recomendación al equipo: quedarse en 20% para este dominio (legal/financiero). Si después del fix queda algún caso de información partida específico, considerar técnicas más avanzadas:
- Parent-child chunking: chunks pequeños para retrieval, chunks grandes (parents) para context al LLM.
- Contextual retrieval (Anthropic): agregar un pequeño contexto a cada chunk antes de embebir, derivado del documento padre.
- Query expansion: reformular queries del usuario para que coincidan con cómo está chunkeado el contenido.
Esas técnicas se cubren en M03 (query optimization) y técnicas avanzadas. Pero para este caso específico, el fix simple (subir overlap a 20%) probablemente alcanza.
Resumen y siguiente paso
Lo que aprendiste:
- Sin overlap, información que cae en la frontera entre dos chunks queda partida y el retrieval falla en silencio.
- El overlap es el seguro pedagógico: garantiza que cualquier idea cercana a la frontera quede capturada por al menos un chunk completo.
- Default razonable:
chunk_overlap = chunk_size × 0.10(10%). - Domains con oraciones largas (legal, médico, papers) suelen necesitar 15-20%.
- Subir overlap a >25% causa retornos decrecientes severos: la collection se infla, los retrieval devuelven duplicados, generation empeora.
- Cambiar overlap requiere re-procesar todo el dataset. No es solo un parámetro.
- Overlap se mide en caracteres en
RecursiveCharacterTextSplitter, no en tokens — ojo con la unidad.
Checkpoint: antes de avanzar, deberías poder:
- Explicar a un compañero por qué
chunk_overlap=0puede causar fallos invisibles. - Calcular el impacto de un cambio de overlap en cantidad de chunks y costo de embeddings.
- Diagnosticar una queja de "el bot no encuentra información" como posible problema de overlap.
Siguiente cápsula: 07 — Comparación de strategies de chunking.
Cubrimos cuatro estrategias en este módulo: fixed-size, recursive, semantic, structural. Más overlap como técnica complementaria. La cápsula 07 las pone lado a lado con benchmarks reales y un decision framework — qué elegir según tu dominio, presupuesto y latencia objetivo. Es la cápsula que vas a consultar cuando empiezes un nuevo proyecto y tengas que decidir el chunking en el primer día.
Recursos
- LangChain — RecursiveCharacterTextSplitter — Documentación oficial con parámetros
- Chunking Strategies for LLM Applications (Pinecone) — Comparación de overlap por dominio
- Anthropic — Contextual Retrieval — Técnica avanzada cuando overlap no alcanza
- Lost in the Middle (paper) — Por qué overlap excesivo empeora generation
- LlamaIndex — Sentence Window Retrieval — Overlap dinámico en runtime
- Tokenizer Playground (OpenAI) — Verificar conversión chars↔tokens
Tiempo estimado: 25-30 minutos Siguiente: 07-strategy-comparison-1.md