Estrategias de segmentación en comparación de RAG: Alternativas, concesiones y ejemplos

Comparación de estrategias de segmentación en RAG

Índice

Chunking es el hiperparámetro más subestimado en Generación Aumentada por Recuperación (RAG): silenciosamente determina lo que ve tu LLM, cuán costosa se vuelve la ingesta, y cuánto del contexto de la LLM consumes por respuesta.

Este artículo trata el chunking como un problema de optimización de ingeniería: define objetivos, elige una estrategia, mide y luego itera.

Si eres nuevo en la arquitectura RAG, empieza con el principal Tutorial de Generación Aumentada por Recuperación (RAG): Arquitectura, Implementación y Guía de Producción.

tetris de colores en la mesa

TL;DR (Resumen ejecutivo)

Los sistemas RAG recuperan chunks, no documentos. Por lo tanto, el chunking define la unidad de recuperación, la unidad de costo de incrustación y la unidad de evidencia que puedes mostrar o citar. En la formulación original de RAG, la recuperación proporciona pasajes a la generación; los límites de los pasajes son efectivamente los límites de los chunks.

Una buena estrategia de chunking busca una frontera de Pareto entre: calidad de recuperación (recordatorio/precisión de la evidencia), coherencia (los chunks deben ser interpretables) y costo (incrustación, almacenamiento y latencia de consulta). No existe un tamaño óptimo global ni un método, y los sistemas de producción suelen mezclar estrategias (por ejemplo, chunking estructura-aware para PDFs + divisiones semánticas para prosa + chunking AST para código).

Para la mayoría de las “preguntas y respuestas de documentación” y bases de conocimiento internas, un valor predeterminado seguro es un splitter recursivo que respeta la estructura con un pequeño solapamiento (para reducir la pérdida en los bordes), respaldado por una base de datos vectorial con filtrado de metadatos y reordenamiento opcional. El RecursiveCharacterTextSplitter de LangChain es una implementación común de esta idea de separador jerárquico; el solapamiento existe específicamente para mitigar la pérdida de información cuando el contexto relevante se corta en los bordes.

Cuando los documentos tienen una estructura fuerte (PDFs con encabezados, tablas, listas, leyendas), el chunking basado en elementos / estructura-aware puede superar a la división por cuenta de tokens mientras produce menos chunks. Un estudio de 2024 sobre documentos de SEC encontró que el chunking basado en tipo de elemento mejoró los resultados de RAG y también redujo el número de chunks (y por lo tanto vectores) aproximadamente a la mitad en comparación con métodos sin estructura—reduciendo el costo de indexación y potencialmente mejorando la latencia de consulta.

Si puedes permitirte más cálculo previo, el chunking semántico (dividir en cambios de tema usando similitud de incrustación) puede mejorar materialmente la precisión de recuperación para texto narrativo y páginas de temas mixtos. Algoritmos antiguos de segmentación de temas como TextTiling muestran el principio general: los cambios fuertes en vocabulario/semántica son buenos candidatos para bordes.

Para materiales muy largos, internamente referenciados (políticas, RFCs, estándares, manuales grandes), el chunking jerárquico + recuperación/merging jerárquico (nodos padre/hijo) puede recuperar más contexto continuo en demanda. El analizador de nodos jerárquico de LlamaIndex produce jerarquías de chunks de grueso a fino, y el AutoMergingRetriever puede fusionar nodos hoja en nodos padre en tiempo de recuperación cuando se recuperan suficientes hijos relacionados.

Objetivos del chunking y equilibrios

El chunking no es solo “dividir texto para que quepa en un modelo de incrustación”. Controla varios comportamientos posteriores y operativos.

Granos de recuperación vs ruido de recuperación. Los chunks más pequeños aumentan la probabilidad de que la frase exacta que contiene la respuesta sea recuperable (mayor potencial de recordatorio a top-k fijo). Pero también producen más vectores, aumentando el tamaño del índice y a veces mostrando “cercanos coincidencias” que son semánticamente similares pero no realmente evidenciales (menor precisión). Los recuperadores densos como DPR se construyeron alrededor de recuperar pasajes eficazmente para QA, destacando que los límites de los pasajes importan para el rendimiento de QA end-to-end.

Coherencia del contexto vs pérdida en los bordes. Los chunks coherentes ayudan al LLM a razonar correctamente y reducir las alucinaciones al proporcionar contexto local completo (definiciones, restricciones, requisitos previos). El solapamiento reduce la pérdida en los bordes pero crea texto duplicado, lo que puede llevar a resultados de recuperación redundantes y alargamiento del prompt si no se deduplica/mergea.

