Volver a los artículos

La factura de un agente: dónde se va el dinero y cómo recuperarlo

La factura de un agente: dónde se va el dinero y cómo recuperarlo
En el post anterior sostuve que un sistema agéntico cuesta más de lo que casi nadie calcula antes de construirlo, y la razón es concreta: cada vuelta del loop es una llamada nueva al modelo, y cada llamada reenvía todo el contexto acumulado hasta ese punto. Si el costo es real o no, no lo vuelvo a discutir aquí: esa pregunta ya quedó respondida allá. La que me interesa ahora es otra, y es la que de verdad te toca resolver: cuánto cuesta exactamente, y qué puedes hacer para que cueste menos.
Lo que falta es medirlo y reducirlo. En un sistema agéntico no pagas principalmente por lo que el modelo genera: pagas por lo que le vuelves a mandar en cada vuelta del loop. El contexto reenviado es la línea gorda de la factura, y ahí es donde vale la pena mirar primero.
Este post recorre dos partes. Primero, un desglose concreto de la factura: qué componente crece con cada turno y por qué. Después, seis tácticas: cinco para bajarla (prompt caching y sus trampas, enrutamiento por modelo, poda de contexto, presupuesto de turnos y batch) y una sexta, instrumentación, que no baja ninguna línea pero confirma que las otras cinco de verdad funcionaron.

El desglose de una factura agéntica

Toma un caso concreto: un agente que investiga un bug reportado en un repositorio. Tiene cuatro herramientas disponibles: read_file, search_code, run_tests y propose_patch. En cada vuelta del loop decide cuál invocar, observa el resultado y decide la siguiente, hasta que junta suficiente evidencia para proponer un parche.
Esa factura no es una cifra: es la suma de piezas que crecen a ritmos distintos. Las definiciones de las cuatro herramientas y el system prompt tienen tamaño fijo, pero se reenvían completos en cada una de las llamadas. El historial de mensajes (cada tool_use del modelo, encadenado con el tool_result que le devolviste) crece con cada vuelta. Y dentro de ese historial, lo más pesado no lo escribiste tú: es el contenido de los archivos que read_file devolvió, la salida completa de run_tests, los fragmentos que encontró search_code.
Hay una pieza que suele pasar desapercibida: lo que el modelo genera en la vuelta 3 (su razonamiento, su llamada a run_tests) se paga una vez como output de esa vuelta, y después se reenvía como input en la vuelta 4, en la 5 y en todas las que sigan mientras el agente siga trabajando. El output de hoy es el input reenviado de mañana.
ComponenteQué contieneCómo se paga
Definiciones de herramientasLos schemas de read_file, search_code, run_tests y propose_patchTamaño fijo, reenviado completo en cada una de las N llamadas
System promptInstrucciones de cómo debe comportarse el agenteTamaño fijo, reenviado completo en cada una de las N llamadas
Historial de mensajesCada tool_use del modelo encadenado con su turno anteriorCrece con cada vuelta, se reenvía completo en la siguiente
Resultados de herramientasContenido de archivos, salida de tests, fragmentos de búsquedaLa parte más pesada del historial: no la escribiste tú, la generó el sistema
Tokens generadosEl razonamiento y las llamadas a herramientas que produce el modeloSe paga como output una vez, y luego como input reenviado en cada vuelta posterior
De las cinco filas, solo una no se reenvía tal cual: los tokens generados, y aun esos terminan reenviados en cuanto pasan a formar parte del historial. Todo lo demás es input que vuelve a viajar completo en cada llamada. Esa es la observación que sostiene todo este post: el input reenviado en cada vuelta, no el output generado, es la línea dominante de la factura.

La aritmética de la vuelta N

En el post anterior afirmé que el costo de un agente crece más rápido que el número de pasos, sin demostrarlo. Toca demostrarlo.
La intuición es simple: en la vuelta N el modelo recibe otra vez todo lo que ya recibió en las vueltas 1 a N-1, más lo nuevo de esta vuelta. No recibe solo lo nuevo. Recibe lo nuevo encima de todo lo viejo, porque el modelo no tiene memoria entre llamadas: cada llamada es autocontenida, y lo único que la conecta con la anterior es el historial que le reenvías completo.
Para ver la forma exacta que toma ese crecimiento conviene pensar en bloques de contexto en vez de dólares: es un modelo simplificado para ilustrar la forma de la curva, no una medición de tokens reales. Si cada vuelta agrega un bloque nuevo de contenido, la vuelta 1 reenvía 1 bloque, la vuelta 2 reenvía 2 (el de la vuelta 1 más el suyo), la vuelta 3 reenvía 3, y así sucesivamente. El total reenviado a lo largo de N vueltas no es N, es la suma de 1 a N: crece con N², no con N.

