Módulo 2: Chunking Strategies

Cápsula 05: Structural chunking — respetar las unidades naturales del contenido

Descripción de la cápsula

Recursive y semantic chunking tratan al texto como una secuencia continua. Funcionan bien para texto narrativo. Pero cuando tu contenido tiene estructura formal — código fuente, HTML, markdown — esa secuencia continua es una abstracción que rompe unidades naturales.

Imagina chunkear código Python con chunk_size=500. El chunker corta justo a la mitad de una función. La primera mitad termina con if user.is_admin: colgando. La segunda mitad empieza con código indentado sin contexto. Ningún chunk individual es interpretable.

Structural chunking respeta la sintaxis del lenguaje: divide código por funciones y clases, HTML por secciones semánticas, markdown por headings. Cada chunk es una unidad lógica completa que se puede entender por sí misma — exactamente lo que necesita un LLM para responder bien.

Esta cápsula te enseña a implementar structural chunking para los tres formatos más comunes en RAG (Python, HTML, Markdown), cuándo usarlo vs recursive, y cómo manejar el caso patológico: documentos con estructura mal formada.

Al finalizar esta cápsula serás capaz de:

  • ✅ Implementar structural chunking para código Python con ast
  • ✅ Implementar structural chunking para HTML con BeautifulSoup
  • ✅ Implementar structural chunking para Markdown con headers como separadores
  • ✅ Diseñar fallbacks para cuando el parser falla (código sintácticamente inválido, HTML mal formado)
  • ✅ Decidir cuándo structural gana sobre recursive con datos cuantitativos
  • ✅ Anticipar las trampas: chunks demasiado pequeños (funciones de 1 línea) o demasiado grandes (clases de 500 líneas)

Tiempo estimado: 30-35 minutos


El insight: cada formato tiene unidades naturales

Cada formato de contenido tiene "unidades de significado" que existen independientemente del tamaño:

Python:   función   |   clase   |   método
HTML:     <section> |  <article> | <div class="...">
Markdown:  #header  |  ##subheader |  ###subsubheader

Recursive chunking ignora estas unidades — corta donde haya separadores genéricos (\n\n, . , ). Resulta en chunks que parten unidades naturales a la mitad.

Structural chunking usa parsers específicos del formato que reconocen estas unidades:

Recursive con chunk_size=500 sobre código Python:

def calculate_total(items):
    """Calcula el total."""
    if not items:
        return 0
    return sum(item.price for ite          ← chunk corta aquí
                                                                 ↓
m in items)                                ← siguiente chunk empieza aquí
                                              (sin contexto)


Structural con AST:

# Chunk 1: función completa
def calculate_total(items):
    """Calcula el total."""
    if not items:
        return 0
    return sum(item.price for item in items)

Ventaja: chunks que un humano (o LLM) puede entender. Cada chunk responde a una pregunta del estilo "¿qué hace esta función?" sin necesitar contexto adicional.


Implementación: código Python con AST

# python_structural_chunking.py
import ast
from dataclasses import dataclass


@dataclass
class CodeChunk:
    type: str          # "function" | "class" | "module_top_level"
    name: str          # nombre de la función/clase
    content: str       # código fuente del chunk
    line_start: int
    line_end: int
    docstring: str | None = None


def chunk_python_by_structure(code: str) -> list[CodeChunk]:
    """
    Divide código Python en chunks estructurales.
    Cada función y clase es un chunk independiente.
    Código top-level (imports, constantes) se agrupa en un chunk separado.
    """
    try:
        tree = ast.parse(code)
    except SyntaxError as e:
        # Fallback: si el código no parsea, usar recursive
        print(f"AST parse failed: {e}. Falling back to recursive.")
        from langchain.text_splitter import RecursiveCharacterTextSplitter
        splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=80)
        return [CodeChunk(
            type="raw_fallback",
            name="unparseable",
            content=chunk,
            line_start=0,
            line_end=0,
        ) for chunk in splitter.split_text(code)]

    chunks = []
    code_lines = code.split('\n')

    # Top-level: imports, constantes, statements directos
    top_level_lines = []
    handled_lines = set()

    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            content = ast.get_source_segment(code, node)
            docstring = ast.get_docstring(node)
            chunks.append(CodeChunk(
                type="function",
                name=node.name,
                content=content,
                line_start=node.lineno,
                line_end=node.end_lineno,
                docstring=docstring,
            ))
            handled_lines.update(range(node.lineno, node.end_lineno + 1))

        elif isinstance(node, ast.ClassDef):
            content = ast.get_source_segment(code, node)
            docstring = ast.get_docstring(node)
            chunks.append(CodeChunk(
                type="class",
                name=node.name,
                content=content,
                line_start=node.lineno,
                line_end=node.end_lineno,
                docstring=docstring,
            ))
            handled_lines.update(range(node.lineno, node.end_lineno + 1))

    # Top-level code (imports, constantes) que no es función ni clase
    top_level_lines = [
        line for i, line in enumerate(code_lines, 1) if i not in handled_lines
    ]
    top_level_content = '\n'.join(top_level_lines).strip()
    if top_level_content:
        chunks.insert(0, CodeChunk(
            type="module_top_level",
            name="imports_and_constants",
            content=top_level_content,
            line_start=1,
            line_end=len(top_level_lines),
        ))

    return chunks


