Inicio / Artículos

Tamaño del bloque y solapamiento en RAG: paso, límites de tokens y uniones

Cómo dividir un libro largo para RAG: elegir el tamaño y la superposición, comprender el paso a seguir, mantenerse por debajo del límite de tokens de embedding, almacenar metadatos y ver la superposición en una división real.

Editor: REACTAPP.TOP8 min de lectura1742 palabras

Fuentes: Chunking is the hidden design decision in RAG

Tome la autobiografía de Benjamin Franklin del Proyecto Gutenberg. Pegar todo el libro en una solicitud es un desperdicio, incluso si la ventana de contexto pudiera contenerlo. Una pregunta como por qué Franklin quería que Gran Bretaña se quedara con Canadá probablemente tenga su respuesta en dos o tres pasajes, y las páginas restantes consumirían el contexto y ocultarían las pruebas. La generación mejorada por recuperación de información (RAG) existe para esta situación: el sistema localiza los pasajes relevantes para la pregunta y solo les pasa esa información al modelo de lenguaje. El libro permanece almacenado, y la recuperación selecciona las pruebas necesarias.

Embebidos y por qué el texto se divide

RAG comienza convirtiendo fragmentos de texto en embeddings, representaciones numéricas del significado. Los pasajes que expresan ideas similares se ubican cerca uno del otro en un espacio vectorial de alta dimensión, incluso si utilizan formulaciones diferentes. Cuando llega una pregunta, también se convierte en un embedding; luego se buscan los vectores almacenados más cercanos y se recuperan los pasajes de origen correspondientes. El vector localiza el texto, y ese es el texto que el modelo lee.

Un único embedding no puede representar un libro entero. Un solo vector para la vida de Franklin mezclaría su infancia en Boston, sus experimentos con impresión y electricidad, *Pobre Richard* y su testimonio ante el Parlamento en algo que no describe bien ninguno de estos aspectos. Por eso, el texto debe dividirse en fragmentos antes de ser convertido en embeddings.

Tamaño, superposición y paso

Un punto de partida razonable:

DEFAULT_CHUNK_SIZE = 800      # characters
DEFAULT_CHUNK_OVERLAP = 150   # characters

La gente a menudo los trata como dos botones sin relación entre sí. En realidad definen una sola cosa: cuánto se mueve la ventana hacia adelante en cada paso.

stride = chunk_size − chunk_overlap
       = 800 − 150
       = 650 characters

Ese valor representa la cantidad de texto nuevo que añade cada bloque nuevo.

Considere el tamaño del bloque como un límite máximo y no como un objetivo. Los divisores prefieren hacer el corte en una separación natural, ya sea entre párrafos, líneas, oraciones o palabras; por lo tanto, solicitar 800 caracteres puede generar bloques de 640, 731 o exactamente 800.

La superposición es el texto compartido por los bloques adyacentes, y cumple una función importante. Sin ella, un corte puede dejar un fragmento suelto como este:

"…but left school at the age of ten."

Por sí solo, esa línea dice poco. Con solapamiento, el fragmento siguiente conserva suficiente contexto anterior para seguir siendo legible y localizable. Es especialmente importante en oraciones largas y argumentos estrechamente relacionados. La carta de Franklin sobre Canadá contiene una larga cadena de cláusulas que podrían traspasar un límite, dejando un fragmento sin su conclusión y el siguiente sin su premisa. Un solapamiento moderado mantiene la conexión en ambos registros.

El precio a pagar es la duplicación. Los documentos que comparten texto pueden ser recuperados juntos, lo cual es normal. Cuando las duplicaciones desplazan evidencias más relevantes, un método sensible a la diversidad como la relevancia marginal máxima (MMR) ayuda.

Qué contiene cada fragmento almacenado

Cada fragmento conserva cuatro campos:

id
document
embedding
metadata

Un registro de ejemplo:

id         7c2a1e90-4b11-4d3e-9f08-12a6c0e84b21

document   "His boyhood in Boston was a stern beginning of the habit
            of hard work and rigid economy which marked the man. For
            a year he went to the Latin Grammar School on School
            Street, but left off at the age of ten to help his father
            in making soap and candles."

embedding  384 floats — [-0.0412, 0.0187, 0.0621, -0.0094, 0.0330, …]
           norm = 1.0

