Optimización de costos para sistemas de LLM: ¿Dónde se va realmente el dinero?

Gasta tokens donde realmente importan.

Índice

Los costos de los LLMs escalan de manera lineal con el uso. Un sistema que procesa 10.000 solicitudes al día a $0.01 por solicitud cuesta $100 diarios — $365 al año. A escala empresarial, eso es más de $10.000.

La optimización de costos no se trata de hacer recortes. Se trata de gastar tokens donde realmente importan.

Cada token que desperdicias es un token que podrías haber gastado en una mejor respuesta.

Estrategias de optimización de costos de LLM

Presupuesto de tokens

La forma más sencilla de controlar los costos es establecer límites. Por sesión, por tarea o por día.

Estrategia 1: Presupuestos por sesión

Los presupuestos por sesión son sencillos:

class SessionBudget:
    def __init__(self, budget_tokens: int = 10000):
        self.budget = budget_tokens
        self.used = 0

    def allocate(self, tokens: int) -> bool:
        if self.used + tokens <= self.budget:
            self.used += tokens
            return True
        return False

    def remaining(self) -> int:
        return self.budget - self.used

Estrategia 2: Presupuestos por tarea

Los presupuestos por tarea son más útiles. Diferentes tareas necesitan diferentes cantidades de contexto:

task_budgets:
  classify:
    max_tokens: 100
    model: qwen2.5-1.5b
  summarize:
    max_tokens: 500
    model: qwen2.5-7b
  code_review:
    max_tokens: 2000
    model: qwen2.5-coder-7b
  reason:
    max_tokens: 4000
    model: qwen2.5-32b

Estrategia 3: Presupuestos adaptativos

Los presupuestos adaptativos se ajustan según lo que realmente ocurre. Si las tareas de clasificación usan consistentemente 80 tokens, deja de asignar 100:

class AdaptiveBudget:
    def __init__(self):
        self.task_history = {}

    def allocate(self, task_type: str) -> int:
        if task_type in self.task_history:
            return int(self.task_history[task_type] * 1.5)
        return 1000

    def record(self, task_type: str, tokens_used: int):
        if task_type not in self.task_history:
            self.task_history[task_type] = tokens_used
        else:
            self.task_history[task_type] = (
                0.9 * self.task_history[task_type] + 0.1 * tokens_used
            )

El promedio móvil exponencial (peso de 0.9) significa que el uso reciente importa más que el historial. Ajusta el peso según qué tan volátiles sean tus cargas de trabajo.

API vs inferencia local

La inferencia local es más barata a gran escala. El punto de equilibrio depende de tu hardware y de las tarifas de la API.

Modelo API ($/M tokens) Costo local/hora Punto de equilibrio
GPT-4o $2.50 / $10.00 N/A
Claude Sonnet 4 $3.00 / $15.00 N/A
Qwen2.5-72B $0.50 / $2.00 ~$0.50 ~4 horas/día
Qwen2.5-32B $0.30 / $1.20 ~$0.20 ~2 horas/día
Qwen2.5-7B $0.10 / $0.40 ~$0.05 ~1 hora/día

La matemática del hardware:

Hardware Inversión inicial Electricidad mensual Punto de equilibrio vs API
RTX 3090 (usada) $600 $15 ~4 meses
RTX 4090 $1,500 $20 ~6 meses
RTX 5080 $1,000 $18 ~5 meses
DGX Spark $2,000 $30 ~8 meses

Con un uso moderado — una hora o más al día — la inferencia local se paga sola. Con un uso alto, los ahorros son dramáticos. El inconveniente es el capital inicial. Una RTX 5080 cuesta $1.000. Una factura de API puedes pausarla. El hardware no.

Estrategias de respaldo

Cuando tu modelo preferido es demasiado caro o demasiado lento, utiliza un respaldo más económico. La clave es saber cuándo la calidad es “suficientemente buena”.

Estrategia 1: Respaldo basado en calidad

El respaldo basado en calidad prueba modelos hasta que la salida cumpla un umbral:

class QualityFallback:
    def __init__(self, quality_threshold: float = 0.8):
        self.threshold = quality_threshold
        self.models = [
            {"model": "claude-sonnet-4", "cost": 0.015},
            {"model": "qwen2.5-72b", "cost": 0.002},
            {"model": "qwen2.5-32b", "cost": 0.001},
            {"model": "qwen2.5-7b", "cost": 0.0004},
        ]

    def route(self, prompt: str) -> str:
        for model_config in self.models:
            result = self.call_model(model_config["model"], prompt)
            if self.evaluate_quality(result) >= self.threshold:
                return result
        return self.call_model(self.models[0]["model"], prompt)