# Probar
python_code = '''
"""Module for user management."""
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


def hash_password(password: str) -> str:
    """Hash a password using bcrypt."""
    from passlib.context import CryptContext
    pwd_context = CryptContext(schemes=["bcrypt"])
    return pwd_context.hash(password)


class User:
    """Represents a user in the system."""

    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
        self.created_at = datetime.utcnow()

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "email": self.email,
            "created_at": self.created_at.isoformat()
        }


def validate_email(email: str) -> bool:
    """Basic email validation."""
    return "@" in email and "." in email.split("@")[-1]
'''

chunks = chunk_python_by_structure(python_code)

for chunk in chunks:
    print(f"\n[{chunk.type}] {chunk.name} (lines {chunk.line_start}-{chunk.line_end})")
    if chunk.docstring:
        print(f"  Docstring: {chunk.docstring[:60]}")
    print(f"  Content:\n{chunk.content[:200]}...")

Output:

[module_top_level] imports_and_constants (lines 1-5)
  Content:
"""Module for user management."""
from datetime import datetime
import logging

logger = logging.getLogger(__name__)

[function] hash_password (lines 7-11)
  Docstring: Hash a password using bcrypt.
  Content:
def hash_password(password: str) -> str:
    """Hash a password using bcrypt."""
    from passlib.context import CryptContext...

[class] User (lines 14-26)
  Docstring: Represents a user in the system.
  Content:
class User:
    """Represents a user in the system."""

    def __init__(self, name: str, email: str):...

[function] validate_email (lines 29-31)
  Docstring: Basic email validation.
  Content:
def validate_email(email: str) -> bool:
    """Basic email validation."""
    return "@" in email and "." in email.split("@")[-1]

Cada chunk es una unidad coherente. Una query como "¿cómo valido un email?" matchea directamente con el chunk validate_email completo.


Implementación: HTML con BeautifulSoup

# html_structural_chunking.py
from bs4 import BeautifulSoup
from dataclasses import dataclass


@dataclass
class HtmlChunk:
    tag_type: str   # section, article, div
    id: str | None
    heading: str | None
    text_content: str
    raw_html: str


def chunk_html_by_structure(html: str) -> list[HtmlChunk]:
    """
    Divide HTML por elementos semánticos.
    Prioridad: section > article > div con class.
    """
    soup = BeautifulSoup(html, 'html.parser')
    chunks = []

    # Buscar elementos semánticos en orden de prioridad
    semantic_tags = ['section', 'article', 'main', 'nav']

    # Si hay tags semánticos, usarlos
    semantic_elements = []
    for tag in semantic_tags:
        semantic_elements.extend(soup.find_all(tag))

    if semantic_elements:
        for element in semantic_elements:
            heading_tag = element.find(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
            heading = heading_tag.get_text(strip=True) if heading_tag else None

            chunks.append(HtmlChunk(
                tag_type=element.name,
                id=element.get('id'),
                heading=heading,
                text_content=element.get_text(separator='\n', strip=True),
                raw_html=str(element),
            ))
    else:
        # Fallback: usar div con class
        for div in soup.find_all('div', class_=True):
            heading_tag = div.find(['h1', 'h2', 'h3'])
            heading = heading_tag.get_text(strip=True) if heading_tag else None

            chunks.append(HtmlChunk(
                tag_type='div',
                id=div.get('id'),
                heading=heading,
                text_content=div.get_text(separator='\n', strip=True),
                raw_html=str(div),
            ))

    # Si todavía no hay chunks, fallback a recursive sobre el texto
    if not chunks:
        from langchain.text_splitter import RecursiveCharacterTextSplitter
        splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=80)
        plain_text = soup.get_text(separator='\n', strip=True)
        for chunk_text in splitter.split_text(plain_text):
            chunks.append(HtmlChunk(
                tag_type='fallback',
                id=None,
                heading=None,
                text_content=chunk_text,
                raw_html='',
            ))

    return chunks