Costo de incrustación e indexación. El costo de incrustación suele ser proporcional a los tokens incrustados, y el tiempo de ingesta escala con el número de chunks (más el sobrecarga de escritura de la base de datos vectorial). Para incrustaciones de OpenAI, las solicitudes tienen un límite máximo de token por entrada (8192 tokens para todos los modelos de incrustación) y un límite máximo de tokens totales sumados por entrada por solicitud (300,000 tokens). Para grandes corpora, la API por lotes puede reducir los costos en ~50% con un proceso asíncrono, con un tiempo de entrega de 24 horas—útil para actualizaciones retroactivas y reindexación periódica.

Tamaño del índice de vectores, RAM y latencia. Más chunks significa más vectores y potencialmente más memoria y consultas más lentas (dependiendo del tipo de índice). FAISS enmarca explícitamente el diseño del índice como un conjunto de equilibrios entre tiempo de búsqueda, calidad de búsqueda y memoria por vector indexado; también ofrece implementaciones de GPU para búsqueda rápida exacta y aproximada.

Longitud del prompt del LLM posterior / uso de la ventana de contexto. La salida del recuperador se convierte en presupuesto del prompt. Una estrategia de chunking que recuperar “justo lo suficiente” de contexto puede mejorar la calidad de la respuesta y reducir el costo. Por el contrario, el solapamiento y chunks demasiado grandes inflan la longitud del prompt. En la práctica, suelen sintonizar: (tamaño del chunk, solapamiento, top-k, reordenamiento/merge) juntos.

Costo de actualización/ingesta y deduplicación. El chunking afecta cuán caro es actualizar datos. Los chunks más pequeños hacen que las actualizaciones parciales sean más económicas (puedes re-embed solo la sección cambiada) pero también hacen que la deduplicación sea más difícil si proliferan chunks superpuestos o casi duplicados.

Dónde se encuentra el chunking en el flujo de trabajo RAG

chunking en flujo RAG

Estrategias de chunking y alternativas

A continuación se presentan las principales familias de chunking que encontrarás en RAG moderno. En la práctica, suelen mezclar dos: chunking basado en estructura (respetar los límites del documento) más enfoque de presupuesto de tokens (asegurar que los chunks se ajusten a tus presupuestos de incrustación y prompt).

Chunking de tamaño fijo

¿Qué es. Dividir el texto en bloques de tamaño igual por caracteres o tokens.

¿Por qué existe. Es simple, rápido, predecible y fácil de paralelizar. También es la estrategia más fácil para la ingesta en streaming donde no tienes el contexto completo del documento.

¿Dónde falla. Ignora los límites (oraciones, secciones, bloques de código) por lo que puede romper definiciones o dividir “parejas de pregunta/respuesta” entre chunks, aumentando el error de recuperación.

Perfil operativo. Menor complejidad de ingesta; cantidad de chunks predecible; más fácil de cachear. Pero generalmente necesitas solapamiento (abajo) para evitar la pérdida en los bordes.

Chunking con solapamiento

¿Qué es. Cualquier estrategia donde los chunks consecutivos comparten una región de solapamiento fijo (por ejemplo, 10–20% del tamaño del chunk). El solapamiento es estándar en muchos marcos porque reduce la pérdida de información cuando el contexto se divide.

¿Por qué importa. El solapamiento es efectivamente un “borde suave”—permite que la recuperación capture un hecho que cruza un borde.

Costos y trampas. Más tokens incrustados; más texto duplicado en el índice; mayor riesgo de recuperar múltiples chunks casi idénticos a menos que deduplique en tiempo de recuperación (por ejemplo, fusionar por desplazamientos de origen o usar MMR).

Chunking basado en oraciones y párrafos

¿Qué es. Dividir el texto en límites de oración o párrafo, luego empaquetar oraciones/párrafos en chunks hasta un presupuesto de tokens.

¿Por qué los ingenieros lo usan. Mejora la coherencia para el lenguaje natural y es robusto para documentos con puntuación y espaciado convencionales.

Herramientas. sent_tokenize() de NLTK usa por defecto la detección de límites de oración de Punkt, y spaCy ofrece herramientas basadas en reglas para límites de oración como Sentencizer (útil cuando quieres divisiones de oración sin un modelo de dependencia completo).

Modos de falla. Puntuación no convencional (registros, transcripciones de chat), tablas, código y listas con viñetas pueden romper las suposiciones de segmentación de oraciones.

Chunking de ventana deslizante

¿Qué es. Crear chunks usando un tamaño de ventana fijo y un paso (desplazamiento). Es la versión “sobreposición sistemática” del chunking.

¿Cuándo es bueno. Texto de series temporales, transcripciones, registros de chat, actas de reunión—cualquier cosa donde los hechos relevantes puedan aparecer en vecindarios locales y se desee una recuperación robusta.

