Módulo 4: Medir latencia con honestidad

`TOOL_LATENCY_MS` como dato fijo

Descripción

Con el problema de honestidad ya resuelto en la lección 03, esta lección fija, de una vez y para el resto de este módulo, el artefacto central: TOOL_LATENCY_MS. Ya lo usaste, de pasada, en las lecciones 02 y 03 — esta es la lección que se detiene en él con precisión: de dónde salen sus cuatro números, por qué book_room es, con diferencia, la tool más cara, y cómo se convierte en el primer bloque de observability/latency_model.py, el artefacto que este módulo entrega en la lección 08.

Conexión con el módulo

Esta lección corresponde al segundo artefacto de la capa de operación de esta guía: observability/run_logger.py (Módulo 2) y observability/cost_calculator.py (Módulo 3) ya existen; observability/latency_model.py empieza aquí, con su primera pieza. Las lecciones 05 a 08 construyen encima de este diccionario sin volver a declararlo — exactamente como el Módulo 3 fijó el pricing de claude-sonnet-5 una sola vez en su lección 04 y lo reusó sin cambios el resto de la guía.


Analogía: la tarifa fija de un taller mecánico

Un taller mecánico que cotiza por adelantado no le dice al cliente "vamos a ver cuánto tarda, y después te cobramos según el reloj" — le da una tarifa fija por tipo de trabajo: cambiar el aceite, una hora; alinear las llantas, media hora; una revisión completa, dos horas. Esas tarifas no son el tiempo exacto que tardó el mecánico esta vez en particular —a veces tarda menos, a veces más—; son un compromiso declarado, pensado para que el cliente sepa qué esperar antes de que el trabajo empiece, y para que el taller pueda planificar su día sin depender de cuánto tarde, al segundo, cada trabajo específico.

TOOL_LATENCY_MS es exactamente esa tarifa fija, aplicada a las cuatro tools de Reservo. No promete que book_room vaya a tardar exactamente 120 milisegundos en un sistema real —eso lo dijo, sin rodeos, la lección 03—; declara una tarifa de referencia, pensada para que cada ejercicio de esta guía sepa qué esperar antes de correr el código.


El diccionario, fijado una sola vez

TOOL_LATENCY_MS = {
    "list_rooms": 40,
    "get_quote": 25,
    "book_room": 120,
    "cancel_booking": 90,
}

Cuatro tools, cuatro números, en milisegundos int. Esta guía no vuelve a cambiar ninguno de los cuatro desde este punto hasta el cierre del Módulo 8 — es la misma disciplina que ya viste con el pricing de claude-sonnet-5 en el Módulo 3: una constante se fija una vez, con una razón declarada, y el resto de la guía la cita sin volver a cuestionarla.

Por qué estos cuatro números, y no otros

El orden de estos cuatro valores no es arbitrario — refleja una intuición real sobre qué tipo de operación es cada tool, la misma intuición que ya usaste para distinguir tools de "solo lectura" de tools "de escritura" en agent-fundamentals:

for name, latency_ms in sorted(TOOL_LATENCY_MS.items(), key=lambda item: item[1]):
    kind = "lectura (sin efectos)" if name in ("list_rooms", "get_quote") else "escritura (efectos reales)"
    print(f"{name:15} {latency_ms:4} ms  -- {kind}")

Qué esperar:

get_quote        25 ms  -- lectura (sin efectos)
list_rooms       40 ms  -- lectura (sin efectos)
cancel_booking   90 ms  -- escritura (efectos reales)
book_room       120 ms  -- escritura (efectos reales)

Las dos tools de solo lectura (get_quote, list_rooms) son las más baratas de las cuatro; las dos de escritura (cancel_booking, book_room) son las más caras. get_quote es la más barata de todas porque es, literalmente, aritmética en memoria —ROOM_RATE_CENTS[room] * hours—, sin tocar ningún estado compartido. book_room es la más cara porque, en un sistema real, crear una reserva casi siempre implica escribir a una base de datos —algo que tarda un orden de magnitud más que un cálculo en memoria—. Esta guía no midió eso en un sistema real —sería, de nuevo, el reloj real que la lección 03 descartó—; lo declaró con esa intuición, de la misma forma en que cualquier diseñador de un sistema real declararía sus primeras estimaciones antes de tener datos de producción.


Consultando la latencia de una tool call individual

La forma más simple de usar este diccionario ya la viste en la lección 02: una consulta directa.

print("latencia de get_quote  :", TOOL_LATENCY_MS["get_quote"], "ms")
print("latencia de book_room   :", TOOL_LATENCY_MS["book_room"], "ms")
print("latencia de una tool desconocida:", TOOL_LATENCY_MS.get("delete_everything", 0), "ms")