Cómo crece el contexto reenviado, vuelta a vuelta

Reenvío ↑Vuelta 11 bloqueVuelta 22 bloquesVuelta 33 bloquesVuelta 44 bloquesYa enviado antes (se repite)Nuevo en este turnoTotal: 10 bloques reenviados en 4 vueltas, solo 4 son contenido nuevo
Esa suma triangular es la razón por la que duplicar el número de pasos no duplica el costo: lo aumenta más que eso. Comparar dos tamaños de loop lo deja claro en múltiplos. Con 4 vueltas el total reenviado es 10 bloques. Con 8 vueltas (el doble de pasos) el total es 36 bloques: no el doble, sino 3.6 veces más. Los pasos se multiplicaron por 2×, el contexto reenviado se multiplicó por 3.6×. Esa brecha entre el múltiplo de pasos y el múltiplo de contexto reenviado es exactamente lo que el prompt caching ataca.

Táctica 1: prompt caching

Si el input reenviado domina la factura y ese reenvío crece más rápido que los pasos, la primera táctica ataca exactamente eso: evitar pagar precio completo por el mismo prefijo cada vez que se repite. Es la táctica más rentable de las cinco que bajan la factura, y también la que tiene más formas de fallar en silencio, así que vale la pena mirarla con detalle.

Cómo se cobra un acierto y un fallo de caché

Escribir una entrada en el caché cuesta más que una lectura normal de input: 1.25× el precio base de input con un TTL de 5 minutos, o 2× con un TTL de 1 hora. Leer una entrada ya escrita cuesta, en cambio, apenas alrededor de 0.1× el precio base de input. La ganancia está en la lectura, no en la escritura: por eso el caching solo paga cuando hay más de una petición compartiendo el mismo prefijo.

El punto de equilibrio

Con TTL de 5 minutos el punto de equilibrio llega con solo dos peticiones: escribir cuesta 1.25× y leer cuesta 0.1×, así que la segunda petición ya sale más barata en conjunto (1.25× + 0.1× = 1.35×) que pagar el prefijo completo dos veces sin caché (2×).
Con TTL de 1 hora la escritura cuesta más (2× en vez de 1.25×), así que hacen falta al menos tres peticiones para amortizarla: 2× + 0.2× (dos lecturas a 0.1× cada una) = 2.2×, contra 3× de no cachear nada. La conclusión práctica: el TTL largo no es gratis, requiere más lecturas para amortizarse. Si tu agente hace ráfagas de llamadas seguidas, 5 minutos alcanza. Si las llamadas quedan espaciadas en el tiempo, necesitas el TTL de 1 hora, pero solo vale la pena si de verdad va a haber una tercera lectura.

El prefijo mínimo, y depende del modelo

No cualquier prefijo es cacheable: tiene que superar un mínimo de tokens, y ese mínimo cambia de un modelo a otro.
Prefijo mínimo cacheableModelos
4096 tokensOpus 4.8, 4.7, 4.6, 4.5; Haiku 4.5
2048 tokensFable 5, Sonnet 4.6, Haiku 3.5, Haiku 3
1024 tokensSonnet 4.5, 4.1, 4, 3.7

La trampa del prefijo mínimo

Un prompt de 3000 tokens cachea sin problema en Sonnet 4.5, cuyo mínimo es 1024 tokens. El mismo prompt, contra Opus 4.8, no cachea en absoluto: su mínimo es 4096. No hay error ni advertencia, la petición se procesa con normalidad y cache_creation_input_tokens sale en cero. Cambiar de modelo sin revisar esta tabla es la forma más silenciosa de perder el caching por completo.

La invariante: coincidencia de prefijo