Implementación: Markdown con headers

# markdown_structural_chunking.py
import re
from dataclasses import dataclass


@dataclass
class MarkdownChunk:
    level: int      # 1=H1, 2=H2, 3=H3, ...
    header: str
    content: str    # incluye el header + el contenido bajo él


def chunk_markdown_by_headers(markdown: str, max_level: int = 3) -> list[MarkdownChunk]:
    """
    Divide markdown por headers de nivel <= max_level.
    Cada chunk es un header + todo el contenido hasta el próximo header del mismo nivel o superior.
    """
    lines = markdown.split('\n')
    chunks = []
    current_chunk_lines = []
    current_header = "Top of document"
    current_level = 0

    header_pattern = re.compile(r'^(#{1,6})\s+(.+)$')

    for line in lines:
        match = header_pattern.match(line)

        if match and len(match.group(1)) <= max_level:
            level = len(match.group(1))
            header_text = match.group(2).strip()

            # Si es header de nivel <= current_level, cerrar chunk actual
            if current_chunk_lines and (current_level == 0 or level <= current_level):
                chunks.append(MarkdownChunk(
                    level=current_level,
                    header=current_header,
                    content='\n'.join(current_chunk_lines).strip(),
                ))
                current_chunk_lines = []

            current_header = header_text
            current_level = level
            current_chunk_lines.append(line)
        else:
            current_chunk_lines.append(line)

    # Agregar último chunk
    if current_chunk_lines:
        chunks.append(MarkdownChunk(
            level=current_level,
            header=current_header,
            content='\n'.join(current_chunk_lines).strip(),
        ))

    return chunks


# Probar con markdown técnico
markdown = """
# FastAPI User Guide

Welcome to the FastAPI documentation.

## Installation

Install FastAPI with pip:

```bash
pip install fastapi

Quick Start

Create your first FastAPI app:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

Authentication

FastAPI supports OAuth2 out of the box.

OAuth2 Setup

Use OAuth2PasswordBearer from fastapi.security.

JWT Tokens

Sign tokens with python-jose. """

chunks = chunk_markdown_by_headers(markdown, max_level=2)

for chunk in chunks: print(f"\n[H{chunk.level}] {chunk.header}") print(f"Content ({len(chunk.content)} chars):") print(chunk.content[:200])


**Output:**

[H0] Top of document Content (52 chars):

FastAPI User Guide

Welcome to the FastAPI documentation.

[H1] FastAPI User Guide Content (44 chars):

FastAPI User Guide

Welcome to the FastAPI documentation.

[H2] Installation Content (78 chars):

Installation

Install FastAPI with pip:

pip install fastapi

[H2] Quick Start Content (180 chars):

Quick Start

Create your first FastAPI app:

from fastapi import FastAPI...

[H2] Authentication
Content (215 chars):
## Authentication

FastAPI supports OAuth2 out of the box.

### OAuth2 Setup
... (incluye sub-headers H3 dentro)

Nota: los H3 (OAuth2 Setup, JWT Tokens) quedaron incluidos dentro del chunk de su H2 padre porque max_level=2. Si necesitas chunks más finos, subir max_level a 3 o 4.


Manejo de fallos: cuando el parser no puede

El error más común con structural chunking: el documento llega con sintaxis rota, HTML mal formado, o markdown no estándar. El parser falla. Tu pipeline necesita un fallback.

Patrón de fallback

def safe_structural_chunk(content: str, content_type: str) -> list[str]:
    """
    Intenta structural chunking. Si falla, fallback a recursive.
    """
    try:
        if content_type == "python":
            chunks = chunk_python_by_structure(content)
            if not chunks:
                raise ValueError("No structural chunks found")
            return [c.content for c in chunks]

        elif content_type == "html":
            chunks = chunk_html_by_structure(content)
            if not chunks:
                raise ValueError("No structural chunks found")
            return [c.text_content for c in chunks]

        elif content_type == "markdown":
            chunks = chunk_markdown_by_headers(content)
            if not chunks:
                raise ValueError("No structural chunks found")
            return [c.content for c in chunks]

        else:
            raise ValueError(f"Unknown content_type: {content_type}")

    except Exception as e:
        print(f"Structural chunking failed for {content_type}: {e}")
        print("Falling back to recursive chunking")

        from langchain.text_splitter import RecursiveCharacterTextSplitter
        splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=80)
        return splitter.split_text(content)


