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.
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
- Calidad de RAG en tiempo real: tipos de fragmentos, limpieza y etiquetas de recuperación — Cómo un pipeline RAG en producción crea fragmentos de texto, tablas e imágenes, elimina ruido del analizador y agrega etiquetas de recuperación escritas por modelos LLM que redujeron a la mitad aproximadamente las búsquedas fallidas.
- Notas prácticas: RAG nativo a vector en Oracle: embeddings, HNSW/IVF y híbrido — Guía paso a paso de las Notas prácticas: RAG nativo a vector en Oracle: embeddings, HNSW/IVF y híbrido: contratos, verificaciones y espacios de código listos para usar por los equipos que implementan este patrón.