Inicio / Artículos / Dividir Markdown para RAG: cinco experimentos con encabezados en LangChain

Dividir Markdown para RAG: cinco experimentos con encabezados en LangChain

Prueba MarkdownHeaderTextSplitter opción por opción: metadatos de encabezados, strip_headers, return_each_line y su encadenamiento con RecursiveCharacterTextSplitter para RAG.

1086 palabras

Trocear un archivo Markdown en ventanas de caracteres de tamaño fijo es una forma rápida de destrozar su estructura. Los encabezados acaban en un fragmento y sus párrafos en otro, y una lista queda cortada a mitad de un elemento. Si ese texto alimenta una canalización RAG, la recuperación empeora por ello. MarkdownHeaderTextSplitter de LangChain toma otro camino: corta en los encabezados y guarda como metadatos bajo qué encabezados estaba cada trozo de texto.

La forma más fácil de ver lo que hace es cambiar un ajuste cada vez sobre un documento diminuto y leer la salida. Eso es lo que viene a continuación.

Qué hace el divisor

La clase divide el texto Markdown allí donde aparecen los encabezados que indiques. Toma este documento en miniatura:

# Header 1

Here is the test content of Header 1

## Header 2

1. Content
2. Content
3. Content
4. Content

Las líneas # y ## dan forma al texto. El divisor trata esas líneas como puntos de corte y coloca el texto del encabezado en los metadatos de cada documento resultante. Para RAG es muy práctico, porque cada fragmento conserva de qué parte del archivo original procede.

El archivo de prueba

Todos los experimentos de abajo usan el mismo archivo, simple_water_cycle.md, una página deliberadamente pequeña sobre el ciclo del agua. Tiene dos niveles de encabezado. Un encabezado de primer nivel, "The Water Cycle", abre la página y le sigue una frase introductoria: el ciclo del agua describe cómo se mueve el agua por el medio ambiente. Bajo un encabezado de segundo nivel, "Main Stages", aparecen cuatro etapas numeradas (evaporación, condensación, precipitación y recogida) y después una frase final que dice que el ciclo se repite continuamente. Los nombres de las etapas van en negrita.

Preparación

Empieza con pathlib y lee el archivo mediante su objeto Path. Luego define la lista de encabezados por los que dividir. Cada entrada es un par: primero el símbolo de Markdown y después la clave de metadatos con la que guardar ese encabezado. Aquí los pares son # asociado a "Header 1" y ## asociado a "Header 2". Hora de experimentar.

Experimento 1: los valores por defecto

Ejecuta el divisor sin opciones adicionales. El resultado son dos objetos Document.

El primero contiene la frase de introducción, con los metadatos {'Header 1': 'The Water Cycle'}. El segundo contiene las cuatro etapas más la frase final, y sus metadatos llevan ambos niveles: {'Header 1': 'The Water Cycle', 'Header 2': 'Main Stages'}.

Llaman la atención dos cosas. Primero, las líneas de encabezado no están dentro de page_content por defecto. El texto # The Water Cycle nunca aparece en el contenido del primer documento, porque esa información vive en los metadatos. Segundo, el encabezado padre acompaña al hijo. Los metadatos de la sección "Main Stages" incluyen "Header 1" aunque ese encabezado esté por encima. Así que el divisor hace algo más que cortar texto. Mantiene viva la jerarquía de encabezados del documento a través de los metadatos.

Experimento 2: conservar los encabezados en el texto

Los encabezados se eliminan del contenido salvo que digas lo contrario. Poner strip_headers=False lo cambia. Ahora el primer documento empieza con # The Water Cycle seguido de la frase de introducción, y el segundo empieza con ## Main Stages y después su lista.

Los puntos de corte y los metadatos siguen siendo idénticos. Solo cambia que el texto del encabezado esté dentro de page_content. Por defecto un encabezado va solo a los metadatos; con esta opción va a los metadatos y al contenido. Es lo que quieres cuando las palabras reales del encabezado deben viajar con el texto hacia lo que lo consuma después, como un modelo de embeddings o un prompt.

Experimento 3: return_each_line

Vuelve al comportamiento por defecto de los encabezados y activa solo return_each_line=True. La salida pasa de dos documentos a tres. El primero sigue siendo la introducción. El segundo son las cuatro etapas numeradas. El tercero es la frase final sobre la repetición del ciclo. Los documentos dos y tres llevan los metadatos completos de "Header 1" y "Header 2".

No lo interpretes como un documento por cada línea física. Los cuatro elementos de la lista se mantienen juntos en un solo bloque, mientras que el último párrafo se convierte en su propio documento. Lo que obtienes son bloques de contenido más finos, cada uno con los metadatos de encabezado correctos.

Experimento 4: las dos opciones a la vez

Ahora combina strip_headers=False con return_each_line=True. Vuelven cinco documentos, en este orden: el encabezado superior, el párrafo de introducción, el encabezado de segundo nivel, la lista de etapas y el párrafo final. Los encabezados se convierten en documentos independientes, con page_content='# The Water Cycle' y page_content='## Main Stages', y siguen llevando metadatos.

Las dos opciones hacen trabajos distintos. Una decide si los encabezados se quedan en el contenido. La otra decide lo finos que son los bloques. Juntas, convierten los encabezados en documentos propios.

Cómo se hereda la jerarquía

La lista de encabezados usada en todo momento fue # y ##, y el documento se anida así: "The Water Cycle" arriba y "Main Stages" debajo. Los metadatos reflejan ese anidamiento. El texto que está justo bajo el encabezado superior recibe solo {'Header 1': 'The Water Cycle'}. El texto bajo "Main Stages" recibe las dos claves. En pocas palabras, una sección hija hereda los metadatos de su encabezado padre, una de las propiedades más útiles de dividir de esta manera.

Experimento 5: añadir un segundo divisor

Una sección obtenida al dividir por encabezados puede seguir siendo demasiado grande para un sistema RAG. La solución es pasar estos Document a RecursiveCharacterTextSplitter, que los corta en trozos más pequeños. Cada trozo pequeño sigue sabiendo a qué sección pertenece, y esos metadatos ayudan más adelante cuando necesitas entender el contexto de un fragmento recuperado.

Con el archivo de ejemplo, el párrafo de introducción sigue siendo un único Document. Las cuatro etapas numeradas, que eran un solo bloque, se convierten en cuatro Document separados, uno para la evaporación, otro para la condensación, otro para la precipitación y otro para la recogida. Cada uno conserva los metadatos "Header 1" y "Header 2". El orden importa: primero el divisor de Markdown aprovecha la estructura del documento y después el divisor recursivo reduce los resultados.

Conclusiones

Con un solo archivo pequeño has visto que MarkdownHeaderTextSplitter divide por encabezados y guarda su jerarquía como metadatos. strip_headers controla si el texto del encabezado se queda en el contenido, y return_each_line produce Document más finos. Combinarlo con RecursiveCharacterTextSplitter da fragmentos más pequeños sin perder la información de la sección. Para Markdown estructurado que va a parar a RAG, esa combinación te da un tamaño de fragmento manejable y el contexto de cada pieza.