metadata   {
             book: "Franklin's Autobiography",
             source: "https://www.gutenberg.org/cache/epub/36151/pg36151-images.html",
             gutenberg_id: 36151,
             page_label: "5",
             chunk_index: 2
           }

El embedding sirve para búsquedas, mientras que el texto original se utiliza para lectura, citación y referencias. Los metadatos también requieren atención. Si la edición imprime etiquetas de página como [Pg 5], consérvelas para que una cita indique una página que los lectores puedan encontrar en su ejemplar.

Elimine el material de bajo valor antes de indexarlo: páginas de título, texto genérico, texto de licencia, encabezados repetidos y listas completas de notas al pie. Estos elementos generan embeddings débiles y afectan negativamente la capacidad de recuperación.

Vectores normalizados

Los vectores de embedding suelen estar normalizados en L2, lo que escala cada uno a una longitud de 1:

‖v‖ = √(v₁² + v₂² + … + vₙ²)

La similitud de coseno compara dos vectores de la siguiente manera:

cos θ = (a · b) / (‖a‖ ‖b‖)

Cuando ambos tienen longitud unitaria, eso se reduce a un producto escalar simple:

If ‖a‖ = ‖b‖ = 1:
cos θ = a · b

Por lo tanto, el ranking refleja cuán estrechamente apuntan en la misma dirección dos significados, y la magnitud vectorial bruta no influye en ello, lo que permite que la recuperación sea rápida y significativa.

Tokens, no caracteres

Los ajustes en términos de caracteres pueden ocultar un límite estricto, ya que los modelos de embedding cuentan tokens. El modelo all-MiniLM-L6-v2 acepta como máximo 256 tokens. Un fragmento más largo podría ser truncado sin ninguna advertencia. La base de datos sigue conteniendo el texto completo, pero su embedding solo abarca la parte inicial, y la cola truncada resulta difícil o imposible de recuperar. Una estimación cautelosa:

maximum safe chunk size in characters
≈ model token limit × 4

Un modelo de 256 tokens genera aproximadamente 1,000 caracteres. Al utilizar cuatro caracteres por token queda margen adicional, ya que el diálogo, las abreviaturas, la puntuación y los textos formateados pueden tokenizarse con mayor densidad que la prosa.

Los modelos más recientes aceptan entradas mucho más largas, pero el particionamiento sigue siendo importante. Una ventana más grande no resuelve el problema de la recuperación, porque los bloques excesivamente amplios mezclan temas no relacionados en un único vector difuso.

Recorriendo el proceso

Primero, particione las páginas cargadas:

from rag_qa.gutenberg import load_pages
from rag_qa.chunk import chunk_documents
from rag_qa.config import load_settings

s = load_settings()
print(s.summary())
# {
#   'chunk_size_chars': 800,
#   'chunk_overlap_chars': 150,
#   'stride_chars': 650,            # size − overlap; this sets chunk count
#   'model': 'all-MiniLM-L6-v2',
#   'model_max_tokens': 256,        # hard cap; overflow is silent
#   'embedding_dims': 384,
#   'max_safe_chunk_chars': 1024,   # 256 × 4
#   'book': "Franklin's Autobiography",
#   'gutenberg_id': 36151,
# }
pages = load_pages()
chunks = chunk_documents(pages, s.chunk_size, s.chunk_overlap)
print(len(pages), len(chunks), s.stride)

En segundo lugar, verifique cada fragmento en relación con el límite de tokens del modelo:

from sentence_transformers import SentenceTransformer
from rag_qa.tokens import check_chunk

m = SentenceTransformer("all-MiniLM-L6-v2")
print(m.max_seq_length)                    # 256
for c in chunks:
    r = check_chunk(c)
    if r.truncated:
        print("silent truncate:", r.n_chars, "chars /", r.n_tokens, "tokens")

El script imprime la longitud máxima del modelo y luego enumera únicamente los fragmentos que serían eliminados en silencio. Una ejecución habitual solo muestra el límite:

256

Cuando todo cabe, eso es lo único que se ve. Si algunos fragmentos sobrepasan ese límite, aparecen líneas adicionales:

256
silent truncate: 1820 chars / 412 tokens
silent truncate: 960 chars / 301 tokens

Cómo leer esas líneas:

Tercero, observe qué se almacena al final:

from rag_qa.store import ingest, open_store

