Módulo 2: Latencia y costo como arquitectura

2. El LLM es lento y cuesta por llamada

Descripción

Al terminar esta lección vas a poder poner un número a las dos frases que la lección 1 dejó como intuición: el LLM es lento y cuesta por llamada. "Lento" deja de ser un adjetivo cuando lo mides: una respuesta de un LLM tarda cientos de milisegundos a segundos, y esa latencia crece con la cantidad de tokens que genera —una respuesta larga tarda más que una corta, de forma proporcional—. "Cuesta por llamada" deja de ser vago cuando ves el precio: se paga por token, con una tarifa por cada mil tokens de entrada y otra por cada mil de salida, y el modelo caro cuesta cerca de diez veces el barato por el mismo trabajo. Vas a medir ambas cosas en el stub simulado, y luego vas a ver el taxímetro corriendo: cómo esa llamada que cuesta una fracción de centavo se convierte en cientos o miles de dólares al mes cuando la multiplicas por el volumen real de Mercado.

Esto importa porque las dos restricciones son invisibles en la prueba y brutales en producción. Cuando pruebas la búsqueda semántica una vez, con una query, la latencia se siente aceptable ("medio segundo, va bien") y el costo es imperceptible ("$0.008, ni se nota"). El problema es que ninguna feature vive de una sola llamada: vive de decenas de miles al día. Y ahí las dos cifras que ignoraste se agrandan hasta volverse el problema principal: la latencia, multiplicada por cada usuario que espera, define si la feature se siente rápida o pastosa; el costo, multiplicado por cada llamada, define si la feature es rentable o si se come el margen. Medir las dos por llamada y proyectarlas a escala es el paso que separa "probé que funciona" de "sé lo que va a costar y cuánto va a tardar cuando lo enciendan de verdad".

Conexión con el módulo: esta lección es la báscula del módulo. La lección 1 te dio la intuición y las analogías; aquí las conviertes en cifras que puedes reproducir. No puedes presupuestar (lección 3) lo que no sabes medir, ni decidir si un cascade (lección 4) o una caché (lección 5) valen la pena sin saber cuánto cuesta y tarda una llamada. Todo lo que sigue en el módulo se apoya en el stub que construyes aquí —call_llm(model, in_tokens, out_tokens), que devuelve la latencia y el costo simulados de una llamada— y en el taxímetro, la idea de que el costo escala lineal con el número de llamadas. Es la lección más aritmética del módulo, y es la base de todas las demás.

El taxímetro que corre en cada llamada

Piénsalo así. Subes a un taxi para cruzar la ciudad. En cuanto arranca, un aparato en el tablero empieza a correr: cada tramo que avanzas, el número sube unos centavos. No pagas una tarifa plana por "usar el taxi ese día"; pagas por cada unidad de recorrido, y al bajarte, lo que debes es exactamente lo que marca el aparato. Un viaje corto cuesta poco y ni lo piensas. Pero si hicieras cien viajes cortos en un día —algo que una persona nunca hace, pero un sistema sí—, la suma de todos esos "poquitos" sería una cuenta enorme.

Ahora cambia el taxi por una llamada al LLM. Cada llamada tiene su propio taxímetro, y corre en dos ejes a la vez: tiempo (tarda cientos de ms a segundos, y tarda más mientras más larga sea la respuesta, igual que un viaje más largo cuesta más) y dinero (paga por token, entrada y salida). Una llamada suelta es como un viaje corto: la latencia se tolera y el costo no se siente. Pero tu feature no hace un viaje: hace decenas de miles al día. El taxímetro que en una llamada marca $0.008 y 750 ms, multiplicado por 100,000 llamadas diarias, marca miles de dólares al mes de costo y define la experiencia de cada usuario que espera.

La diferencia con una función normal es exactamente esta. Una función clásica —un lookup en un índice, una consulta simple a la base de datos— no tiene taxímetro: cuesta esencialmente cero y responde en microsegundos, así que llamarla mil veces o un millón no cambia nada. El LLM sí lo tiene, en los dos ejes. Por eso "es solo otra llamada a una API" es tan peligroso: trata al taxi como si fuera el metro de tarifa plana. Esta lección es aprender a leer el taxímetro —medir la llamada y proyectar la cuenta— antes de subirte al viaje.