¿Cuándo es malo. Amplifica la redundancia y puede ser costoso a gran escala. También tiende a recuperar ventanas redundantes a menos que se deduplique.

Chunking recursivo / jerárquico de separadores

¿Qué es. Comenzar con grandes “separadores naturales” (por ejemplo, \n\n para párrafos) y dividir recursivamente en unidades más pequeñas (oraciones, espacios) solo cuando sea necesario para mantenerse bajo un presupuesto de tamaño. LangChain documenta este comportamiento explícitamente: un splitter recursivo intenta mantener unidades más grandes intactas y solo recurre a separadores más pequeños cuando una unidad aún excede el tamaño del chunk.

¿Por qué es un buen valor predeterminado. Respeta la estructura sin requerir un análisis complejo del documento. Es un punto intermedio práctico para Markdown, HTML como texto y documentación.

Palancas clave de ajuste. chunk_size, chunk_overlap y la length_function (caracteres vs tokens), más separadores personalizados para repositorios de múltiples idiomas.

Chunking semántico (aware de incrustación)

¿Qué es. Detectar cambios de tema usando representaciones semánticas (incrustaciones) y dividir donde disminuye la similitud. Esto imita ideas clásicas de segmentación como TextTiling, que usa cambios en cohesión léxica para encontrar límites de subtemas.

¿Por qué puede superar al chunking basado en tamaño. Dejas de dividir en cuentas de token arbitrarias y alineas los chunks con límites de tema—a menudo mejorando la precisión de recuperación para documentos multitema (blogs, documentos de diseño, tickets, informes de incidentes).

Costos. Puedes necesitar incrustaciones adicionales durante el chunking (a nivel de oración o párrafo) antes de las incrustaciones finales de chunk. Eso puede duplicar o triplicar las llamadas de incrustación a menos que reutilices incrustaciones intermedias.

Truco práctico. “Empaquetado semántico-aware”: calcular incrustaciones de oración una vez, agrupar oraciones en segmentos coherentes de tema, luego incrustar cada segmento final.

Chunking jerárquico (padre/hijo)

¿Qué es. Construir una representación de múltiples granularidades: chunks gruesos padre (por ejemplo, tamaño de sección) con chunks más finos hijo (por ejemplo, tamaño de párrafo). El análisis jerárquico de nodos de LlamaIndex produce “jerarquías de grueso a fino” por defecto (por ejemplo, escalas de 2048 → 512 → 128 tokens), y el AutoMergingRetriever puede fusionar nodos hijo en padres en tiempo de recuperación cuando se recuperan suficientes hijos relacionados.

¿Por qué ayuda. Evita elegir entre “chunks pequeños para recordatorio” y “chunks grandes para coherencia” al almacenar ambos y seleccionar en tiempo de consulta.

Costos. Lógica más compleja de ingesta y recuperación, más potencialmente almacenamiento (porque almacenas múltiples granularidades).

Chunking adaptativo / basado en LLM

¿Qué es. Usar un LLM para decidir los límites de chunk (y opcionalmente generar resúmenes o encabezados contextuales). Weaviate describe explícitamente el chunking basado en LLM como tener el LLM crear chunks coherentes semánticamente, en lugar de depender de reglas fijas o similitud de incrustación.

¿Cuándo vale la pena. Corpora de alto valor donde la precisión domina el costo (legal, cumplimiento, runbooks de soporte), y donde los documentos son desordenados, heterogéneos y mal segmentados.

Riesgos. Costo, latencia y no determinismo. Querrás caché, decodificación determinística y pruebas de regresión (ver sección de evaluación).

Chunking basado en estructura y elementos (los documentos no son solo texto plano)

¿Qué es. Analizar el documento en elementos (títulos, párrafos, listas, tablas, leyendas) usando una capa de comprensión de documentos, luego chunkar usando esos elementos. Las funciones de chunking de Unstructured usan explícitamente metadatos y elementos de documento (producidos por particionamiento) para producir chunks para RAG. El HierarchicalChunker de Docling crea chunks por elemento detectado de documento y adjunta metadatos estructurales como encabezados/leyendas.

Evidencia de trabajo reciente. Un estudio de 2024 sobre documentos de SEC argumenta que el chunking solo por párrafos ignora la estructura del documento y propone chunking por elementos estructurales; informa resultados mejorados de RAG y menos chunks/vectores que en enfoques sin estructura.

¿Por qué importa para multimodal. Las tablas, figuras y leyendas suelen contener la verdad objetiva. “Aplanar"las en texto plano puede destruir señales que la recuperación de otro modo explotaría.

Chunking consciente del código (AST/estructura)