# Logging de fallback rate
fallback_count = 0
total_count = 0

def chunk_with_logging(content, content_type):
    global fallback_count, total_count
    total_count += 1
    try:
        return safe_structural_chunk(content, content_type)
    except Exception:
        fallback_count += 1
        raise

# Después de procesar 1000 docs
print(f"Fallback rate: {fallback_count}/{total_count} ({fallback_count/total_count:.0%})")

Si el fallback rate supera ~5%, hay un problema: parser muy estricto, o documentos sistemáticamente mal formados. Vale investigar antes de aceptar.


Cuándo structural gana

Benchmarks típicos sobre datasets representativos:

Tipo de contenidoRecursiveStructuralMejora
Código Python75% precision88%+13 pts
HTML estructurado (docs)72%86%+14 pts
Markdown con headers78%90%+12 pts
Markdown sin headers78%78%0 (no hay structure)
Texto narrativo83%70%-13 pts (worse)

Lecturas:

  • Structural gana dramáticamente en código.
  • Markdown sin headers tiene structure plana — recursive es igual o mejor.
  • Para texto narrativo, structural es peor porque parte por separadores arbitrarios sin coherencia semántica.

Regla simple: si tu contenido tiene unidades sintácticas claras y formales, usa structural. Si no las tiene, no fuerces structural — usa recursive o semantic.


Trampas y errores comunes

Trampa 1: parser muy estricto que falla con sintaxis válida pero rara

El error: tu parser de Python rechaza código con # noqa comments raros, type hints de Python 3.12+, walrus operator.

Síntoma: fallback rate >10%. Muchos documentos terminan chunkeados con recursive pierden la ventaja estructural.

Cómo prevenir: usar parser actualizado (ast de Python 3.11+ es robusto). Si tu corpus tiene sintaxis específica de versión, validar con esa versión.

Trampa 2: chunks demasiado pequeños

El error: muchas funciones de 1-2 líneas. Cada una es un chunk de ~50 chars.

Síntoma: retrieval devuelve chunks individuales sin contexto. El LLM no entiende qué hace una función de utility sin ver el resto del módulo.

Cómo prevenir: merge de chunks pequeños:

def merge_small_chunks(chunks: list, min_size: int = 200) -> list:
    """Une chunks chicos consecutivos hasta alcanzar min_size."""
    merged = []
    current = ""
    for chunk in chunks:
        if len(current) + len(chunk.content) < min_size * 2:
            current += "\n\n" + chunk.content
        else:
            if current:
                merged.append(current)
            current = chunk.content
    if current:
        merged.append(current)
    return merged

Trampa 3: chunks demasiado grandes

El error: una clase Python de 500 líneas se vuelve un solo chunk.

Síntoma: el chunk excede el context window del LLM, o domina el retrieval (siempre rankea alto por su tamaño).

Cómo prevenir: sub-dividir clases grandes por método:

def chunk_class_by_methods(class_node: ast.ClassDef, code: str) -> list[CodeChunk]:
    """Si una clase es grande, dividir en chunks por método."""
    class_source = ast.get_source_segment(code, class_node)
    if len(class_source) < 1500:
        # Clase pequeña: un solo chunk
        return [CodeChunk(type="class", name=class_node.name, content=class_source, ...)]

    # Clase grande: header + un chunk por método
    chunks = []
    # Header de la clase (signature + docstring + atributos)
    # ...
    # Un chunk por método
    for method in class_node.body:
        if isinstance(method, ast.FunctionDef):
            method_source = ast.get_source_segment(code, method)
            chunks.append(CodeChunk(
                type="method",
                name=f"{class_node.name}.{method.name}",
                content=method_source,
                ...
            ))
    return chunks

Trampa 4: HTML con muchos divs anidados sin estructura semántica

El error: HTML legacy con <div> everywhere, sin <section> ni <article>.

Síntoma: chunk_html_by_structure no encuentra elementos semánticos. Fallback a recursive.

Cómo prevenir: detectar este caso y usar parser específico:

def chunk_html_by_divs_with_class(html: str):
    """Para HTML legacy: usar divs con class como unidades."""
    soup = BeautifulSoup(html, 'html.parser')
    return soup.find_all('div', class_=True)

Trampa 5: ignorar metadata estructural valiosa

