Módulo 4: Medir latencia con honestidad

Qué latencia estamos midiendo

Descripción

"Latencia" suena a una sola cifra —"este run tardó 185 milisegundos"— pero esa cifra es, en realidad, la suma de varias preguntas más pequeñas, y confundirlas es el primer error que comete casi cualquiera que mide latencia por primera vez. Esta lección separa, con precisión, dos preguntas relacionadas pero distintas: ¿cuánto tarda una tool call individual? y ¿cuánto tarda el run completo? La segunda depende de la primera, pero no es lo mismo saber que book_room tarda 120 ms que saber que un run entero, con tres tool calls, tardó 185 ms — la segunda cifra te dice algo sobre la experiencia completa del usuario; la primera te dice dónde, exactamente, se fue ese tiempo.

Conexión con el módulo

La lección 01 ya adelantó el diccionario TOOL_LATENCY_MS, heredado sin cambios del Módulo 1. Esta lección lo usa por primera vez en este módulo —sin todavía formalizarlo como el artefacto central que la lección 04 va a fijar— para responder las dos preguntas de arriba sobre un run real. El desarrollo a fondo de la suma total, con su matiz sobre los tool_use rechazados, es trabajo de la lección 05; esta lección se queda, deliberadamente, en el caso simple: un run sin ningún error en el camino.


Analogía: el tablero, con dos agujas que miden cosas distintas

Ya conoces, del Módulo 1, la analogía del auto sin tablero. Ahora que el tablero tiene una aguja de temperatura —la latencia—, vale la pena notar que un tablero real no tiene una sola aguja de temperatura: un auto con varios sistemas —motor, transmisión, frenos— podría, en teoría, mostrarte la temperatura de cada uno por separado, además de una lectura general. Sirven para preguntas distintas: la temperatura de un componente específico te dice dónde enfocar una reparación; la lectura general te dice si, en este momento, el auto completo está funcionando dentro de un rango razonable. Ninguna de las dos reemplaza a la otra.

Eso es, con precisión, la diferencia entre la latencia de una tool call y la latencia total de un run. La primera —cuánto tarda book_room, específicamente— te dice dónde enfocar la atención si algo anda lento. La segunda —cuánto tardó el run completo— te dice si, en conjunto, la experiencia que tuvo ese usuario fue razonable. Necesitas las dos.


Ejemplo trabajado: las dos preguntas, sobre el mismo run

Pregunta 1: ¿cuánto tarda una tool call individual?

Esta es la pregunta más simple de las dos — es, literalmente, una consulta a un diccionario. TOOL_LATENCY_MS, heredado del Módulo 1 sin cambiar un solo valor:

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

for tool_name, latency_ms in TOOL_LATENCY_MS.items():
    print(f"{tool_name:15} {latency_ms:4} ms")

Qué esperar:

list_rooms       40 ms
get_quote        25 ms
book_room       120 ms
cancel_booking   90 ms

Cuatro números, cuatro tools, sin ninguna ejecución de por medio — esto es una tabla de referencia, no una medición. La lección 04 va a detenerse en por qué estos cuatro números son los que son; por ahora, basta con confirmar que responder "¿cuánto tarda book_room?" es tan simple como TOOL_LATENCY_MS["book_room"].

Pregunta 2: ¿cuánto tarda el run completo?

Esta pregunta es distinta porque un run casi nunca llama a una sola tool — llama a varias, en secuencia, y la latencia total es la suma de lo que tardó cada una. Retoma el guion de Sofía, del Módulo 1 lección 05: sin ningún error en el camino, Boardroom pro 1 hora.

import reservo_agent as ra

script_sofia = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "list_rooms", "input": {}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_02", "name": "get_quote",
         "input": {"room": "Boardroom", "tier": "pro", "hours": 1}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_03", "name": "book_room",
         "input": {"room": "Boardroom", "tier": "pro", "hours": 1, "member": "Sofía"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Boardroom pro por 1 hora para Sofía. Total $64.00. Confirmación #1."}]},
]

final, history = ra.run_reservo_agent("Reserva Boardroom pro 1h para Sofía", script_sofia)

tools_called = [
    block["name"]
    for turn in history if turn["role"] == "assistant" and isinstance(turn["content"], list)
    for block in turn["content"] if block["type"] == "tool_use"
]
total_latency_ms = sum(TOOL_LATENCY_MS[name] for name in tools_called)

print("tools llamadas   :", tools_called)
for name in tools_called:
    print(f"  {name:15} {TOOL_LATENCY_MS[name]:4} ms")
print("latencia total    :", total_latency_ms, "ms")

Qué esperar:

tools llamadas   : ['list_rooms', 'get_quote', 'book_room']
  list_rooms       40 ms
  get_quote        25 ms
  book_room       120 ms
latencia total    : 185 ms

185 ms, la misma cifra que ya viste en el Módulo 1: 40 + 25 + 120 = 185. La latencia total de este run no es un número nuevo que se mide aparte — es, literalmente, la suma de las tres latencias individuales que la Pregunta 1 ya sabía calcular. Este código funciona bien para este caso, porque ninguna tool call de este guion falló — cada tool_use que aparece en tools_called corresponde a una tool que de verdad se ejecutó. La lección 05 va a mostrarte, con el guion de Ana, por qué este mismo código sería incorrecto si alguna tool call hubiera sido rechazada por validación.


Por qué ninguna de las dos preguntas vivía en history antes de este módulo

Vale la pena confirmarlo una vez más, con el vocabulario preciso de este módulo: history —el mismo history que acabas de recorrer arriba— nunca tuvo un campo de tiempo. TOOL_LATENCY_MS no es parte de la salida del agente; es una tabla externa que tú consultas, después, usando el nombre de la tool que sí está en history (block["name"]) como clave. Esa distinción importa: la latencia no se "extrae" de history como se extraería, por ejemplo, el booking_id de un tool_result — se calcula, combinando lo que history sí sabe (qué tool se llamó) con lo que TOOL_LATENCY_MS declara (cuánto "tarda" esa tool, según el modelo de esta guía).


Errores comunes

  1. Confundir "la latencia de una tool" con "la latencia de un run" cuando se habla en abstracto. Son la misma unidad (milisegundos) pero responden preguntas distintas — y un run con una sola tool call tiene, por definición, la misma latencia individual que total, lo que puede hacer parecer que son "la misma cosa" cuando en realidad coinciden solo por casualidad de ese caso particular.

  2. Sumar la latencia de tools que aparecen en tools_called sin verificar que de verdad se ejecutaron. El código de esta lección funciona porque el guion de Sofía no tiene ningún error — pero tools_called, tal como está escrito aquí, incluye cualquier tool_use, haya o no haya fallado su validación. La lección 05 corrige esto con precisión.

  3. Pensar que la latencia total de un run "promedia" las latencias de sus tools, en vez de sumarlas. No — se suman. Un run con tres tool calls no tarda "el promedio de las tres"; tarda la suma de las tres, porque (en el modelo secuencial de esta guía, heredado de agent-fundamentals) el agente despacha una tool, espera su resultado, y recién entonces decide la siguiente.

  4. Buscar la latencia total en algún campo de final o de un solo turno de history. No existe. La latencia total siempre se calcula recorriendo el run completo y sumando — nunca es un valor que ya viene listo en ningún bloque individual.

  5. Olvidar que TOOL_LATENCY_MS es el mismo diccionario del Módulo 1, no uno nuevo de este módulo. Esta lección lo reusa sin cambiar un solo valor — declararlo de nuevo con números distintos rompería la continuidad con cada cifra que ya viste en el Módulo 1.


Ejercicios

Ejercicio 1: Calcula la latencia total de un run de una sola tool (Fácil)

Sin ejecutar nada: si un run solo llama a cancel_booking, sin ninguna otra tool, ¿cuál es su latencia total? Confirma con código, usando TOOL_LATENCY_MS y un guion de un solo paso.

Ver solución

90 ms — la latencia individual de cancel_booking, sin nada que sumarle, porque un run de una sola tool tiene, por definición, latencia total igual a la latencia individual de esa única tool.

script_cancel = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "cancel_booking", "input": {"id": 1}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Cancelé la reserva #1."}]},
]
final, history = ra.run_reservo_agent("Cancela la reserva 1", script_cancel)
tools_called = [
    block["name"]
    for turn in history if turn["role"] == "assistant" and isinstance(turn["content"], list)
    for block in turn["content"] if block["type"] == "tool_use"
]
print("latencia total:", sum(TOOL_LATENCY_MS[name] for name in tools_called), "ms")

Salida esperada:

latencia total: 90 ms

Explicación: con una sola tool call, no hay nada que distinga "latencia individual" de "latencia total" — coinciden porque el run no tiene más pasos que sumar. Esa coincidencia deja de ser cierta apenas un run tiene dos o más tool calls, como el guion de Sofía del ejemplo trabajado.

Ejercicio 2: ¿Existen dos combinaciones de tools distintas con la misma latencia total? (Medio)

Usando TOOL_LATENCY_MS, investiga si existen dos subconjuntos distintos de las cuatro tools (sin repetir ninguna dentro de un mismo guion) cuya latencia total sea exactamente igual. No lo resuelvas a ojo, probando combinaciones sueltas — escribe código que recorra todas las combinaciones posibles y lo confirme.

