Todo lo construido en este módulo consume dos recursos que no aparecen en el código: dinero y tiempo. Cada token del SYSTEM_DOCUBOT, de los few-shot del clasificador y del historial de 03-03 se factura en cada llamada; y cada llamada tarda un tiempo que el empleado de Nubelia percibe. En esta lección ponemos números a ambos: entenderemos el modelo de precios por tokens, escribiremos una función de estimación de coste y la proyección mensual de DocuBot (refinando aquella estimación de ~400 €/mes que hicimos a ojo en 01-04), repasaremos las palancas para bajarla, mediremos la latencia con las métricas correctas y terminaremos con la palanca que ataca coste y latencia a la vez: el caching, tanto el de prompts del proveedor como uno de respuestas hecho por nosotros.

Contenido

  1. El modelo de precios: tokens de entrada y de salida
  2. Estimar el coste por llamada en Python
  3. La proyección mensual de DocuBot (refinando la cifra de 01-04)
  4. Palancas de coste
  5. Latencia: TTFT, tiempo total y cómo medirlos
  6. Prompt caching del proveedor: el prefijo estable
  7. Caching de respuestas propio: hash, TTL y validez
  8. Presupuestos y alertas: diseño

El modelo de precios: tokens de entrada y de salida

Las APIs de LLM facturan por tokens procesados, con dos tarifas distintas expresadas normalmente en precio por millón de tokens:

  • Tokens de entrada (input): todo lo que envías — system prompt, few-shot, documentación, historial, pregunta.
  • Tokens de salida (output): todo lo que el modelo genera. Suele costar entre 3 y 5 veces más que la entrada, porque generar es computacionalmente más caro que leer.

Las cifras concretas cambian con frecuencia y dependen del modelo y del proveedor, así que en este curso trabajaremos siempre con variables ilustrativas — sustitúyelas por las tarifas vigentes de tu proveedor el día que calcules en serio:

# precios.py — valores ILUSTRATIVOS, no tarifas reales.
# Consulta la página de precios de tu proveedor y actualiza aquí.

PRECIO_ENTRADA = 3.0        # € por millón de tokens de entrada (modelo gama media)
PRECIO_SALIDA = 15.0        # € por millón de tokens de salida

PRECIO_ENTRADA_MINI = 0.5   # € por millón — modelo pequeño (para el clasificador)
PRECIO_SALIDA_MINI = 2.5

# Factores típicos del prompt caching del proveedor (ver sección 6):
FACTOR_CACHE_LECTURA = 0.1     # leer del caché ≈ 10% del precio de entrada
FACTOR_CACHE_ESCRITURA = 1.25  # escribir en caché ≈ 25% de recargo

Dos consecuencias estructurales de este modelo que ya hemos ido intuyendo:

  • La entrada domina en sistemas tipo DocuBot. Nuestras llamadas envían miles de tokens (system + few-shot + documentación) para generar unos cientos. Optimizar la entrada rinde más que acortar respuestas.
  • Todo lo repetido se re-paga. El SYSTEM_DOCUBOT viaja íntegro en cada una de las miles de llamadas del mes. Esa repetición es exactamente lo que el prompt caching ataca.

Estimar el coste por llamada en Python

El objeto usage de 03-01 nos da los datos exactos; solo falta ponerles precio:

# costes.py
from precios import PRECIO_ENTRADA, PRECIO_SALIDA

def estimar_coste(usage, precio_entrada: float = PRECIO_ENTRADA,
                  precio_salida: float = PRECIO_SALIDA) -> float:
    """Coste en euros de una llamada a partir de su objeto usage."""
    coste_entrada = usage.input_tokens / 1_000_000 * precio_entrada
    coste_salida = usage.output_tokens / 1_000_000 * precio_salida
    return coste_entrada + coste_salida

# Uso tras cualquier llamada:
# respuesta = client.messages.create(...)
# print(f"Esta llamada: {estimar_coste(respuesta.usage):.4f} €")

Para estimar antes de llamar (por ejemplo, para presupuestar un lote), los proveedores ofrecen un endpoint de conteo de tokens que tokeniza sin generar (en el SDK de Anthropic, client.messages.count_tokens(...), que devuelve input_tokens); a falta de él, la regla aproximada de 01-02 (1 token ≈ 0,75 palabras en español/inglés) sirve para órdenes de magnitud.

