Saltar al contenido
Lección 5 de 12

Estrategias de Chunking

7 min read

Por qué el Chunking Importa

No puedes embeber un documento entero como un solo vector y esperar buena recuperación. Un PDF de 50 páginas comprimido en un solo embedding pierde la especificidad necesaria para coincidir con preguntas precisas. Por el contrario, embeber oraciones individuales pierde el contexto necesario para que el LLM genere una respuesta coherente.

El chunking es el proceso de dividir documentos en segmentos más pequeños que se embeben independientemente. El objetivo es crear fragmentos lo suficientemente pequeños para estar semánticamente enfocados (un tema por fragmento) pero lo suficientemente grandes para llevar contexto suficiente para que el LLM los use efectivamente.

Hacer bien el chunking es una de las optimizaciones de mayor impacto en cualquier sistema RAG. Una base de conocimiento mal fragmentada recuperará consistentemente información irrelevante o incompleta, sin importar qué tan bueno sea tu modelo de embedding o algoritmo de recuperación.

Chunking de Tamaño Fijo

El enfoque más simple: dividir texto en fragmentos de un número fijo de caracteres, con algo de superposición entre fragmentos consecutivos.

from langchain.text_splitter import CharacterTextSplitter

splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=1000,      # Maximo de caracteres por fragmento
    chunk_overlap=200,    # Caracteres compartidos entre fragmentos
    length_function=len,
)

text = "Tu texto largo de documento aqui..."
chunks = splitter.split_text(text)

Cuándo usarlo: Prototipado rápido, documentos uniformes sin estructura clara, línea base inicial antes de probar métodos más sofisticados.

Limitaciones: Los fragmentos pueden dividirse a mitad de oración o párrafo, rompiendo la coherencia semántica. Un fragmento sobre "beneficios de empleados" podría terminar a mitad de la política de vacaciones, haciéndolo inútil para responder preguntas sobre PTO.

División Recursiva por Caracteres

Este es el splitter más comúnmente usado en LangChain y la recomendación predeterminada para la mayoría de los sistemas RAG. Intenta dividir en límites de párrafo primero, luego oraciones, luego palabras, cayendo recursivamente a separadores más pequeños.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],  # Orden de prioridad
    length_function=len,
)

chunks = splitter.split_text(document_text)

for i, chunk in enumerate(chunks):
    print(f"Fragmento {i}: {len(chunk)} caracteres")
    print(chunk[:100])
    print("---")

La lista de separadores define la prioridad: primero intenta dividir en dobles saltos de línea (límites de párrafo), luego saltos simples, luego oraciones, luego palabras. Esto preserva los límites semánticos tanto como sea posible.

Cuándo usarlo: Sistemas RAG de propósito general, la mayoría de tipos de documentos, cuando quieres un valor predeterminado confiable.

Chunking Semántico

En lugar de dividir por conteo de caracteres, el chunking semántico divide basándose en el significado. Computa embeddings para oraciones y coloca límites de fragmento donde la similitud semántica entre oraciones consecutivas cae significativamente.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95,  # Dividir en el top 5% de disimilitud
)

chunks = splitter.split_text(document_text)

Cuándo usarlo: Documentos que cubren múltiples temas sin formato claro, transcripciones de conversaciones, artículos de formato largo donde los cambios de tema no están marcados por encabezados.

Limitaciones: Más lento que la división basada en caracteres (requiere embeber cada oración), más costoso (llamadas API para embeddings), y los resultados pueden ser impredecibles para documentos cortos.

División Basada en Oraciones

Divide texto en oraciones individuales o grupos de oraciones. Esto produce fragmentos muy precisos pero puede carecer de contexto.

from langchain.text_splitter import RecursiveCharacterTextSplitter

# Configurar para dividir principalmente en limites de oracion
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", "? ", "! ", " ", ""],
)

chunks = splitter.split_text(document_text)

Cuándo usarlo: Bases de datos de FAQ (cada par pregunta-respuesta es naturalmente un fragmento), documentos legales donde cláusulas individuales importan, cualquier contenido donde la precisión es más importante que el contexto.

Chunking Padre-Hijo

Esta es una de las estrategias más poderosas para RAG en producción. La idea: usar fragmentos pequeños para recuperación (coinciden con consultas precisamente) pero retornar fragmentos padre más grandes para contexto (dan al LLM suficiente información para generar una buena respuesta).

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.storage import InMemoryStore
from langchain.retrievers import ParentDocumentRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Fragmentos pequenos para recuperacion precisa
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400,
    chunk_overlap=50,
)

