El movimiento instintivo cuando una factura de LLM se vuelve incómoda es cambiar el modelo por uno más barato y dar el asunto por terminado. A veces eso está genuinamente bien. Con frecuencia no lo está, y la forma en que no lo está es insidiosa: el agente sigue respondiendo, la demo sigue luciendo bien, y tres semanas después alguien nota que la precisión de clasificación bajó silenciosamente cuatro puntos, o que el agente empezó a fallar en un edge case que antes manejaba. Nadie conecta esa regresión con el cambio de modelo porque nada crasheó — simplemente empeoró de una manera que no aparece a menos que estés midiendo específicamente para eso.
El problema real de la optimización de costos de LLM no es encontrar técnicas que ahorren dinero — model routing, prompt caching, context trimming y semantic caching todas lo hacen confiablemente. Es que cada una de ellas es una palanca que intercambia una cantidad de dinero por un riesgo de calidad, y la mayoría de los equipos mueven la palanca sin construir nunca el test que les diría cuánta calidad sacrificaron realmente. Este artículo cubre cuatro técnicas concretas y, para cada una, el regression test específico que necesita antes de acercarse al tráfico de producción.
Aprenderás:
- Por qué “solo usar un modelo más barato” falla silenciosamente en lugar de con ruido, y qué medir en su lugar
- Cómo construir un model router que envía llamadas fáciles a un modelo barato sin adivinar
- Cómo funciona el prompt caching mecánicamente y por qué la estabilidad del prefijo es todo el juego
- Una estrategia de context trimming que no borra silenciosamente el dato que el agente necesitó tres turnos después
- Cuándo el semantic caching vale el riesgo de staleness, y cuándo envenena silenciosamente las respuestas
- Una metodología concreta y reproducible para probar cada técnica antes de hacer el deploy
- Cómo combinar las cuatro en un solo call path consciente del costo sin apilar sus riesgos
Tabla de contenidos
- Por qué “solo degradar el modelo” falla silenciosamente
- Model Routing
- Prompt Caching
- Context Trimming
- Semantic Caching
- Una metodología de testing reproducible
- Poniéndolo todo junto: Un call path consciente del costo
- De un vistazo
- Conclusión
Por qué “solo degradar el modelo” falla silenciosamente
Un cambio de modelo no lanza una excepción cuando hace que tu agente empeore. Tu latencia incluso podría mejorar. El fallo aparece después, y usualmente de forma indirecta — un ticket de soporte por una respuesta incorrecta, una tasa de escalación ligeramente más alta que nadie está siguiendo lo suficientemente de cerca como para notarla durante semanas, un pipeline de structured-extraction que empieza a descartar silenciosamente un campo opcional que un modelo más pequeño rellena con menos confiabilidad. Nada de eso dispara una alerta, porque en un setup típico nada está mirando si “cambió la calidad”, solo si “el servicio siguió funcionando.”
Ese es el argumento real para construir un regression test antes de tocar los costos, no después: el costo es trivialmente observable (es un número en una factura) y la calidad no lo es, y optimizar lo que puedes ver mientras ignoras lo que no puedes es exactamente cómo un cambio de reducción de costos se convierte silenciosamente en una regresión del producto. Cada técnica de abajo ahorra de una manera que es fácil de verificar y arriesga calidad de una manera que no lo es — a menos que construyas el harness para ello de antemano. Si el agente que estás optimizando es basado en grafos con lógica de branching en lugar de una sola llamada, la distinción entre una chain simple y algo que necesita LangGraph vale la pena tenerla resuelta primero, ya que dónde insertas el routing y el caching difiere bastante entre los dos.
Model Routing
No todas las llamadas a un agente necesitan tu modelo más capaz. Un paso de clasificación que decide “¿es esta una pregunta de facturación o técnica?” y un paso redactando una respuesta matizada a un cliente enojado no tienen la misma dificultad, y pagar precios de modelo frontier por ambos es gastar dinero en el fácil sin razón.
El mecanismo que realmente funciona es un clasificador liviano — a menudo un modelo pequeño en sí mismo — que puntúa la dificultad de la tarea y enruta en consecuencia, en lugar de un mapeo fijo por endpoint que tiene que mantenerse manualmente a medida que cambia tu producto.
import anthropic
client = anthropic.Anthropic()
def classify_difficulty(query: str) -> str:
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=10,
messages=[{
"role": "user",
"content": f"Classify this support query as 'simple' or 'complex'. "
f"Reply with one word only.\n\nQuery: {query}",
}],
)
return response.content[0].text.strip().lower()
def route_and_answer(query: str) -> str:
difficulty = classify_difficulty(query)
model = "claude-sonnet-5" if difficulty == "simple" else "claude-opus-5"
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": query}],
)
return response.content[0].text
La llamada al clasificador en sí cuesta algo, entonces el routing solo vale la pena cuando los ahorros ponderados por volumen de degradar una porción significativa del tráfico superan el overhead de clasificación — en la práctica esto es casi siempre el caso una vez que estás más allá de un número pequeño de llamadas diarias, ya que el clasificador es una llamada pequeña, rápida y barata comparada con la generación que está controlando. Al momento de escribir esto, Claude Haiku 4.5 corre alrededor de $1 por millón de tokens de entrada frente a los aproximadamente $3 de Claude Sonnet 5, así que un prompt de clasificación de unos pocos cientos de tokens es un error de redondeo al lado de lo que ahorras al no enviar consultas fáciles a un modelo más caro — pero trata esas cifras como ilustrativas, no como una promesa, ya que los precios de lista cambian y tus propias cantidades de tokens diferirán.
El modo de fallo es un router que está equivocado en la dirección cara — enrutando algo genuinamente complejo al tier barato y obteniendo una respuesta peor que nadie nota porque la respuesta todavía parece plausible. Es exactamente por eso que el routing necesita el harness de regresión en la sección de testing de abajo, no solo una verificación puntual de algunos ejemplos que resultaron enrutarse correctamente.
Prompt Caching
El prompt caching es lo más cercano a una ganancia gratis en esta lista, porque no toca la elección de modelo ni recorta nada — simplemente evita volver a pagar por tokens que estás enviando sin cambios, una y otra vez, entre requests. El prompt caching de Claude funciona por coincidencia de prefijo: marca un punto en tu request con un cache breakpoint, y si un request posterior comparte ese prefijo exacto, la porción cacheada se factura con un gran descuento en lugar del precio completo de input.
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[{
"type": "text",
"text": LONG_SYSTEM_PROMPT_WITH_TOOL_DEFINITIONS,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": user_query}],
)
print(response.usage.cache_read_input_tokens) # tokens served from cache, billed at a discount
El detalle mecánico que determina si esto realmente funciona es la estabilidad del prefijo. El caching es una coincidencia de prefijo byte a byte — un solo carácter cambiado en cualquier lugar antes del cache breakpoint invalida todo lo que sigue. La forma más común en que los equipos accidentalmente anulan su propio caching es poner algo volátil antes del breakpoint: una marca de tiempo interpolada en el system prompt, un dict sin ordenar serializado diferente entre requests, un ID de request horneado en instrucciones que debían haber permanecido genéricas. response.usage.cache_read_input_tokens es el número a vigilar — si es consistentemente cero entre requests que deberían estar llegando al cache, algo upstream está variando silenciosamente el prefijo.
El riesgo de calidad aquí es genuinamente bajo comparado con las otras tres técnicas, porque el caching no cambia lo que ve el modelo — cambia cómo se factura ese contenido sin cambios. El test que igual quieres es una verificación de que las respuestas con cache-hit y cache-miss son conductualmente idénticas para la misma entrada, lo que principalmente sirve para detectar bugs en tu propio setup de caching (un breakpoint colocado en el lugar incorrecto, el orden de contenido que difiere entre “tools luego system” versus “system luego tools”) en lugar de cualquier riesgo inherente en la técnica.
Context Trimming
El contexto de un agente crece cada turno — más mensajes, más resultados de tools, más historial — y cada uno de esos tokens se reenvía y se vuelve a facturar en cada llamada posterior a menos que algo lo pode activamente. El context trimming corta ese crecimiento, pero es la técnica más probable de romper algo sutil, porque el token que recortas podría ser el único dato que un turno posterior realmente necesitaba.
Una estrategia de trimming defendible mantiene los turnos recientes verbatim y resume o descarta los más viejos, en lugar de hacer un trimming uniforme o solo por conteo raw de tokens:
def trim_context(messages: list[dict], keep_recent: int = 6) -> list[dict]:
if len(messages) <= keep_recent:
return messages
older, recent = messages[:-keep_recent], messages[-keep_recent:]
summary = summarize_messages(older) # a cheap model call, or a rule-based extractor
return [{"role": "user", "content": f"[Earlier conversation summary]: {summary}"}, *recent]
El modo de fallo contra el que diseñar específicamente es perder un dato que solo importa varios turnos después — una restricción que el usuario declaró temprano (“soy alérgico a los mariscos”, “el presupuesto es máximo $500”) que un trimming ingenuo basado en recencia descarta silenciosamente una vez que envejece fuera de la ventana “recent”. Un paso de resumen que está explícitamente instruido para preservar restricciones, decisiones y datos declarados (no solo el hilo narrativo) es significativamente más seguro que un corte duro de conteo de tokens, y vale la pena escribir esa instrucción explícitamente en lugar de confiar en un prompt genérico de “resume esto” para priorizar los detalles correctos.
Hacer trimming de los resultados de tools merece tratamiento separado del trimming de los turnos de conversación — un payload JSON grande de una llamada de tool anterior a menudo es seguro de comprimir o descartar una vez que el modelo ya lo usó para responder, ya que son datos crudos en lugar de un dato declarado, mientras que la conclusión propia del modelo extraída de esos datos usualmente vale la pena conservar.
Semantic Caching
El semantic caching omite la llamada al modelo completamente cuando una nueva consulta se juzga suficientemente similar a una que ya respondiste — típicamente mediante similitud de embedding contra un cache de pares de consulta/respuesta anteriores, en lugar del caching de coincidencia exacta que daría un simple cache clave-valor.
def semantic_cache_lookup(query: str, threshold: float = 0.92) -> str | None:
query_embedding = embed(query)
best_match, best_score = None, 0.0
for cached_query, cached_response, cached_embedding in cache_store:
score = cosine_similarity(query_embedding, cached_embedding)
if score > best_score:
best_match, best_score = cached_response, score
return best_match if best_score >= threshold else None
def answer_with_semantic_cache(query: str) -> str:
cached = semantic_cache_lookup(query)
if cached is not None:
return cached
response = client.messages.create(
model="claude-sonnet-5", max_tokens=1024,
messages=[{"role": "user", "content": query}],
)
answer = response.content[0].text
cache_store.append((query, answer, embed(query)))
return answer
Esta es la técnica más riesgosa de esta lista, y vale la pena ser directo sobre por qué: la premisa completa es responder una nueva pregunta con una respuesta generada para una pregunta diferente que simplemente puntuó alto en similitud de embedding. Dos consultas pueden ser semánticamente cercanas y aún tener una respuesta correcta materialmente diferente — “¿cuál es tu política de devolución para electrónicos?” y “¿cuál es tu política de devolución para electrónicos comprados durante la venta de temporada?” se embeddearán juntas y pueden tener respuestas genuinamente diferentes. Un cache con un umbral de similitud demasiado bajo no falla en voz alta; sirve respuestas confidentemente incorrectas que se leen como respuestas completamente normales.
El semantic caching gana su riesgo en situaciones estrechas: alto volumen de consultas con repetición real (un bot de soporte estilo FAQ, no un agente de coding abierto), contenido donde el staleness es aceptable o los hechos subyacentes cambian lentamente, y — de manera crítica — un umbral que realmente has calibrado contra ejemplos etiquetados en lugar de un valor que elegiste porque “se sentía suficientemente estricto.” En cualquier lugar donde la redacción exacta cambia la respuesta correcta, esta técnica no debería estar cerca del call path.
Una metodología de testing reproducible
Cada técnica de arriba necesita lo mismo básico antes de acercarse al tráfico de producción: un conjunto de evaluación fijo, una métrica de calidad que no sea una corazonada, y una comparación antes/después ejecutada contra ambos. Nada de esto requiere nada exótico — requiere hacerlo realmente, que es el paso que la mayoría de los cambios de reducción de costos se salta.
Construye un conjunto de tests fijo a partir del tráfico real. Extrae 100–300 consultas reales que tu sistema haya manejado (anonimizadas según sea necesario), cubriendo el rango de dificultad que ves en producción — no solo los casos fáciles que hacen que cualquier técnica parezca segura. Si aún no tienes tráfico de producción, escribe a mano casos que específicamente sondeen los modos de fallo descritos arriba: una conversación con múltiples restricciones para context trimming, un par de consultas casi-duplicadas-pero-con-diferente-respuesta para semantic caching, una mezcla de requests genuinamente fáciles y genuinamente difíciles para model routing.
Puntúa con algo reproducible, no un escaneo humano único. Para tareas con una respuesta verificable (clasificación, extracción, decisiones de routing), la exactitud de coincidencia exacta o a nivel de campo contra un conjunto etiquetado es directa y barata de calcular. Para la generación abierta, un LLM-como-juez que compare la respuesta baseline contra la optimizada en una rúbrica fija (corrección, completitud, tono) es un sustituto razonable de la revisión humana — pero valida el juez mismo contra una pequeña muestra etiquetada por humanos primero, ya que un modelo juez con sus propios puntos ciegos puede aprobar con sello de goma una regresión que comparte.
Ejecuta baseline y optimizado en paralelo, no secuencialmente. Compara exactamente el mismo conjunto de tests contra el pipeline antes y después del cambio en la misma ejecución, no “la calidad promedio del mes pasado” contra “la de este mes” — demasiadas otras cosas cambian entre esas dos ventanas para atribuir un cambio a tu cambio específico con confianza.
Establece un umbral concreto antes de mirar los resultados, no después. Decide de antemano qué caída de calidad es aceptable para qué ahorros — solo para ilustración, un equipo podría decidir que una caída de 2 puntos en precisión es aceptable para una reducción de costos del 40% y una caída de 10 puntos no lo es, sean cuales sean los números reales para su propio sistema. Decidir el umbral después de ver el número es cómo una regresión marginal se racionaliza como “aceptable” después del hecho.
Vuelve a ejecutar el mismo conjunto de tests después de cualquier cambio de prompt, modelo o umbral. Los umbrales de model routing, los cortes de similitud de cache y los tamaños de ventana de trimming todos se deslizan a medida que un producto evoluciona — el regression test no es un gate de una sola vez, es algo que vuelves a ejecutar cada vez que se mueve una de estas perillas.
Poniéndolo todo junto: Un call path consciente del costo
Las cuatro técnicas se componen, pero cada una que apilas multiplica el número de lugares donde podría estar escondida una regresión silenciosa, que es exactamente por qué la metodología de testing de arriba necesita ejecutarse contra el pipeline combinado, no solo cada técnica de forma aislada.
def answer_query(query: str, history: list[dict]) -> str:
cached = semantic_cache_lookup(query)
if cached is not None:
return cached
trimmed_history = trim_context(history)
difficulty = classify_difficulty(query)
model = "claude-sonnet-5" if difficulty == "simple" else "claude-opus-5"
response = client.messages.create(
model=model,
max_tokens=1024,
system=[{
"type": "text",
"text": STABLE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}],
messages=[*trimmed_history, {"role": "user", "content": query}],
)
answer = response.content[0].text
cache_store.append((query, answer, embed(query)))
return answer
El orden importa: verificación del semantic cache primero (la salida más barata posible), luego recortar el contexto antes del routing para que el clasificador de dificultad no puntúe contra un historial inflado, luego prompt caching en el system prompt estable independientemente de qué tier de modelo se seleccione. Cada capa es un ahorro real e independiente — pero cada una es también un lugar real e independiente donde un bug o un umbral demasiado laxo puede costarte calidad silenciosamente, que es exactamente el caso para ejecutar tu regression set contra este path completo, no cuatro piezas separadas sin probar ensambladas y asumidas como seguras para combinarse.
De un vistazo
| Técnica | En qué ahorra | Riesgo de calidad primario | Dónde encaja mejor |
|---|---|---|---|
| Model routing | Tokens de output/input facturados a una tasa por token más baja | Consultas difíciles enrutadas a un modelo más débil | Alto volumen de llamadas con una distribución real de dificultad |
| Prompt caching | Refacturación de tokens de prefijo sin cambios | Bajo — principalmente bugs de implementación (cache hits rotos) | Cualquier agente con un system prompt o lista de tools estable |
| Context trimming | Tokens reenviados cada turno a medida que crece el historial | Perder un dato declarado temprano que importa después | Conversaciones largas, agentes multi-turno |
| Semantic caching | Omitir la llamada al modelo completamente | Servir una respuesta incorrecta para una consulta similar-pero-diferente | Cargas de trabajo de alta repetición y baja tolerancia al staleness |
Leer la tabla de izquierda a derecha rastrea aproximadamente el potencial de ahorro y el riesgo — el semantic caching puede eliminar el costo de una llamada completamente, que es también exactamente por qué un hit malo es una respuesta incorrecta, no solo una peor. El prompt caching está en el otro extremo: ahorros garantizados comparativamente pequeños en contenido estable, con casi ninguna manera de servir accidentalmente una respuesta incorrecta, porque nunca cambia lo que el modelo realmente ve.
Una palanca más que vale la pena mencionar aunque no sea una de las cuatro técnicas principales: la Batch API, para todo lo que no sea sensible a la latencia. Los requests batch típicamente tienen un precio bien por debajo de la tarifa síncrona estándar a cambio del procesamiento asíncrono — una buena opción para trabajos de resumen nocturno, clasificación masiva, o cualquier pipeline donde “la respuesta aparece en unas pocas horas” es un intercambio aceptable por una factura sustancialmente más baja. No se compone con las otras cuatro de la manera en que estas se componen entre sí — o envías un request síncronamente a través del path consciente del costo de arriba o asíncronamente a través del batch — pero para el subconjunto de tu carga de trabajo que puede tolerar el retraso, a menudo es el mayor ahorro único disponible antes de tocar la elección de modelo, caching o trimming.
Conclusión
Ninguna de estas cuatro técnicas es ingeniería cuestionable — model routing, prompt caching, context trimming y semantic caching son todas formas estándar y bien entendidas de bajar la factura de un LLM, y la mayoría de los sistemas en producción deberían estar usando al menos las primeras dos. La parte que es opcional no es la técnica, es si construyes el regression test antes o después de descubrir que importaba. El costo es el número fácil de vigilar porque está ahí mismo en la factura; la calidad solo aparece si construyes algo que la mida a propósito, contra un conjunto de tests fijo, antes de que el cambio llegue a producción y cada vez que se mueva una de las perillas de ajuste. Haz eso, y reducir costos deja de ser una apuesta contra la calidad de tu propio producto y se convierte en un cambio de ingeniería como cualquier otro — medido, reversible, y aburrido de la mejor manera.