Conecta estimar_coste a la contabilidad de la clase Conversacion de 03-03 (que ya acumula tokens_entrada_total y tokens_salida_total) y tendrás el coste por conversación sin esfuerzo extra.

La proyección mensual de DocuBot

En 01-04 estimamos "~400 €/mes" casi a ojo, como orden de magnitud para decidir si el proyecto tenía sentido. Ahora tenemos datos reales de usage y una arquitectura concreta (clasificador + respuesta, el encadenado de 02-02). Refinemos con números ficticios pero coherentes con lo medido en el módulo:

Perfil de una consulta a DocuBot (tokens, medidos/estimados):

Componente Entrada Salida
Llamada de clasificación (few-shot de EJEMPLOS_CLASIFICACION) ~900 ~10
Llamada de respuesta: SYSTEM_DOCUBOT (~600) + documentación inyectada (~2.000) + pregunta (~100) + plantilla JSON (~150) ~2.850 ~300

Volumen ficticio: 250 empleados, ~1.200 consultas por día laborable, 22 días/mes → 26.400 consultas/mes.

# proyeccion.py
from precios import (PRECIO_ENTRADA, PRECIO_SALIDA,
                     PRECIO_ENTRADA_MINI, PRECIO_SALIDA_MINI)

def coste_llamada(t_entrada: int, t_salida: int,
                  p_entrada: float, p_salida: float) -> float:
    return t_entrada / 1e6 * p_entrada + t_salida / 1e6 * p_salida

def proyeccion_mensual(consultas_mes: int = 26_400,
                       clasificador_en_mini: bool = False) -> float:
    """Proyección mensual de DocuBot en euros (cifras ilustrativas)."""
    p_ent_cls = PRECIO_ENTRADA_MINI if clasificador_en_mini else PRECIO_ENTRADA
    p_sal_cls = PRECIO_SALIDA_MINI if clasificador_en_mini else PRECIO_SALIDA

    clasificacion = coste_llamada(900, 10, p_ent_cls, p_sal_cls)
    respuesta = coste_llamada(2_850, 300, PRECIO_ENTRADA, PRECIO_SALIDA)

    return consultas_mes * (clasificacion + respuesta)

print(f"Base (todo en gama media): {proyeccion_mensual():.0f} €/mes")
print(f"Clasificador en modelo pequeño: {proyeccion_mensual(clasificador_en_mini=True):.0f} €/mes")

Con las tarifas ilustrativas: la llamada de clasificación cuesta ~0,0029 € y la de respuesta ~0,0130 €, es decir ~0,016 € por consulta y ~417 €/mes. La estimación de servilleta de 01-04 (~400 €) era, para variar, razonable — pero ahora sabemos de dónde sale cada céntimo, y eso es lo que convierte una cifra en una palanca: el 82% del coste es la llamada de respuesta, y dentro de ella el 78% es entrada. Ya sabemos dónde apretar.

Palancas de coste

Ordenadas por relación esfuerzo/impacto para DocuBot, todas con lo aprendido en el módulo:

  1. Modelo más pequeño donde sobra capacidad. El clasificador elige entre 6 etiquetas con few-shot; no necesita el modelo de respuestas. Moverlo al modelo pequeño (clasificador_en_mini=True) baja su coste de ~0,0029 € a ~0,00047 € por consulta: ~62 €/mes ahorrados cambiando un parámetro. La regla general: cada tarea del pipeline al modelo más barato que supere tu evaluación de 02-04 — la rúbrica decide, no la intuición.
  2. Prompt caching del proveedor (sección 6): el prefijo estable (SYSTEM_DOCUBOT + few-shot) pasa a facturarse a ~10% en las llamadas con acierto de caché.
  3. max_tokens ajustado y formato conciso. El system prompt ya pide <150 palabras (02-01); max_tokens=1024 es red de seguridad, no invitación. La salida se paga a precio caro: cada palabra de relleno evitada es ahorro directo.
  4. Recortar la entrada repetida: ¿los 6 ejemplos few-shot son necesarios o la evaluación de 02-04 da igual con 4? ¿La documentación inyectada puede ser más selectiva? (Esta última pregunta es literalmente el módulo 4: RAG es, entre otras cosas, una palanca de coste — inyectar 2 fragmentos relevantes en vez de 10 genéricos.)
  5. Gestión del historial (03-03). En conversaciones, truncado o ventana deslizante convierten el coste cuadrático acumulado en lineal. El resumen con LLM cuesta una llamada, pero se amortiza si evita reenviar miles de tokens por turno.
  6. Caching de respuestas propio (sección 7): la consulta repetida no llama a la API en absoluto — coste cero y latencia cero.

