Módulo 3: Medir costo y tokens por run
Los tokens son la unidad de costo
Descripción
Antes de escribir una sola fórmula de costo, vale la pena responder una pregunta que la lección 01 dejó pendiente: ¿en qué unidad se mide, exactamente, lo que cuesta un run del agente de Reservo? No son dólares directamente —esos son el resultado final de un cálculo—, y no es "una llamada" ni "un segundo de cómputo", aunque ambas ideas parecen razonables a primera vista. La unidad real, la que usa claude-sonnet-5 y prácticamente cualquier proveedor de modelos de lenguaje, es el token. Esta lección explica qué es, por qué esa es la unidad que se eligió, y por qué no es lo mismo que un carácter ni que una palabra —aunque las tres midan, de formas distintas, la misma pregunta: "¿cuánto texto hay aquí?".
Esta lección no ejecuta ninguna fórmula de costo todavía —eso empieza en la lección 05—. Lo que hace es dejar el vocabulario exacto en su lugar, con ejemplos ejecutados sobre texto real de Reservo, para que la lección 03 pueda construir estimate_tokens sobre una base sólida.
Conexión con el módulo
Esta lección no agrega código a observability/cost_calculator.py —eso empieza en la lección 03—. Es la base conceptual de todo lo que sigue: sin tener claro qué es un token y por qué se cuenta como se cuenta, la fórmula len(texto) // 4 de la lección 03 sonaría a un truco arbitrario, en vez de a lo que realmente es — una aproximación razonada a una unidad real.
Analogía: no se factura el papel, se factura la tinta usada
Cuando una imprenta cotiza un trabajo, no cobra por la cantidad de hojas que le entregaste para leer, ni por los minutos que tardó la máquina en correr — cobra, con precisión, por la cantidad de tinta que el trabajo consumió: cuánta tinta entra en imprimir el documento que le diste, y cuánta tinta entra en el documento que produce como resultado. Dos documentos con la misma cantidad de páginas pueden consumir cantidades de tinta muy distintas —una página llena de texto denso gasta más tinta que una con mucho espacio en blanco—, así que "páginas" nunca fue una unidad honesta para lo que la imprenta realmente gasta.
Un modelo de lenguaje factura de una forma parecida. No cobra por "una pregunta" (el equivalente a una hoja), ni por segundos de cómputo (el equivalente a minutos de máquina) — cobra por tokens, la unidad que sí refleja, con precisión razonable, cuánto trabajo real le costó procesar lo que le enviaste y generar lo que respondió. Dos preguntas de la misma longitud en caracteres pueden costar cantidades de tokens distintas, exactamente como dos páginas del mismo tamaño pueden gastar cantidades de tinta distintas.
Qué es un token, con precisión razonable
Un token es el fragmento de texto que un modelo de lenguaje procesa como una sola unidad — ni siempre un carácter, ni siempre una palabra completa. Un tokenizer real (el componente que convierte texto en tokens) divide el texto en fragmentos según patrones aprendidos de un enorme corpus de entrenamiento: una palabra común en inglés como "the" suele ser un solo token; una palabra menos común, o una palabra en otro idioma, puede dividirse en dos o tres fragmentos; la puntuación, los números, y los espacios también cuentan como tokens o parte de ellos. El resultado no es una regla simple como "una palabra, un token" — es un mapeo aprendido, específico de cada familia de modelos, que no coincide exactamente con ninguna unidad lingüística tradicional.
Esta guía no tiene acceso a un tokenizer real —la lección 03 explica por qué, y qué convención usa en su lugar—, pero el concepto importa igual: cuando una línea de este módulo dice "este run costó 120 tokens", esos 120 no son ni 120 caracteres ni 120 palabras — son la unidad real que un proveedor de LLM usa para medir cuánto texto entró y cuánto texto salió de una llamada al modelo.
Dos flujos de tokens, contados y cobrados por separado
Cada llamada a un modelo de lenguaje mueve texto en dos direcciones, y ambas se cuentan —y se cobran— de forma independiente:
- Tokens de entrada (
input_tokens): todo el texto que el modelo lee antes de responder — la pregunta original, el system prompt, y —en un agente como Reservo— cadatool_resultque se le devuelve en el camino. Cuanto más larga la conversación acumulada, más tokens de entrada consume cada turno nuevo, porque el modelo vuelve a leer todo el historial cada vez. - Tokens de salida (
output_tokens): todo el texto que el modelo genera — eltool_useque pide (el nombre de la tool y sus argumentos, como texto), y la respuesta final en lenguaje natural.
Esta distinción no es un detalle contable — la lección 04 va a mostrar que el precio por token de salida es cinco veces el precio por token de entrada en claude-sonnet-5. Un agente que genera respuestas largas y verbosas paga esa asimetría de una forma que un agente que lee mucho contexto pero responde en pocas palabras no paga igual.
Ejemplo trabajado: token, carácter y palabra no son la misma unidad
Para dejar esto fuera de toda ambigüedad, compara las tres formas de medir "cuánto texto hay" sobre tres fragmentos reales de un run de Reservo: la pregunta del usuario, la respuesta final del agente, y el tool_result de una cotización.
import json
import reservo_tools as rt
def estimate_tokens(text):
"""Adelanto de la lección 03: la convención de esta guía. Por ahora,
solo la usamos para comparar -- la justificación completa llega en la
próxima lección."""
return len(text) // 4
textos = {
"pregunta del usuario": "Reserva Focus pro 3h para Ana",
"respuesta final del agente": "Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1.",
"tool_result de get_quote": json.dumps({"price_cents": 6000}),
}
print(f"{'fragmento':<28} {'caracteres':>10} {'palabras':>9} {'tokens (estim.)':>16}")
for label, text in textos.items():
n_chars = len(text)
n_words = len(text.split())
n_tokens = estimate_tokens(text)
print(f"{label:<28} {n_chars:>10} {n_words:>9} {n_tokens:>16}")
Qué esperar:
fragmento caracteres palabras tokens (estim.)
pregunta del usuario 29 6 7
respuesta final del agente 70 12 17
tool_result de get_quote 21 2 5
Ninguna de las tres columnas es igual a otra. La pregunta tiene 29 caracteres y 6 palabras — casi 5 caracteres por palabra, un promedio razonable para español—, y su estimado de tokens (7) no coincide ni con los caracteres ni con las palabras. El tool_result es el caso más revelador: solo 2 "palabras" si cuentas por espacios ({"price_cents": y 6000} no son palabras reales, son fragmentos de sintaxis JSON), pero 21 caracteres que producen 5 tokens estimados — un JSON compacto, sin espacios entre claves y valores, empaqueta más significado por carácter que una frase en prosa, y eso se refleja en la proporción de tokens.
Por qué se cobra por token, y no por llamada ni por segundo
Vale la pena entender la razón de fondo, no solo memorizar la unidad. Un modelo de lenguaje procesa texto de forma autoregresiva: genera su respuesta un token a la vez, y cada token nuevo requiere que el modelo vuelva a considerar todo el texto que ya tiene por delante —la pregunta, el contexto acumulado, y lo que ya generó de la respuesta—. El costo computacional real de una llamada crece, aproximadamente, con la cantidad de texto involucrada —tanto el que se lee como el que se genera—, no con la cantidad de "llamadas" que se hicieron ni con cuántos segundos de reloj tardó en responder (eso depende de factores externos, como la carga del servidor, que no reflejan el trabajo real que le tomó al modelo).
Cobrar por token, entonces, no es una elección arbitraria de facturación — es la unidad que más de cerca refleja el trabajo computacional real detrás de cada llamada. Dos llamadas del mismo "tamaño" en tokens cuestan, aproximadamente, lo mismo, sin importar si una tardó más en responder por congestión de red o por la carga del servidor en ese momento — esa variabilidad es, con precisión, el tema del Módulo 4 (latencia), una señal completamente distinta del costo que mide este módulo.
Errores comunes
-
Pensar que "token" y "palabra" son sinónimos. El ejemplo trabajado lo confirma:
"Reserva Focus pro 3h para Ana"tiene6palabras pero se estima en7tokens — cerca, pero no igual, y la brecha crece con textos que tienen más puntuación, números o sintaxis (como el JSON de untool_result). -
Asumir que los tokens de entrada y salida cuestan lo mismo. Esta lección solo lo menciona; la lección 04 lo confirma con la tabla de precios real: el token de salida de
claude-sonnet-5cuesta cinco veces más que el de entrada. Diseñar un agente sin tener esa asimetría en mente es un error de costo silencioso. -
Creer que "cobrar por token" significa "cobrar por carácter, con otro nombre". El
tool_resultdel ejemplo trabajado lo desmiente:21caracteres, pero una proporción de tokens por carácter distinta a la de la pregunta en prosa. Un token no es una unidad de longitud fija — depende del contenido. -
Ignorar que el historial completo de una conversación se re-lee en cada turno. En un agente como Reservo, cada
tool_resultnuevo que se agrega ahistoryno solo se cuenta una vez — el modelo (concepto) vuelve a "leer" todo el historial acumulado en cada turno siguiente, así que los tokens de entrada de un run de varios pasos crecen con cada paso, no se mantienen constantes. -
Pensar que esta lección ya te da una forma de contar tokens de verdad. Todavía no — el
estimate_tokensde este ejemplo es un adelanto sin justificar de la lección 03, que es donde se explica, con precisión, por qué esta guía usalen(texto) // 4en vez de un tokenizer real, y qué se pierde al hacerlo.
Ejercicios
Ejercicio 1: Compara las tres unidades sobre una tarea de cancelación (Fácil)
Toma el texto "Cancela la reserva 999" (la pregunta de un usuario cancelando una reserva inexistente) y calcula, a mano primero y después con código, su cantidad de caracteres, de palabras, y su estimado de tokens con len(texto) // 4.
Ver solución
text = "Cancela la reserva 999"
print("caracteres:", len(text))
print("palabras :", len(text.split()))
print("tokens (estim.):", len(text) // 4)
Salida esperada:
caracteres: 22
palabras : 4
tokens (estim.): 5
Explicación: 22 caracteres, 4 palabras (Cancela, la, reserva, 999), y un estimado de 5 tokens — de nuevo, ninguna de las tres cifras coincide con otra, confirmando que son tres unidades genuinamente distintas sobre el mismo texto.
Ejercicio 2: Confirma que el estimado de tokens crece con el texto acumulado (Medio)
Toma el tool_result de list_rooms() (JSON de las tres salas). Calcula su estimado de tokens una vez, y después calcula el estimado de ese mismo texto repetido tres veces seguidas (simulando, de forma simplificada, cómo crecería el contexto si el mismo resultado se re-leyera varias veces en una conversación larga). Confirma que el estimado crece, aproximadamente, en la misma proporción que el texto.
Ver solución
import json
import reservo_tools as rt
def estimate_tokens(text):
return len(text) // 4
one = json.dumps(rt.list_rooms())
three = one + one + one
print("una copia :", len(one), "caracteres ->", estimate_tokens(one), "tokens")
print("tres copias :", len(three), "caracteres ->", estimate_tokens(three), "tokens")
print("proporción :", round(estimate_tokens(three) / estimate_tokens(one), 2))
Salida esperada:
una copia : 122 caracteres -> 30 tokens
tres copias : 366 caracteres -> 91 tokens
proporción : 3.03
Explicación: triplicar el texto triplica, casi exactamente, el estimado de tokens (3.03, no 3.00 exacto, por el redondeo hacia abajo de la división entera en cada punto) — la estimación de esta guía es lineal en la longitud del texto, una propiedad que la lección 03 confirma con más detalle y que explica, con precisión, por qué el costo de un run crece con cada paso adicional que agrega texto al historial.
Ejercicio 3: Demuestra que len(texto) // 4 es ciego al contenido (Difícil)
Construye dos textos de exactamente la misma longitud en caracteres (35): uno con prosa real en español ("Reserva Boardroom pro 1h para Sofia", ojo: cuenta los caracteres exactos) y otro con un carácter sin sentido repetido ("x" repetida 35 veces). Confirma que ambos producen el mismo estimado de tokens, y explica en una frase por qué esto es una limitación real de la convención de esta guía —no un error de cálculo—.
Ver solución
def estimate_tokens(text):
return len(text) // 4
prose = "Reserva Boardroom pro 1h para Sofia"
nonsense = "x" * len(prose)
print("longitud de ambos:", len(prose), "==", len(nonsense))
print("tokens (prosa) :", estimate_tokens(prose))
print("tokens (sin sentido):", estimate_tokens(nonsense))
Salida esperada:
longitud de ambos: 35 == 35
tokens (prosa) : 8
tokens (sin sentido): 8
Explicación: ambos textos producen exactamente 8 tokens estimados, aunque uno es prosa real en español y el otro es una secuencia sin ningún significado. Un tokenizer real sí distinguiría entre ambos —"Sofia" es una palabra reconocible que probablemente se tokeniza distinto que "xxxxx"—, pero len(texto) // 4 solo mira la longitud, nunca el contenido. Esta es, con precisión, la limitación central que la lección 03 nombra y rotula: una estimación de orden de magnitud, útil para el propósito de esta guía, pero ciega a cualquier diferencia que no sea de longitud.
Resumen y siguiente paso
- El token es la unidad de costo real de un modelo de lenguaje — ni un carácter, ni una palabra, sino un fragmento aprendido por un tokenizer, específico de cada familia de modelos.
- Cada llamada mueve dos flujos de tokens, contados y cobrados por separado: entrada (lo que el modelo lee: la pregunta, el historial acumulado, cada
tool_result) y salida (lo que el modelo genera: eltool_useque pide, la respuesta final). - Confirmamos, ejecutado, que caracteres, palabras y tokens estimados son tres cifras distintas sobre el mismo texto — y que el estimado de tokens crece linealmente con la longitud del texto, sin distinguir su contenido.
- Se cobra por token, no por llamada ni por segundo, porque el token es la unidad que más de cerca refleja el trabajo computacional real de un modelo autoregresivo.
Siguiente lección: 03 — Estimando tokens con len // 4. Con el concepto de token ya claro, construimos estimate_tokens, la función real que esta guía usa en todo lo que sigue — con su justificación completa, sus límites confirmados con código, y la honestidad explícita de que nunca reemplaza a un tokenizer real.
Recursos adicionales
- Anthropic — Token counting — Cómo cuenta tokens la API de Claude de verdad; la referencia contra la que esta guía confronta su propia estimación desde la próxima lección.
- Anthropic — Glossary: tokens — La definición oficial de token, entrada y salida, en la documentación de Claude.
- Python —
str.split()— El método usado en el ejemplo trabajado para contar palabras por separación en espacios. - Python —
json.dumps— La función que produce el texto compacto de untool_result, usada en varios de los ejemplos de esta lección. - Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.