Ejemplo trabajado: medir una llamada y ver el taxímetro

Vamos a construir el stub que simula el LLM y a medir con él. El stub es una función determinista: no llama a ninguna API, no usa la red, no tiene claves. Le pones números fijos de latencia y precio, y te devuelve la latencia y el costo de una llamada. Con eso medimos la búsqueda semántica de Mercado y proyectamos su costo mensual a distintos volúmenes.

Los hechos fijos del stub, declarados una sola vez: cada modelo tiene un precio por 1000 tokens de entrada (usd_in) y otro por 1000 de salida (usd_out), una latencia base por llamada (base_ms) y una latencia extra por cada token de salida (ms_per_tok, porque el modelo genera la respuesta token a token, y más tokens = más tiempo). El cheap model es rápido y barato; el strong model es más lento y cuesta cerca de diez veces más. Son números elegidos para el ejemplo —no de ningún proveedor real—, pero la geometría es la del mundo real: un modelo pequeño responde en fracciones del tiempo y del costo de uno grande.

# Leccion 02 — el LLM es lento y cuesta por llamada
# STUB del LLM: todo SIMULADO. Cero red, cero API, cero claves.

MODELS = {
    "cheap":  dict(usd_in=0.0008, usd_out=0.004, base_ms=90,  ms_per_tok=0.4),
    "strong": dict(usd_in=0.008,  usd_out=0.040, base_ms=300, ms_per_tok=3.0),
}

def call_llm(model, in_tokens, out_tokens):
    """Simula UNA llamada al LLM. Devuelve (latency_ms, cost_usd). Determinista."""
    m = MODELS[model]
    latency_ms = m["base_ms"] + out_tokens * m["ms_per_tok"]
    cost_usd = (in_tokens / 1000) * m["usd_in"] + (out_tokens / 1000) * m["usd_out"]
    return latency_ms, cost_usd

# Una llamada tipica de la busqueda semantica de Mercado:
# el prompt lleva la query del usuario + instrucciones (~300 tokens de entrada)
# y el modelo responde una lista reordenada (~150 tokens de salida).
IN_TOK, OUT_TOK = 300, 150

print(f"{'model':<8}{'latency_ms':>12}{'cost_usd':>14}")
for name in ("cheap", "strong"):
    lat, cost = call_llm(name, IN_TOK, OUT_TOK)
    print(f"{name:<8}{lat:>12.1f}{cost:>14.6f}")

# Contraste: una funcion normal (un lookup en un indice) — el "componente clasico".
# Un lookup ronda el microsegundo (~0.001 ms) y no cuesta dinero. Numero representativo.
normal_ms = 0.001
print(f"\n{'lookup':<8}{normal_ms:>12.3f}{0.0:>14.6f}   (funcion normal: ~0 ms, $0)")

# El taximetro: el costo escala LINEAL con el numero de llamadas.
print("\nEl taximetro — costo mensual segun volumen (solo el modelo strong):")
_, unit_cost = call_llm("strong", IN_TOK, OUT_TOK)
for calls_per_day in (1_000, 10_000, 100_000):
    monthly = unit_cost * calls_per_day * 30
    print(f"  {calls_per_day:>7,} llamadas/dia  ->  ${monthly:>10,.2f}/mes")

Qué esperar. Al correrlo:

model     latency_ms      cost_usd
cheap          150.0      0.000840
strong         750.0      0.008400

lookup         0.001      0.000000   (funcion normal: ~0 ms, $0)

El taximetro — costo mensual segun volumen (solo el modelo strong):
    1,000 llamadas/dia  ->  $    252.00/mes
   10,000 llamadas/dia  ->  $  2,520.00/mes
  100,000 llamadas/dia  ->  $ 25,200.00/mes

Lee estos números despacio, porque cada uno es una de las dos restricciones hecha cifra.

La latencia. El cheap model responde en 150 ms; el strong en 750 ms —cinco veces más lento por la misma query—. Compáralos con el lookup, que ronda 0.001 ms: el strong model es unas 750,000 veces más lento que una función normal. Ese es el salto que "es solo otra llamada a una API" ignora. Y fíjate de dónde sale la latencia del strong: base_ms (300) más 150 tokens de salida × ms_per_tok (3.0) = 300 + 450 = 750. La mayor parte del tiempo se va en generar la respuesta token a token. Por eso una respuesta larga tarda más: si el strong tuviera que generar 400 tokens en vez de 150, tardaría 300 + 400×3 = 1500 ms. La latencia no es una constante del modelo; depende de cuánto texto produce. Recuérdalo, porque es la clave del streaming (lección 6).