Latencia: TTFT, tiempo total y cómo medirlos

La segunda moneda. Dos métricas distintas que ya rozamos en 03-02, ahora con nombre propio:

  • TTFT (time to first token): tiempo desde que envías la petición hasta que llega el primer token. Es lo que el usuario percibe como "reacción" del bot. Con streaming, es LA métrica de experiencia.
  • Tiempo total: hasta el último token. Gobierna el throughput de procesos por lotes y los timeouts.

Qué influye en cada una:

Factor Afecta a TTFT Afecta al total Está en tu mano
Tamaño de la entrada (prompt + historial) ✅ (hay que procesarla entera antes del primer token) Sí: palancas 4 y 5
Longitud de la salida ✅ (token a token) Sí: max_tokens, formato conciso
Modelo elegido (tamaño) Sí: palanca 1
Carga del proveedor / hora del día No: solo medir (y reintentos de 03-02)
Prompt caching del proveedor ✅ (el prefijo cacheado no se reprocesa) Sí: sección 6

Medición con el streaming de 03-02 — el ejercicio 1 de aquella lección era exactamente esto; lo consolidamos como utilidad:

# latencia.py
import time

def medir_llamada(fn_stream, *args, **kwargs) -> dict:
    """Ejecuta una función de streaming y devuelve TTFT y tiempo total."""
    inicio = time.perf_counter()
    ttft = None
    fragmentos = []

    for texto in fn_stream(*args, **kwargs):     # fn_stream: generador de fragmentos
        if ttft is None:
            ttft = time.perf_counter() - inicio
        fragmentos.append(texto)

    return {
        "ttft_s": round(ttft, 2),
        "total_s": round(time.perf_counter() - inicio, 2),
        "texto": "".join(fragmentos),
    }

Dos consejos de medición: (1) mide percentiles, no medias — la latencia de las APIs tiene cola larga, y el p95 (el peor caso que ve 1 de cada 20 usuarios) cuenta la verdad que la media esconde; (2) mide sobre el conjunto de evaluación de 02-04, que ya representa la distribución real de preguntas — así coste, calidad y latencia se evalúan sobre los mismos casos.

Prompt caching del proveedor: el prefijo estable

Los proveedores ofrecen prompt caching: si el comienzo de tu petición (el prefijo) es byte a byte idéntico al de una petición reciente, el proveedor reutiliza el procesamiento ya hecho y te cobra esa parte a una fracción del precio (lectura de caché ≈ 10% del precio de entrada, con un recargo la primera vez que se escribe ≈ +25%; el caché expira a los pocos minutos sin uso).

La palabra clave es prefijo: el caché casa desde el principio de la petición y cualquier byte distinto invalida todo lo que venga después. De ahí la regla de oro de diseño: lo estable primero, lo variable al final.

Mira nuestra llamada de respuesta con esos ojos:

[system: SYSTEM_DOCUBOT]                 ← idéntico en TODAS las llamadas ✅
[few-shot / plantilla fija]              ← idéntico en todas ✅
[documentación inyectada]                ← varía por pregunta ❌
[pregunta del usuario]                   ← varía siempre ❌

SYSTEM_DOCUBOT y los EJEMPLOS_CLASIFICACION son el candidato perfecto: miles de tokens idénticos repetidos en cada una de las 26.400 llamadas mensuales. En el SDK de Anthropic se marca el final del prefijo cacheable con cache_control; otros proveedores lo aplican automáticamente sobre prefijos repetidos — el concepto es el mismo:

respuesta = client.messages.create(
    model=MODELO,
    max_tokens=1024,
    system=[{
        "type": "text",
        "text": SYSTEM_DOCUBOT,
        "cache_control": {"type": "ephemeral"},   # "cachear hasta aquí"
    }],
    messages=[{"role": "user", "content": prompt_respuesta(pregunta, documentacion)}],
)

# Verificación: ¿está funcionando?
print(respuesta.usage.cache_creation_input_tokens)  # tokens escritos en caché
print(respuesta.usage.cache_read_input_tokens)      # tokens LEÍDOS del caché (el ahorro)

Si cache_read_input_tokens es 0 llamada tras llamada, algo rompe el prefijo. Los saboteadores silenciosos habituales:

  • Un timestamp o dato variable interpolado en el system prompt ("Hoy es {fecha}..."): cada llamada tiene un prefijo distinto. Lo variable, al final del mensaje de usuario.
  • Prefijo demasiado corto: hay un mínimo cacheable (del orden de mil tokens según el modelo); prefijos menores no se cachean, sin error.
  • Versiones mezcladas: si media flota envía PROMPT_VERSION 1.2 y la otra media la 1.1, cada variante compite por su propio caché. Otro motivo para el versionado disciplinado de 02-04.

Cuánto vale en DocuBot: los ~600 tokens del system (más few-shot si comparten prefijo) a 10% en vez de 100%, sobre 26.400 llamadas/mes, son del orden de 40–50 €/mes con nuestras tarifas ilustrativas — y una mejora de TTFT gratis, porque el prefijo cacheado no se reprocesa. En el módulo 4, cuando la documentación inyectada crezca, el orden "estable primero" será aún más rentable.

Caching de respuestas propio: hash, TTL y validez

El prompt caching abarata la llamada; un caché de respuestas la elimina. Si veinte empleados preguntan "¿cómo exporto a CSV?" la misma semana, ¿por qué generar veinte veces la misma respuesta?

La implementación clásica: clave = hash de la pregunta normalizada, valor = la respuesta validada, con un TTL (tiempo de vida) tras el cual la entrada caduca:

# cache_respuestas.py
import hashlib
import time
import unicodedata

def normalizar(texto: str) -> str:
    """Reduce variantes triviales a una misma clave de caché."""
    texto = texto.lower().strip()
    # quitar acentos: "cómo" y "como" deben dar la misma clave
    texto = unicodedata.normalize("NFKD", texto)
    texto = "".join(c for c in texto if not unicodedata.combining(c))
    return " ".join(texto.split())      # espacios repetidos -> uno

class CacheRespuestas:
    def __init__(self, ttl_segundos: int = 24 * 3600):
        self.ttl = ttl_segundos
        self.datos: dict[str, tuple[float, dict]] = {}   # clave -> (timestamp, respuesta)

    def _clave(self, pregunta: str) -> str:
        return hashlib.sha256(normalizar(pregunta).encode("utf-8")).hexdigest()

    def obtener(self, pregunta: str) -> "dict | None":
        entrada = self.datos.get(self._clave(pregunta))
        if entrada is None:
            return None
        timestamp, respuesta = entrada
        if time.time() - timestamp > self.ttl:
            del self.datos[self._clave(pregunta)]        # caducada
            return None
        return respuesta

    def guardar(self, pregunta: str, respuesta: dict):
        self.datos[self._clave(pregunta)] = (time.time(), respuesta)

Integración delante del pipeline de 03-02:

cache = CacheRespuestas(ttl_segundos=24 * 3600)

def responder_con_cache(pregunta: str, documentacion: str) -> dict:
    if (guardada := cache.obtener(pregunta)) is not None:
        return guardada                       # 0 €, ~0 ms
    resultado = responder(pregunta, documentacion)   # pipeline completo de 03-02
    if resultado["confianza"] != "baja":      # no fosilizar respuestas dudosas
        cache.guardar(pregunta, resultado)
    return resultado