¿Qué es. Chunkar código por unidades sintácticas (funciones, clases, módulos), opcionalmente incluyendo docstrings y comentarios.

¿Por qué importa. Las divisiones de tamaño fijo de token tienden a cortar funciones a la mitad y separar docstrings de implementaciones—malo para búsqueda de código y casos de uso RAG “explica esta función”.

Opciones de implementación. Para Python, el módulo integrado ast suele ser suficiente. Para repositorios de múltiples idiomas, los chunkers basados en tree-sitter son comunes.

Dimensiones de evaluación y cómo comparar estrategias de chunking

El chunking debe evaluarse como un componente del sistema.

Métricas de calidad de recuperación

Usa métricas estándar de IR para la capa de recuperación:

  • Recall@k / Precision@k: ¿El top-k contiene la evidencia dorada?
  • MRR / nDCG: ¿La evidencia dorada se clasifica alta?

BEIR es una benchmark heterogéneo ampliamente utilizado para la evaluación de IR en tareas/dominios, y resalta los equilibrios entre enfoques dispersos, densos, de interacción tardía y reordenamiento.

El chunking afecta estas métricas porque define qué cuenta como “un elemento recuperado relevante”.

Métricas de calidad de respuesta RAG end-to-end

Si estás construyendo QA o asistentes, las métricas de recuperación son necesarias pero no suficientes. También necesitas:

  • Recordatorio / precisión del contexto: si los contextos recuperados contienen evidencia relevante y evitan el ruido.
  • Fiabilidad: si la respuesta generada está respaldada por el contexto recuperado.

RAGAS proporciona definiciones concretas e implementaciones para “fiabilidad” y otras métricas orientadas a RAG.

Dimensiones de costo y rendimiento del sistema

El chunking cambia estos palancas:

Latencia (p50/p95). La latencia de consulta suele aumentar con más vectores y más postprocesamiento. También importa tu índice vectorial: los tipos de índice de FAISS intercambian tiempo de búsqueda, calidad, memoria y tiempo de entrenamiento/adición.[^faiss]

Costo de incrustación y throughput. Las incrustaciones de OpenAI se facturan por tokens; la API de incrustaciones tiene límites explícitos por entrada y por solicitud.[^openai_embed_create] Para ingesta offline, la API por lotes reduce el costo y ofrece un mayor cupo a cambio de un tiempo de entrega no real tiempo.[^openai_batch]

Tamaño del índice y memoria. Aproximadamente, almacenar N vectores de flotante de 32 bits de dimensión d cuesta ~4 * N * d bytes solo para los vectores brutos (más metadatos + sobrecarga del índice). El chunking afecta N. La dimensionalidad de incrustación afecta d, y la API de incrustaciones de OpenAI permite controlar la dimensionalidad de salida mediante el parámetro dimensions.[^openai_embed_create]

Presupuesto del prompt del LLM. Los chunks más grandes y el solapamiento inflan los tokens del prompt. Esto puede aumentar la latencia y el costo, y aumentar los modos de falla estilo “perdido en el medio” donde los modelos prestan menos atención a algún contexto. En la práctica, suelen:

  1. recuperar chunks pequeños,
  2. fusionar/deduplicar,
  3. resumir opcionalmente,
  4. enviar un conjunto compacto de evidencia al LLM.

Costo de actualización/ingesta. Los chunks más pequeños permiten re-embedding parcial pero aumentan la contabilidad. Para ingesta en streaming, prefiera chunking determinístico, incremental (fijo o ventana deslizante) y adjunte IDs estables (document_id, rangos de desplazamiento, hash).

Diseño experimental: un bucle de benchmark pragmático

Un benchmark reproducible de chunking típicamente tiene:

  • Un snapshot fijo del corpus + un conjunto fijo de consultas con evidencia dorada (o al menos rangos de respuesta esperados).
  • Un modelo fijo de incrustación y una configuración fija del índice vectorial.
  • Una evaluación “solo recuperación” (recall@k, nDCG) más una evaluación “RAG” (fiabilidad, relevancia de la respuesta).
  • Telemetría de costo: #chunks, tokens incrustados, $/mes de almacenamiento, latencia de consulta p95, tokens del prompt.

El artículo Unstructured sobre SEC-filings es un buen ejemplo de evaluar estrategias de chunking con métricas orientadas a recuperación y medidas de precisión de QA.

Guías prácticas, matriz de decisión y valores predeterminados recomendados

Valores predeterminados recomendados que funcionan sorprendentemente bien

