Módulo 3: Medir costo y tokens por run

El costo como señal operacional

Descripción

El Módulo 1, lección 04, nombró cuatro señales operacionales que este módulo y el que sigue calculan con precisión: tasa de error (a nivel de run), tasa de fallo por herramienta (a nivel de tool call), costo por run, y latencia por run. Las lecciones 05 y 06 de este módulo resolvieron el costo — pero solo como un número: cuánto costó un run, cuánto costarían miles. Esta lección da el último paso: tratar ese número como una señal, exactamente con la misma seriedad que la tasa de error, capaz de decirte cuándo algo en un run —o en un tipo de run— merece atención, antes de que se convierta en un problema de presupuesto real.

Vas a reusar, sin cambios, el lote de cuatro runs de la lección 06 —Ana, Sofía, Diego, Carla— y vas a encontrar, con un criterio simple y reproducible, cuál de los cuatro es una señal de alerta: un run que consumió sustancialmente más tokens que sus compañeros, sin que eso signifique, necesariamente, que algo falló.

Conexión con el módulo

Esta lección no agrega ninguna pieza nueva a estimate_tokens ni a estimate_cost_cents — los usa, tal como quedaron, para construir un criterio de decisión: flag_expensive_runs, la primera función de este módulo que no solo mide, sino que clasifica. Es, en un sentido preciso, el puente hacia el Módulo 5: ahí, un criterio parecido a este se convierte en un gate de PASS/FAIL determinista.


Costo y correctitud son señales distintas

Antes de construir el criterio, vale la pena una precisión importante: un run caro no es lo mismo que un run con errores. Repasa el lote de la lección 06: el run de Ana tuvo un tool_result con is_error: True (el tier inválido) y, sin embargo, no es el más caro del lote. El run de Carla —comparar las seis combinaciones de sala y tier antes de decidir— no tuvo ningún error, y es, con una diferencia clara, el más caro de los cuatro. Un run puede ser perfectamente correcto y aun así costoso —porque hizo más trabajo, no porque algo haya fallado—; y un run puede fallar sin ser particularmente caro —como el intento con tier inválido, rechazado antes incluso de ejecutar nada—.

Esta distinción importa para el diseño de cualquier sistema de alertas: la tasa de error (Módulo 1) responde "¿el sistema está funcionando mal?"; el costo (este módulo) responde una pregunta ortogonal, "¿el sistema está funcionando de una forma más cara de lo esperado?". Ambas señales, juntas, dan una imagen más completa que cualquiera de las dos por separado.


Ejemplo trabajado: encontrando el run anómalo del lote

Retoma los cuatro CostReport de la lección 06 —mismos trace_id, mismos tokens— y calcula qué tan lejos está cada uno del promedio del lote:

import statistics

# Los mismos cuatro reports de la lección 06 (reejecutados aquí para esta lección).
reports_summary = [
    ("run-8487582448eb", 64, 56, "Reserva Focus pro 3h para Ana"),
    ("run-ae6ff85cf0b0", 53, 49, "Reserva Boardroom pro 1h para Sofía"),
    ("run-2c27934d8a39", 27, 31, "Reserva y cancela Studio basic 1h para Diego"),
    ("run-cecde864aa84", 88, 118, "Compara todas las salas antes de reservar la mejor opción para Carla"),
]

totals = [input_tokens + output_tokens for _, input_tokens, output_tokens, _ in reports_summary]
avg_tokens = statistics.mean(totals)

print(f"promedio del lote: {avg_tokens} tokens totales")
print()
print(f"{'trace_id':<18} {'total_tokens':>12} {'ratio vs promedio':>18}")
for trace_id, input_tokens, output_tokens, question in reports_summary:
    total = input_tokens + output_tokens
    ratio = total / avg_tokens
    print(f"{trace_id:<18} {total:>12} {ratio:>17.2f}x")

Qué esperar:

promedio del lote: 121.5 tokens totales

trace_id             total_tokens  ratio vs promedio
run-8487582448eb              120              0.99x
run-ae6ff85cf0b0               102              0.84x
run-2c27934d8a39                58              0.48x
run-cecde864aa84               206              1.70x

El run de Carla —run-cecde864aa84— usa 1.70 veces el promedio del lote. No es un accidente: su guion compara seis combinaciones de sala y tier antes de reservar, generando el doble de tool calls que el run típico. El de Diego, en el otro extremo, usa menos de la mitad del promedio (0.48x) — un run corto y barato, dos pasos únicamente.


flag_expensive_runs: un criterio simple, determinista

Convierte esa observación en una función reusable — un umbral fijo, sin ninguna llamada a un modelo para "juzgar" si un run es caro:

def flag_expensive_runs(reports, threshold_ratio=1.5):
    """Marca los CostReport cuyo total de tokens supera threshold_ratio
    veces el promedio del lote. Un criterio de FORMA, determinista -- sin
    ningún juicio semántico sobre el contenido del run."""
    totals = [r.input_tokens + r.output_tokens for r in reports]
    avg = statistics.mean(totals)
    flagged = []
    for r, total in zip(reports, totals):
        ratio = total / avg
        if ratio >= threshold_ratio:
            flagged.append((r, ratio))
    return flagged


flagged = flag_expensive_runs(reports, threshold_ratio=1.5)
print(f"runs marcados como anómalamente caros (>= 1.5x el promedio del lote): {len(flagged)}")
for r, ratio in flagged:
    print(f"  {r.trace_id}: {ratio:.2f}x -- {r.question!r}")

Qué esperar:

runs marcados como anómalamente caros (>= 1.5x el promedio del lote): 1
  run-cecde864aa84: 1.70x -- 'Compara todas las salas antes de reservar la mejor opción para Carla'

Un umbral de 1.5x sobre el promedio del lote —un número arbitrario, elegido aquí solo como ejemplo razonable— marca exactamente un run: el de Carla. Este es, con precisión, el tipo de criterio que en el Módulo 5 se convierte en parte de un gate de regresión: no "¿la respuesta es buena?" —eso es un juicio semántico, fuera del alcance de esta guía—, sino "¿el costo se mantiene bajo un umbral fijo, sí o no?".


Por qué esta señal importa a escala: el costo del run anómalo, proyectado

Un 1.70x sobre cuatro runs parece un detalle menor. Proyéctalo, con la misma técnica de la lección 06, a la escala en la que Reservo realmente opera:

n = 100_000
avg_input = sum(r.input_tokens for r in reports) / len(reports)
avg_output = sum(r.output_tokens for r in reports) / len(reports)
carla_report = next(r for r in reports if r.trace_id == "run-cecde864aa84")

cost_typical = estimate_cost_cents(int(avg_input * n), int(avg_output * n))
cost_carla_type = estimate_cost_cents(carla_report.input_tokens * n, carla_report.output_tokens * n)

print(f"costo proyectado, run TÍPICO x {n}      : {cost_typical} centavos = ${cost_typical / 100:.2f}")
print(f"costo proyectado, run tipo-Carla x {n}  : {cost_carla_type} centavos = ${cost_carla_type / 100:.2f}")
print(f"diferencia atribuible a este patrón     : {cost_carla_type - cost_typical} centavos = ${(cost_carla_type - cost_typical) / 100:.2f}")

Qué esperar:

costo proyectado, run TÍPICO x 100000      : 11265 centavos = $112.65
costo proyectado, run tipo-Carla x 100000  : 20340 centavos = $203.40
diferencia atribuible a este patrón        : 9075 centavos = $90.75

Si el patrón de "comparar todas las combinaciones antes de decidir" se volviera común entre los usuarios de Reservo —y no una excepción aislada—, la diferencia de costo frente a un run típico sería de casi $91 por cada 100.000 runs de ese tipo, solo por la forma en que ese patrón consulta el sistema. Esta es, con precisión, la utilidad real de tratar el costo como una señal: no para juzgar si run-cecde864aa84 "hizo algo mal" —no lo hizo, resolvió la tarea correctamente—, sino para saber que ese patrón de uso, si se repite a escala, tiene un impacto de presupuesto medible y cuantificado.


La frontera: medir el costo, no reducirlo

Esta lección —y este módulo completo— se detienen exactamente aquí. Identificar que un run (o un patrón de runs) es anómalamente caro es toda la responsabilidad de esta guía en materia de costo. Lo que no hace esta lección, a propósito, es proponer ninguna forma de bajar ese costo: no cachea el tool_result de list_rooms entre llamadas repetidas, no sugiere usar un modelo más barato para tareas simples, no agrupa (batching) preguntas similares para ahorrar tokens de sistema repetidos.

Todo eso —prompt caching, selección de modelo por costo, batching, la anatomía completa de qué compone el costo de una llamada y cómo se optimiza cada componente— es el contenido central de cost-optimization-caching-guide, una guía hermana del ecosistema de AI Engineering. Cuando la pregunta deja de ser "¿cuánto costó, y qué run costó más de lo esperado?" y se convierte en "¿cómo hago que esto cueste menos?", esa es la guía a la que ir — nombrada aquí, con precisión, en el punto exacto donde esta guía se detiene.


