Módulo 7: Operar RAG en producción

Medir qué se recupera

Descripción

Cada búsqueda ejecutada en esta guía devolvió chunks de texto, y hasta ahora la pregunta siempre fue "¿son los chunks correctos?" — el Módulo 6 la responde con recall y precision, las lecciones 02-05 de este módulo la operan con citas, umbrales y filtros de vigencia. Esta lección hace una pregunta distinta, que no depende de si el contenido es correcto: ¿cuánto pesa lo que se recupera? Cada chunk que search_docs devuelve, sin importar su relevancia, va a ocupar espacio en el prompt final que recibe el modelo — caracteres que se convierten en tokens, tokens que cuestan dinero y cuentan contra el límite de la ventana de contexto. Un sistema que nunca mide esto no tiene forma de responder preguntas operativas básicas: ¿cuánto contexto añade, en promedio, cada llamada a search_docs? ¿Cambia mucho entre k=3 y k=5? ¿El filtrado de las Lecciones 04 y 05 reduce esa factura, o es irrelevante para el tamaño? Esta lección construye la medición.

Conexión con el módulo

Esta lección retoma los resultados ya filtrados de las Lecciones 04 y 05 —no los crudos de search_docs— y les agrega una dimensión nueva: tamaño. Es, deliberadamente, la última pieza antes de hablar de costo (Lección 07): medir cuánto se recupera es el paso previo a medir cuánto cuesta recuperarlo.


Analogía: cuántos libros caben en el escritorio

Volviendo al mostrador: encontrar los libros correctos y descartar los que no aplican (Lecciones 03-05) resuelve cuáles libros entregar. Queda una pregunta distinta, práctica: ¿cuántos libros, físicamente, le caben en el escritorio a la persona que los va a leer? Traer diez libros relevantes de una sola vez no es útil si solo hay espacio para tres — sencillamente no se puede sostener todo a la vez, y en algún momento hay que decidir cuántos volúmenes, y de qué grosor, se llevan. Esta lección no decide el orden en que se acomodan en el escritorio, ni cuáles se resumen para que ocupen menos lugar —eso es trabajo de context-engineering-guide—; mide, con una cinta métrica, cuánto pesa lo que ya se decidió llevar.


retrieval_footprint: la pieza mínima

from search_docs_tool import search_docs

CHARS_PER_TOKEN_APPROX = 4  # heuristica de regla general para ingles, NO un
                             # tokenizador real -- ver la nota mas abajo


def retrieval_footprint(hits):
    """Mide el tamano de un lote de resultados de search_docs: cuantos
    chunks, cuantos caracteres de texto en total, y una ESTIMACION de
    tokens (no un conteo exacto -- ver la nota de esta leccion)."""
    total_chars = sum(len(h["text"]) for h in hits)
    approx_tokens = round(total_chars / CHARS_PER_TOKEN_APPROX)
    return {"chunks": len(hits), "chars": total_chars, "approx_tokens": approx_tokens}

Antes de usarla, una aclaración honesta que vale la pena leer con atención: CHARS_PER_TOKEN_APPROX = 4 es una heurística de regla general para texto en inglés —el mismo tipo de aproximación que circula en cualquier documentación sobre presupuestar contexto—, no el resultado de un tokenizador real. Este entorno no tiene acceso a red para instalar el tokenizador oficial de Claude, así que esta lección no finge tener un conteo exacto: approx_tokens es, literalmente, una estimación gruesa, etiquetada como tal en cada resultado. Un sistema en producción con acceso a la librería oficial de conteo de tokens obtendría un número exacto en su lugar — el reemplazo es directo (misma firma, otra implementación de approx_tokens), y no cambia nada del resto de esta lección.


Ejemplo trabajado: el footprint de las seis queries ancla, k=3 y k=5

anchor_queries = [
    "What is the cancellation policy for Boardroom bookings?",
    "How much discount does the pro tier get?",
    "What equipment is in the Focus room?",
    "Can I get a refund if I didn't show up?",
    "What payment methods does Reservo accept?",
    "Is there wifi in the Lounge?",
]

for k in (3, 5):
    print(f"--- k={k} ---")
    for q in anchor_queries:
        hits = search_docs(q, k=k)
        footprint = retrieval_footprint(hits)
        print(f"  {footprint}  {q}")

Qué esperar:

--- k=3 ---
  {'chunks': 3, 'chars': 560, 'approx_tokens': 140}  What is the cancellation policy for Boardroom bookings?
  {'chunks': 3, 'chars': 593, 'approx_tokens': 148}  How much discount does the pro tier get?
  {'chunks': 3, 'chars': 533, 'approx_tokens': 133}  What equipment is in the Focus room?
  {'chunks': 3, 'chars': 556, 'approx_tokens': 139}  Can I get a refund if I didn't show up?
  {'chunks': 3, 'chars': 510, 'approx_tokens': 128}  What payment methods does Reservo accept?
  {'chunks': 3, 'chars': 534, 'approx_tokens': 134}  Is there wifi in the Lounge?