El caching no entiende de significado, entiende de bytes. Es una coincidencia exacta de prefijo: si un solo byte cambia en algún punto del prompt, todo lo que viene después de ese punto deja de coincidir con lo que ya estaba cacheado, sin importar cuán irrelevante parezca el cambio. El prompt se arma en un orden fijo (tools, después system, después messages), y ese orden es también el orden en el que se propaga la invalidación: tocar algo temprano invalida todo lo que va después.
Esa invariante tiene consecuencias que no son obvias hasta que las pisas. Estos son los invalidadores silenciosos más comunes en código agéntico real:
  • datetime.now() interpolado en el system prompt
  • UUIDs o IDs de request colocados al inicio del contenido
  • json.dumps sin sort_keys=True
  • Iterar un set, cuyo orden no está garantizado
  • Interpolar el ID de usuario o de sesión en el system prompt
  • Secciones condicionales del system prompt que cambian según el contexto
  • Un set de herramientas que varía de un usuario a otro
Ninguno de estos parece un problema de caching a simple vista. Todos lo son.
cached_debug_agent.py
from anthropic import Anthropic

client = Anthropic()

SYSTEM_PROMPT = "You are a debugging agent. Investigate, don't guess."

TOOLS = [
    {"name": "read_file", "description": "Read a file from the repository", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
    {"name": "search_code", "description": "Search the codebase for a pattern", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
    {"name": "run_tests", "description": "Run the test suite", "input_schema": {"type": "object", "properties": {"target": {"type": "string"}}, "required": ["target"]}},
    {
        "name": "propose_patch",
        "description": "Propose a fix as a unified diff",
        "input_schema": {"type": "object", "properties": {"diff": {"type": "string"}}, "required": ["diff"]},
        # Breakpoint 1: cachea las cuatro definiciones de herramientas, TTL de una hora.
        "cache_control": {"type": "ephemeral", "ttl": "1h"},
    },
]

messages = [{"role": "user", "content": "The tests in test_auth.py are failing after the last commit."}]

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=TOOLS,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            # Breakpoint 2: cachea el system prompt, TTL de 5 minutos (el default).
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=messages,
)

# input_tokens es solo el remanente sin cachear.
# El tamaño real del prompt es la suma de los tres campos.
total_prompt_tokens = (
    response.usage.input_tokens
    + response.usage.cache_creation_input_tokens
    + response.usage.cache_read_input_tokens
)
El orden de los dos breakpoints no es arbitrario: cuando se mezclan TTLs distintos, el de una hora tiene que declararse antes que el de 5 minutos, siguiendo el mismo orden fijo del prompt (tools, luego system, luego messages). Por eso el TTL largo va en las herramientas y el corto en el system prompt, y no al revés.

Jerarquía de invalidación

No todo cambio invalida lo mismo. El prompt tiene tres niveles (tools, system y messages), y qué tan arriba ocurre el cambio determina cuántos niveles se pierden.
Qué cambiaQué invalida
Definiciones de herramientas o el modeloLos tres niveles: tools, system y messages
Contenido del system promptSystem y messages, pero no tools
tool_choice, imágenes, o activar/desactivar thinkingSolo messages
La implicación práctica vale la pena señalarla aparte: se puede alternar tool_choice de una petición a otra (forzar una herramienta específica en una llamada, dejarlo libre en la siguiente) sin perder el caché de tools y system. Ese nivel de la jerarquía no se entera del cambio.

Límites prácticos: breakpoints y la ventana de 20 bloques

Cada petición admite un máximo de 4 breakpoints de cache_control. Y cada breakpoint busca una entrada previa retrocediendo como máximo 20 bloques de contenido: si no la encuentra dentro de esa ventana, falla en silencio, sin error.
Ese límite importa particularmente en loops agénticos, donde un solo turno puede agregar varios pares tool_use/tool_result de una sola vez. Un turno que agrega más de 20 bloques empuja el siguiente breakpoint fuera de la ventana de búsqueda, y el caché de ese punto en adelante deja de encontrarse, aunque el prefijo siga siendo idéntico.

Cómo verificar que el caché está funcionando

La verificación es un solo campo: usage.cache_read_input_tokens. Si sale en cero entre dos peticiones que comparten el mismo prefijo, hay un invalidador silencioso en algún punto de la cadena tools, system, messages, y toca revisar la lista anterior en ese orden.
Vale la pena repetirlo porque es fácil de leer mal: input_tokens no es el tamaño del prompt, es solo el remanente que no se pudo cachear. El tamaño real del prompt es input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Mirar solo input_tokens subestima la factura real de cada llamada.

Peticiones concurrentes no comparten caché de inmediato

Una entrada de caché solo queda disponible para lectura después de que la primera respuesta empieza a transmitirse. Si lanzas N peticiones en paralelo con el mismo prefijo (un patrón común cuando se procesan varios tickets a la vez), ninguna de ellas alcanza a leer el caché de las otras: las N pagan precio completo de escritura, no solo la primera.
Verificado el 24 de junio de 2026 contra la documentación oficial de Anthropic. Los precios de los modelos cambian con el tiempo, conviene confirmarlos contra la documentación vigente antes de usarlos para presupuestar.
ModeloEntrada (por millón de tokens)Salida (por millón de tokens)
Claude Fable 5$10.00$50.00
Claude Opus 4.8$5.00$25.00
Claude Sonnet 5$3.00 (introductorio $2.00 hasta 2026-08-31)$15.00 (introductorio $10.00)
Claude Haiku 4.5$1.00$5.00

Táctica 2: enrutamiento por modelo

No todo paso del loop necesita el modelo más caro. Clasificar un ticket, extraer un campo de un resultado, decidir cuál de cuatro herramientas llamar a partir de una salida bien delimitada: son subpasos que un modelo pequeño resuelve igual de bien que uno grande, a una fracción del precio. La idea de esta táctica es reservar el modelo grande para el razonamiento que de verdad lo necesita, y despachar el resto a uno más barato.

La trampa: los cachés son por modelo

Aquí es donde la optimización obvia se convierte en una trampa. La tentación es alternar de modelo dentro del mismo loop: modelo barato para los pasos simples, modelo caro para los que lo justifican, turno a turno. Pero la invariante de la Táctica 1 no distingue casos especiales: el caché exige coincidencia exacta de prefijo, y esa coincidencia es contra un modelo específico. Cambiar de modelo a mitad de sesión no es un cambio más en el prefijo, es un prefijo distinto por completo, contra un caché distinto por completo. El caché acumulado de todo el loop se pierde de una vez, y la siguiente llamada al modelo grande vuelve a pagar escritura completa desde cero.
La optimización ingenua se paga sola: lo que ahorras en tokens de un paso barato lo devuelves, con intereses, en caché perdido para el resto de la sesión.

Alternar modelos invalida el caché entero

Meter un modelo barato a mitad de loop para resolver un paso simple no solo paga ese paso al precio del modelo pequeño: invalida el caché acumulado del modelo grande que venía sosteniendo el resto de la sesión. La siguiente llamada a ese modelo grande vuelve a escribir el prefijo completo desde cero, al precio de escritura, no al de lectura.

La solución: subagente, no alternancia

La forma de aprovechar un modelo barato sin pagar esta factura es no tocar el loop principal: delegar el subpaso barato a un subagente con su propio contexto, su propia llamada, su propio caché aislado. El loop principal sigue en un solo modelo, con su prefijo intacto y su caché acumulándose sin interrupciones. El subagente entra, resuelve su tarea puntual con el modelo barato, y devuelve el resultado como una pieza más del contexto del loop principal, sin haber tocado su caché en ningún momento.

LiteLLM como capa de enrutamiento

Para no escribir una integración distinta por proveedor, LiteLLM ofrece una interfaz única: la misma función completion() acepta un parámetro model que decide a qué proveedor y modelo se dirige la llamada, con el prefijo anthropic/ para forzar el enrutamiento explícito hacia la API de Anthropic. El resto de la llamada (mensajes, cache_control) no cambia según el modelo detrás.
model_routing.py
from litellm import completion

# Cheap sub-step: its own call, its own context, its own cache.
classification = completion(
    model="anthropic/claude-haiku-4-5",
    messages=[{"role": "user", "content": "Classify this support ticket: ..."}],
)

AGENT_SYSTEM_PROMPT = "You are a debugging agent. Investigate, don't guess."
conversation_history = [{"role": "user", "content": "The tests in test_auth.py are failing."}]

# Main reasoning loop: stays on one model, cache stays intact across turns.
reasoning = completion(
    model="anthropic/claude-sonnet-5",
    messages=[
        {
            "role": "system",
            "content": [
                {"type": "text", "text": AGENT_SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}},
            ],
        },
        *conversation_history,
    ],
)
LiteLLM también expone una clase Router pensada para administrar varios modelos a la vez: recibe una lista de configuraciones (cada una con su modelo real, sus credenciales y, opcionalmente, límites de tasa) y la usa para enrutar cada llamada según esa lista. Sirve para pools de modelos intercambiables entre sí, no para alternar modelos distintos dentro del mismo loop: esa distinción es justamente la que evita la trampa de la sección anterior.