El costo. El cheap model cuesta $0.000840 por llamada; el strong $0.008400 —diez veces más—. En una llamada, los dos son imperceptibles: menos de un centavo. Aquí es donde el instinto falla: "cuesta centavos, no importa". Pero entonces llega el taxímetro.

El taxímetro. La misma llamada al strong model, multiplicada por el volumen:

  • 1,000 llamadas/día → $252/mes. Un piloto pequeño; se tolera.
  • 10,000 llamadas/día → $2,520/mes. Empieza a doler.
  • 100,000 llamadas/día → $25,200/mes. Esa es una factura que un director financiero nota, por una sola feature.

El costo escala lineal con las llamadas: diez veces más tráfico, diez veces más factura, exactamente. No hay economía de escala que te salve —cada llamada paga su taxímetro completo—. Y este número, $25,200/mes, es el que va a perseguir a la búsqueda semántica todo el módulo: es lo que cuesta la versión ingenua ("todo al strong model"), y es lo que el cascade y la caché van a tener que bajar para que la feature quepa en su presupuesto.

Profundización: los tokens, el input/output, y por qué el caro cuesta más

Vale la pena entender de dónde salen estas cifras, porque el resto del módulo las manipula.

Qué es un token. Un LLM no procesa caracteres ni palabras completas: procesa tokens, que son fragmentos de texto (aproximadamente 3-4 caracteres, o cerca de ¾ de una palabra en inglés). "Búsqueda semántica" son unos pocos tokens; un párrafo son decenas; un documento largo, miles. Todo lo que le mandas al modelo (el prompt: la query del usuario más tus instrucciones más cualquier contexto) son tokens de entrada, y todo lo que el modelo genera son tokens de salida. El precio se cobra por los dos, por separado, típicamente por cada millón (o mil) de tokens.

Por qué la salida cuesta más que la entrada. En casi todos los modelos, el precio por token de salida es varias veces el de entrada —en el stub, usd_out (0.004 / 0.040) es 5× y 5× el usd_in (0.0008 / 0.008)—. La razón es técnica y no la enseñamos aquí (es de la frontera de inferencia): generar cada token de salida es más caro computacionalmente que leer un token de entrada. Lo que sí importa a nivel de arquitectura es la consecuencia: una respuesta larga es cara por partida doble —cuesta más dinero (más tokens de salida al precio alto) y más tiempo (más ms_per_tok)—. Por eso una decisión de diseño tan simple como "pide respuestas concisas" o "limita la longitud de la salida" es, literalmente, una decisión de costo y latencia.

Por qué el modelo caro cuesta ~10x. El strong cuesta diez veces el cheap porque es un modelo más grande y capaz. En el mundo real esta brecha existe y es grande: un modelo pequeño de una familia (algo como Claude Haiku) frente a uno grande (algo como Claude Sonnet u Opus) tiene una diferencia de precio de varias veces —el orden de magnitud del stub es realista, aunque los números exactos cambian con el proveedor y la versión, que por eso no fijamos—. Esta brecha es la razón de ser del model cascade (lección 4): si el barato puede resolver una query bien, mandarla al caro es pagar 10× de más por nada. Y es la razón de ser de la caché (lección 5): si ya calculaste una respuesta, volver a pagarla —aunque sea al barato— es tirar dinero.

Cómo se estima el costo de una feature. El patrón es siempre el mismo: costo_mensual = costo_por_llamada × llamadas_por_día × 30. Y costo_por_llamada depende de los tokens: (in_tokens/1000)×usd_in + (out_tokens/1000)×usd_out. Con esas dos fórmulas puedes estimar, antes de escribir la feature, cuánto va a costar al volumen esperado. Ese es el taxímetro puesto en el diseño, y es lo que la lección 3 convierte en un presupuesto.

Errores comunes

