Experimentando con diferentes estrategias de fragmentación mediante LangChain para aplicaciones de LLM
La fragmentación es uno de los problemas más desafiantes al crear aplicaciones de generación aumentada por recuperación (RAG). La fragmentación es el proceso de dividir oraciones en partes más pequeñas y manejables para su procesamiento posterior. Aunque suena simple, el diablo está en los detalles. Una estrategia de fragmentación mal elegida puede generar resultados irrelevantes o incompletos, lo que dificulta que un sistema de IA proporcione respuestas precisas. Por ejemplo, los fragmentos demasiado pequeños pueden perder contexto, mientras que los fragmentos demasiado grandes pueden devolver información irrelevante.
En este tutorial, exploramos cómo diferentes estrategias de fragmentación afectan el rendimiento de recuperación para el mismo conjunto de datos; en particular, nos centraremos en un caso de uso con fragmentación de Langchain. Al final de esta guía, entenderás cómo el tamaño y la superposición de los fragmentos influyen en la calidad de la recuperación y obtendrás ideas prácticas para elegir los parámetros adecuados para tu caso de uso específico. El código de esta publicación se puede encontrar en este repositorio de GitHub sobre experimentación con LLM.
Descripción general de LangChain y por qué importa la fragmentación
LangChain es un marco de orquestación de modelos de lenguaje grandes con herramientas integradas para la carga de documentos y la división de texto. Su flexibilidad lo convierte en una opción popular para crear aplicaciones RAG, donde la fragmentación desempeña un papel fundamental en la determinación de la relevancia y la completitud de la información recuperada.
La fragmentación implica seleccionar dos parámetros principales: un fragmento determinado, tamaño y superposición.
Los tamaños de fragmento definen el número de caracteres o tokens en cada fragmento. Los fragmentos más grandes capturan más contexto, pero corren el riesgo de devolver información irrelevante, mientras que los fragmentos más pequeños garantizan precisión, pero pueden perder el contexto necesario. La superposición determina cuánto texto se comparte entre fragmentos consecutivos. Ayuda a preservar la continuidad, especialmente para ideas conectadas entre párrafos, pero una superposición excesiva aumenta la sobrecarga de procesamiento. Optimizar estos parámetros garantiza un equilibrio entre capturar contexto y mantener el foco cuando divides texto, ambos aspectos críticos para una recuperación precisa.
Las estrategias de fragmentación tienen amplias aplicaciones más allá de los flujos de trabajo RAG. Son cruciales en chatbots conscientes del contexto, que dependen del texto fragmentado para proporcionar respuestas concisas pero completas a las consultas de los usuarios. Por ejemplo, un bot de soporte puede usar la fragmentación para dividir guías de solución de problemas en pasos accionables sin abrumar al usuario. De manera similar, en la construcción de grafos de conocimiento, la fragmentación extrae entidades y relaciones del texto sin procesar, ayudando a transformar documentos no estructurados en datos estructurados. Estos ejemplos demuestran cómo la estrategia de fragmentación adecuada puede mejorar las tareas de IA posteriores.
Comprender los desafíos de una fragmentación deficiente
Una estrategia de fragmentación mal optimizada puede crear problemas significativos en la recuperación. Los tamaños de fragmento realmente pequeños a menudo carecen de contexto suficiente, lo que produce resultados fragmentados o incompletos. Por ejemplo, consultar un manual de producto podría devolver fragmentos aislados como: “Paso 1: Encienda la alimentación”, sin información complementaria sobre los pasos posteriores. Por el contrario, cuando tu aplicación divide el texto en fragmentos demasiado grandes, puede diluir la especificidad de la información recuperada. Una consulta de similitud semántica que espera instrucciones concisas de solución de problemas podría devolver una sección completa, lo que dificulta que los usuarios identifiquen los detalles relevantes.
La superposición añade otra capa de complejidad. Sin superposición, los sistemas pueden perder la continuidad del pensamiento entre fragmentos consecutivos, lo cual es fundamental para narrativas o procesos conectados. Sin embargo, una superposición excesiva puede introducir redundancia, aumentando los costos de almacenamiento y procesamiento. Equilibrar estas compensaciones es vital para garantizar que los sistemas de recuperación entreguen respuestas precisas y accionables.
Importaciones de código y configuración de LangChain
Esta primera sección se centra en importaciones y otras herramientas de configuración. Quizás lo primero que notes sobre el código de abajo es que hay un MONTÓN de importaciones. Las más utilizadas son: os y dotenv, así que no las cubriré. Simplemente se usan para tus variables de entorno. Recorramos la división de texto en LangChain con python y el cliente pymilvus.
En la parte superior, verás nuestras tres importaciones para incorporar el doc. Primero, está NotionDirectoryLoader, que carga un directorio con docs de markdown/Notion. Luego, tenemos los divisores de texto Markdown Header y Recursive Character. Estos dividen el texto dentro del doc markdown según los encabezados (el divisor de encabezados), o un conjunto de saltos de caracteres preseleccionados (el divisor recursivo).
A continuación, tenemos las importaciones del recuperador. Milvus es nuestra base de datos vectorial, OpenAIEmbeddings es nuestro modelo de embeddings, y OpenAI es nuestro LLM. El SelfQueryRetriever es el recuperador nativo de LangChain que permite que una base de datos vectorial se “consulte a sí misma”. Escribí más sobre usar LangChain para consultar una base de datos vectorial en este artículo.
Nuestra última importación de LangChain es AttributeInfo, que pasa un atributo con información al recuperador de autoconsulta, como uno podría suponer. Por último, quiero tocar las importaciones de pymilvus. Estas son estrictamente por razones de utilidad; no las necesitamos para trabajar con una base de datos vectorial en LangChain. Uso estas importaciones para limpiar la base de datos al final.
Lo último que hacemos antes de escribir la función es cargar nuestras variables de entorno y declarar algunas constantes. La variable headers_to_split_on es esencial: enumera todos los encabezados que esperamos ver y por los que queremos dividir en el markdown. path simplemente le indica a LangChain dónde encontrar los docs de Notion.
import os
from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.vectorstores import Milvus
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from pymilvus import connections, utility
from dotenv import load_dotenv
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_CLUSTER_01_URI")
zilliz_token = os.getenv("ZILLIZ_CLUSTER_01_TOKEN")
headers_to_split_on = [
("##", "Section"),
]
path='./notion_docs'
Construcción de una función de experimentación de fragmentación
Construir la función de experimentación es la parte más crítica del tutorial. Como se mencionó, esta función toma algunos parámetros para la ingesta de documentos y la experimentación. Necesitamos proporcionar la ruta a los docs, los encabezados por los que dividir (splitters), el tamaño del fragmento, la superposición máxima del tamaño del fragmento, y si queremos o no limpiar eliminando las colecciones al final. La eliminación de la colección está configurada como true de forma predeterminada
Si podemos evitarlo, queremos crear y eliminar colecciones con la menor frecuencia posible porque tienen sobrecostes que podemos prevenir. Puede que veas cambiar el script mientras busco buenas soluciones alternativas.
Esta función es muy similar a la que enlazamos arriba sobre usar Notion con LangChain. La primera sección carga el documento desde la ruta usando Notion Directory Loader. Observa que solo tomamos el contenido html de la primera página web (y solo tenemos una página).
A continuación, tomamos nuestros splitters. Primero, usamos el splitter de markdown para dividir según los encabezados que pasamos arriba. Luego, usamos nuestro método de splitter recursivo y dividimos según el tamaño del chunk y el solapamiento.
Eso es todo el splitting que necesitamos. Con el splitting terminado, damos un nombre de colección e inicializamos una instancia de LangChain Milvus usando las variables de entorno predeterminadas, embeddings de OpenAI, splits y el nombre de la colección. También creamos una lista de campos de metadatos mediante el objeto AttributeInfo para indicarle al self-query retriever que tenemos “secciones.”
Con toda esta configuración, obtenemos nuestro LLM y luego lo pasamos a un self-query retriever en python. A partir de ahí, el retriever hace su magia cuando le hacemos una pregunta sobre nuestros docs. Lo he configurado también para que nos diga qué estrategia de chunks estamos probando. Finalmente, podemos eliminar la colección si queremos.
def test_langchain_chunking(docs_path, splitters, chunk_size, chunk_overlap, drop_collection=True):
path=docs_path
loader = NotionDirectoryLoader(path)
docs = loader.load()
md_file=docs[0].page_content
# Let's create groups based on the section headers in our page
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=splitters)
md_header_splits = markdown_splitter.split_text(md_file)
# Define our text splitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
all_splits = text_splitter.split_documents(md_header_splits)
test_collection_name = f"EngineeringNotionDoc_{chunk_size}_{chunk_overlap}"
vectordb = Milvus.from_documents(documents=all_splits,
embedding=OpenAIEmbeddings(),
connection_args={"uri": zilliz_uri,
"token": zilliz_token},
collection_name=test_collection_name)
metadata_fields_info = [
AttributeInfo(
name="Section",
description="Part of the document that the text comes from",
type="string or list[string]"
),
]
document_content_description = "Major sections of the document"
llm = OpenAI(temperature=0)
retriever = SelfQueryRetriever.from_llm(llm, vectordb, document_content_description, metadata_fields_info, verbose=True)
res = retriever.get_relevant_documents("What makes a distinguished engineer?")
print(f"""Responses from chunking strategy:
{chunk_size}, {chunk_overlap}""")
for doc in res:
print(doc)
# this is just for rough cleanup, we can improve this
# lots of user considerations to understand for real experimentation use cases though
if drop_collection:
connections.connect(uri=zilliz_uri, token=zilliz_token)
utility.drop_collection(test_collection_name)
Pruebas y resultados de LangChain
Muy bien, ¡ahora viene la parte emocionante! Veamos las pruebas y los resultados.
Código para probar chunks de LangChain
Este breve bloque de código a continuación muestra cómo podemos ejecutar nuestra función para la experimentación. He añadido cinco experimentos. Este tutorial prueba estrategias de chunks de 32 a 512 de longitud en potencias de 2, con solapamientos que van de 4 a 64 también en potencias de 2. Para probar, recorremos la lista de tuplas y llamamos a la función que escribimos arriba.
chunking_tests = [(32, 4), (64, 8), (128, 16), (256, 32), (512, 64)]
for test in chunking_tests:
test_langchain_chunking(path, headers_to_split_on, test[0], test[1])
Así es como se ve la salida completa. Ahora echemos un vistazo a las salidas individuales. Recuerda que nuestra pregunta de ejemplo elegida es: "¿Qué hace que un ingeniero sea distinguido?"
Longitud 32, solapamiento 4
Bien, entonces a partir de esto, podemos ver claramente que 32 es demasiado corto. Esta oración es completamente inútil. “Es un Ingeniero Distinguido” es el razonamiento circular más evidente posible.
Longitud 64, solapamiento 8
64 y 8 no es mucho mejor desde el principio. Aunque sí nos da un ejemplo de un ingeniero distinguido. Werner Vogels, CTO de Amazon.
Longitud 128, solapamiento 16
Con 128, empezamos a ver oraciones más completas. Menos palabras y respuestas del tipo “ingeniero.”. Esto no está mal, logra extraer la parte sobre Werner Vogel y “Ha logrado notables logros técnicos y profesionales mientras trabajaba como ingeniero.” La última entrada en realidad proviene de la sección de ingeniero principal.
Una desventaja aquí es que ya vemos aparecer ejemplos de estos caracteres especiales como \xa0 y \n. Esto nos indica que quizá estamos yendo demasiado lejos con la longitud de los chunks.
Longitud 256, solapamiento 32
Creo que esta longitud de chunk es definitivamente demasiado larga. Extrae las entradas requeridas, pero también extrae entradas de “Fellow”, “Ingeniero Principal” e “Ingeniero Senior Staff”. Sin embargo, la primera entrada es de Ingeniero Distinguido, y cubre tres puntos sobre ello.
Longitud de chunk 512, solapamiento 64
Ya establecimos que 256 probablemente es demasiado largo. Sin embargo, la primera extracción de este 512 es en realidad toda la sección de ingenieros distinguidos. Ahora tenemos un dilema: ¿queremos “líneas” o “notas” individuales, o extraer una sección completa? Eso depende de tu caso de uso.
Resumen de la experimentación con diferentes estrategias de chunking
Genial, entonces, vimos cinco estrategias diferentes de text splitter con un enfoque parametrizado que destaca estrategias de tamaño de chunks y solapamiento de chunks en este tutorial usando langchain chunking python. Uno de los dilemas que vimos con solo hacer estas 5 estrategias simples de chunks está entre obtener fragmentos individuales y recuperar una sección completa dependiendo del tamaño del chunk. Vimos que 128 era bastante bueno para obtener “líneas” o “notas” individuales sobre ingenieros distinguidos, pero que 512 podía recuperar toda nuestra sección.
Sin embargo, 256 no fue tan bueno.
Estos tres puntos de datos nos dicen algo sobre el text splitter. No es solo que encontrar un tamaño ideal de chunks sea difícil. También es una señal de que necesitas pensar en lo que quieres obtener de tus respuestas al diseñar tus tamaños de chunks.
Ten en cuenta que ni siquiera hemos llegado a probar diferentes solapamientos. Después de aprender y conseguir una buena estrategia de chunks, comprobar los solapamientos es el siguiente paso lógico. Quizá lo cubramos en un tutorial futuro, quizá con otra biblioteca. ¡Mantente atento!
Perspectivas de la experimentación
Estos experimentos destacan la relación matizada entre el tamaño del chunk y el solapamiento. Los chunks más pequeños destacan en tareas que requieren precisión milimétrica, mientras que los chunks más grandes son más adecuados para preguntas que exigen un contexto extenso. El solapamiento, por otro lado, desempeña un papel fundamental en equilibrar estos objetivos.
Vale la pena enfatizar que las compensaciones observadas aquí no son universales. Los parámetros ideales de chunking dependen en gran medida de tu caso de uso específico. Aplicaciones como agentes conversacionales o herramientas de consulta rápida pueden beneficiarse de chunks más pequeños para respuestas precisas. Por otro lado, los flujos de trabajo intensivos en investigación, como resumir documentos legales complejos, requieren chunks más grandes para garantizar un contexto completo.
En escenarios del mundo real, un enfoque híbrido a menudo puede proporcionar los mejores resultados. Al ajustar dinámicamente los tamaños de los fragmentos en función de la consulta y la intención del usuario, los desarrolladores pueden lograr un equilibrio entre eficiencia y calidad de recuperación. Estos enfoques pueden implicar añadir capas de enrutamiento inteligente de consultas o pipelines de fragmentación adaptativa adaptados a las necesidades del usuario.
Consideraciones Futuras
Los experimentos realizados en este tutorial son solo el comienzo. Las exploraciones futuras podrían examinar estrategias de fragmentación jerárquica que combinen diferentes tamaños de fragmentos para una segmentación multinivel. Además, expandirse a la recuperación de datos multimodales, como combinar embeddings de texto e imagen, abre posibilidades para casos de uso más complejos.
También existe potencial en experimentar con solapamientos de manera sistemática para comprender mejor cómo influyen en la recuperación en diversos conjuntos de datos. Integrar otros frameworks junto con LangChain podría refinar aún más los enfoques de fragmentación y descubrir nuevas técnicas de optimización. Por ejemplo, frameworks como Haystack ofrecen capacidades de recuperación adicionales que pueden complementar el flujo de trabajo de LangChain.
Finalmente, incorporar los comentarios de los usuarios en el proceso de recuperación podría dar lugar a sistemas adaptativos que aprendan y optimicen las estrategias de fragmentación con el tiempo. Con una experimentación más ajustada, es posible establecer pipelines RAG robustos y centrados en el usuario.
Conclusión
La fragmentación es un componente indispensable de los flujos de trabajo de recuperación en aplicaciones RAG. Este tutorial demostró la importancia de ajustar cuidadosamente el tamaño de los fragmentos y el solapamiento, mostrando cómo diferentes estrategias influyen en los resultados de recuperación.
Al equilibrar las compensaciones entre contexto y precisión, los desarrolladores pueden optimizar sus sistemas para una variedad de casos de uso. Si bien este tutorial cubrió experimentos fundamentales, las posibilidades de exploración adicional son enormes. Mantente atento a más tutoriales avanzados e información sobre cómo optimizar sistemas de recuperación impulsados por IA.
Referencias de Fragmentación
La fragmentación, o estrategias de divisores de texto, continúa evolucionando, por lo que hemos comenzado a crear una colección de estas diferentes estrategias para analizarlas y potencialmente implementarlas en tu aplicación. ¡Disfruta!
Una Guía de Estrategias de Fragmentación para Generación Aumentada por Recuperación (RAG). Exploramos diversas facetas de las estrategias de fragmentación dentro de los sistemas de Generación Aumentada por Recuperación (RAG) en esta guía.
Una Guía para Principiantes sobre Fragmentación e Incrustación de Sitios Web para tus Aplicaciones RAG. En esta publicación, explicaremos cómo extraer contenido de un sitio web y usarlo como contexto para LLMs en una aplicación RAG. Sin embargo, antes de hacerlo, necesitamos comprender los fundamentos de los sitios web.
Explorando Tres Estrategias Clave para Crear Generación Aumentada por Recuperación (RAG) Eficiente. La Generación Aumentada por Recuperación (RAG) es una técnica útil para usar tus propios datos en un Chatbot impulsado por IA. Esta publicación de blog te guiará a través de tres estrategias clave para aprovechar al máximo RAG.
Pandas DataFrame: Fragmentación y Vectorización con Milvus. Si almacenamos todos los datos, incluido el texto del fragmento y el embedding, dentro de Pandas DataFrame, podemos integrarlos e importarlos fácilmente en la base de datos vectorial Milvus.
Sigue leyendo

Why Context Engineering Is Becoming the Full Stack of AI Agents
Discover how context engineering unifies prompts, RAG, and tools to build smarter, production-ready AI agents powered by Milvus.

Our Journey to 35K+ GitHub Stars: The Real Story of Building Milvus from Scratch
Join us in celebrating Milvus, the vector database that hit 35.5K stars on GitHub. Discover our story and how we’re making AI solutions easier for developers.

What is the K-Nearest Neighbors (KNN) Algorithm in Machine Learning?
KNN is a supervised machine learning technique and algorithm for classification and regression. This post is the ultimate guide to KNN.