--- k=5 ---
  {'chunks': 5, 'chars': 986, 'approx_tokens': 246}  What is the cancellation policy for Boardroom bookings?
  {'chunks': 5, 'chars': 857, 'approx_tokens': 214}  How much discount does the pro tier get?
  {'chunks': 5, 'chars': 890, 'approx_tokens': 222}  What equipment is in the Focus room?
  {'chunks': 5, 'chars': 869, 'approx_tokens': 217}  Can I get a refund if I didn't show up?
  {'chunks': 5, 'chars': 821, 'approx_tokens': 205}  What payment methods does Reservo accept?
  {'chunks': 5, 'chars': 847, 'approx_tokens': 212}  Is there wifi in the Lounge?

Dos observaciones directas de estos números. Primero, con k=3 cada búsqueda añade entre 128 y 148 tokens aproximados al prompt — un rango angosto, porque MAX_CHARS=280 (Módulo 3) acota el tamaño individual de cada chunk, así que el tamaño total con k fijo varía poco entre preguntas. Segundo, subir de k=3 a k=5 casi duplica el footprint (de ~140 a ~220 tokens promedio) sin que eso implique el doble de información útil — ya sabes, del Módulo 2 (Lección 07), que k no reordena resultados, solo agrega más cola de baja relevancia al final de una lista ya ordenada. Cada unidad de k adicional tiene un costo de tamaño garantizado y un beneficio de relevancia decreciente — el mismo trade-off, ahora con números concretos.


El footprint después de filtrar: el beneficio colateral de las Lecciones 04-05

Vale la pena confirmar algo que las Lecciones 04 y 05 no midieron explícitamente: filtrar por umbral o por vigencia no solo mejora la calidad de lo que se cita — también reduce el footprint, como efecto colateral directo de tener menos chunks.

THRESHOLD = 4.0


def apply_threshold(hits, min_score=THRESHOLD):
    return [h for h in hits if h["score"] >= min_score]


for q in anchor_queries:
    hits = search_docs(q, k=3)
    before = retrieval_footprint(hits)
    after = retrieval_footprint(apply_threshold(hits))
    print(f"{q}")
    print(f"  antes:   {before}")
    print(f"  despues: {after}")

Qué esperar:

What is the cancellation policy for Boardroom bookings?
  antes:   {'chunks': 3, 'chars': 560, 'approx_tokens': 140}
  despues: {'chunks': 3, 'chars': 560, 'approx_tokens': 140}
How much discount does the pro tier get?
  antes:   {'chunks': 3, 'chars': 593, 'approx_tokens': 148}
  despues: {'chunks': 3, 'chars': 593, 'approx_tokens': 148}
What equipment is in the Focus room?
  antes:   {'chunks': 3, 'chars': 533, 'approx_tokens': 133}
  despues: {'chunks': 3, 'chars': 533, 'approx_tokens': 133}
Can I get a refund if I didn't show up?
  antes:   {'chunks': 3, 'chars': 556, 'approx_tokens': 139}
  despues: {'chunks': 3, 'chars': 556, 'approx_tokens': 139}
What payment methods does Reservo accept?
  antes:   {'chunks': 3, 'chars': 510, 'approx_tokens': 128}
  despues: {'chunks': 2, 'chars': 336, 'approx_tokens': 84}
Is there wifi in the Lounge?
  antes:   {'chunks': 3, 'chars': 534, 'approx_tokens': 134}
  despues: {'chunks': 2, 'chars': 373, 'approx_tokens': 93}

Solo dos de las seis queries pierden un chunk con THRESHOLD=4.0 (ya lo viste en la Lección 04), y en esas dos el footprint cae de forma proporcional —de 128 a 84 tokens aproximados, de 134 a 93—. El punto no es que el ahorro sea dramático en este corpus de 57 chunks —no lo es—, sino que el efecto es real y gratuito: el mismo filtro que ya se aplicaba por calidad reduce, sin ningún trabajo adicional, la factura de contexto de la búsqueda. En un corpus de producción con miles de chunks y queries que traen más ruido de baja relevancia, ese mismo efecto colateral se vuelve mucho más significativo.


Lo que esta lección NO decide