Si necesitas una estrategia robusta “día 1” para QA general de documentación:

  1. Parsear ligeramente: preservar encabezados y metadatos básicos (origen, título de sección, URL/ruta, marca de tiempo).
  2. Chunkar con un splitter de separador recursivo (párrafo → oración → palabra), con solapamiento moderado.
  3. Incrustar con un modelo de incrustación general fuerte.
  4. Indexar con metadatos (ID de documento, sección, ACLs) y deduplicar durante la recuperación.
  5. Agregar reordenamiento o merging jerárquico solo si tu evaluación muestra una brecha.

Esto se alinea con cómo los marcos RAG comunes describen el solapamiento de chunk y el splitting que respeta la estructura.

¿Qué método de chunking usar - Matriz de decisión

¿Qué método de chunking usar - Diagrama

Caso de uso Valor predeterminado de chunking recomendado Parámetros clave para ajustar Modo de falla común Camino de mejora
QA de documentos de corta forma (FAQs, wiki interno) Chunking recursivo/separador + solapamiento chunk_size, solapamiento, top-k Falta de evidencia entre oraciones en el borde Agregar chunking semántico o reordenador
QA de larga forma (políticas, estándares, manuales) Chunking jerárquico + recuperador de merging tamaños de padre/hijo, umbral de merge Recupera fragmentos pequeños; LLM carece de contexto completo Merging automático/recuperación jerárquica
Resumen (por documento / por sección) Chunking estructura-aware (secciones) detección de sección, tokens máximos Resúmenes pierden enlaces entre secciones Resumen jerárquico + gráfico de sección
Búsqueda de código y “explica esta función” Chunking a nivel de AST/función incluir docstring/comentarios, tokens máximos Función dividida; pierde firma/usos Jerarquía de repositorio (módulo→clase→función)
PDFs multimodales (tablas/figuras) Chunking basado en elementos (consciente de título/tabla/leyenda) serialización de tabla, fusión de leyenda Contenido de tabla perdido o corrompido Usar Docling/Unstructured + serializadores estructurados
Ingesta en streaming (registros, chats, tickets) Ventana deslizante o tokens de tamaño fijo ventana, paso, dedup Recuperación excesiva de ventanas redundantes Agregar detección de bordes semánticos en lotes

chunking - Comparación cualitativa de rendimiento

Trátalo como “dirección esperada del cambio” (valida con tus propios datos).

Estrategia Potencial de precisión de recuperación Coherencia del contexto recuperado Complejidad de ingesta Cantidad de vectores / tamaño del índice Costo de incrustación Impacto en latencia de consulta Mejor para
Tamaño fijo (sin solapamiento) Medio Bajo Bajo Medio Bajo Medio prototipos rápidos, texto homogéneo
Tamaño fijo + solapamiento Medio–Alto Medio Bajo Alto Medio–Alto Medio–Alto QA donde la pérdida en el borde afecta
Empaquetado de oraciones/párrafos Alto (prosa) Alto Medio Medio Medio Medio documentos, artículos, prosa limpia
Ventana deslizante Alto recordatorio Medio Medio Muy alto Muy alto Alto transcripciones, registros, chat
Recursivo/separador Alto Alto Medio Medio Medio Medio “predeterminado” para RAG de documentos
Chunking semántico Alto–Muy alto Alto Alto Medio Alto Medio páginas multitema, texto narrativo
Jerárquico (padre/hijo) Muy alto Muy alto Alto Alto Alto Medio manuales largos / estándares
Basado en LLM/Adaptativo Muy alto Muy alto Muy alto Medio Muy alto Medio–Alto corpora de alto riesgo
Basado en elementos/estructura Alto–Muy alto Alto Alto Bajo–Medio Medio Medio PDFs, informes, tablas, diseños mixtos
Consciente del código (AST) Alto (código) Alto Medio Medio Medio Medio búsqueda de código, asistentes de repositorio

Notas de DevOps y hardware (a menudo ignoradas)

Las elecciones de chunking afectan cuánta infraestructura necesitas:

  • Chunking más pequeño → más vectores → índices más grandes y más RAM/disco. Para FAISS autohospedado, esto puede forzar el shard o índices basados en disco.
  • Si incrustas localmente, el throughput de incrustación se vuelve un problema de programación de GPU; si incrustas mediante API, el volumen de tokens se vuelve un problema de FinOps (la API por lotes es tu amiga para actualizaciones retroactivas).
  • Algunos motores (FAISS) proporcionan búsqueda acelerada por GPU; esto puede cambiar el costo de RAM-limitado CPU a memoria de GPU y throughput de PCIe.
  • El análisis estructura-aware (diseño de PDF, OCR, extracción de tablas) suele ser limitado por CPU y puede superar el costo de incrustación para documentos escaneados; presupuestalo por separado.

Chunking - Implementaciones de referencia en Python

Todos los ejemplos están diseñados para ser legibles y ejecutables. Si necesitas una clave API o una base de datos en ejecución, esto se deduce claramente del código.