Cuándo es válido cachear — y cuándo no. Este caché solo es correcto si se cumplen tres condiciones:

  1. Misma pregunta ⇒ misma respuesta deseada. Cierto para preguntas de documentación ("¿cómo exporto?"); falso para todo lo que dependa del usuario o su estado ("¿qué plan tengo yo?"). En DocuBot conviene cachear solo consultas sin contexto de sesión — de hecho, integrar el caché antes del historial de 03-03: si la pregunta llega dentro de una conversación con contexto, no aplica.
  2. La fuente no ha cambiado. Si la documentación de Nubelia se actualiza, las respuestas cacheadas quedan obsoletas. El TTL es la defensa pasiva (24h acota el desfase); la activa es invalidar el caché al publicar documentación — un hook en el proceso editorial que haga cache.datos.clear() o invalide por sección.
  3. La respuesta merecía ser conservada. De ahí el filtro por confianza: cachear un fallback o una respuesta de baja confianza multiplica un fallo puntual por todos los usuarios que hereden la entrada.

La normalización tiene un límite: "¿cómo exporto a CSV?" y "¿de qué manera saco un CSV del proyecto?" son la misma pregunta para un humano y claves distintas para el hash. El caché exacto solo captura repeticiones literales (que en soporte interno son más frecuentes de lo que parece: la gente copia y pega). La versión que agrupa por significado usa embeddings — la técnica que abre el módulo 4 y que nos regalará, de paso, el caché semántico.

Como con las sesiones de 03-03: el dict en memoria sirve para un proceso; con varios, el caché vive en un almacén compartido (Redis es lo habitual) — el patrón no cambia.

Presupuestos y alertas: diseño

Última pieza, a nivel de diseño (la instrumentación fina — dashboards, trazas — es de 06-04). Un sistema que factura por uso necesita tres números acordados antes de producción:

  1. Presupuesto mensual (p. ej. 500 €): la cifra que dirección ha aprobado. De él se deriva el diario (~23 €/día laborable) — la señal temprana.
  2. Umbral de alerta (p. ej. 80%): al cruzarlo, aviso al equipo. No corta el servicio; da tiempo a investigar (¿pico legítimo de adopción? ¿bucle de reintentos descontrolado? ¿alguien pegando documentos enormes?).
  3. Techo duro (p. ej. 150%): al cruzarlo, el sistema degrada — por ejemplo, activa RESPUESTA_FALLBACK para consultas nuevas o restringe a caché — en vez de gastar sin límite. Duele, pero es la diferencia entre un mal día y una mala factura.

En el diseño de DocuBot esto se concreta en poco código y mucha disciplina: un contador de coste por llamada (estimar_coste volcado a un registro, junto al CSV de evaluación de 02-04 que ya conocemos), agregado por día, y comparado contra los umbrales. Además, los proveedores ofrecen límites de gasto y alertas en su consola de facturación: actívalos como red externa — tu contador puede tener bugs; el suyo es el que factura. Y una métrica de diseño más valiosa que el total: coste por consulta resuelta. Si baja mes a mes mientras la calidad (rúbrica de 02-04) se mantiene, estás optimizando de verdad y no simplemente gastando menos en dar peores respuestas.

Errores Comunes y Consejos

  • Presupuestar solo la salida (o solo "las respuestas"). En DocuBot la entrada es ~80% del coste: system, few-shot, documentación e historial pesan más que lo generado.
  • Copiar tarifas de un tutorial (incluido este). Los precios cambian; las variables PRECIO_* existen para actualizarse. Fija la fecha de la tarifa junto al número en tu documentación.
  • Poner la fecha/hora dentro del system prompt y preguntarse por qué el prompt caching no acierta nunca. Lo variable rompe el prefijo: siempre al final, en el mensaje de usuario.
  • Cachear respuestas sin TTL ni invalidación. El día que cambie la documentación de Nubelia, DocuBot repetirá instrucciones obsoletas con total seguridad en sí mismo — una alucinación fabricada por tu propia infraestructura.
  • Cachear también los fallbacks. Un error transitorio de la API queda fosilizado y servido a todos. Filtra qué merece entrar al caché (confianza, validez del contrato).
  • Medir latencia con medias. La media esconde la cola: informa p50 y p95. Un p50 de 1,2s con p95 de 9s es un problema real que la media de 2s disimula.
  • Consejo: apunta las tres cifras de cada optimización — coste, latencia (p95) y calidad (rúbrica de 02-04) — antes y después. Una palanca que ahorra 30% de coste pero suspende la evaluación no es una palanca: es un recorte.
  • Consejo: revisa usage.cache_read_input_tokens en tu registro la primera semana tras activar prompt caching. Es la forma más barata de descubrir un saboteador de prefijo silencioso.