Qué esperar:

latencia de get_quote  : 25 ms
latencia de book_room   : 120 ms
latencia de una tool desconocida: 0 ms

Fíjate en la tercera línea: TOOL_LATENCY_MS.get("delete_everything", 0) en vez de TOOL_LATENCY_MS["delete_everything"]. Esta guía usa .get(name, 0) en cada función que consulta este diccionario —nunca el acceso directo con corchetes— por una razón concreta: una tool que no está en el diccionario (porque nunca se declaró, o porque hay un error de tipeo en su nombre) no debería tirar abajo todo el cálculo de latencia con un KeyError; debería, en cambio, aportar 0 ms de forma explícita, para que el error se note en el reporte final (una latencia sospechosamente baja) en vez de en una excepción que interrumpe todo el proceso.


El total con las cuatro tools, una vez cada una

Ya conoces esta cifra del Módulo 1, Ejercicio 3 de la lección 05 — vale la pena confirmarla aquí, como el límite superior natural de un run que usa cada tool exactamente una vez:

total_todas = sum(TOOL_LATENCY_MS.values())
print("suma de las cuatro tools, una vez cada una:", total_todas, "ms")

Qué esperar:

suma de las cuatro tools, una vez cada una: 275 ms

275 ms — el mismo número que ya calculaste a mano en el Módulo 1. Esta cifra no depende del orden en que se llamen las tools (la suma es conmutativa), solo de cuáles se llaman. Guárdatela: es el techo natural de un run "normal" de Reservo (uno que no repite ninguna tool), y te va a servir como punto de referencia cuando la lección 06 calcule percentiles sobre un lote real — un run que se acerque a 275 ms está, casi con certeza, usando las cuatro tools en un solo intercambio.


Errores comunes

  1. Declarar un TOOL_LATENCY_MS distinto en cada lección, "para variar el ejemplo". No — este diccionario se fija una sola vez, aquí, y se reusa sin cambios el resto del módulo (y de la guía). Cambiarlo rompería cada cifra que ya calculaste en las lecciones 02 y 03.

  2. Usar TOOL_LATENCY_MS[name] con corchetes en vez de .get(name, 0). Con las cuatro tools canónicas de Reservo nunca vas a notar la diferencia —siempre están las cuatro—, pero en cuanto una lección posterior (o tu propio código) registre una tool nueva sin agregarla también a este diccionario, el acceso con corchetes revienta con un KeyError en vez de degradar de forma controlada a 0 ms.

  3. Asumir que el orden de las claves en el diccionario importa para algo. No importa — un dict de Python (desde la versión 3.7) preserva el orden de inserción para iterarlo, pero ninguna función de este módulo depende de en qué orden aparecen las cuatro tools dentro de TOOL_LATENCY_MS. El sorted(...) del ejemplo trabajado ordena explícitamente por valor, precisamente porque el orden de inserción no es el que importa aquí.

  4. Confundir "tool de lectura" con "tool barata" como una regla universal. En Reservo, la correlación se cumple —las dos tools de lectura son, de hecho, las más baratas—, pero es una decisión de diseño de esta guía, no una ley general. Un sistema real podría tener una tool de lectura lenta (una consulta compleja a una base de datos) y una de escritura rápida (un INSERT simple en una tabla pequeña).

  5. Pensar que 275 ms es "la latencia normal" de cualquier run. Es el techo, no el promedio — la mayoría de los runs de Reservo, como ya viste en el Módulo 1 y vas a confirmar con el lote de la lección 06, usan menos de las cuatro tools, así que su latencia total está, casi siempre, muy por debajo de 275.


Ejercicios

Ejercicio 1: Calcula la latencia de dos get_quote seguidas (Fácil)

Sin ejecutar nada: si un guion llama a get_quote dos veces (para comparar dos cotizaciones, sin reservar nada), ¿cuál es su latencia total? Confirma con código.

Ver solución

50 ms — 25 + 25, porque get_quote se llama dos veces y cada llamada aporta su latencia individual, sin importar que sea "la misma" tool repetida.

print("dos get_quote seguidas:", TOOL_LATENCY_MS["get_quote"] * 2, "ms")

Salida esperada:

dos get_quote seguidas: 50 ms

Explicación: la latencia se suma por llamada, no por tool única — dos llamadas a la misma tool cuestan el doble que una, exactamente igual que dos llamadas a tools distintas se suman entre sí. No hay ningún "descuento" por repetir la misma tool dentro de un run.