Utilidades compartidas: conteo de tokens y IDs de chunk estables

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Any, Iterable, Optional

from transformers import AutoTokenizer  # pip install transformers

@dataclass(frozen=True)
class Chunk:
    text: str
    meta: dict[str, Any]

def sha1_id(*parts: str) -> str:
    h = hashlib.sha1()
    for p in parts:
        h.update(p.encode("utf-8"))
        h.update(b"\x1e")
    return h.hexdigest()

# Usa cualquier tokenizador que se asemeje a la tokenización de tu LLM/embedding.
TOKENIZER = AutoTokenizer.from_pretrained("bert-base-uncased")

def token_len(text: str) -> int:
    return len(TOKENIZER.encode(text, add_special_tokens=False))

Chunking por tamaño fijo de tokens

def chunk_fixed_tokens(
    text: str,
    *,
    chunk_size: int = 512,
) -> list[Chunk]:
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    for i in range(0, len(token_ids), chunk_size):
        window = token_ids[i : i + chunk_size]
        chunk_text = TOKENIZER.decode(window)
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "fixed_tokens", "start_token": i, "end_token": i + len(window)},
            )
        )
    return out

Chunking con tamaño fijo y superposición + ventana deslizante

def chunk_sliding_window(
    text: str,
    *,
    window_tokens: int = 512,
    stride_tokens: int = 384,  # menor stride = mayor superposición
) -> list[Chunk]:
    assert 1 <= stride_tokens <= window_tokens, "el stride debe estar dentro del rango (0, window]"
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    start = 0
    while start < len(token_ids):
        end = min(start + window_tokens, len(token_ids))
        window = token_ids[start:end]
        out.append(
            Chunk(
                text=TOKENIZER.decode(window),
                meta={"strategy": "sliding_window", "start_token": start, "end_token": end},
            )
        )
        if end == len(token_ids):
            break
        start += stride_tokens

    return out

Chunking basado en oraciones (NLTK) con empaquetamiento por presupuesto de tokens

# pip install nltk
import nltk
from nltk.tokenize import sent_tokenize

nltk.download("punkt", quiet=True)

def chunk_by_sentences_nltk(
    text: str,
    *,
    max_tokens: int = 512,
    overlap_sentences: int = 1,
) -> list[Chunk]:
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    out: list[Chunk] = []

    buf: list[str] = []
    buf_tokens = 0

    def flush():
        nonlocal buf, buf_tokens
        if not buf:
            return
        chunk_text = " ".join(buf).strip()
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "sentences_nltk", "sent_count": len(buf)},
            )
        )
        # Superposición manteniendo las últimas N oraciones
        if overlap_sentences > 0:
            buf = buf[-overlap_sentences:]
            buf_tokens = token_len(" ".join(buf))
        else:
            buf, buf_tokens = [], 0

    for s in sents:
        s_tokens = token_len(s)
        # Si una sola oración excede el presupuesto, recurre al chunking de tamaño fijo en ese fragmento
        if s_tokens > max_tokens:
            flush()
            out.extend(chunk_fixed_tokens(s, chunk_size=max_tokens))
            continue

        if buf_tokens + s_tokens > max_tokens and buf:
            flush()

        buf.append(s)
        buf_tokens += s_tokens

    flush()
    return out

Chunking basado en oraciones (spaCy) cuando se quiere SBD basado en reglas o modelos

# pip install spacy
# python -m spacy download en_core_web_sm
import spacy

def chunk_by_sentences_spacy(
    text: str,
    *,
    max_tokens: int = 512,
) -> list[Chunk]:
    # Para divisiones basadas en reglas ligeros (sin análisis de dependencia), usa el sentencizer.
    nlp = spacy.blank("en")
    nlp.add_pipe("sentencizer")  # detección de límites de oraciones basada en reglas
    doc = nlp(text)

    sents = [sent.text.strip() for sent in doc.sents if sent.text.strip()]
    return chunk_by_sentences_nltk(" ".join(sents), max_tokens=max_tokens, overlap_sentences=1)

Chunking recursivo con separadores (LangChain)

# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter

def chunk_recursive_langchain(
    text: str,
    *,
    chunk_size: int = 1200,
    chunk_overlap: int = 150,
) -> list[Chunk]:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=token_len,  # presupuesto consciente de tokens
        separators=["\n\n", "\n", ". ", " ", ""],  # personaliza según tu contenido (ej: código)
    )
    pieces = splitter.split_text(text)

    return [
        Chunk(text=p, meta={"strategy": "recursive_langchain", "chunk_size": chunk_size, "overlap": chunk_overlap})
        for p in pieces
    ]