# Fragmentos mas grandes para contexto
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=2000,
    chunk_overlap=200,
)

# Configurar el retriever
vectorstore = Chroma(
    collection_name="split_parents",
    embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
docstore = InMemoryStore()

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=docstore,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)

# Agregar documentos -- los hijos se embeben, los padres se almacenan
retriever.add_documents(documents)

# Consultar -- recupera por similitud del hijo, retorna fragmentos padre
results = retriever.invoke("Cual es la politica de vacaciones?")

Cuándo usarlo: Sistemas en producción donde tanto la precisión de recuperación como la calidad de respuesta importan, documentos con secciones que tienen relaciones padre-hijo claras (capítulos que contienen subsecciones).

División por Markdown y Encabezados

Para documentos estructurados (Markdown, HTML, documentación), dividir por encabezados preserva la estructura natural del contenido.

from langchain.text_splitter import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "header_1"),
    ("##", "header_2"),
    ("###", "header_3"),
]

splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers=False,  # Mantener encabezados en el texto del fragmento
)

chunks = splitter.split_text(markdown_text)

for chunk in chunks:
    print(f"Encabezados: {chunk.metadata}")
    print(f"Contenido: {chunk.page_content[:100]}")
    print("---")

Cuándo usarlo: Documentación, artículos de base de conocimiento, cualquier contenido donde los encabezados indican confiablemente límites de tema.

Superposición de Fragmentos: ¿Cuánto?

La superposición asegura que la información cerca de un límite de fragmento no se pierda. Si una oración clave cae justo en el punto de división, la superposición garantiza que aparece en al menos un fragmento completo.

Reglas generales:

  • 10-20% del tamaño del fragmento es la superposición estándar. Para fragmentos de 1000 caracteres, usa 100-200 caracteres de superposición.
  • Muy poca superposición arriesga perder contexto en los límites.
  • Demasiada superposición desperdicia almacenamiento y puede causar resultados de recuperación duplicados.
  • Cero superposición es aceptable para fragmentos con límites naturales (como entradas de FAQ o funciones de código) donde los límites son limpios.

Tamaños Óptimos de Fragmento

No hay un mejor tamaño de fragmento universal -- depende de tu caso de uso, modelo de embedding y tipos de pregunta.

Directrices generales:

| Caso de Uso | Tamaño de Fragmento | Razón | |-------------|---------------------|-------| | FAQ / respuestas cortas | 200-500 caracteres | Las preguntas mapean a respuestas específicas y concisas | | Documentación | 500-1000 caracteres | Las secciones necesitan suficiente contexto para explicación | | Legal / políticas | 1000-2000 caracteres | Las cláusulas y políticas necesitan contexto completo | | Código | Por función/clase | Los límites naturales definen los fragmentos | | Artículos de formato largo | 800-1500 caracteres | Equilibrio entre especificidad y contexto |

Consejo: La mejor manera de encontrar tu tamaño óptimo de fragmento es experimentar. Crea conjuntos de fragmentos a 500, 1000 y 1500 caracteres, ejecuta el mismo conjunto de consultas de prueba contra cada uno, y mide cuál produce los resultados más relevantes. La diferencia es a menudo dramática.

Errores Comunes de Chunking

Dividir código a mitad de función. Los splitters basados en caracteres no entienden la estructura del código. Una función dividida entre dos fragmentos será insignificante tanto para el modelo de embedding como para el LLM. Usa división basada en AST o límites de función para código.

Ignorar la estructura del documento. Si tus documentos tienen encabezados, úsalos. Dividir un documento bien estructurado por conteo de caracteres solamente desperdicia información estructural valiosa.

Fragmentos demasiado pequeños. Un fragmento de 100 caracteres como "Ver sección 4.2 para detalles" es inútil -- no tiene contenido informativo. Establece tamaños mínimos de fragmento y filtra los pedazos.

Sin propagación de metadatos. Cuando divides un documento en 50 fragmentos, cada fragmento debería llevar los metadatos del documento original (fuente, título, fecha). Sin esto, no puedes filtrar por fuente ni citar apropiadamente.

Usar una sola estrategia para todo. Diferentes tipos de contenido necesitan diferentes estrategias de chunking. El código debería dividirse por función. Los FAQs deberían dividirse por par pregunta-respuesta. La documentación debería dividirse por sección. Construye tu pipeline para manejar cada tipo apropiadamente.

En la siguiente lección, aprenderás cómo buscar estos fragmentos efectivamente usando técnicas avanzadas de recuperación que van mucho más allá de la búsqueda básica de similitud.