Ejercicios

Ejercicio 1

Amplía proyeccion_mensual() con dos parámetros: tasa_acierto_cache: float (fracción de consultas servidas por el caché de respuestas propio, que cuestan 0 €) y prompt_caching: bool (si es True, los ~600 tokens del system de la llamada de respuesta se facturan al 10%). Calcula el escenario combinado: clasificador en mini + 20% de acierto de caché + prompt caching. ¿Cuánto queda de los ~417 €/mes base?

Ejercicio 2

La normalización de CacheRespuestas decide qué preguntas comparten clave. Para cada par, indica si comparten clave con la implementación actual y si deberían compartirla: (a) "¿Cómo exporto a CSV?" / "como exporto a csv"; (b) "¿Cómo exporto a CSV?" / "¿Cómo exporto a PDF?"; (c) "¿Cómo exporto a CSV?" / "¿De qué forma se puede sacar un CSV?"; (d) "¿Qué plan tengo?" / "¿qué plan tengo?". Justifica (d) con especial cuidado.

Ejercicio 3

Diseña la función registrar_llamada(usage, tipo_llamada: str) que alimente el control de presupuesto: debe acumular el coste del día (usa estimar_coste) en un fichero o estructura simple, y devolver una de tres señales: "ok", "alerta" (superado el 80% del presupuesto diario) o "techo" (superado el 150%). Indica en qué punto del pipeline de 03-02 la integrarías y qué debería hacer responder() con cada señal.

Soluciones

Solución 1:

def proyeccion_mensual(consultas_mes=26_400, clasificador_en_mini=False,
                       tasa_acierto_cache=0.0, prompt_caching=False):
    p_ent_cls = PRECIO_ENTRADA_MINI if clasificador_en_mini else PRECIO_ENTRADA
    p_sal_cls = PRECIO_SALIDA_MINI if clasificador_en_mini else PRECIO_SALIDA

    clasificacion = coste_llamada(900, 10, p_ent_cls, p_sal_cls)

    entrada_respuesta = 2_850
    if prompt_caching:
        # ~600 tokens de system al 10% del precio de entrada
        entrada_respuesta = 2_850 - 600
        extra_system = 600 * FACTOR_CACHE_LECTURA / 1e6 * PRECIO_ENTRADA * 1e6 / 1e6
        # más legible: coste del system cacheado calculado aparte
        coste_system = 600 / 1e6 * PRECIO_ENTRADA * FACTOR_CACHE_LECTURA
    else:
        coste_system = 0.0
        entrada_respuesta = 2_850

    respuesta = coste_llamada(entrada_respuesta, 300, PRECIO_ENTRADA, PRECIO_SALIDA) + coste_system

    consultas_api = consultas_mes * (1 - tasa_acierto_cache)   # el resto: caché propio, 0 €
    return consultas_api * (clasificacion + respuesta)

print(f"{proyeccion_mensual(clasificador_en_mini=True, tasa_acierto_cache=0.2, prompt_caching=True):.0f} €/mes")

Resultado con las tarifas ilustrativas: ~270 €/mes — un ~35% menos que la base de ~417 €, sin tocar la calidad (el clasificador cambia de modelo solo si supera la evaluación de 02-04, el caché solo sirve respuestas ya validadas y el prompt caching es neutro en calidad). Nota didáctica: el cálculo omite el recargo de escritura de caché (~25% en fallos de caché) por simplicidad; inclúyelo si buscas la cifra fina.