Ejercicio 2: Encuentra la tool que, agregada a un run de list_rooms + get_quote, más aumenta la latencia total (Medio)

Un run ya tiene list_rooms + get_quote (65 ms). Sin ejecutar nada primero, decide cuál de las dos tools restantes (book_room o cancel_booking) agregaría más latencia si se sumara al run, y confirma con código.

Ver solución

book_room (120 ms) agrega más que cancel_booking (90 ms) — la diferencia es 30 ms.

base = TOOL_LATENCY_MS["list_rooms"] + TOOL_LATENCY_MS["get_quote"]
con_book = base + TOOL_LATENCY_MS["book_room"]
con_cancel = base + TOOL_LATENCY_MS["cancel_booking"]
print("base (list_rooms + get_quote):", base, "ms")
print("+ book_room                  :", con_book, "ms")
print("+ cancel_booking              :", con_cancel, "ms")

Salida esperada:

base (list_rooms + get_quote): 65 ms
+ book_room                  : 185 ms
+ cancel_booking              : 155 ms

Explicación: 185 (con book_room) supera a 155 (con cancel_booking) — y 185 es, de hecho, la misma cifra del run canónico de Ana, que sigue exactamente esta secuencia. Esto confirma, con números, algo que ya intuiste en la sección "Por qué estos cuatro números": book_room no es solo la tool individual más cara — es también la que más impacto tiene cuando se agrega a un run que ya tenía otras tools.

Ejercicio 3: Diseña una quinta tool hipotética y decide su latencia, con una justificación (Difícil)

Reservo podría necesitar, en el futuro, una tool send_confirmation_email(booking_id) que envía un correo de confirmación después de una reserva. Sin implementarla —esto es un ejercicio de diseño, no de código—, decide qué latencia modelada le asignarías en TOOL_LATENCY_MS, y justifica tu elección comparándola con las cuatro tools existentes.

Ver solución

Una asignación razonable: TOOL_LATENCY_MS["send_confirmation_email"] = 200. La justificación: enviar un correo implica, típicamente, una llamada de red a un servicio externo (un proveedor de email transaccional) — una operación que cruza la red es, casi siempre, más lenta que una escritura a una base de datos local como book_room (120 ms), porque suma la latencia de red de ida y vuelta a la latencia de procesamiento del proveedor externo. Una cifra en el rango de 150-250 ms sería defendible; una cifra menor a 120 (la de book_room) sería difícil de justificar, porque implicaría que hablar con un servicio externo es más rápido que escribir localmente — lo contrario de lo que ocurre en la mayoría de los sistemas reales. Este ejercicio no tiene una única respuesta "correcta" —a diferencia de las cuatro tools canónicas de esta guía, cuyos valores son fijos y citados—, pero sí tiene respuestas mejor y peor justificadas, y esa justificación (lectura vs. escritura, local vs. red) es exactamente el mismo criterio que ya usaste para entender por qué book_room es más cara que get_quote.


Resumen y siguiente paso

  • Fijamos TOOL_LATENCY_MSlist_rooms=40, get_quote=25, book_room=120, cancel_booking=90— como el primer bloque de observability/latency_model.py, reusado sin cambios el resto de este módulo.
  • Confirmamos, ejecutado, por qué el orden de estos cuatro valores no es arbitrario: las dos tools de solo lectura son las más baratas; las dos de escritura, las más caras — la misma distinción que ya usaste en agent-fundamentals para diseñar los contratos de cada tool.
  • Confirmamos, de nuevo, que la suma de las cuatro tools una vez cada una es 275 ms — el techo natural de un run "normal" de Reservo, un punto de referencia que vas a reusar en la lección 06.
  • Con TOOL_LATENCY_MS ya fijado, la siguiente lección construye la función central de este módulo: la latencia total de un run completo, con el matiz correcto sobre los tool_use rechazados.

Siguiente lección: 05 — Latencia total del run. Construimos total_run_latency_ms, ejecutada sobre el run canónico de Ana, y confirmamos por qué un tool_use rechazado por validación no le agrega ni un milisegundo al total.


Recursos adicionales

  1. Python — diccionarios, el método .get() — La forma segura de consultar TOOL_LATENCY_MS, usada en cada función de este módulo desde esta lección en adelante.
  2. Anthropic — Building effective agents — Sobre por qué las tools de escritura de un agente —las que tienen efectos reales— suelen ser, también, las más costosas de ejecutar en un sistema real.
  3. Python — funciones sorted() con key — La técnica usada para ordenar TOOL_LATENCY_MS.items() por latencia en el ejemplo trabajado.
  4. Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.