Módulo 1: Parseo y chunking de documentos
El trade-off del tamaño de chunk
Descripción
La lección 05 te dio tres formas de cómo cortar un documento. Esta lección se queda con una sola pregunta —cuán grande debería ser cada chunk— y la responde con evidencia, no con un número mágico copiado de un tutorial. La respuesta corta, que vas a ver ejecutada en un momento, es que no hay un tamaño correcto: hay un trade-off. Un chunk chico es preciso pero se queda sin contexto; un chunk grande tiene contexto de sobra pero diluye lo que lo hace relevante. Esta lección hace ese trade-off visible, con números reales, sobre el mismo documento cortado a tres tamaños distintos.
Conexión con el módulo
Las tres estrategias de la lección 05 (fija, por oración, consciente de estructura) definían cómo se decide dónde cortar. El parámetro max_size que usaron todas, sin explicarlo a fondo, es el protagonista de esta lección. La lección 07 va a fijar el max_size que usa el pipeline final del módulo — y esta lección es la que te da el criterio para justificar ese número, en vez de elegirlo al azar.
Analogía: la porción de degustación y el plato combinado
En un menú de degustación, cada plato es una porción mínima: un bocado, un sabor, nada más. Es preciso —sabes exactamente qué estás probando— pero te falta contexto: ¿con qué se acompañaba? ¿Qué vino antes? Cada bocado vive aislado de los demás.
En el extremo opuesto, un plato combinado familiar trae de todo junto: la entrada, el plato principal, el acompañamiento y el postre, todo en la misma bandeja. Tienes todo el contexto de la comida completa — pero si alguien te pregunta "¿qué tenía el postre?", tienes que señalar un rincón de una bandeja llena de cosas que no son el postre. La pregunta específica se ahoga en el volumen de todo lo demás.
Un chunk chico es el bocado de degustación: preciso, aislado. Un chunk grande es la bandeja combinada: con contexto, pero diluido. Ningún tamaño es "el correcto" para todas las preguntas — depende de qué tan específica es la pregunta que vas a hacerle al documento.
El experimento: el mismo documento, tres tamaños
Vamos a chunkear no-show-policy.md con chunk_by_sentence (de la lección 05) a tres tamaños muy distintos: 60, 250 y 900 caracteres. Usamos chunk_by_sentence en vez de chunk_by_structure a propósito, para aislar el efecto del TAMAÑO sin que la estrategia "consciente de estructura" amortigüe el experimento por su cuenta.
raw = Path("corpus/no-show-policy.md").read_text()
title, sections = parse_markdown(raw)
full_text = " ".join(body for _, body in sections)
print("len(full_text) =", len(full_text))
Qué esperar:
len(full_text) = 755
Tamaño 60: preciso, sin contexto
chunks_60 = chunk_by_sentence(full_text, max_size=60)
print(f"max_size=60 -> {len(chunks_60)} chunks")
for i, c in enumerate(chunks_60):
print(f"[{i}] ({len(c)} chars) {c!r}")
Qué esperar:
max_size=60 -> 6 chunks
[0] (113 chars) 'A no-show is a booking where the member never checks in during the reserved hours and never cancelled beforehand.'
[1] (86 chars) 'This is different from a late cancellation, which is covered in `cancellation-policy`.'
[2] (52 chars) 'No-shows are charged the full amount of the booking.'
[3] (155 chars) 'Unlike a late cancellation, there is no partial leniency: the no-show fee equals the entire reserved price, and it is never refunded under `refund-policy`.'
[4] (198 chars) 'Members with three or more no-shows in a rolling 30-day window lose the ability to book same-day reservations; all future bookings must be made at least 24 hours in advance until the pattern clears.'
[5] (146 chars) 'Rooms held for a no-show cannot be re-offered to another member during that window, so the fee reflects real lost capacity, not a punitive charge.'
(Nota: con max_size=60, ninguna oración individual del documento mide 60 caracteres o menos — la más corta mide 52 — así que chunk_by_sentence termina poniendo una sola oración por chunk, porque agregar una segunda ya excedería el límite. Seis oraciones, seis chunks.)
Mira el chunk [2]: "No-shows are charged the full amount of the booking." Preciso — dice exactamente una cosa, sin ruido de otros temas. Pero solo. Si un sistema de búsqueda te devolviera ÚNICAMENTE este chunk como respuesta a "¿qué pasa si no me presento?", tendrías la respuesta correcta pero sin ningún matiz: no sabrías que esto es DISTINTO de una cancelación tardía (esa comparación vive en el chunk [1], un chunk completamente separado), ni que la política existe porque el cupo perdido es real (chunk [5], también separado).
Tamaño 250: equilibrado
chunks_250 = chunk_by_sentence(full_text, max_size=250)
print(f"max_size=250 -> {len(chunks_250)} chunks")
for i, c in enumerate(chunks_250):
print(f"[{i}] ({len(c)} chars) {c!r}")
Qué esperar:
max_size=250 -> 4 chunks
[0] (200 chars) 'A no-show is a booking where the member never checks in during the reserved hours and never cancelled beforehand. This is different from a late cancellation, which is covered in `cancellation-policy`.'
[1] (208 chars) 'No-shows are charged the full amount of the booking. Unlike a late cancellation, there is no partial leniency: the no-show fee equals the entire reserved price, and it is never refunded under `refund-policy`.'
[2] (198 chars) 'Members with three or more no-shows in a rolling 30-day window lose the ability to book same-day reservations; all future bookings must be made at least 24 hours in advance until the pattern clears.'
[3] (146 chars) 'Rooms held for a no-show cannot be re-offered to another member during that window, so the fee reflects real lost capacity, not a punitive charge.'
Con max_size=250, cada chunk termina agrupando exactamente las mismas oraciones que su sección original (las 4 secciones de no-show-policy.md de la lección 04: qué es, qué pasa, repetición, por qué existe). El chunk [1] ahora tiene AMBAS oraciones sobre el cargo: qué se cobra y por qué no hay reembolso parcial, juntas — la comparación que en max_size=60 estaba partida entre dos chunks distintos ahora vive en uno solo.
Tamaño 900: contexto completo, todo diluido en un bloque
chunks_900 = chunk_by_sentence(full_text, max_size=900)
print(f"max_size=900 -> {len(chunks_900)} chunks")
for i, c in enumerate(chunks_900):
print(f"[{i}] ({len(c)} chars) {c!r}")
Qué esperar:
max_size=900 -> 1 chunks
[0] (755 chars) 'A no-show is a booking where the member never checks in during the reserved hours and never cancelled beforehand. This is different from a late cancellation, which is covered in `cancellation-policy`. No-shows are charged the full amount of the booking. Unlike a late cancellation, there is no partial leniency: the no-show fee equals the entire reserved price, and it is never refunded under `refund-policy`. Members with three or more no-shows in a rolling 30-day window lose the ability to book same-day reservations; all future bookings must be made at least 24 hours in advance until the pattern clears. Rooms held for a no-show cannot be re-offered to another member during that window, so the fee reflects real lost capacity, not a punitive charge.'
Todo el documento —definición, cargo, patrón repetido, y la razón de negocio detrás de la política— colapsó en un solo chunk. Tiene todo el contexto posible: nada quedó afuera. Pero piensa en lo que significa esto para un sistema de búsqueda (Módulo 2): si alguien pregunta específicamente "¿qué pasa si tengo tres no-shows seguidos?", el índice no puede devolverte SOLO la parte sobre repetición — solo puede devolverte este chunk entero, con la definición y la justificación de negocio mezcladas adentro, como ruido, junto a la respuesta que realmente importaba.
Lo que el experimento deja ver
max_size chunks chunk más chico chunk más grande mezcla temas por chunk
60 6 52 chars 198 chars 1 tema c/u (muy fragmentado)
250 4 146 chars 208 chars ~1 tema c/u (alineado a secciones)
900 1 755 chars 755 chars 4 temas en 1 chunk (todo junto)
A max_size=60, ganas precisión (cada chunk es una afirmación atómica) pero pierdes la posibilidad de que un chunk, por sí solo, cuente la historia completa de un subtema — la comparación entre "no-show" y "cancelación tardía" quedó partida en dos chunks que un sistema de recuperación podría no devolver juntos. A max_size=900, ganas que CUALQUIER pregunta sobre no-shows encuentra "el" chunk correcto (solo hay uno) — pero pierdes la capacidad de decirle al usuario "la parte relevante de la respuesta es esta oración", porque la unidad mínima que el sistema puede devolver es el documento entero. En el medio, max_size=250 no es mágico ni universalmente "el mejor" — coincide, en este documento particular, con el tamaño natural de cada sección, que fue una decisión editorial de quien escribió la política, no una propiedad matemática del texto.
Errores comunes
-
Asumir que "más grande siempre es mejor" porque tiene más contexto. El chunk de
max_size=900de arriba tiene MÁS información, no MEJOR información para una pregunta específica. Más grande no es gratis: cada chunk grande que un futuro índice devuelva le va a costar más presupuesto de contexto al agente que lo reciba (tema decontext-engineering-guide, no de esta guía) y le va a dar al modelo más texto irrelevante que filtrar antes de encontrar la respuesta. -
Asumir que "más chico siempre es más preciso" sin costo. El chunk
[2]demax_size=60("No-shows are charged the full amount of the booking.") es preciso, pero también es frágil: si la pregunta del usuario usa palabras que no están en ESA oración específica pero sí en una oración vecina ("¿pierdo la reserva completa si no aviso?"), un sistema de recuperación léxico (Módulo 2) podría no encontrar match en un chunk tan chico, mientras que un chunk más grande que incluyera varias oraciones relacionadas tendría más superficie de palabras para matchear. -
Elegir un tamaño de chunk sin mirar el documento. El tamaño "correcto" para
no-show-policy.md(donde las secciones miden entre 146 y 208 caracteres) no tiene por qué ser el mismo que el tamaño correcto para un documento con secciones mucho más largas. No existe una constante universal — existe la pregunta "¿qué tamaño tienen las unidades de sentido naturales de ESTE documento?", y la respuesta varía por documento. -
Confundir esta lección con indexar y medir recall/precision de verdad. Todo lo que viste aquí es estructural: contamos caracteres, comparamos chunks, razonamos sobre qué información queda junta o separada. Medir de verdad si un tamaño de chunk mejora o empeora lo que un sistema de búsqueda recupera —con un set de preguntas y una métrica real— es el trabajo del Módulo 6 (
evaluating-retrieval-quality), después de que el Módulo 2 indexe. No hay todavía ningún número de recall o precision que citar — sería adelantar contenido que este módulo no cubre.
Ejercicios
Ejercicio 1: Predecir antes de ejecutar (Fácil)
Sin ejecutar código, para payment-methods-faq.md (cuyas 4 secciones miden entre 112 y 181 caracteres cada una, según lo que parseaste en la lección 04), predice: ¿con max_size=100, esperas que cada sección produzca 1 chunk o más de 1? ¿Con max_size=500?
Ver solución
Con max_size=100, cada sección individual (112-181 caracteres) excede el límite, así que chunk_by_sentence va a tener que partir cada una en al menos 2 chunks (probablemente más, dependiendo de dónde caigan los puntos). Con max_size=500, cada sección individual entra cómoda dentro del límite (la más larga son 181 caracteres), así que cada sección produce exactamente 1 chunk — y si usaras chunk_by_sentence sobre el TEXTO COMPLETO del documento (sin section boundaries) con max_size=500, es probable que dos secciones completas quepan juntas en un mismo chunk, mezclando temas, algo que solo chunk_by_structure evitaría por diseño.
Confirmando con código:
raw = Path("corpus/payment-methods-faq.md").read_text()
title, sections = parse_markdown(raw)
full_text = " ".join(body for _, body in sections)
print("100 ->", len(chunk_by_sentence(full_text, max_size=100)), "chunks")
print("500 ->", len(chunk_by_sentence(full_text, max_size=500)), "chunks")
Ejercicio 2: Medir la fragmentación (Medio)
Escribe una función avg_chunk_size(chunks) que calcule el tamaño promedio de una lista de chunks, y úsala para comparar el promedio de chunk_by_sentence(full_text, max_size=60) contra chunk_by_sentence(full_text, max_size=900) sobre no-show-policy.md. ¿El promedio a max_size=60 es igual a 60? ¿Por qué sí o por qué no?
Ver solución
def avg_chunk_size(chunks: list[str]) -> float:
return sum(len(c) for c in chunks) / len(chunks)
raw = Path("corpus/no-show-policy.md").read_text()
title, sections = parse_markdown(raw)
full_text = " ".join(body for _, body in sections)
chunks_60 = chunk_by_sentence(full_text, max_size=60)
chunks_900 = chunk_by_sentence(full_text, max_size=900)
print("avg a max_size=60: ", round(avg_chunk_size(chunks_60), 1))
print("avg a max_size=900:", round(avg_chunk_size(chunks_900), 1))
Salida esperada:
avg a max_size=60: 125.0
avg a max_size=900: 755.0
Explicación: el promedio a max_size=60 (125.0) es más del doble del propio max_size (60), y NO es casualidad ni un bug: chunk_by_sentence nunca corta una oración a la mitad, así que si la oración más corta del documento ya mide 52 caracteres, ningún chunk puede medir menos que eso — y varias oraciones superan holgadamente los 60 caracteres por sí solas (max_size es un techo que intenta no cruzar AGRUPANDO oraciones, no un límite duro sobre una oración individual). El max_size funciona como un objetivo de empaquetado, no como una garantía de tamaño exacto — la garantía real es "nunca corto una oración", y el tamaño resultante es una consecuencia de eso, no un control directo.
Ejercicio 3: Diseñar el tamaño para dos preguntas distintas (Difícil)
Reservo te da dos preguntas típicas que la gente le hace al sistema: (a) "¿Cuánto tiempo antes puedo cancelar sin cargo si soy pro?" — una pregunta muy específica, con una respuesta de una sola oración en cancellation-policy.md. (b) "Explícame toda la política de cancelación." — una pregunta que espera una respuesta completa, con las cuatro secciones del documento. Argumenta si un ÚNICO tamaño de chunk podría servir bien para ambas preguntas, o si el problema pide algo más que "elegir un buen max_size".
Ver solución
Un único tamaño de chunk fijo va a favorecer a una de las dos preguntas a costa de la otra. Un chunk chico (alineado a una sección, ~150-200 caracteres) responde muy bien a (a): devuelve justo la ventana de cancelación pro, sin ruido. Pero para (b), un sistema que solo puede devolver chunks individuales tendría que devolver VARIOS chunks —los 4 de la política completa— y ensamblarlos, lo cual ya no es un problema de "elegir el tamaño de chunk" sino un problema de cuántos chunks devolver y cómo combinarlos — eso es trabajo del Módulo 2 en adelante (search_docs(query, k), con k controlando cuántos chunks vuelven) y, más allá de recuperación, de cómo se ensamblan esos chunks en el contexto del agente (context-engineering-guide).
La conclusión honesta es que el tamaño de chunk no resuelve solo el problema de "preguntas específicas vs. preguntas amplias" — ese problema se resuelve combinando un tamaño de chunk razonable (ni tan chico que fragmente cada hecho, ni tan grande que diluya cada tema, como viste en el experimento de esta lección) CON un k (cuántos chunks devolver por búsqueda) elegido según el tipo de pregunta. Este módulo te da la primera pieza; el resto de la guía construye las demás.
Resumen y siguiente paso
- No existe un "tamaño de chunk correcto" universal: es un trade-off, y esta lección lo hizo visible con evidencia ejecutada, no con una regla de dedo.
- Cortamos
no-show-policy.mdconchunk_by_sentencea tres tamaños: 60 (6 chunks, cada uno una sola afirmación aislada, sin contexto de las demás), 250 (4 chunks, alineados con las 4 secciones originales del documento), y 900 (1 chunk, todo el documento junto, con cuatro subtemas mezclados en un solo bloque). - El costo de un chunk chico es fragmentar hechos relacionados en piezas separadas; el costo de un chunk grande es que un sistema de búsqueda ya no puede devolver "la parte relevante" — solo puede devolver el bloque entero.
- Medir de verdad si un tamaño mejora o empeora la recuperación (con números de recall/precision) es trabajo del Módulo 6, después de indexar en el Módulo 2 — esta lección da el criterio estructural, no la métrica final.
Siguiente lección: 07 — Metadatos para poder citar. Con las estrategias de chunking y su trade-off ya entendidos, construimos el Chunk completo: doc_id, título, sección y posición — lo que convierte un fragmento de texto suelto en algo que se puede citar.
Recursos adicionales
- Python — Comprensión de listas y generadores — La base de
avg_chunk_sizey de las comparaciones de esta lección. - Anthropic — Building effective agents — Por qué medir con evidencia, en vez de asumir, es el patrón general de esta guía y de una ingeniería de agentes seria.
advanced-rag-techniques-guide— Técnicas que atacan directamente este trade-off con herramientas más sofisticadas (re-ranking, hybrid search); fuera del alcance de este módulo.context-engineering-guide— Qué hacer con los chunks recuperados una vez que ya decidiste cuántos (k) devolver; la frontera exacta se nombra en el Módulo 7 de esta guía.