Medir la latencia y el costo con una sola llamada (de muestreo). Qué pasa: pruebas la feature una vez, ves "750 ms, $0.008" y concluyes que va bien. Pero una llamada no te dice ni la latencia de cola (algunas queries generan respuestas largas y tardan el doble) ni el costo agregado (una llamada barata × 100,000 = caro). Por qué pasa: en desarrollo siempre pruebas con volumen uno, y el volumen uno esconde las dos restricciones. Cómo detectarlo: si tu estimación de costo o latencia sale de "lo probé y se sintió bien" en vez de "medí la llamada y la multipliqué por el volumen esperado", muestreaste, no mediste. Cómo corregirlo: mide la llamada típica y la pesada (más tokens de salida), y proyecta el costo a volumen real —el taxímetro, no el viaje suelto—.

Ignorar los tokens de salida (de foco equivocado). Qué pasa: el equipo optimiza el prompt de entrada para ahorrar tokens, pero deja que el modelo genere respuestas larguísimas sin límite. Como la salida cuesta más por token y añade latencia por token, la respuesta larga es donde de verdad se va el dinero y el tiempo. Por qué pasa: el prompt de entrada es visible y editable, así que se optimiza; la longitud de la salida se siente "lo que el modelo decida" y se ignora. Cómo detectarlo: si no tienes un límite ni una instrucción sobre la longitud de la respuesta, no estás controlando la mitad más cara del taxímetro. Cómo corregirlo: trata la longitud de la salida como una palanca de diseño —pide concisión, limita max_tokens, y en features de alto volumen esto solo puede recortar el costo de forma notable—.

Tratar el costo por llamada como el costo total (de escala). Qué pasa: alguien dice "una llamada cuesta menos de un centavo, el costo es despreciable" y cierra el tema. El error es confundir el costo unitario (minúsculo) con el costo agregado (enorme), que solo aparece al multiplicar por el volumen. Por qué pasa: el número por llamada es tranquilizadoramente pequeño, y el cerebro no multiplica por 3 millones (100,000/día × 30) de forma intuitiva. Cómo detectarlo: si tu argumento sobre el costo no incluye el volumen mensual, estás mirando el viaje y no el taxímetro. Cómo corregirlo: nunca cites el costo por llamada sin citar, en la misma frase, el costo mensual a volumen esperado —$0.008/llamada es $25,200/mes a 100k/día, y esa segunda mitad es la que decide—.

Ejercicios

Ejercicio 1 — La query pesada. Con el stub del ejemplo, calcula a mano la latencia y el costo de una query pesada (300 tokens de entrada, 400 de salida) para los dos modelos, y di cuánto más tarda y cuesta la pesada frente a la típica (150 de salida) en el strong model.

Ver solución

Fórmulas del stub: latency = base_ms + out_tokens × ms_per_tok; cost = (in/1000)×usd_in + (out/1000)×usd_out.

  • cheap, pesada (in 300, out 400): latencia = 90 + 400×0.4 = 250 ms; costo = 0.3×0.0008 + 0.4×0.004 = 0.00024 + 0.0016 = $0.001840.
  • strong, pesada: latencia = 300 + 400×3.0 = 1500 ms; costo = 0.3×0.008 + 0.4×0.040 = 0.0024 + 0.016 = $0.018400.

La pesada frente a la típica en el strong model: la típica (out 150) tarda 750 ms y cuesta $0.0084; la pesada (out 400) tarda 1500 ms (el doble) y cuesta $0.0184 (2.2×). Casi todo el aumento viene de los tokens de salida: 250 tokens más de salida añaden 250×3 = 750 ms y 0.25×0.040 = $0.010. Moraleja: la longitud de la respuesta domina tanto la latencia como el costo, y por eso es una palanca de diseño de primer orden.

Ejercicio 2 — El taxímetro del agente de soporte. El agente de soporte de Mercado tiene conversaciones de, en promedio, 4 turnos, y cada turno es una llamada al strong model con 500 tokens de entrada y 200 de salida. Si Mercado tiene 5,000 conversaciones al día, ¿cuánto cuesta el agente al mes? (Usa las tarifas del strong: usd_in=0.008, usd_out=0.040.)