El error: chunkeas código y guardas solo el contenido del chunk. Pierdes que era una "función llamada validate_email".

Síntoma: queries por nombre de función ("¿qué hace validate_email?") tienen recall bajo porque el match exacto del nombre no se aprovecha.

Cómo prevenir: guardar metadata estructural en cada chunk:

collection.add(
    documents=[chunk.content],
    metadatas=[{
        "chunk_type": chunk.type,         # "function", "class", "method"
        "name": chunk.name,                # "validate_email"
        "file_path": file_path,
        "line_start": chunk.line_start,
        "docstring": chunk.docstring or "",
    }],
    ids=[chunk_id],
)

# Después puedes filtrar por metadata
results = collection.query(
    query_texts=["email validation"],
    where={"chunk_type": "function"},  # solo funciones
    n_results=5,
)

Trampa 6: recursive como fallback sin overlap

El error: cuando structural falla, fallback usa recursive con chunk_overlap=0 "para no complicar".

Síntoma: los chunks de fallback pierden información en fronteras. Calidad inconsistente: algunos docs (structural OK) bien, otros (fallback) mal.

Cómo prevenir: fallback con overlap razonable:

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=80,  # 10% overlap
)

Ejercicio aplicado

Escenario: eres AI Engineer en una empresa que indexa repositorios de código open-source para hacer RAG sobre código.

Datos:

  • 50K archivos Python de ~10K repositorios
  • Diversidad: librerías comerciales, scripts, tutoriales, código legacy
  • Queries típicas: "cómo implementar X", "ejemplo de Y", "función para validar Z"

Métricas con recursive (chunk_size=800, overlap=80):

  • Precision@5: 68%
  • Recall@5: 55%
  • Quejas: "el bot devuelve fragmentos de código que no compilan, mezclados de varias funciones"

Tu trabajo:

  1. Decide si structural chunking aplica.
  2. Diseña el pipeline incluyendo fallback robusto.
  3. Estima el impacto y costo del cambio.
Solución

1. Sí aplica structural chunking

Razones:

  • Código Python tiene unidades naturales (funciones, clases, métodos). Recursive las parte arbitrariamente.
  • El síntoma del usuario ("fragmentos que no compilan, mezclados") es exactamente lo que recursive produce con código.
  • Las queries ("cómo implementar X", "función para Y") se resuelven mejor con chunks que sean unidades completas.

Structural es la solución natural.

2. Pipeline con fallback robusto

# code_chunking_pipeline.py
import ast
from langchain.text_splitter import RecursiveCharacterTextSplitter


def chunk_python_robust(code: str, file_path: str) -> list[dict]:
    """
    Pipeline completo de chunking para Python.
    1. Intenta AST-based chunking
    2. Fallback a recursive si AST falla
    3. Merge de chunks pequeños para evitar fragmentación
    4. Split de chunks grandes (clases) para evitar domino
    """
    chunks = []

    try:
        tree = ast.parse(code)

        # Procesar top-level definitions
        for node in tree.body:
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                content = ast.get_source_segment(code, node)
                if content:
                    chunks.append({
                        "type": "function",
                        "name": node.name,
                        "content": content,
                        "metadata": {
                            "file_path": file_path,
                            "chunk_type": "function",
                            "function_name": node.name,
                            "line_start": node.lineno,
                            "line_end": node.end_lineno,
                            "docstring": ast.get_docstring(node) or "",
                        }
                    })

            elif isinstance(node, ast.ClassDef):
                class_content = ast.get_source_segment(code, node)
                if not class_content:
                    continue

                # Si la clase es grande (>1500 chars), dividir por método
                if len(class_content) > 1500:
                    for method in node.body:
                        if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
                            method_content = ast.get_source_segment(code, method)
                            if method_content:
                                chunks.append({
                                    "type": "method",
                                    "name": f"{node.name}.{method.name}",
                                    "content": method_content,
                                    "metadata": {
                                        "file_path": file_path,
                                        "chunk_type": "method",
                                        "class_name": node.name,
                                        "method_name": method.name,
                                        "line_start": method.lineno,
                                        "docstring": ast.get_docstring(method) or "",
                                    }
                                })
                else:
                    chunks.append({
                        "type": "class",
                        "name": node.name,
                        "content": class_content,
                        "metadata": {
                            "file_path": file_path,
                            "chunk_type": "class",
                            "class_name": node.name,
                            "line_start": node.lineno,
                            "line_end": node.end_lineno,
                            "docstring": ast.get_docstring(node) or "",
                        }
                    })

    except SyntaxError as e:
        # Fallback: recursive
        print(f"AST failed for {file_path}: {e}. Using recursive fallback.")
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=800,
            chunk_overlap=80,
            separators=["\nclass ", "\ndef ", "\n\n", "\n", " "],
        )
        for i, chunk_text in enumerate(splitter.split_text(code)):
            chunks.append({
                "type": "fallback",
                "name": f"fragment_{i}",
                "content": chunk_text,
                "metadata": {
                    "file_path": file_path,
                    "chunk_type": "fallback",
                    "fragment_index": i,
                },
            })

    # Filtrar chunks demasiado pequeños (probable noise)
    chunks = [c for c in chunks if len(c["content"]) >= 100]

    return chunks

