Estrategias de Chunking
Por que el Chunking Importa
No puedes embeber un documento entero como un solo vector y esperar buena recuperacion. Un PDF de 50 paginas 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 mas pequenos que se embeben independientemente. El objetivo es crear fragmentos lo suficientemente pequenos para estar semanticamente 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 recuperara consistentemente informacion irrelevante o incompleta, sin importar que tan bueno sea tu modelo de embedding o algoritmo de recuperacion.
Chunking de Tamano Fijo
El enfoque mas simple: dividir texto en fragmentos de un numero fijo de caracteres, con algo de superposicion 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)
Cuando usarlo: Prototipado rapido, documentos uniformes sin estructura clara, linea base inicial antes de probar metodos mas sofisticados.
Limitaciones: Los fragmentos pueden dividirse a mitad de oracion o parrafo, rompiendo la coherencia semantica. Un fragmento sobre "beneficios de empleados" podria terminar a mitad de la politica de vacaciones, haciendolo inutil para responder preguntas sobre PTO.
Division Recursiva por Caracteres
Este es el splitter mas comunmente usado en LangChain y la recomendacion predeterminada para la mayoria de los sistemas RAG. Intenta dividir en limites de parrafo primero, luego oraciones, luego palabras, cayendo recursivamente a separadores mas pequenos.
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 linea (limites de parrafo), luego saltos simples, luego oraciones, luego palabras. Esto preserva los limites semanticos tanto como sea posible.
Cuando usarlo: Sistemas RAG de proposito general, la mayoria de tipos de documentos, cuando quieres un valor predeterminado confiable.
Chunking Semantico
En lugar de dividir por conteo de caracteres, el chunking semantico divide basandose en el significado. Computa embeddings para oraciones y coloca limites de fragmento donde la similitud semantica 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)
Cuando usarlo: Documentos que cubren multiples temas sin formato claro, transcripciones de conversaciones, articulos de formato largo donde los cambios de tema no estan marcados por encabezados.
Limitaciones: Mas lento que la division basada en caracteres (requiere embeber cada oracion), mas costoso (llamadas API para embeddings), y los resultados pueden ser impredecibles para documentos cortos.
Division 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)
Cuando usarlo: Bases de datos de FAQ (cada par pregunta-respuesta es naturalmente un fragmento), documentos legales donde clausulas individuales importan, cualquier contenido donde la precision es mas importante que el contexto.
Chunking Padre-Hijo
Esta es una de las estrategias mas poderosas para RAG en produccion. La idea: usar fragmentos pequenos para recuperacion (coinciden con consultas precisamente) pero retornar fragmentos padre mas grandes para contexto (dan al LLM suficiente informacion 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?")
Cuando usarlo: Sistemas en produccion donde tanto la precision de recuperacion como la calidad de respuesta importan, documentos con secciones que tienen relaciones padre-hijo claras (capitulos que contienen subsecciones).
Division por Markdown y Encabezados
Para documentos estructurados (Markdown, HTML, documentacion), 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("---")
Cuando usarlo: Documentacion, articulos de base de conocimiento, cualquier contenido donde los encabezados indican confiablemente limites de tema.
Superposicion de Fragmentos: Cuanto?
La superposicion asegura que la informacion cerca de un limite de fragmento no se pierda. Si una oracion clave cae justo en el punto de division, la superposicion garantiza que aparece en al menos un fragmento completo.
Reglas generales:
- 10-20% del tamano del fragmento es la superposicion estandar. Para fragmentos de 1000 caracteres, usa 100-200 caracteres de superposicion.
- Muy poca superposicion arriesga perder contexto en los limites.
- Demasiada superposicion desperdicia almacenamiento y puede causar resultados de recuperacion duplicados.
- Cero superposicion es aceptable para fragmentos con limites naturales (como entradas de FAQ o funciones de codigo) donde los limites son limpios.
Tamanos Optimos de Fragmento
No hay un mejor tamano de fragmento universal -- depende de tu caso de uso, modelo de embedding y tipos de pregunta.
Directrices generales:
| Caso de Uso | Tamano de Fragmento | Razon | |-------------|---------------------|-------| | FAQ / respuestas cortas | 200-500 caracteres | Las preguntas mapean a respuestas especificas y concisas | | Documentacion | 500-1000 caracteres | Las secciones necesitan suficiente contexto para explicacion | | Legal / politicas | 1000-2000 caracteres | Las clausulas y politicas necesitan contexto completo | | Codigo | Por funcion/clase | Los limites naturales definen los fragmentos | | Articulos de formato largo | 800-1500 caracteres | Equilibrio entre especificidad y contexto |
Consejo: La mejor manera de encontrar tu tamano optimo 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 cual produce los resultados mas relevantes. La diferencia es a menudo dramatica.
Errores Comunes de Chunking
Dividir codigo a mitad de funcion. Los splitters basados en caracteres no entienden la estructura del codigo. Una funcion dividida entre dos fragmentos sera insignificante tanto para el modelo de embedding como para el LLM. Usa division basada en AST o limites de funcion para codigo.
Ignorar la estructura del documento. Si tus documentos tienen encabezados, usalos. Dividir un documento bien estructurado por conteo de caracteres solamente desperdicia informacion estructural valiosa.
Fragmentos demasiado pequenos. Un fragmento de 100 caracteres como "Ver seccion 4.2 para detalles" es inutil -- no tiene contenido informativo. Establece tamanos minimos de fragmento y filtra los pedazos.
Sin propagacion de metadatos. Cuando divides un documento en 50 fragmentos, cada fragmento deberia llevar los metadatos del documento original (fuente, titulo, 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 codigo deberia dividirse por funcion. Los FAQs deberian dividirse por par pregunta-respuesta. La documentacion deberia dividirse por seccion. Construye tu pipeline para manejar cada tipo apropiadamente.
En la siguiente leccion, aprenderas como buscar estos fragmentos efectivamente usando tecnicas avanzadas de recuperacion que van mucho mas alla de la busqueda basica de similitud.