El problema es la evaluación en sí. ¿Cómo mides la calidad sin llamar a otro modelo? Algunos sistemas usan un clasificador pequeño. Otros usan verificaciones heurísticas: longitud, estructura, presencia de palabras clave. Ninguna de estas es perfecta.

Estrategia 2: Respaldo basado en latencia

El respaldo basado en latencia es más simple. Deriva al modelo más rápido que cumpla tu presupuesto de tiempo:

class LatencyFallback:
    def __init__(self, max_latency: float = 5.0):
        self.max_latency = max_latency
        self.models = [
            {"model": "qwen2.5-1.5b", "latency": 0.5},
            {"model": "qwen2.5-7b", "latency": 2.0},
            {"model": "qwen2.5-32b", "latency": 10.0},
            {"model": "claude-sonnet-4", "latency": 5.0},
        ]

    def route(self, prompt: str) -> str:
        for model_config in sorted(self.models, key=lambda x: x["latency"]):
            if model_config["latency"] <= self.max_latency:
                return self.call_model(model_config["model"], prompt)
        return self.call_model(self.models[0]["model"], prompt)

Caché

La caché es la optimización de costos más infravalorada. Los prompts idénticos ocurren más a menudo de lo que piensas: solicitudes de clasificación, consultas estilo FAQ, llamadas repetidas a herramientas.

Estrategia 1: Caché de prompts

La caché exacta de prompts es simple:

import hashlib

class PromptCache:
    def __init__(self, max_size: int = 1000):
        self.cache = {}
        self.max_size = max_size

    def get(self, prompt: str) -> str | None:
        key = hashlib.sha256(prompt.encode()).hexdigest()
        return self.cache.get(key)

    def set(self, prompt: str, response: str):
        key = hashlib.sha256(prompt.encode()).hexdigest()
        if len(self.cache) >= self.max_size:
            self.cache.pop(next(iter(self.cache)))
        self.cache[key] = response

Estrategia 2: Caché semántica

La caché semántica es más útil. Captura prompts que son diferentes pero significan lo mismo:

from sentence_transformers import SentenceTransformer

class SemanticCache:
    def __init__(self, similarity_threshold: float = 0.95):
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.cache = {}
        self.threshold = similarity_threshold

    def get(self, prompt: str) -> str | None:
        prompt_embedding = self.model.encode([prompt])[0]
        for cached_prompt, cached_response in self.cache.items():
            cached_embedding = self.model.encode([cached_prompt])[0]
            similarity = self.cosine_similarity(
                prompt_embedding, cached_embedding
            )
            if similarity >= self.threshold:
                return cached_response
        return None

    def set(self, prompt: str, response: str):
        self.cache[prompt] = response

El umbral importa. 0.95 es agresivo: solo los prompts muy similares coinciden. 0.85 es más indulgente pero corre el riesgo de devolver respuestas incorrectas. Mide tu tasa de fallos y ajusta.

También vale la pena cachear respuestas para consultas comunes. Si los usuarios preguntan “¿cuál es el clima?” o “¿qué hora es?” repetidamente, cachea el patrón, no solo el prompt exacto:

class ResponseCache:
    def __init__(self):
        self.common_queries = {
            "what is the weather": "Check weather API",
            "what is the time": "Check system time",
            "who is the president": "Check current president",
        }

    def get(self, query: str) -> str | None:
        query_lower = query.lower()
        for common_query, response in self.common_queries.items():
            if common_query in query_lower:
                return response
        return None

Esto no es sofisticado, pero funciona. Las consultas comunes son comunes por una razón.

Cuándo ayuda la optimización

La optimización importa cuando estás procesando altos volúmenes, ejecutando cargas de trabajo mixtas o pagando costos de API que se acumulan.

No importa cuando estás prototipando, usando un solo modelo o procesando bajos volúmenes. La complejidad del presupuesto, el respaldo y la caché no vale la pena para un sistema que hace 100 solicitudes al día.

Primero haz que el flujo básico funcione. Añade optimización cuando llegue la factura.

Compromisos

Estrategia Costo Calidad Complejidad
Sin optimización Más alto Consistente Más baja
Presupuesto de tokens Moderado Variable Media
Modelos de respaldo Bajo-Medio Variable Media
Caché Más bajo Alta (para aciertos de caché) Media
Híbrido Optimizado Optimizado Más alta

Los sistemas de producción suelen ejecutar híbridos. Presupuesto por sesión, respaldo basado en calidad o latencia, caché de lo que puedas. La complejidad es real, pero los ahorros también.

Relacionado

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.