Solución 2:

  • (a) Comparten clave (minúsculas + acentos + espacios lo igualan; los signos ¿? conviene también eliminarlos en normalizar — mejora razonable a la implementación). Deberían: ✅ coherente.
  • (b) No comparten (difieren en "CSV"/"PDF") y no deberían: son preguntas distintas con respuestas distintas. La normalización nunca debe borrar contenido semántico.
  • (c) No comparten, pero un humano diría que deberían. Es el límite del caché exacto: capturar esta equivalencia exige comparar significados, no bytes — caché semántico con embeddings, módulo 4.
  • (d) Comparten clave... y no deberían estar en el caché en absoluto. La respuesta depende de quién pregunta: cachearla serviría el plan de un empleado a otro — fuga de información entre usuarios, el mismo fallo del ejercicio 3 de 03-03 pero fabricado por el caché. La defensa está antes de la normalización: excluir del caché toda consulta dependiente de sesión/usuario (por ejemplo, cacheando solo la ruta sin historial, o filtrando por categoría del clasificador).

Solución 3:

import datetime, json, pathlib

PRESUPUESTO_DIARIO = 23.0   # € (≈ 500 €/mes entre 22 días laborables)
FICHERO = pathlib.Path("gasto_diario.json")

def registrar_llamada(usage, tipo_llamada: str) -> str:
    hoy = datetime.date.today().isoformat()
    datos = json.loads(FICHERO.read_text()) if FICHERO.exists() else {}
    datos.setdefault(hoy, 0.0)
    datos[hoy] += estimar_coste(usage)
    FICHERO.write_text(json.dumps(datos))

    gasto = datos[hoy]
    if gasto >= PRESUPUESTO_DIARIO * 1.5:
        return "techo"
    if gasto >= PRESUPUESTO_DIARIO * 0.8:
        return "alerta"
    return "ok"

Integración: en responder() (03-02), justo después de cada llamada exitosa — es donde existe usage. Con "ok" no pasa nada; con "alerta" se notifica al equipo (log de nivel warning hoy; canal de avisos cuando exista la observabilidad de 06-04) pero se sigue sirviendo; con "techo" las consultas nuevas pasan a modo degradado (caché propio + RESPUESTA_FALLBACK para lo no cacheado) hasta revisión humana. Nótese la coherencia con todo el curso: la decisión de cortar o seguir gastando es humana; el sistema solo la prepara y la acota.

Conclusión

Con esta lección, el módulo de integración queda completo — y DocuBot deja de ser solo un sistema que funciona para ser un sistema que sabemos operar: conoce el precio de cada token que envía y recibe (estimar_coste, sobre el usage de 03-01), tiene una proyección mensual con nombre y apellidos que confirma y refina la cifra de 01-04 (~417 €/mes base, ~270 € tras aplicar las palancas), distingue TTFT de tiempo total y los mide en percentiles sobre el conjunto de evaluación de 02-04, explota el prompt caching del proveedor gracias a una decisión de diseño que ahora se revela premonitoria (el SYSTEM_DOCUBOT y los few-shot de 02-01/02-02 son un prefijo estable perfecto), evita llamadas enteras con el caché de respuestas propio (hash normalizado, TTL, invalidación al publicar documentación) y opera dentro de un presupuesto con alerta y techo cuya última palabra es humana. Mirando atrás, el módulo 3 convirtió los artefactos de papel del módulo 2 en un sistema vivo: la primera llamada real con roles system/user (03-01), streaming y una estrategia seria de errores, reintentos y degradación (03-02), conversaciones con memoria fabricada y sesiones aisladas (03-03) y, hoy, la economía y la física del conjunto. Pero el talón de Aquiles sigue intacto: DocuBot responde bien cuando le damos la documentación correcta en la mano — y hasta ahora se la hemos dado nosotros, a mano, en cada ejemplo. ¿Quién encuentra, entre cientos de páginas de documentación de Nubelia, los dos fragmentos que responden a cada pregunta? Ese es exactamente el problema que resuelve la generación aumentada por recuperación. En el módulo 4 construimos esa pieza: empezando por los embeddings y la búsqueda semántica (04-01) — la técnica que compara significados en vez de palabras y que, de regalo, resolverá la limitación de nuestro caché exacto —, siguiendo por las bases de datos vectoriales, la ingesta y el chunking de la documentación, y culminando con el pipeline RAG completo que convertirá a DocuBot en lo que prometimos en 01-01: un asistente que de verdad conoce Nubelia.

© Copyright 2026. Todos los derechos reservados