Hacia dónde va esta señal en el resto de la guía

El costo por run, ya calculado y ya clasificado, no termina en esta lección — el resto de esta guía lo reusa:

  • Módulo 4 agrega la latencia por run, la segunda mitad de la capa de "medir" que el Módulo 1 prometió — con la misma disciplina de honestidad sobre qué se mide y qué se modela.
  • Módulo 5 convierte un umbral como el de flag_expensive_runs en parte de un gate de regresión: un run cuyo costo supera un límite fijo puede hacer fallar el build, exactamente igual que un schema inválido o una tool elegida incorrectamente — nunca un juicio sobre si la respuesta es "buena".
  • Módulo 6, cuando un circuit breaker detiene las llamadas a una tool que falla repetido, también ahorra el costo de esos intentos — una consecuencia medible con las herramientas de este módulo, aunque el circuit breaker en sí se construye por razones de resiliencia, no de costo.
  • Módulo 8 cierra la guía con un reporte de métricas que incluye costo, junto a las demás señales, sobre el agente de Reservo completo.

Errores comunes

  1. Tratar "run caro" y "run con error" como sinónimos. El ejemplo trabajado lo desmiente directamente: el run más caro del lote (Carla) no tuvo ningún error; el run con un error (Ana) no fue el más caro. Ambas señales se calculan y se leen por separado.

  2. Elegir un umbral de flag_expensive_runs sin justificarlo. 1.5x en esta lección es un ejemplo razonable, no una regla universal — un sistema real elegiría ese umbral basándose en el costo real que el negocio puede tolerar, no en una cifra arbitraria copiada de una lección.

  3. Confundir "identificar un run caro" con "identificar un run malicioso o abusivo". flag_expensive_runs marca runs por su costo, sin ningún juicio sobre la intención de quien los generó — un usuario legítimo con una tarea genuinamente compleja produce la misma señal que cualquier otro patrón costoso. Distinguir uso legítimo de abuso es un problema distinto, fuera del alcance de esta lección.

  4. Proponer una optimización de costo apenas se detecta un run caro. Esta lección se detiene, a propósito, en identificar y cuantificar — nunca en proponer una solución de caching, batching o selección de modelo. Esa frontera, nombrada arriba con precisión, es una de las más fáciles de cruzar sin darse cuenta.

  5. Calcular el promedio del lote sobre muy pocos runs y confiar en el umbral resultante. Con solo cuatro runs, un único outlier —como el de Carla— también mueve el promedio hacia arriba, lo cual hace que el umbral sea menos estable que sobre un lote de miles. El Módulo 8 va a mostrar esta misma técnica sobre un lote más representativo.


Ejercicios

Ejercicio 1: Prueba un umbral más estricto (Fácil)

Usando flag_expensive_runs y el mismo lote de cuatro CostReport, prueba un umbral de 1.2 en vez de 1.5. ¿Cuántos runs se marcan ahora?

Ver solución
flagged_strict = flag_expensive_runs(reports, threshold_ratio=1.2)
print(f"runs marcados con umbral 1.2x: {len(flagged_strict)}")
for r, ratio in flagged_strict:
    print(f"  {r.trace_id}: {ratio:.2f}x")

Salida esperada:

runs marcados con umbral 1.2x: 1
  run-cecde864aa84: 1.70x

Explicación: con este lote específico, bajar el umbral de 1.5x a 1.2x no cambia el resultado — el segundo run más caro del lote (Ana, 0.99x) sigue muy por debajo de 1.2x. La brecha entre el run de Carla y el resto del lote es lo suficientemente grande como para que el umbral exacto, dentro de un rango razonable, no cambie la conclusión.

Ejercicio 2: Encuentra el umbral mínimo que NO marca ningún run (Medio)

Encuentra, con código, el valor de threshold_ratio justo por encima del cual flag_expensive_runs deja de marcar cualquier run del lote —es decir, el ratio exacto del run más caro—.

Ver solución
totals = [r.input_tokens + r.output_tokens for r in reports]
avg = statistics.mean(totals)
max_ratio = max(total / avg for total in totals)
print(f"ratio del run más caro del lote: {max_ratio:.4f}x")
print(f"con threshold_ratio > {max_ratio:.4f}, ningún run del lote se marca")

# Confirmación:
sin_marcar = flag_expensive_runs(reports, threshold_ratio=max_ratio + 0.01)
print("runs marcados justo por encima de ese umbral:", len(sin_marcar))

Salida esperada:

ratio del run más caro del lote: 1.6955x
con threshold_ratio > 1.6955, ningún run del lote se marca
runs marcados justo por encima de ese umbral: 0

