Medir el consumo de tokens de un agente: en qué se te van
El mes pasado revisé el agente de un cliente. Se comía unos 340 dólares al mes de API y el equipo quería bajarlo.
Lo primero que hicieron fue lo que hace todo el mundo: recortar el system prompt. Lo dejaron en la mitad, de 1.800 tokens a 900.
El ahorro real fue del 1,2% por llamada.
No porque recortar el prompt sea mala idea. Porque nadie se había parado a medir el consumo de tokens antes de tocar nada. El system prompt era el 2,3% de cada petición. El 67% se lo comían los resultados de las herramientas, que nadie había mirado.
Llevo meses viendo la misma escena. Hay una biblioteca entera de trucos para gastar menos —caching, recortes, modelos más baratos— y casi nadie tiene el paso previo: saber en qué se le van los tokens. Optimizan a ciegas y aciertan por casualidad.
Este post es ese paso previo.
Un agente no gasta tokens: reenvía tokens
La confusión empieza aquí. La gente piensa en "el prompt" como si fuera una cosa que mandas una vez.
En un agente, cada input que envías se reparte en cuatro bloques:
- System prompt. Tus instrucciones. Fijo, se manda igual en cada llamada.
- Definiciones de herramientas. Nombres, descripciones y JSON Schema de cada tool, más el system prompt interno que la API añade para habilitar tool use — en Claude Opus 5 son 286 tokens con
tool_choice: auto, y 406 conanyotool. También fijo. - Historial de la conversación. Todos los turnos previos: lo que dijo el usuario, lo que respondió el modelo, cada bloque
tool_useque emitió. - Resultados de herramientas. El contenido de cada
tool_result: el JSON que devolvió tu API, el fichero que leyó, los 40 kB de HTML que trajo el scraper.
Los dos primeros son constantes. Los dos últimos crecen.
Y crecen de la peor manera posible, porque la Messages API no tiene estado. En el turno 12 no mandas el turno 12: mandas los turnos 1 a 12 otra vez, enteros, incluida la respuesta de 8.000 tokens que devolvió aquella herramienta en el turno 3 y que ya nadie va a volver a leer.
Ahí está el efecto que casi nadie ve. Si cada turno añade d tokens al contexto, el total de una sesión de n turnos no crece con n, crece con n²/2. En cristiano: duplicar los turnos de tu agente no duplica el coste, lo multiplica por casi cuatro.
Un ejemplo con números redondos. Base fija (system + tools) de 6.000 tokens, y cada turno añade otros 6.000 entre respuesta del modelo y resultado de herramienta. La columna de la derecha es el contenido único: el contexto que llega a ver el último turno, a 6.000 tokens por turno.
| Sesión | Input acumulado | Contenido único |
|---|---|---|
| 6 turnos | 126.000 tokens | 36.000 tokens |
| 12 turnos | 468.000 tokens | 72.000 tokens |
| 24 turnos | 1.800.000 tokens | 144.000 tokens |
En la sesión de 12 turnos pagas 468.000 tokens de input para procesar 72.000 tokens de material distinto. Seis veces y media. Con Claude Opus 5 a 5 $/millón de input, esa sesión te cuesta 2,34 dólares solo en entrada.
Si tienes esto claro, ya sabes por qué a aquel equipo recortar el system prompt le ahorró un 1,2%.
El desglose de un turno real
Esto es lo que salió al medir el turno 12 de aquel agente de research:
| Componente | Tokens | % del input |
|---|---|---|
| System prompt | 1.800 | 2,3% |
| Definiciones de herramientas (6 tools) | 4.200 | 5,4% |
| Historial de la conversación | 19.400 | 24,9% |
| Resultados de herramientas | 52.600 | 67,4% |
| Total input | 78.000 | 100% |
Mira la última columna y haz las cuentas tú mismo.
Partir el system prompt por la mitad ahorra 900 tokens: el 1,2% de la llamada. Meter un limit en la query de la base de datos y recortar un 40% los resultados de herramientas ahorra 21.040 tokens: el 27%.
Mismo esfuerzo de ingeniería. Más de veinte veces más impacto.
Este desglose no es universal, y ese es justo el punto. Un chatbot de soporte sin herramientas tiene el reparto invertido. Un agente de código con MCP servers cargados puede tener 30.000 tokens solo en definiciones de tools. Por eso el número que importa es el tuyo, no el mío.
Cómo medir el consumo de tokens de un agente
Medir el consumo de tokens de un agente es atribuir cada token de input a uno de los cuatro bloques que lo componen —system prompt, definiciones de herramientas, historial y resultados de herramientas— para saber qué porcentaje del gasto genera cada uno. La API de Anthropic da dos instrumentos para hacerlo: uno mira hacia atrás y otro mira hacia delante.
1. El campo usage de cada respuesta
Cada respuesta de la Messages API trae un objeto usage. Registra los cuatro campos, siempre, desde el primer día:
import Anthropic from 39;@anthropic-ai/sdk39;;
import type { MessageCreateParamsNonStreaming } from 39;@anthropic-ai/sdk/resources/messages39;;
const client = new Anthropic();
interface RegistroUso {
ts: string;
sesion: string;
turno: number;
input: number;
output: number;
cacheWrite: number;
cacheRead: number;
}
const registro: RegistroUso[] = [];
export async function llamada(
params: MessageCreateParamsNonStreaming,
meta: { sesion: string; turno: number },
) {
const res = await client.messages.create(params);
const u = res.usage;
registro.push({
ts: new Date().toISOString(),
sesion: meta.sesion,
turno: meta.turno,
input: u.input_tokens,
output: u.output_tokens,
cacheWrite: u.cache_creation_input_tokens ?? 0,
cacheRead: u.cache_read_input_tokens ?? 0,
});
return res;
}
Aquí hay una trampa que se traga a mucha gente. input_tokens no son todos los tokens que enviaste. Son solo los que van después del último punto de corte de caché. La documentación de Anthropic lo define así:
total_input = cache_read_input_tokens + cache_creation_input_tokens + input_tokens
Si activas prompt caching y sigues graficando input_tokens a secas, verás una caída espectacular que no significa nada. No has reducido el contexto: lo has movido de columna.
El coste real se calcula con las tres columnas y sus precios respectivos. Para Claude Opus 5:
const PRECIO_OPUS_5 = {
input: 5 / 1_000_000,
output: 25 / 1_000_000,
cacheWrite5m: 6.25 / 1_000_000,
cacheRead: 0.5 / 1_000_000,
} as const;
export const costeUSD = (r: RegistroUso) =>
r.input * PRECIO_OPUS_5.input +
r.output * PRECIO_OPUS_5.output +
r.cacheWrite * PRECIO_OPUS_5.cacheWrite5m +
r.cacheRead * PRECIO_OPUS_5.cacheRead;
Precios verificados en la documentación de pricing de Anthropic en agosto de 2026. Si usas otro modelo, cambia la tabla — no los copies de un post de hace seis meses.
2. El endpoint de conteo para atribuir por componente
usage te da el total. No te dice cuánto pesa cada bloque. Para eso está /v1/messages/count_tokens, que en el SDK de TypeScript es client.messages.countTokens().
Acepta los mismos bloques de entrada que messages.create —model, system, tools, messages— y devuelve { input_tokens: number }. No acepta max_tokens. Es gratis y tiene su propio límite de peticiones, separado del de generación: 2.000 RPM en el tier Start.
La técnica es medir por diferencias:
import type { MessageParam, Tool } from 39;@anthropic-ai/sdk/resources/messages39;;
// el mismo client del primer bloque
const MODEL = 39;claude-opus-539;;
const PING: MessageParam[] = [{ role: 39;user39;, content: 39;.39; }];
const contar = async (p: {
system?: string;
tools?: Tool[];
messages: MessageParam[];
}) => (await client.messages.countTokens({ model: MODEL, ...p })).input_tokens;
/** Sustituye el contenido de cada tool_result por un carácter. */
function vaciarToolResults(messages: MessageParam[]): MessageParam[] {
return messages.map((m) => {
if (typeof m.content === 39;string39;) return m;
return {
...m,
content: m.content.map((b) =>
b.type === 39;tool_result39; ? { ...b, content: 39;.39; } : b,
),
};
});
}
export async function desglosar(input: {
system: string;
tools: Tool[];
messages: MessageParam[];
}) {
const [piso, conSystem, conTools, sinResultados, total] = await Promise.all([
contar({ messages: PING }),
contar({ system: input.system, messages: PING }),
contar({ tools: input.tools, messages: PING }),
contar({ ...input, messages: vaciarToolResults(input.messages) }),
contar(input),
]);
const system = conSystem - piso;
const tools = conTools - piso;
const toolResults = total - sinResultados;
return {
system,
tools,
toolResults,
historial: total - system - tools - toolResults - piso,
piso, // andamiaje fijo de la API: sin esta fila, las otras cuatro no suman el total
total,
};
}
Cinco llamadas en paralelo y tienes tu tabla. Lánzalo contra una conversación real serializada de producción, no contra un caso de prueba de tres turnos.
Tres avisos. El conteo es una estimación y puede desviarse ligeramente del cobro real. El . con el que sustituyes cada tool_result cuenta como token, así que toolResults sale un pelín corto y esa diferencia se te va a historial. Y el endpoint responde con el tokenizador del model que le pases: los modelos desde Opus 4.7 usan uno nuevo que produce en torno a un 30% más de tokens para el mismo texto, así que un conteo hecho contra un modelo viejo no sirve para presupuestar uno nuevo. El idioma añade su propia variación sobre esa base: en español el mismo texto tokeniza un 26% más caro.
Los cuatro pasos, en orden
- Serializa una conversación real de producción, de 10 turnos o más, a un objeto
{ system, tools, messages }. - Registra el
usagede cada llamada con la sesión y el turno como etiquetas. - Pasa esa conversación por
desglosar()y obtén los cuatro componentes. - Ordena las filas por porcentaje y ataca solo la primera.
Qué hacer con cada hallazgo
Ya tienes el número. Ahora el diagnóstico. Cada fila de la tabla apunta a una intervención distinta:
Si el bloque fijo (system + tools) es grande pero cache_read_input_tokens sale en cero, no tienes un problema de tamaño, tienes uno de caché. Estás pagando 5 $/millón por reprocesar en cada turno lo mismo que podrías estar leyendo a 0,50 $. Es la palanca con mejor ratio impacto/esfuerzo de esta lista y la expliqué entera en prompt caching en la API de Claude.
Con los números de antes: de esos 468.000 tokens de input, con la caché refrescándose en cada turno solo 72.000 se escriben (a 6,25 $/millón) y los otros 396.000 se leen (a 0,50 $/millón). La sesión pasa de 2,34 dólares a 0,65. Un 72% menos, sin tocar una sola instrucción.
Si el system prompt sí es el problema de verdad —pasa, sobre todo dentro de herramientas que inyectan contexto por su cuenta—, entonces sí toca recortar. En Claude Code buena parte de ese peso no lo has escrito tú, y se quita con un flag: lo cuento en el flag que recorta el system prompt dinámico.
Si el contenido está en español, el desglose ya viene con un recargo de fábrica. El mismo texto tokeniza alrededor de un 26% más caro que en inglés, y eso afecta a tus prompts, a tus tool results y a la salida del modelo. Antes de recortar nada, léete el recargo del tokenizador en español: a veces la optimización correcta es escribir el system prompt en inglés y responder en español.
Si lo que se disparó es el número de turnos, tu problema no está en ninguna fila de la tabla: está en cuántas veces la construyes. Ojo especialmente después de cambiar de modelo, porque cuánto delega es una propiedad del modelo y se mueve entre versiones — el mecanismo completo está en por qué sube el coste de los subagentes al cambiar de modelo.
Y si los resultados de herramientas son el 60-70%, como en el caso que abre este post, tienes tres salidas por orden de esfuerzo: paginar y filtrar en tu propia tool antes de devolver nada, resumir los resultados antiguos, o dejar que la API vaya limpiando los tool results caducados.
Lo último se hace con la estrategia clear_tool_uses_20250919 de context editing, que todavía está en beta. Elegir entre las tres es exactamente el trabajo que describo en gestión estratégica del contexto.
Un apunte sobre la primera opción, que es la que más gente se salta. Si tu herramienta devuelve el objeto completo de la base de datos porque "por si acaso el modelo lo necesita", estás pagando por cada campo en cada turno restante de la sesión. Definir el contrato de salida de cada tool con un esquema estricto —y devolver solo eso— es una decisión de coste, no de estilo. Un esquema de salida con Zod en la frontera de cada tool tira los campos que no declaraste antes de que lleguen al contexto.
Empieza por la tabla
No apliques ni un truco de ahorro esta semana.
Coge una conversación real de producción, pásala por desglosar() y monta la tabla de cuatro filas. Eso es todo el trabajo de medir el consumo de tokens de un agente: media hora. Y lo más probable es que el mayor porcentaje esté donde no lo esperabas, porque el componente que más pesa suele ser el que nadie mira: el que se genera solo.
Después optimiza. En ese orden.
Instrumentar el gasto antes de tocarlo es parte del mismo hábito que enseño en Construye con IA: decidir con datos en lugar de con intuición, también cuando lo que decides es dónde recortar. Y si quieres ver desgloses de agentes reales, con sus facturas y sus tablas delante, eso lo hacemos en Dominicode Labs.
Preguntas frecuentes sobre el consumo de tokens
¿Por qué mi factura sube si no he cambiado el prompt?
Porque en un agente el coste no depende solo del prompt, depende de cuántos turnos dura cada sesión. El historial se reenvía entero en cada llamada, así que el gasto crece de forma cuadrática con el número de turnos: pasar de 12 a 24 turnos multiplica el input acumulado por casi cuatro, no por dos. Si tu agente empezó a necesitar más iteraciones para cerrar la misma tarea, la factura sube sin que hayas tocado una línea.
¿El endpoint de conteo de tokens cuesta dinero?
No. /v1/messages/count_tokens es gratuito. Sí tiene límite de peticiones por minuto según tu tier —2.000 RPM en Start, 4.000 en Build y 8.000 en Scale—, pero es un límite independiente del de generación de mensajes: usar uno no consume el cupo del otro. No hay excusa de coste para no medir.
¿Sirve un tokenizador local como tiktoken para medir esto?
Para una estimación rápida y offline, vale. Para presupuestar, no. Anthropic no publica su tokenizador y el conteo depende del modelo concreto: los modelos desde Claude Opus 4.7 usan un tokenizador nuevo que genera alrededor de un 30% más de tokens para el mismo texto que los anteriores. Un tokenizador de otra familia de modelos te dará un número que no se parece al que te van a cobrar.
¿Por qué input_tokens baja tanto al activar el prompt caching?
Depende de qué estés midiendo. input_tokens cuenta solo lo que va después del último punto de corte de caché, así que activar caching hace que ese número se desplome sin que hayas reducido el contexto ni un token. Para saber lo que realmente enviaste tienes que sumar cache_read_input_tokens y cache_creation_input_tokens. Grafica siempre las tres series juntas, nunca input_tokens solo.
¿Cada cuánto hay que volver a medir el consumo de tokens?
Cada vez que cambies de modelo, añadas o quites herramientas, o toques lo que devuelve una tool. Los tres modifican el reparto. Lo eficiente es no repetirla a mano: deja el registro de usage corriendo en producción con la sesión y el turno como etiquetas, y el desglose por componente pásalo cuando el coste medio por sesión se salga de su rango normal.
¿Y si la conclusión es que necesito menos turnos, no menos tokens?
Es la conclusión más común y la más incómoda, porque no se arregla con un flag. Un agente que tarda 20 turnos en algo que debería resolver en 6 casi siempre tiene un problema de definición de la tarea, no de contexto. Ahí la palanca no es técnica: es escribir mejor qué tiene que hacer antes de dejarlo correr, que es de lo que va el libro de Spec-Driven Development.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