Ver solución

La respuesta honesta es que no existen — con estos cuatro valores específicos (40, 25, 120, 90), ninguna combinación de tools distintas entre sí suma exactamente lo mismo que otra. Antes de aceptar esa respuesta a mano, confírmala con código, recorriendo todos los subconjuntos posibles de las cuatro tools:

from itertools import combinations

values = {"list_rooms": 40, "get_quote": 25, "book_room": 120, "cancel_booking": 90}
sums = {}
for r in range(1, 5):
    for combo in combinations(values.items(), r):
        total = sum(v for _, v in combo)
        names = tuple(sorted(k for k, _ in combo))
        sums.setdefault(total, set()).add(names)

empates = {total: combos for total, combos in sums.items() if len(combos) > 1}
print("sumas con mas de una combinacion de tools distintas:", empates)

Salida esperada:

sumas con mas de una combinacion de tools distintas: {}

Explicación: con solo cuatro tools y estos cuatro valores específicos (40, 25, 120, 90), no existen dos subconjuntos distintos que sumen exactamente lo mismo — cada combinación posible de tools da una latencia total única. Esto no es una propiedad matemática garantizada de cualquier conjunto de números (podría no cumplirse con otros valores) — es, simplemente, el resultado real de estos cuatro números concretos, confirmado con código en vez de asumido.

Ejercicio 3: Diseña un guion cuya latencia total sea mayor a la de cualquier tool individual, pero menor que la suma de las cuatro (Difícil)

Sin ejecutar nada primero: diseña un guion (usando dos o tres de las cuatro tools, alguna repetida si hace falta) cuya latencia total esté estrictamente entre 120 (la tool individual más cara) y 275 (la suma de las cuatro, del Módulo 1). Calcula la latencia esperada, y confirma con código.

Ver solución

Una opción válida: book_room + get_quote (120 + 25 = 145), estrictamente entre 120 y 275.

script_mixed = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "get_quote",
         "input": {"room": "Studio", "tier": "basic", "hours": 1}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_02", "name": "book_room",
         "input": {"room": "Studio", "tier": "basic", "hours": 1, "member": "Luis"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Studio basic 1h para Luis. Confirmación #1."}]},
]
final, history = ra.run_reservo_agent("Reserva Studio basic 1h para Luis", script_mixed)
tools_called = [
    block["name"]
    for turn in history if turn["role"] == "assistant" and isinstance(turn["content"], list)
    for block in turn["content"] if block["type"] == "tool_use"
]
print("tools:", tools_called)
print("latencia total:", sum(TOOL_LATENCY_MS[name] for name in tools_called), "ms")

Salida esperada:

tools: ['get_quote', 'book_room']
latencia total: 145 ms

Explicación: 145 cumple las dos condiciones —mayor que 120 (la tool individual más cara, book_room sola) y menor que 275 (las cuatro tools juntas)—. El punto del ejercicio es notar que cualquier combinación de dos o más tools que incluya book_room va a superar automáticamente a 120, porque la suma de positivos siempre crece; el límite superior (275) solo se alcanza usando las cuatro tools, cada una exactamente una vez, sin repetir ninguna.


Resumen y siguiente paso

  • Distinguimos dos preguntas de latencia: la de una tool call (una consulta directa a TOOL_LATENCY_MS) y la total de un run (la suma de las tools que de verdad participaron).
  • Confirmamos, ejecutado, que el run de Sofía —sin ningún error en el camino— tiene latencia total 185 ms, la suma exacta de list_rooms (40) + get_quote (25) + book_room (120).
  • Dejamos pendiente, a propósito, el caso con errores en el camino — cuando un tool_use es rechazado por validación, la suma simple de este ejemplo daría un número incorrecto. Ese matiz es, con precisión, el contenido de la lección 05.

Siguiente lección: 03 — El problema de honestidad: modelada vs. reloj real. Antes de seguir construyendo sobre TOOL_LATENCY_MS, respondemos la pregunta de fondo que esta guía no puede evitar: ¿por qué modelar la latencia en vez de medirla con el reloj real, y qué se pierde al hacerlo?


Recursos adicionales

  1. Anthropic — Tool use (function calling) overview — La forma exacta de tool_use que esta lección recorre para extraer el nombre de cada tool llamada.
  2. Python — diccionarios — La estructura de datos detrás de TOOL_LATENCY_MS, tan simple como una tabla de consulta.
  3. Python — itertools.combinations — Usado en el Ejercicio 2 para confirmar, con código, que ninguna combinación de tools distinta empata en latencia total.
  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.