Chunking semántico con similitud de embeddings (embeddings de OpenAI)

Este enfoque calcula embeddings para unidades candidatas (oraciones/párrafos), luego encuentra “puntos de ruptura” semánticos.

# pip install openai numpy
import os
import numpy as np
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"))

def embed_texts_openai(texts: list[str], *, model: str = "text-embedding-3-small") -> np.ndarray:
    # NOTA: para lotes grandes, respeta los límites de tokens de la solicitud y los límites de tamaño del lote.
    resp = client.embeddings.create(model=model, input=texts)
    embs = np.array([d.embedding for d in resp.data], dtype=np.float32)
    return embs

def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
    denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8
    return float(np.dot(a, b) / denom)

def chunk_semantic(
    text: str,
    *,
    max_tokens: int = 800,
    breakpoint_threshold: float = 0.70,
) -> list[Chunk]:
    # 1) Comienza con candidatos de oraciones
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    if len(sents) <= 1:
        return [Chunk(text=text, meta={"strategy": "semantic", "note": "no_split"})]

    # 2) Embed cada oración
    embs = embed_texts_openai(sents)

    # 3) Calcula caídas de similitud
    sims = [cosine_sim(embs[i], embs[i + 1]) for i in range(len(sents) - 1)]

    # 4) Crea segmentos en puntos de ruptura
    out: list[Chunk] = []
    buf: list[str] = []
    buf_tokens = 0

    for i, s in enumerate(sents):
        # Añade oración
        s_tok = token_len(s)
        if buf and buf_tokens + s_tok > max_tokens:
            out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "max_tokens"}))
            buf, buf_tokens = [], 0
        buf.append(s)
        buf_tokens += s_tok

        # Decide punto de ruptura después de la oración i (basado en similitud con la siguiente)
        if i < len(sims) and sims[i] < breakpoint_threshold:
            out.append(
                Chunk(
                    text=" ".join(buf),
                    meta={"strategy": "semantic", "reason": "sim_drop", "sim_to_next": sims[i]},
                )
            )
            buf, buf_tokens = [], 0

    if buf:
        out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "final"}))

    return out

Chunking jerárquico + fusión de recuperación (LlamaIndex)

# pip install llama-index llama-index-llms-openai
from llama_index.core import Document, StorageContext, VectorStoreIndex
from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore

def build_hierarchical_index(text: str):
    docs = [Document(text=text)]

    node_parser = HierarchicalNodeParser.from_defaults()  # por defecto a tamaños de grueso a fino
    nodes = node_parser.get_nodes_from_documents(docs)

    docstore = SimpleDocumentStore()
    docstore.add_documents(nodes)
    storage_context = StorageContext.from_defaults(docstore=docstore)

    leaf_nodes = get_leaf_nodes(nodes)
    base_index = VectorStoreIndex(leaf_nodes, storage_context=storage_context)

    base_retriever = base_index.as_retriever(similarity_top_k=6)
    retriever = AutoMergingRetriever(base_retriever, storage_context, verbose=True)
    return retriever

Chunking basado en elementos para PDFs (Docling)

# pip install docling
# NOTA: la calidad del análisis de PDF depende de tu entorno (fuentes, OCR, etc.).
from docling.document_converter import DocumentConverter
from docling.transforms.chunker.hierarchical_chunker import HierarchicalChunker

def chunk_pdf_docling(pdf_path: str) -> list[Chunk]:
    converter = DocumentConverter()
    doc = converter.convert(pdf_path).document  # DoclingDocument
    chunker = HierarchicalChunker()
    doc_chunks = list(chunker.chunk(doc))

    out: list[Chunk] = []
    for c in doc_chunks:
        # c.text contiene contenido serializado del chunk; c.meta lleva información de estructura
        out.append(Chunk(text=c.text, meta={"strategy": "docling_hierarchical", **dict(c.meta)}))
    return out

Chunking con conciencia de código para Python (AST)

import ast

def chunk_python_by_ast(code: str, *, filepath: str = "<memory>") -> list[Chunk]:
    tree = ast.parse(code)
    out: list[Chunk] = []

    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            start = getattr(node, "lineno", None)
            end = getattr(node, "end_lineno", None)
            if start is None or end is None:
                continue

            lines = code.splitlines()
            snippet = "\n".join(lines[start - 1 : end])

            kind = "class" if isinstance(node, ast.ClassDef) else "function"
            name = getattr(node, "name", "<anon>")
            out.append(
                Chunk(
                    text=snippet,
                    meta={
                        "strategy": "python_ast",
                        "kind": kind,
                        "name": name,
                        "filepath": filepath,
                        "start_line": start,
                        "end_line": end,
                    },
                )
            )

    return out

Ejemplos de indexación