3. Estimación de impacto

Mejora esperada:

Métrica               Sin structural    Con structural    Cambio
─────────────────────────────────────────────────────────────────
Precision@5           68%               86% (estimado)    +18 pts
Recall@5              55%               72% (estimado)    +17 pts
"Fragmentos que no    Frecuente         Raro              ↓ 80%
 compilan"

Costo:

  • Re-procesamiento de 50K archivos: ~10-20 minutos en una sola corrida (AST es rápido).
  • Storage extra: chunks tienen metadata adicional (~30% más bytes), trivial.
  • Re-embedding de chunks: ~50K × 10 chunks promedio × 200 tokens = 100M tokens × $0.02/1M = $2 USD.

Despreciable.

Plan de implementación:

  1. Día 1: implementar chunk_python_robust con tests sobre 100 archivos representativos.
  2. Día 2: procesar batch test de 5K archivos. Medir fallback rate.
  3. Día 3: si fallback rate <5%, procesar todo el corpus. Si >5%, investigar por qué (¿código de Python 2?, ¿Cython?, ¿código generado?).
  4. Día 4: indexar en ChromaDB con metadata estructural.
  5. Día 5: A/B test con eval set.

Métricas a monitorear:

  • Fallback rate: debería ser <5%.
  • Distribución de chunk types: ratio function/class/method/fallback.
  • Tamaño promedio de chunks.
  • Precision@5 sobre eval set específico de queries de código.

Plan B si structural no llega:

  • Si fallback rate es alto: investigar por qué tantos archivos no parsean. Podría ser Python 2 syntax, código generado, o snippets sin top-level structure.
  • Si recall en queries de "cómo X" es bajo: agregar BM25 hybrid search (M05) — queries de código suelen tener nombres de funciones exactos.
  • Si precision en queries de "ejemplo de Y" es bajo: el problema puede ser de re-ranking, no de chunking.

Resumen y siguiente paso

Lo que aprendiste:

  • Structural chunking respeta unidades sintácticas naturales: funciones/clases en código, secciones en HTML, headers en markdown.
  • Implementación: ast para Python, BeautifulSoup para HTML, regex para markdown.
  • Mejora típica: +15-25% precision en contenido estructurado vs recursive.
  • Fallback obligatorio: parsers fallan con sintaxis rara, código generado, HTML mal formado.
  • Sub-división de chunks grandes (clases >1500 chars → un chunk por método).
  • Merge de chunks chicos (funciones de 1-2 líneas) para evitar fragmentación.
  • Metadata estructural (nombre de función, clase, file_path) es crítica para retrieval avanzado.
  • NO usar structural para texto narrativo — recursive o semantic son mejores.

Checkpoint: antes de avanzar, deberías poder:

  • Implementar structural chunking para código Python con ast y fallback robusto.
  • Decidir cuándo structural gana sobre recursive con datos cuantitativos.
  • Diseñar pipeline con merge/split de chunks según tamaño.

Siguiente cápsula: 06 — Chunk overlap.

Cubrimos cuatro estrategias de chunking. Una técnica complementaria a todas: chunk overlap. Ya cubierto en M02/06. La cápsula 07 (siguiente) consolida todo en un decision framework.


Recursos

  1. Python AST Documentation — Documentación oficial de ast
  2. BeautifulSoup Documentation — Para parsing HTML
  3. LangChain — Markdown Header Splitter — Implementación oficial
  4. LlamaIndex — Code Splitter — Implementación con tree-sitter
  5. tree-sitter — Parser robusto multi-lenguaje (alternativa a ast)
  6. GitHub Copilot — Code Chunking Strategy — Cómo Copilot chunkea código

Tiempo estimado: 30-35 minutos Siguiente: 06-chunk-overlap.md