Táctica 3: poda de contexto

El desglose de la primera sección ya dejó claro que el historial de mensajes es la pieza que más crece. La táctica obvia es empequeñecerlo. Pero podar el contexto no es una sola técnica: son dos, y no son intercambiables. Confundirlas lleva a aplicar la que no corresponde y no obtener el resultado esperado.

Context editing: poda, no resume

Context editing elimina resultados de herramientas viejos y bloques de razonamiento (thinking) que ya cumplieron su función: el tool_result de la vuelta 2 que ya no aporta nada en la vuelta 15, el bloque de thinking que llevó a una decisión que ya quedó tomada. Lo elimina, no lo comprime ni lo reescribe. Lo que sobrevive queda intacto; lo que se poda, desaparece. Es la técnica correcta cuando el contexto tiene partes identificablemente obsoletas: el contenido de un archivo que ya no se va a volver a citar, la salida completa de una corrida de tests que ya se resolvió.

Compaction: resumir del lado del servidor

Compaction hace lo contrario: cuando la sesión se acerca al límite de la ventana de contexto, resume del lado del servidor todo lo anterior en una versión más corta, que reemplaza el historial completo. No elige qué eliminar pieza por pieza, comprime el conjunto. Es la técnica correcta cuando el problema no es que haya partes obsoletas identificables, sino que la sesión entera se volvió demasiado larga para caber, y ya no quedan piezas discretas que podar sin perder continuidad.
La pregunta que separa una técnica de la otra: ¿sé exactamente qué partes del contexto ya no sirven, o el problema es que todo junto ya no cabe? Lo primero pide context editing. Lo segundo pide compaction.