Vale la pena ser explícito sobre la frontera, porque es fácil confundir "medir el tamaño" con "decidir qué hacer con ese tamaño". retrieval_footprint mide cuántos chunks, caracteres y tokens aproximados salen de search_docs (ya filtrados por las Lecciones 04-05) — no decide en qué orden entran al prompt final, ni si conviene resumir un chunk largo en vez de incluirlo completo, ni cómo se reparte el espacio de la ventana de contexto entre estos chunks y el resto de la conversación (el historial de turnos, el system prompt, otros tool_result). Esas tres decisiones —orden, compresión, presupuesto compartido— son exactamente el contenido de context-engineering-guide. Esta lección entrega el número que esa guía necesita como insumo de entrada; no toma ninguna de sus decisiones.


Errores comunes

  1. Confundir approx_tokens con un conteo exacto. La heurística de 4 caracteres por token es una aproximación de regla general, no el resultado de un tokenizador real — puede desviarse notablemente en texto con muchos números, símbolos, o términos fuera de un vocabulario común en inglés (como los doc_id con guiones de este corpus, cancellation-policy, que un tokenizador real podría partir de forma distinta a como lo estima esta heurística). Útil para una estimación de orden de magnitud; no para un presupuesto de producción sin verificar contra el tokenizador real.

  2. Medir el footprint sobre los resultados crudos de search_docs, sin aplicar antes los filtros de las Lecciones 04-05. El footprint que realmente le llega al prompt final es el de los chunks que sobrevivieron a drop_stale_sources y apply_threshold — medir antes de filtrar sobreestima sistemáticamente cuánto contexto se está gastando de verdad.

  3. Asumir que más k siempre significa mejor respuesta, ignorando el costo de tamaño. La sección "Ejemplo trabajado" mostró que subir de k=3 a k=5 casi duplica el footprint sin traer, necesariamente, más relevancia —el Módulo 2 ya estableció que k alto solo expone más cola de baja relevancia—. Cada unidad de k es una decisión de costo, no una mejora gratuita.

  4. Pensar que esta lección decide el presupuesto final de contexto. Como aclaró la sección anterior, retrieval_footprint mide, no decide. Confundir esto lleva a intentar resolver aquí problemas —orden de los chunks en el prompt, cuándo comprimir— que pertenecen a context-engineering-guide.


Ejercicios

Ejercicio 1: Mide el footprint de una sola query (Fácil)

Ejecuta search_docs("Is there wifi in the Lounge?", k=1) y calcula su retrieval_footprint. ¿Cuántos tokens aproximados agrega al prompt un único chunk?

Ver solución
hits = search_docs("Is there wifi in the Lounge?", k=1)
print(retrieval_footprint(hits))

Salida esperada:

{'chunks': 1, 'chars': 174, 'approx_tokens': 44}

Explicación: un único chunk (lounge-room-manual-004, el mismo del Módulo 3) agrega aproximadamente 44 tokens al prompt — una fracción pequeña frente a los ~130-150 tokens que agregaban las mismas queries con k=3 en el ejemplo trabajado, confirmando que el footprint escala linealmente con el número de chunks devueltos, no con la pregunta en sí.

Ejercicio 2: Compara el footprint total de un lote antes y después del filtro completo (Medio)

Suma el retrieval_footprint de las seis queries ancla con k=3, antes y después de aplicar apply_threshold. Reporta cuántos tokens aproximados se ahorran en total sobre el lote completo.

Ver solución
total_before_chars = 0
total_after_chars = 0

for q in anchor_queries:
    hits = search_docs(q, k=3)
    total_before_chars += retrieval_footprint(hits)["chars"]
    total_after_chars += retrieval_footprint(apply_threshold(hits))["chars"]

tokens_before = round(total_before_chars / CHARS_PER_TOKEN_APPROX)
tokens_after = round(total_after_chars / CHARS_PER_TOKEN_APPROX)

print(f"total antes:   {total_before_chars} chars (~{tokens_before} tokens)")
print(f"total despues: {total_after_chars} chars (~{tokens_after} tokens)")
print(f"ahorro: ~{tokens_before - tokens_after} tokens sobre el lote de seis queries")

Salida esperada:

total antes:   3286 chars (~822 tokens)
total despues: 2951 chars (~738 tokens)
ahorro: ~84 tokens sobre el lote de seis queries

Explicación: el ahorro sobre este lote específico es real pero modesto (~84 de ~822 tokens, ~10%) porque THRESHOLD=4.0 solo recorta un chunk en dos de las seis queries — un resultado consistente con lo que ya viste en la Lección 04: este umbral está calibrado para no perder ningún top-1 correcto, no para maximizar el ahorro de tamaño. El punto de este ejercicio no es que el ahorro sea grande en este corpus de juguete, sino que es medible y acumulativo: sobre miles de búsquedas por día en un sistema de producción, un ahorro del 10% por búsqueda —sin ninguna pérdida de calidad, porque son exactamente los chunks que el filtro de relevancia ya descartaba— se vuelve una cifra real en la factura de contexto agregada.