Ver solución
  • Costo por turno: (500/1000)×0.008 + (200/1000)×0.040 = 0.004 + 0.008 = $0.012.
  • Costo por conversación: 4 turnos × $0.012 = $0.048.
  • Llamadas/día: 5,000 conversaciones × 4 turnos = 20,000 llamadas/día.
  • Costo mensual: $0.048 × 5,000 conversaciones × 30 = $7,200/mes (equivalente: $0.012 × 20,000 × 30 = $7,200).

$7,200/mes por el agente de soporte, si todo va al strong model. La lección clave para el resto del módulo: el costo del agente se acumula por turno —una conversación no es una llamada, son cuatro—, así que la restricción de costo muerde más fuerte en features conversacionales que en una búsqueda de un solo tiro. Es exactamente por eso que la lección 7 aplica el cascade y la caché al agente de soporte.

Ejercicio 3 — ¿Cuándo un LLM es "solo otra llamada a una API"? Un compañero argumenta: "en el fondo, llamar al LLM es como llamar a nuestro microservicio de inventario: le mandas una petición, te devuelve una respuesta. No veo por qué necesita un trato especial." Responde con las dos diferencias medibles que viste en esta lección, y da un ejemplo de una decisión de arquitectura que cambia por cada una.

Ver solución

Las dos diferencias, con su número:

  1. Latencia. El microservicio de inventario responde en pocos milisegundos; el LLM tarda cientos de ms a segundos (750 ms el strong en el ejemplo, y más si la respuesta es larga). Decisión de arquitectura que cambia: dónde lo pones en el flujo. Un servicio de milisegundos puede ir síncrono en el camino crítico sin que nadie lo note; un LLM de segundos en el camino crítico hace que la feature se sienta lenta, así que hay que decidir si usar streaming, sacarlo del camino con async, o acotar su latencia con un presupuesto.

  2. Costo. El microservicio de inventario es infraestructura propia: su costo marginal por llamada es esencialmente cero. El LLM cobra por token, cada vez: $0.008 por llamada, que a 100k/día son $25,200/mes. Decisión de arquitectura que cambia: qué modelo atiende qué y qué cacheas. Con un servicio gratis no importa llamarlo de más; con un LLM, cada llamada innecesaria es dinero, así que aparecen el cascade (barato para lo fácil) y la caché (no repagar) —decisiones que no tendrían sentido para el microservicio de inventario—.

La síntesis: el LLM se llama como una API, pero se comporta como un taxi con taxímetro en dos ejes, no como el metro de tarifa plana. Ese comportamiento es lo que exige el rediseño, y es todo lo que este módulo enseña.

Resumen y siguiente paso

En esta lección mediste las dos restricciones que la lección 1 solo nombró. Con el stub call_llm viste que el cheap model responde en 150 ms y el strong en 750 ms —cientos de miles de veces más lento que un lookup—, y que la latencia crece con los tokens de salida (300 base + 3 ms por token en el strong), lo que hace de la longitud de la respuesta una palanca de diseño. Viste que se paga por token, entrada y salida por separado, con la salida más cara, y que el strong cuesta ~10× el cheap. Y viste el taxímetro: la misma llamada de $0.008 se convierte en $252, $2,520 o $25,200 al mes según el volumen, porque el costo escala lineal con las llamadas. Ese $25,200/mes de la búsqueda semántica al strong model es el número que el resto del módulo va a atacar.

Antes de avanzar deberías poder: estimar la latencia y el costo de una llamada al LLM a partir de sus tokens de entrada y salida; proyectar el costo mensual de una feature a volumen real (el taxímetro); explicar por qué la longitud de la respuesta domina tanto el costo como la latencia; y responder a "es solo otra llamada a una API" con las dos diferencias medibles.

Lo que sigue es convertir estos números en una restricción de diseño. En la lección 3 vas a definir el latency budget y el cost budget de una feature —cuánto puede tardar una búsqueda antes de que el usuario se vaya, cuánto puede costar al mes antes de que desaparezca el margen— y vas a escribir la compuerta que verifica si una opción los respeta o los viola. Vas a ver, ejecutando, que mandar todo al strong model viola los dos presupuestos de la búsqueda semántica de Mercado. Con eso, las tres lecciones que siguen (cascade, caché, async) dejan de ser trucos sueltos y se vuelven lo que son: las técnicas para hacer que el diseño quepa en su presupuesto.

Recursos