La tensión con el caché

Ninguna poda es gratis del lado del caché, y esto conecta directo con la invariante de la Táctica 1: el caché es coincidencia exacta de prefijo. Podar contexto (eliminar un bloque, resumir un tramo) cambia bytes en medio del prefijo, y todo lo que viene después de ese punto deja de coincidir con lo que estaba cacheado. La siguiente llamada paga escritura completa desde ahí en adelante, exactamente como si hubiera cambiado el system prompt. La poda no es una operación aislada del costo de caching: es, ella misma, un evento de invalidación. Podar demasiado seguido cambia el prefijo constantemente y nunca deja que el caché se asiente; podar muy poco deja crecer el historial sin control. El punto de equilibrio depende de cuántas llamadas más va a tener el loop después de la poda, la misma pregunta que ya se hizo para amortizar la escritura de un breakpoint.

Táctica 4: presupuesto de turnos y herramientas

max_tokens no es un presupuesto: es un techo que el modelo no ve

max_tokens limita cuántos tokens puede generar el modelo en una respuesta, pero el modelo no tiene visibilidad de ese límite mientras genera: no sabe que le quedan 50 tokens, sigue razonando como si tuviera espacio ilimitado y, si se topa con el techo a mitad de una idea, la respuesta se corta ahí, sin aviso ni cierre. Es un corte, no una negociación.
Un presupuesto de tarea es otra cosa: una cuenta que el modelo sí ve, expresada en el propio contexto (turnos que quedan, herramientas que quedan, tokens que quedan), y que puede usar para dosificarse. Un modelo que sabe que le quedan dos vueltas antes de agotar su presupuesto prioriza distinto que uno que no lo sabe: cierra el razonamiento y entrega una respuesta parcial pero ordenada, en vez de quedarse a medio camino cuando el sistema le corta la llamada por fuera. La diferencia no está en el tamaño del límite, está en si el modelo puede planificar contra él.

El parámetro de esfuerzo

El nivel de esfuerzo funciona como una segunda perilla, distinta del presupuesto de turnos: controla cuánta deliberación interna hace el modelo antes de responder, no cuántos turnos tiene disponibles. Subirlo para un subpaso trivial (clasificar, extraer un campo) gasta presupuesto en profundidad que esa tarea no necesita; bajarlo para un problema genuinamente difícil deja al modelo respondiendo con menos análisis del que el problema pide. Ajustarlo por tarea, en vez de dejarlo fijo en el nivel más alto para todo el loop, es la misma lógica de la Táctica 2 (modelo pequeño para lo simple, grande para lo que lo justifica) aplicada dentro de una sola llamada.

El costo escondido de las definiciones de herramientas