store = open_store()
ingest(chunks, store)
row = store.get(limit=1)
print(row["ids"][0])
print(row["documents"][0][:200])
print(len(row["embeddings"][0]), row["metadatas"][0])
# 384 floats, norm 1.0, metadata.page_label == printed [Pg N]

Ese fragmento muestra un único bloque después de la ingestión e imprime tres líneas:

doc-12-p3-c0
She left school at the age of ten. The next sentence continues on the same page…
384 {'page_label': '3', 'source': 'notes.pdf'}

Cuarto, haz una consulta al respecto:

from rag_qa.retrieve import search, search_mmr

q = "why did Franklin want Britain to keep Canada"

for hit in search(q, k=5):
    print(f"{hit['score']:.3f}  p.{hit['metadata']['page_label']}  {hit['document'][:120]}")
# overlap makes near-duplicate hits; MMR trades a little score for diversity

for hit in search_mmr(q, k=5):
    print(hit["metadata"]["page_label"], hit["score"])

Un ejemplo de los resultados:

0.812  p.7  I have long been of opinion that the foundations of the future grandeur and stability of the British empire lie in America
0.781  p.7  they are, nevertheless, broad and strong enough to support the greatest political structure that human wisdom ever yet
0.744  p.7  I am, therefore, by no means for restoring Canada. If we keep it all the country from the St. Lawrence to the Mississippi
0.691  p.8  I left England about the end of August, 1762, in company with ten sail of merchant ships
0.640  p.6  In this Autobiography Franklin tells of his own life to the year 1757, when he went to England

Los pocos resultados más destacados suelen provenir de la misma página. Eso es el efecto de la superposición, ya que los fragmentos adyacentes de una misma oración coinciden todos.

Superposición mostrada en una pequeña muestra

Tome un breve pasaje de unos 190 caracteres de la introducción del libro, que describe la infancia del autor en Boston: sus hábitos tempranos de trabajo duro y ahorro, un año en la Escuela de Gramática Latina de School Street, y su partida a los diez años. Divídalо con un tamaño de fragmento de solo 60.

Sin superposición, el divisor corta exactamente en el límite:

[0] 59  His boyhood in Boston was a stern beginning of the habit of
[1] 55  hard work and rigid economy which marked the man. For a
[2] 58  year he went to the Latin Grammar School on School Street,
[3] 31  but left off at the age of ten.

El bloque 0 termina en “the habit of” y el bloque 1 comienza en “hard work”, sin que haya nada que los conecte. La frase “habit of hard work” nunca aparece completa en ningún bloque. Si el corte ocurre a mitad de oración, cada vector codifica media idea, y una consulta que requiere la oración completa no coincide bien con ninguna de las dos mitades.

Con una superposición de 15 caracteres:

[0] 59  His boyhood in Boston was a stern beginning of the habit of
[1] 57  ⟦the habit of⟧ hard work and rigid economy which marked the
[2] 55  ⟦marked the⟧ man. For a year he went to the Latin Grammar
[3] 58  ⟦Latin Grammar⟧ School on School Street, but left off at the
[4] 22  ⟦off at the⟧ age of ten.

El marcador a continuación indica qué texto se repite del fragmento anterior:

⟦…⟧ = text repeated from the previous chunk

Ningún fragmento alcanza los 60 caracteres, lo que demuestra una vez más que ese tamaño es solo un límite máximo. Omitir las superposiciones genera un índice más pequeño y evita resultados duplicados, pero todo lo que el divisor se encuentra por casualidad al dividir se pierde. Las superposiciones garantizan esas conexiones y no constituyen contexto adicional por sí mismas.

Las cuatro tareas y las herramientas para ellas

RAG consta de cuatro tareas: cargar texto, dividirlo, incrustarlo y almacenar e investigar los vectores. No existe una única biblioteca que sea “RAG”. Herramientas para dividir y orquestar:

Modelos de incrustación:

Las bases de datos vectoriales populares completan el conjunto necesario.

Lecturas relacionadas

  • Fragmentación: La decisión de RAG pasada por alto detrás de las respuestas incorrectas — Por qué dividir los documentos de forma errónea reduce la calidad de la recuperación, cómo ayudan la fragmentación consciente de la estructura y la jerárquica padre-hijo, y cómo medir si tus fragmentos funcionan.