Explicación: el ratio exacto del run de Carla es 1.6955... (no el 1.70 redondeado que se imprimió en el ejemplo trabajado) — cualquier umbral por encima de ese valor exacto deja el lote sin ninguna alerta, mientras que cualquier umbral igual o por debajo lo marca. Esto confirma que flag_expensive_runs es una función puramente determinista: el mismo lote y el mismo umbral producen, siempre, el mismo resultado.

Ejercicio 3: Diseña un criterio compuesto, costo Y errores (Difícil)

Escribe una función flag_concerning_runs(reports, error_counts, cost_threshold=1.5) que reciba los CostReport de esta lección junto con un diccionario error_counts (trace_id -> cantidad de tool_errors, del Módulo 2), y devuelva los runs que son anómalamente caros O que tuvieron al menos un error —una unión de ambas señales, no una intersección—. Pruébala con error_counts = {"run-8487582448eb": 1, "run-ae6ff85cf0b0": 0, "run-2c27934d8a39": 0, "run-cecde864aa84": 0} (los tool_errors reales del lote, según el Módulo 2).

Ver solución
def flag_concerning_runs(reports, error_counts, cost_threshold=1.5):
    """Marca un run si es anómalamente caro (costo) O si tuvo al menos un
    tool_error (correctitud) -- unión de dos señales independientes,
    ninguna sustituye a la otra."""
    expensive = {r.trace_id for r, _ in flag_expensive_runs(reports, cost_threshold)}
    concerning = []
    for r in reports:
        is_expensive = r.trace_id in expensive
        has_errors = error_counts.get(r.trace_id, 0) > 0
        if is_expensive or has_errors:
            concerning.append((r.trace_id, is_expensive, has_errors))
    return concerning


error_counts = {
    "run-8487582448eb": 1, "run-ae6ff85cf0b0": 0,
    "run-2c27934d8a39": 0, "run-cecde864aa84": 0,
}
result = flag_concerning_runs(reports, error_counts)
for trace_id, is_expensive, has_errors in result:
    print(f"{trace_id}: caro={is_expensive}  con_errores={has_errors}")

Salida esperada:

run-8487582448eb: caro=False  con_errores=True
run-cecde864aa84: caro=True  con_errores=False

Explicación: la unión marca dos runs, cada uno por una razón distinta — el de Ana por tener un tool_error (aunque su costo es normal, 0.99x), y el de Carla por ser anómalamente caro (aunque no tuvo ningún error). Ninguno de los dos habría aparecido si el criterio hubiera sido una intersección ("caro Y con errores") — la unión es la elección correcta aquí porque ambas señales, costo y correctitud, merecen atención por separado, no solo cuando coinciden en el mismo run.


Resumen y siguiente paso

  • Confirmamos que costo y correctitud son señales distintas: el run más caro del lote no tuvo errores; el run con un error no fue el más caro.
  • Construimos flag_expensive_runs: un criterio determinista, basado en un umbral fijo sobre el ratio de tokens de un run frente al promedio del lote — sin ningún juicio semántico, la misma disciplina de FORMA que el Módulo 5 va a exigir de cada gate de esta guía.
  • Proyectamos el impacto real de un patrón de uso costoso —el run de Carla, 1.70x el promedio— a 100.000 runs: $90.75 de diferencia frente a un run típico, una cifra de presupuesto real, no una curiosidad de cuatro ejemplos.
  • Trazamos la frontera con precisión: esta guía mide el costo y lo usa como señal; cost-optimization-caching-guide enseña a reducirlo con caching, selección de modelo y batching.

Siguiente lección: 08 — Mini-proyecto: un reporte de costo para los runs de Reservo. Cerramos el módulo juntando cada pieza —estimate_tokens, el pricing fijo, cost_for_run, la agregación, el escalado, y el criterio de esta lección— en observability/cost_calculator.py completo, ejecutado sobre un lote de runs trazados.


Recursos adicionales

  1. Python — statistics.mean — La función usada para calcular el promedio del lote, la base de flag_expensive_runs.
  2. Anthropic — Building effective agents — Sobre por qué el costo, junto a la tasa de error, es una de las señales que determinan si un sistema agentic está listo para operar a escala.
  3. Python — comprensión de conjuntos y diccionarios — El patrón {r.trace_id for r, _ in ...} usado en el Ejercicio 3 para construir un criterio compuesto.
  4. Guía hermana — cost-optimization-caching-guide (AI Engineering): la anatomía completa del costo de una llamada y cómo reducirlo — prompt caching, selección de modelo, batching. El punto exacto donde esta guía se detiene y esa guía continúa.
  5. Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.