Las definiciones de herramientas no son gratis por estar quietas. Viajan completas en cada una de las N llamadas del loop, como ya quedó dicho en el desglose inicial, y además se renderizan en la posición cero del prompt: antes del system prompt, antes de los mensajes, en el mismo orden tools, system, messages que ya explicó la Táctica 1. Un set de cuatro herramientas con descripciones detalladas puede pesar más que el propio system prompt, y ese peso se paga en cada vuelta, se use o no se use ninguna herramienta en esa vuelta específica. Y por ocupar el primer nivel de la jerarquía de invalidación, cualquier cambio en el set de herramientas expuesto invalida los tres niveles a la vez, no solo uno. Recortar el número de herramientas expuestas a las que el paso actual realmente necesita reduce esa posición cero en cada llamada, no solo en las que terminan usándolas.

Táctica 5: batch

La Batch API ofrece un 50% de descuento sobre el precio estándar de input y output, a cambio de renunciar a la respuesta inmediata: la petición se encola y se resuelve de forma asíncrona, no en el mismo turno en que se envió.
El descuento es real, pero antes de adoptarlo vale la pena hacerse la pregunta que es el punto de esta sección: si una tarea tolera esperar por su respuesta en vez de necesitarla en el mismo turno, ¿de verdad necesitaba ser un agente? La autonomía interactiva (decidir, invocar una herramienta, observar el resultado y decidir de nuevo, todo en la misma conversación) es exactamente lo que un flujo por lotes no ofrece. Si el trabajo se deja encolar y esperar sin perder nada, probablemente nunca fue una conversación: era un conjunto de tareas independientes con forma de loop.
Esa es la pregunta que ya se planteó en el post anterior: si el problema pedía un agente. El batch no compite con las otras cuatro tácticas de reducir costo, es el caso frontera que devuelve esa pregunta a la mesa. Cuando aplica, es la reducción de precio más grande de esta lista sin tocar una sola línea de arquitectura. Cuando no aplica, porque la tarea de verdad necesita reaccionar a lo que devuelve cada herramienta antes de decidir el siguiente paso, no hay nada que optimizar ahí: el ahorro hay que buscarlo en las cuatro que quedan.

Táctica 6: instrumentación

La Táctica 1 ya asomó la idea sin nombrarla: la única forma de confirmar que el caché funciona es mirar usage.cache_read_input_tokens después de cada llamada. Esa costumbre generaliza a las otras cuatro. ¿El enrutamiento por modelo de verdad está mandando los subpasos baratos al modelo barato? ¿La poda de contexto está reduciendo el historial o solo desplazando el problema una vuelta más adelante? ¿El presupuesto de turnos se respeta, o el agente sigue corriendo quince vueltas cuando el presupuesto decía diez? Sin instrumentación, esas preguntas se responden a ojo, y las cinco tácticas anteriores quedan reducidas a una apuesta a ciegas.

Trazas por llamada, no por sesión

Langfuse instrumenta cada llamada individual, no la sesión completa como una caja negra. El SDK de Python, en su versión actual (v4), se inicializa con get_client(). Para capturar automáticamente el uso de tokens de cada llamada a Anthropic, se combina con un instrumentador de OpenTelemetry para el SDK de Anthropic (AnthropicInstrumentor), que intercepta cada client.messages.create y lo reenvía como un span con su consumo de tokens incluido. El decorador @observe() agrupa esos spans bajo la función que los originó, así que una vuelta del loop queda registrada como una traza propia, no mezclada con el resto de la sesión.
langfuse_instrumentation.py
from langfuse import get_client, observe
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
from anthropic import Anthropic

langfuse = get_client()
AnthropicInstrumentor().instrument()
client = Anthropic()

@observe()
def agent_step(prompt: str):
    message = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1000,
        messages=[{"role": "user", "content": prompt}],
    )
    return message

Costo por request, sin calculadora

El costo por request no hay que calcularlo a mano: Langfuse lo calcula automáticamente al ingerir cada generación, siempre que haya uso de tokens registrado y exista una definición de precio para el modelo (trae precios ya cargados para los modelos más comunes de Anthropic, OpenAI y Google). Cuando la respuesta del modelo ya incluye el costo, Langfuse prefiere ingerir ese valor directo; si no, lo infiere a partir del conteo de tokens contra su propia tabla de precios. Para Claude en particular, las trazas capturan también cache_read_input_tokens junto a los tokens de entrada, salida y latencia: la misma métrica que verificó el caching en la Táctica 1 queda disponible por request, sin que haya que leerla a mano de cada respuesta.

