M6 — Latencia, costo y operación
Outcome del módulo: medir dónde se va el tiempo y el dinero en cada query de scholar-rag, atacar el cuello con datos, y dejar el sistema observable en producción. Aquí cierras el ciclo: de “funciona” a “sé operarlo”.
Concepto 1 — Percentiles: por qué el promedio miente
Idea núcleo: el tiempo de respuesta se mide en percentiles (p50, p95, p99), no en promedio. El usuario siente el p99, y el promedio lo esconde.
Entender (capa 1)
Texto: si mides latencia con el promedio, un sistema que responde en 200ms casi siempre pero se va a 5s el 2% de las veces te da un promedio bonito que oculta el desastre. Los percentiles cuentan la verdad: p50 (mediana) es “la mitad responde más rápido que esto”; p95 es “el 5% peor tarda más que esto”; p99 es “el 1% peor”. Un p99 alto significa que uno de cada cien usuarios sufre, y en un servicio con volumen eso es mucha gente, todo el tiempo.
Por qué importa el p99 y no el promedio: las latencias no se distribuyen parejo, tienen cola larga (una query que cayó en un pico de GC, un cold start, un lock de BD). Esa cola es la experiencia real de los momentos malos, y es donde vive la percepción de “el sistema a veces se traba”. Optimizar el promedio puede dejar el p99 intacto; optimizar el p99 es lo que hace que el sistema se sienta sólido.
Visual:
flowchart LR
D[distribucion de latencias] --> P50["p50 = 180ms: la experiencia tipica"]
D --> P95["p95 = 600ms: dias regulares"]
D --> P99["p99 = 4200ms: la cola que el usuario recuerda"]
AVG["promedio = 240ms: esconde la cola"] -.engaña.-> D
Ejemplo:
import numpy as np
lat = np.array(latencies_ms)
print("p50", np.percentile(lat, 50),
"p95", np.percentile(lat, 95),
"p99", np.percentile(lat, 99))
# el promedio (lat.mean()) casi nunca es la métrica que reportas
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué se reporta p99 y no el promedio de latencia?
- Back: porque las latencias tienen cola larga (picos de GC, cold starts, locks); el promedio la esconde y el usuario justamente siente esa cola. p99 = “el 1% peor tarda más que esto”; con volumen, ese 1% es mucha gente sufriendo seguido. Optimizar el promedio puede no tocar el p99.
Feynman: “Si meto la cabeza en el horno y los pies en el congelador, en promedio estoy tibio. El promedio de latencia es igual de útil. Los percentiles dicen qué tan mal la pasa el que peor la pasa, que es lo que la gente recuerda.”
Aplicar (capa 3)
Instrumenta scholar-rag para registrar la latencia de cada query y calcula p50/p95/p99 sobre unas cientos. Anótalos. Ese p99 es el número que vas a bajar, y el antes/después es la respuesta a la pregunta 3 de las cinco.
Concepto 2 — Profiling: mide dónde se va el tiempo, no adivines
Idea núcleo: el cuello casi nunca está donde crees. Antes de optimizar, mides la latencia por etapa (embed, retrieve, grade, generate) y atacas la que domina.
Entender (capa 1)
Texto: la regla de oro del rendimiento: mide, no adivines. Es tentador “optimizar” el retrieval porque suena lento, cuando el 80% del tiempo se va en la llamada al LLM. Perfilar es medir cuánto tarda cada etapa del grafo por separado, para saber cuál domina el p99. En un RAG el reparto típico es: embedding (chico si está bien), retrieval (chico con índice, enorme sin él — ver M2), grade (una llamada extra al LLM), generate (casi siempre la más grande, porque generar tokens es lento). Pero “típico” no es “el tuyo”: lo mides.
Con el desglose en mano, la optimización se vuelve obvia: atacas la etapa que más pesa. Si generate domina, quizá streamear la respuesta o usar un modelo más rápido; si retrieve domina, ya sabes (índice, M2); si grade cuesta demasiado para lo que aporta (M4), lo quitas. Optimizar sin perfilar es apostar; con perfil, es cirugía.
Visual:
flowchart LR
Q[query] --> E["embed: 15ms"]
E --> R["retrieve: 40ms"]
R --> G["grade: 300ms - llamada LLM"]
G --> GE["generate: 900ms - domina"]
GE --> T[total p99]
GE -.ataca esto primero.-> O[optimizar]
Ejemplo:
import time
async def timed(name, coro):
t0 = time.perf_counter()
r = await coro
stage_ms[name] = (time.perf_counter() - t0) * 1000
return r
ctx = await timed("retrieve", retrieval.hybrid_search(vec))
ans = await timed("generate", llm.generate(q, ctx))
# suma stage_ms sobre muchas queries: la etapa dominante es tu objetivo
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué perfilar antes de optimizar?
- Back: porque el cuello casi nunca está donde uno cree; el desglose de latencia por etapa (embed/retrieve/grade/generate) muestra cuál domina el p99. Optimizar sin perfilar es apostar; con el perfil, atacas la etapa que de verdad pesa (normalmente
generate).
Feynman: “Perfilar es medir el tráfico antes de ensanchar una calle. Ensanchar la calle vacía porque ‘se ve angosta’ no sirve; el trancón estaba tres cuadras más allá. El perfil te muestra dónde está el trancón real.”
Aplicar (capa 3)
Agrega timers por etapa en scholar-rag y corre 100 queries. Ordena las etapas por tiempo. Identifica la dominante: esa, y solo esa, es la que optimizas primero. Guarda el desglose para el design doc.
Concepto 3 — El LLM como dependencia hostil: el wrapper robusto
Idea núcleo: toda llamada al LLM puede tardar de más, fallar o devolver basura. Un wrapper con timeout + retry con backoff + validación convierte esos fallos en algo manejable. Y es reusable en tus otros productos.
Entender (capa 1)
Texto: este es el gap técnico que atraviesa todo tu portafolio: el LLM se trata como servicio confiable, y no lo es. Un cliente LLM listo para producción envuelve cada llamada con: timeout (no colgar indefinido, M5), retry con backoff exponencial (un 429 o 5xx se reintenta esperando cada vez más, para no golpear un servicio caído), validación de output (parsear contra schema antes de usar, M4), y fallback (qué responder cuando, tras los reintentos, no hay respuesta buena). Sin esto, un hipo del proveedor se propaga como un crash a tu usuario.
Lo valioso: este wrapper no es solo para scholar-rag. Es un módulo que importas en market-discovery, internal-console, outlier-miner, cualquier producto tuyo que llame a un LLM. Construirlo bien una vez, con tests, es a la vez una mejora de scholar-rag y una pieza de infraestructura de tu portafolio, la que demuestra que sabes operar LLMs en serio.
Visual:
flowchart TB
C[llamada al LLM] --> T{timeout}
T -->|ok| V{valida schema?}
T -->|tarda de mas| RT[retry con backoff]
RT -->|reintentos agotados| FB[fallback: respuesta degradada]
V -->|si| U[usar]
V -->|no| RT
Ejemplo:
async def robust_complete(prompt, *, retries=3, timeout_s=20):
for attempt in range(retries):
try:
async with asyncio.timeout(timeout_s):
raw = await client.complete(prompt)
return RagAnswer.model_validate(raw) # valida (M4)
except (TimeoutError, RateLimitError, ValidationError) as e:
await asyncio.sleep(2 ** attempt) # backoff: 1s, 2s, 4s
last = e
return RagAnswer(answer="No disponible ahora.", grounded=False) # fallback
Fijar (capa 2)
Nota atómica:
- Front: ¿qué cuatro cosas envuelve un cliente LLM listo para producción?
- Back: timeout (no colgar), retry con backoff exponencial (429/5xx sin golpear al servicio caído), validación de output contra schema (no confiar en la forma) y fallback (respuesta degradada cuando se agotan los reintentos). Y conviene que sea un módulo reusable en todos tus productos que llaman al LLM.
Feynman: “Llamar al LLM sin wrapper es marcar un teléfono que a veces no contesta, a veces contesta en chino, a veces tarda una hora, y creer que siempre responde claro y rápido. El wrapper es el protocolo: cuelgo si tarda, vuelvo a marcar con paciencia, y si nada, tengo un plan B.”
Aplicar (capa 3)
Extrae las llamadas al LLM de scholar-rag a un robust_complete con timeout, retry+backoff, validación y fallback. Escríbele tests (simulando 429 y salida off-schema). Ese módulo, con tests, es candidato a vivir por separado y ser importado por tus otros productos.
Concepto 4 — Costo por query: la métrica que casi nadie mide
Idea núcleo: cada query tiene un costo en tokens (dinero real). Medirlo por etapa te deja decidir el trade-off calidad/costo con números, no con miedo.
Entender (capa 1)
Texto: un RAG hace varias llamadas al LLM por query (el grade de cada chunk, la generación final), y cada una consume tokens que cuestan. Si no lo mides, no sabes cuánto te cuesta atender a un usuario, ni cuál etapa es la cara. Medir el costo por query —tokens de entrada y salida por llamada, multiplicados por el precio del modelo— abre decisiones concretas: quizá grade cuesta la mitad del presupuesto para lo poco que aporta (M4), o un modelo más barato en la generación baja el costo 5x con una caída mínima de faithfulness (que ya sabes medir, M1). El eje calidad/costo/latencia es un triángulo: mover uno mueve los otros, y solo lo navegas con los tres medidos.
Esta es una métrica que separa al que “montó un RAG” del que piensa como dueño de un sistema en producción: sabe cuánto cuesta cada respuesta y por qué.
Visual:
flowchart TB
Q[una query] --> G["grade: N llamadas x tokens"]
Q --> GE["generate: tokens in + out"]
G --> C["costo = tokens x precio del modelo"]
GE --> C
C --> D{decision: calidad vs costo vs latencia}
Ejemplo:
def query_cost(usage, price_in, price_out) -> float:
return usage.input_tokens * price_in + usage.output_tokens * price_out
# registra costo por etapa; suma sobre queries; compáralo con faithfulness (M1)
# para elegir modelo: el barato que no baje faithfulness es el correcto
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué medir el costo por query cambia tus decisiones de arquitectura?
- Back: porque expone el triángulo calidad/costo/latencia con números: ves qué etapa consume el presupuesto (ej.
grade), y puedes cambiar a un modelo más barato midiendo cuánto cae la faithfulness (M1). Sin medir el costo, decides por miedo; con él, navegas el trade-off como dueño del sistema.
Feynman: “No medir el costo por query es manejar un negocio sin saber cuánto cuesta hacer cada plato. Igual vendes, pero no sabes si ganas o pierdes en cada uno, ni cuál ingrediente te está fundiendo.”
Aplicar (capa 3)
Registra tokens de entrada/salida por llamada en scholar-rag y calcula el costo por query. Identifica la etapa más cara. Prueba un modelo más barato en la generación y mide costo y faithfulness a la vez: si la faithfulness aguanta, acabas de bajar el costo con evidencia.
Concepto 5 — Observabilidad: operar, no solo desplegar
Idea núcleo: un sistema en producción necesita que puedas ver qué hace sin adivinar: trazas por request, logs estructurados, y objetivos claros (SLOs). Es la diferencia entre “lo deployé” y “lo opero”.
Entender (capa 1)
Texto: desplegar es que el código corra en un servidor. Operar es saber, en cualquier momento, si está sano y por qué. Tres piezas: logging estructurado (JSON con trace_id, tenant_id, etapa, latencia, costo — no print() sueltos ni except: pass que se tragan el error, los dos vicios que la auditoría encontró en scholar-rag); tracing por request (seguir una query a través de embed → retrieve → grade → generate para ver dónde falló o se demoró); y SLOs (objetivos explícitos: “p99 < 2s”, “faithfulness > 0.85”), que convierten “va bien” en algo verificable y alertable.
Con esto, cuando algo se rompe a las 3am no adivinas: abres la traza de las peticiones lentas, ves la etapa culpable, revisas el log estructurado con su contexto, y sabes. Esa capacidad —diagnosticar producción con datos— es lo que un ingeniero senior internacional da por sentado y lo que cierra la brecha entre demo y sistema.
Visual:
flowchart LR
R[request] --> TR[trace: id unico por toda la cadena]
TR --> LG[logs estructurados: json con contexto]
LG --> SLO{cumple SLO? p99<2s, faith>0.85}
SLO -->|no| AL[alerta: sabes que y donde]
SLO -->|si| OK[sano, verificable]
Ejemplo:
# structlog: contexto en cada log, no print sueltos
log.info("rag_query", trace_id=tid, stage="generate",
latency_ms=stage_ms["generate"], cost_usd=cost, faithful=score)
# nunca: print("done") ni except Exception: pass
Fijar (capa 2)
Nota atómica:
- Front: ¿qué separa “desplegar” de “operar” un sistema?
- Back: operar es poder saber en cualquier momento si está sano y por qué: logging estructurado (JSON con contexto, no
print/except: pass), tracing por request (seguir una query por todas las etapas) y SLOs explícitos (p99<2s, faithfulness>0.85) que vuelven “va bien” verificable y alertable. Es diagnosticar producción con datos en vez de adivinar.
Feynman: “Desplegar es prender el carro y soltarlo. Operar es tener tablero: velocímetro, temperatura, luz de aceite. Sin tablero, te enteras de que algo anda mal cuando el motor ya se fundió en la autopista.”
Aplicar (capa 3)
Reemplaza los print() de scholar-rag por logging estructurado con trace_id, etapa, latencia y costo. Elimina los except: pass (loguea el error con contexto). Define dos SLOs (uno de latencia, uno de faithfulness). Con eso, scholar-rag pasa de desplegado a operable.
Cierre del módulo y del curso
Cerraste el ciclo: sabes dónde se va el tiempo (perfilado) y el dinero (costo por query), envolviste el LLM como el tercero hostil que es, y dejaste el sistema observable. scholar-rag ya no es un RAG que hiciste, es un sistema que sabes operar.
Lo que puedes responder ahora: “¿cuánto cuesta y tarda una query, dónde está el cuello y cómo lo bajaste?” — con el antes/después medido.
Ya tienes lo necesario para las cinco preguntas del Capstone. Ahí conviertes todo esto en los tres artefactos públicos que hacen visible tu profundidad.
El mapa completo (fases, orden de excavación) vive en tu vault:
Personal/Carrera/Mapa-Profundidad-scholar-rag.md.