Ejercicio 3: Diseña un chequeo de presupuesto máximo (Difícil)

Escribe una función within_budget(hits, max_tokens) que devuelva True si el retrieval_footprint de hits no excede max_tokens, False en caso contrario. Después, escribe trim_to_budget(hits, max_tokens) que, si within_budget da False, vaya descartando el chunk de menor score uno por uno hasta que el footprint entre en el presupuesto (o hasta quedarse sin chunks). Pruébala con la query de Boardroom en k=5 y un presupuesto de max_tokens=150.

Ver solución
def within_budget(hits, max_tokens):
    return retrieval_footprint(hits)["approx_tokens"] <= max_tokens


def trim_to_budget(hits, max_tokens):
    """Descarta, uno a la vez, el chunk de menor score hasta entrar en
    el presupuesto de tokens aproximados -- o hasta quedarse sin chunks."""
    remaining = sorted(hits, key=lambda h: h["score"], reverse=True)
    while remaining and not within_budget(remaining, max_tokens):
        remaining.pop()  # el ultimo de la lista ordenada por score = el mas bajo
    return remaining


hits = search_docs("What is the cancellation policy for Boardroom bookings?", k=5)
print("footprint original:", retrieval_footprint(hits))

trimmed = trim_to_budget(hits, max_tokens=150)
print("footprint recortado:", retrieval_footprint(trimmed))
print("chunks que sobreviven:", [h["chunk_id"] for h in trimmed])

Salida esperada:

footprint original: {'chunks': 5, 'chars': 986, 'approx_tokens': 246}
footprint recortado: {'chunks': 3, 'chars': 560, 'approx_tokens': 140}
chunks que sobreviven: ['cancellation-policy-003', 'cancellation-policy-002', 'membership-tiers-faq-000']

Explicación: trim_to_budget descarta los dos chunks de menor score (refund-policy-002 y no-show-policy-000, los dos últimos del top-5) hasta que el footprint entra en el presupuesto de 150 tokens aproximados, quedando con los tres de mayor score — exactamente los mismos tres, con el mismo footprint (560 caracteres, 140 tokens aproximados), que search_docs con k=3 directamente al principio de esta lección: una confirmación cruzada de que recortar por score desde k=5 reproduce lo que pedir menos k desde el principio ya daba. Este ejercicio es deliberadamente el borde de la frontera con context-engineering-guide: recortar por score hasta entrar en un presupuesto fijo y simple (el chunk de menor score se va primero, sin excepciones) es una versión mínima de presupuestar; una decisión más fina —por ejemplo, priorizar diversidad de doc_id en vez de solo score, o resumir en vez de eliminar— es exactamente el tipo de refinamiento que le corresponde a esa guía vecina, no a esta lección.


Resumen y siguiente paso

  • retrieval_footprint(hits) mide chunks, caracteres, y una estimación de tokens (heurística de 4 caracteres/token, rotulada honestamente como aproximación, no como conteo exacto) de un lote de resultados de search_docs.
  • Con k=3, cada búsqueda de las seis queries ancla agrega entre 128 y 148 tokens aproximados; subir a k=5 casi duplica esa cifra sin traer, necesariamente, más relevancia —el mismo límite de k que el Módulo 2 ya había medido con otro enfoque—.
  • Filtrar por umbral (Lección 04) reduce el footprint como efecto colateral directo: menos chunks irrelevantes citados también es menos contexto gastado, un ahorro pequeño en este corpus de 57 chunks pero acumulativo en un sistema de producción real.
  • Esta lección mide el insumo; no decide el orden, la compresión, ni el presupuesto compartido dentro del prompt final — eso es, explícita y completamente, el trabajo de context-engineering-guide.

Siguiente lección: 07 — El costo del pipeline. De cuánto pesa cada búsqueda a cuánto cuesta ejecutarla — tiempo real de indexar y buscar con BM25, y el contraste honesto con un embedding pagado.


Recursos adicionales

  1. context-engineering-guide — dónde se decide el orden, la compresión, y el presupuesto compartido de la ventana de contexto, usando el footprint que esta lección mide como insumo de entrada.
  2. production-rag-and-document-ingestion-guide, Módulo 2, Lección 07 (search/query/k, ejecutado) — la observación original de que k alto solo expone más cola de baja relevancia, la base del argumento de costo de esta lección.
  3. production-rag-and-document-ingestion-guide, Módulo 3, Lección 06 (Acotando la tool: k y truncamiento) — MAX_CHARS=280, el límite ya establecido que mantiene angosto el rango de footprint por chunk individual.
  4. Python — len() sobre strings — la base de la medición de caracteres en retrieval_footprint, y por qué mide bytes de texto, no tokens reales.