Qué tan lejos llega la detección de loops caros

Sobre detectar un loop caro en el momento en que ocurre, lo verificable es más modesto que una función con nombre propio para eso: Langfuse agrega el costo por traza, por sesión y por usuario, y esa agregación se puede vigilar para alertar cuando una métrica (el costo promedio por traza, por ejemplo) se sale de rango. No es un botón de "detectar loop caro" que se invoque como tal, es la consecuencia de tener el costo de cada request ya trazado y poder ponerle un umbral encima. La diferencia importa: la instrumentación no adivina qué loop se va a disparar, da los datos para notarlo antes de que la factura del mes lo haga en tu lugar.
Un detalle que vale la pena marcar aparte: si algún tutorial que encuentres muestra decoradores distintos a estos o un cliente inicializado de otra forma, es probable que esté describiendo una versión anterior del SDK. Langfuse pasó por más de un cambio de versión mayor, y la sintaxis de instrumentación no es la misma de una versión a la siguiente.

El orden que rinde más

Seis tácticas sin un orden son una lista de tareas. Con un orden, son un plan. La tesis de este post ordena las seis solas: el contexto reenviado es la línea dominante de la factura, así que las tácticas que atacan esa línea van primero, y las que atacan otra cosa van después, sin importar cuánto ahorren por separado.
Antes de la lista, un matiz que no es un séptimo paso: la instrumentación (Táctica 6) no ataca ninguna línea de la factura, la mide. Debería estar corriendo antes de tocar cualquiera de las otras cinco, no después, porque sin ella no hay forma de confirmar que el caching funcionó, que el enrutamiento mandó el subpaso correcto al modelo correcto, o que el presupuesto de turnos se respetó de verdad. Instrumentar primero, optimizar después.
Con eso resuelto, el orden de las cinco que sí compiten por esfuerzo y retorno:
  1. Prompt caching (Táctica 1). Ataca la línea dominante de forma directa: convierte reenviar el mismo prefijo de precio completo a precio de lectura. Es la de menor esfuerzo (agregar unos breakpoints de cache_control) y la de mayor retorno, siempre que se evite la lista de invalidadores silenciosos.
  2. Presupuesto de turnos (Táctica 4). La aritmética de la vuelta N mostró que el reenvío crece con el cuadrado de los pasos, no con los pasos. Reducir N ataca ese exponente directamente: cada vuelta que no ocurre es contexto que nunca se reenvía. Esfuerzo bajo a medio: es diseño del prompt, no infraestructura nueva.
  3. Poda de contexto (Táctica 3). Ataca el tamaño de lo reenviado en cada vuelta, no su cantidad. Va después de las dos anteriores porque su implementación tiene que negociar con el caché que la Táctica 1 ya puso en marcha: podar sin criterio invalida el prefijo que tanto costó cachear.
  4. Enrutamiento por modelo (Táctica 2). Ataca el precio por token de los subpasos, no el volumen reenviado en el loop principal. Requiere separar el subpaso en un subagente con su propio contexto, así que el esfuerzo es mayor que el de las tres anteriores.
  5. Batch (Táctica 5). No es una optimización que se aplique, es una pregunta de encaje: revisa si la tarea lo tolera, y si lo tolera, pregúntate si en verdad necesitabas un agente. El esfuerzo real de esta táctica no está en el código, está en esa pregunta.
TácticaQué línea de la factura atacaEsfuerzo de implementación
1. Prompt cachingReenvío del prefijo: tools, system e historialBajo
2. Enrutamiento por modeloPrecio por token de los subpasosMedio a alto: requiere subagente
3. Poda de contextoTamaño del historial reenviadoMedio: tensión directa con el caché
4. Presupuesto de turnosNúmero de vueltas NBajo a medio
5. BatchPrecio base completo (-50%)Bajo si la tarea aplica; el filtro de aplicabilidad es el trabajo real
6. InstrumentaciónNinguna línea directamente: mide si las otras cinco funcionanBajo a medio
Ninguna de las seis tácticas cambia lo que el agente hace. Cambian cuánto cuesta que lo siga haciendo. Pagar por vuelta es una factura que se explica sola; pagar por lo que insistes en reenviar en cada vuelta sin darte cuenta es la que de verdad sorprende. Las seis tácticas son distintas formas de dejar de pagar esa segunda factura.
Todos los artículos