FAISS (local) — línea base mínima y rápida

# pip install faiss-cpu numpy
import numpy as np
import faiss

def build_faiss_index(vectors: np.ndarray) -> faiss.Index:
    # vectors: forma (N, d), tipo float32
    d = vectors.shape[1]
    index = faiss.IndexFlatIP(d)  # producto interno; similitud coseno si los vectores están normalizados
    faiss.normalize_L2(vectors)
    index.add(vectors)
    return index

def faiss_search(index: faiss.Index, query_vec: np.ndarray, k: int = 5):
    q = query_vec.astype(np.float32).reshape(1, -1)
    faiss.normalize_L2(q)
    scores, ids = index.search(q, k)
    return ids[0].tolist(), scores[0].tolist()

Chroma (local) — persistencia simple para prototipado de RAG

# pip install chromadb
import chromadb

def build_chroma_collection(chunks: list[Chunk], embeddings: np.ndarray, *, path: str = "./chroma_store"):
    client = chromadb.PersistentClient(path=path)
    col = client.get_or_create_collection(name="docs")

    ids = [sha1_id(c.meta.get("strategy", "chunk"), str(i), c.text[:50]) for i, c in enumerate(chunks)]
    col.upsert(
        ids=ids,
        documents=[c.text for c in chunks],
        metadatas=[c.meta for c in chunks],
        embeddings=embeddings.tolist(),
    )
    return col

Weaviate (autohospedado / nube) — vectores propios

# pip install weaviate-client
import weaviate
from weaviate.classes.config import Configure

def weaviate_upsert_self_provided(
    chunks: list[Chunk],
    embeddings: np.ndarray,
):
    client = weaviate.connect_to_local()  # o connect_to_weaviate_cloud(...)
    try:
        collection = client.collections.create(
            name="Chunk",
            vector_config=Configure.Vectors.self_provided(),  # tú proporcionas los vectores
        )

        with collection.batch.dynamic() as batch:
            for c, v in zip(chunks, embeddings):
                batch.add_object(
                    properties={"text": c.text, **{f"m_{k}": str(vv) for k, vv in c.meta.items()}},
                    vector=v.tolist(),
                )

        # Consulta por near-vector
        q = embeddings[0]
        res = collection.query.near_vector(near_vector=q.tolist(), limit=5)
        for obj in res.objects:
            print(obj.properties.get("text")[:200])
    finally:
        client.close()

Algunas fuentes

  • Patrick Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”, NeurIPS 2020; arXiv:2005.11401.
  • Vladimir Karpukhin et al., “Dense Passage Retrieval for Open-Domain Question Answering”, EMNLP 2020; arXiv:2004.04906.
  • Referencia de la API de OpenAI: Crear embeddings (/v1/embeddings) — límites de tokens (8192 por entrada; 300k totales por solicitud) y parámetro dimensions.
  • Guía de la API de OpenAI por lotes + página de precios de la API de OpenAI (los lotes ahorran ~50% con un plazo de 24 horas).
  • Documentación de LangChain: RecursiveCharacterTextSplitter y guía de integración de splitters (tamaño de chunk/superposición, jerarquía de separadores recursivos).
  • Documentación de LlamaIndex: HierarchicalNodeParser y AutoMergingRetriever (nodos de grueso a fino; fusión en tiempo de recuperación).
  • Blog de Weaviate: “Estrategias de chunking para mejorar el rendimiento del pipeline RAG de LLM” (descripción y trade-offs del chunking basado en LLM).
  • Documentación de Docling: HierarchicalChunker crea chunks a partir de la estructura de elementos del documento y adjunta metadatos de encabezados/capturas.
  • Jimeno Yepes et al., “Chunking de informes financieros para una generación de recuperación eficaz” (arXiv:2402.05131v3, 2024).
  • Marti A. Hearst, “TextTiling: Segmentar texto en subtemas de múltiples párrafos”, Computational Linguistics, 1997 (ACL Anthology: J97-1003).
  • Documentación / repositorio de GitHub de FAISS: trade-offs entre tiempo de búsqueda, calidad, memoria; soporte opcional de GPU.
  • Nandan Thakur et al., “BEIR: Un benchmark heterogéneo para la evaluación de modelos de recuperación de información en cero muestra”, NeurIPS 2021; arXiv:2104.08663.
  • Documentación de RAGAS: “Metrica de fidelidad” y métricas relacionadas de evaluación de RAG.
  • Documentación de NLTK: nltk.tokenize.sent_tokenize - tokenizador de oraciones basado en Punkt recomendado.
  • Documentación de la API de spaCy: Sentencizer - detección de límites de oraciones basada en reglas sin análisis de dependencia.

Otros enlaces útiles