LLMs Intermedio 19 min de lectura

Costos de LLMs en juegos: tokens, caching y modelos

Prompt caching, model routing, sliding window, summary rolling. Las palancas reales para que un NPC con LLM no quiebre tu unit economics.

Publicado: · Por Juanjo "Banyo" López

0. Introducción

Tu juego cobra 20 USD una vez. El jugador habla con NPCs movidos por LLM y, sin que nadie lo mida, cada turno cuesta dinero real en la factura de OpenAI o Anthropic. Si el jugador promedio hace 200 turnos a un modelo medio sin caching, multiplica: 200 × 0.005 USD ≈ 1 USD por jugador. Acabas de regalar el 5% del precio de venta a tu proveedor de inferencia. Multiplica por 50 000 ventas y el número duele.

La solución no es dejar de usar LLMs. Es usarlos bien: elegir el modelo más barato que aún funcione, mandarle el menor input posible reutilizando cache, y limitar el output a lo justo. Cada decisión arquitectónica es una palanca de costo.

Este tutorial cubre las palancas. Es la continuación natural de NPCs con LLM: arquitectura básica; si no leíste ese, empieza por ahí.

En este tutorial vas a ver:

  • Cómo se factura un LLM (tokens de entrada, de salida, cache) y cómo estimar costo antes de mandar.
  • Cuatro palancas concretas: prompt caching, model routing, sliding window con summary rolling y brevedad.
  • Cómo medir costo en runtime y cuándo Ollama local sí baja la factura (y cuándo no).

1. Demo

Anatomía de un turno: tokens y costo Cada turno suma input (system + history) y output. El cache reduce el costo del prefijo repetido. Compara con y sin caching.

2. Concepto y arquitectura

Optimizar el costo de un LLM en un juego es un problema de tres dimensiones: qué modelo usas, cuánto input le mandas y cuánto output dejas que produzca. Cada llamada se factura en tokens y cada token tiene un precio distinto según el modelo y según sea de entrada o de salida. La optimización es elegir bien las tres cosas, no una.

2.1 ¿Qué es un token y por qué pagas por él?

Un token es la unidad mínima de texto que el modelo procesa. No es una palabra ni un caracter: es un trozo definido por el tokenizer del modelo. Como referencia: en inglés ~0.75 palabras por token, en español ~0.6 palabras por token (el español tokeniza un poco peor por las terminaciones).

Pagas dos cosas en cada llamada:

  • Input tokens: todo lo que mandas (system prompt + history + mensaje del jugador).
  • Output tokens: todo lo que el modelo genera.

El output suele ser 3 a 5 veces más caro que el input. Tiene sentido: generar token a token es más costoso que leerlos en bloque. Cuando intuyas dónde está tu gasto, recuerda esa asimetría.

Para estimar antes de mandar, los proveedores publican su tokenizer: tiktoken para OpenAI, el SDK de Anthropic tiene su contador. Una llamada a countTokens(prompt) es gratis y te dice cuánto costará la real.

2.2 La regla del 80/20 del costo

El primer impulso es optimizar la respuesta del modelo. Es el lugar equivocado. La pregunta del jugador típica son 20-50 tokens. La respuesta del NPC son 60-150 tokens. El system prompt y el history acumulado son 500-3000 tokens. El 80% de tu factura vive en lo que mandas, no en lo que el modelo devuelve.

Esto cambia todo:

  • Si el system prompt mide 2000 tokens, optimizarlo a 1200 te ahorra el 40% del input fijo. En cada turno. Para siempre.
  • Si recortas el history de 20 a 8 turnos con summary rolling, el ahorro se nota desde el turno 9.
  • Si reduces el output a la mitad con un “máximo 2 frases”, ahorras donde más caro es el token.

2.3 ¿Qué es prompt caching y cómo lo activas?

Prompt caching es el descuento que los proveedores ofrecen cuando dos llamadas comparten el mismo prefijo. La primera vez pagas el prefijo a precio completo y el servidor lo guarda preprocesado. Las siguientes llamadas reutilizan ese prefijo cacheado y te cobran solo entre el 10% y el 50% del precio original por esos tokens.

Implementación según proveedor:

  • OpenAI: caching implícito en modelos como gpt-4o. Si tu prefijo tiene al menos 1024 tokens y otra llamada lo reutiliza en pocos minutos, te aplica ~50% de descuento sobre los tokens cacheados. No tienes que activar nada, pero sí estructurar el prompt para que el prefijo sea estable.
  • Anthropic: caching explícito marcando bloques con cache_control: { type: "ephemeral" }. Descuento de hasta el 90% sobre los tokens cacheados. TTL típico de 5 minutos (se renueva con cada hit).
  • Google Gemini: también ofrece context caching con API dedicada.

Recomendación dura: pon en posición fija al inicio del prompt la identidad del NPC + lore base estático. Eso casi nunca cambia entre turnos del mismo NPC. El history dinámico va después. Así el cache hit es máximo.

Anatomía de un turno: zonas de costo
SYSTEM + LORE~60% del inputHISTORY~30%user~5%out~5%cacheable (paga 10-30% tras 1ª llamada)cambia lentoCosto relativo por zona:system/lore: barato si cacheado · history: medio · user: barato · output: 3-5× más caro/token

System + lore ocupan la mayor parte del input (cacheable). History rolling cambia despacio. User y output son la fracción pequeña pero cara.

Cache hit/miss visualmente:

Cache hit vs miss: dos pipelines
Turno 1 — MISS (paga todo)system 1500t (100%)userout$ plenoTurno 2+ — HIT (system cacheado)system 1500t · paga 10-30%historyuserout$ reducidoAhorro acumulado:~70% input ahorrado tras 2-3 turnos

Primera llamada paga el prefijo completo. Las siguientes reutilizan el system cacheado (segmento pulsante) y pagan solo 10-30% por esos tokens.

2.4 Model routing: no uses siempre el mismo modelo

Llamar al modelo más capaz para cada mensaje del jugador es el equivalente a contratar un cirujano para cortar pan. Una arquitectura básica de routing:

  • Chitchat casual (“hola”, “qué tal”, “lindo día”) → modelo barato (tier Haiku, GPT-mini). Ahorro: 20× respecto al top.
  • Diálogo importante (negociación, decisiones de quest) → modelo medio (tier Sonnet, GPT-4o). Calidad sólida, precio razonable.
  • Función crítica (clasificación de intent, validación de respuesta) → modelo barato + JSON mode. Es trabajo de etiquetar, no de redactar.
  • Cinemática IA o jefe único → modelo top, una vez por sesión. Caro pero memorable.

El primer paso en runtime suele ser clasificar la intención del mensaje con un modelo barato (50 tokens de input, 5 de output, costo despreciable) y enrutar al modelo apropiado. Un mix bien sintonizado ahorra 60-80% respecto a usar el modelo medio para todo.

Model routing: árbol decisivo
turno del jugadorclasificar intent (cheap)small talknormalcrítico / únicoCHEAPhaiku / mini · ~$0.0002MEDIUMsonnet / 4o · ~$0.005TOPopus / turbo · ~$0.04guardias, vendedorescompañero, mercader clavejefe único, cinemáticamix 70/25/5 → ahorra 60-80% vs medium-only

Cada turno se clasifica primero con el modelo barato y se enruta. El top solo se invoca para diálogo crítico o NPCs únicos.

2.5 ¿Cómo medir y monitorear costo en runtime?

Cada respuesta de la API trae un campo usage con la cuenta exacta de tokens. OpenAI devuelve usage.prompt_tokens, usage.completion_tokens, y usage.prompt_tokens_details.cached_tokens. Anthropic devuelve usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens.

Loguea eso por NPC y por turno. Un dashboard básico responde tres preguntas:

  1. ¿Qué NPC me cuesta más? (puede ser uno con system prompt enorme).
  2. ¿Cuál es mi cache hit rate? (debería estar > 70% tras los primeros turnos).
  3. ¿Cuánto cuesta un jugador promedio por sesión?

Sin métricas, optimizar es adivinar. Con un dashboard, el cuello de botella se ve en 30 segundos.

2.6 Sliding window + summary rolling: cuantificado

En el tutorial de arquitectura básica viste el concepto. Aquí va el costo:

  • History de 20 turnos sin recortar ≈ 3000 tokens. A precio medio: ~0.015 USD por turno solo de history.
  • Sliding window de 8 turnos ≈ 1200 tokens. ~0.006 USD por turno.
  • Sliding window de 8 + summary rolling cada 8 turnos ≈ 800 tokens constantes. ~0.004 USD por turno.

La estrategia limpia: cada 8 turnos, le pides al modelo barato que resuma los turnos antiguos en 100-150 tokens y reemplazas esos turnos por el resumen. El history se mantiene en tamaño estable indefinidamente. Conversaciones de 200 turnos cuestan lo mismo por turno que conversaciones de 20.

Summary rolling: history que no crece
history acumulándose →t1t1..t3t1..t6t1..t8 · umbral cruzadoresumir con modelo cheapSUMt7..t8SUMt7..t12Tamaño del history:crece linealmente↓ corteestable (sum + ventana corta)

El history se acumula turno tras turno. Al cruzar el umbral, los turnos antiguos se condensan en un resumen compacto generado por el modelo barato.

2.7 ¿Por qué la brevedad es la palanca más subestimada?

El output es 3-5× más caro que el input. Si en el system prompt escribes “Responde en máximo 2 frases, sin descripciones largas, directo al grano”, recortas el output 2-3× sin tocar nada más. En tokens caros.

Beneficios colaterales:

  • Menor latencia: el modelo genera menos tokens, llega antes la respuesta completa.
  • Mejor pacing del juego: parrafadas en una taberna rompen el ritmo. Un NPC que responde corto se siente más real, no menos.
  • Menos riesgo de alucinación: cuanto más se extiende el modelo, más probabilidad de inventar.

Pruébalo. Tirar 1 USD a output que el jugador iba a saltarse es el error más común y el más fácil de corregir.

2.8 ¿Cuándo Ollama local sí baja el costo?

Ollama y otros runners locales tienen costo cero por token. Es tentador asumir que es siempre la opción barata. No lo es. Depende de dónde corre el modelo:

  • En la máquina del jugador: costo por inferencia = 0 USD para ti. Pero los requerimientos de hardware suben (mínimo 8 GB de VRAM para modelos decentes), distribuyes pesos de varios GB, y el modelo local rinde peor que el medio cloud. Sirve para chitchat o NPCs de bajo perfil.
  • En tu servidor: pagas la GPU 24/7 estés sirviendo o no. Si tienes 10 usuarios concurrentes, una A10G alquilada (~0.75 USD/h) sale más cara que la API. Solo baja cuando llenas la GPU el 70%+ del tiempo.

Conclusión práctica: híbrido. Chitchat y NPCs secundarios al local del jugador (si su hardware lo permite). Diálogo importante a cloud con caching agresivo. Lo mejor de ambos mundos.

3. Pseudocódigo

function estimateCostPerTurn(systemTokens: Int, historyTokens: Int, outputTokens: Int, model: ModelPricing) -> Float
    # Asume system cacheado tras la primera vez; history fresco
    cachedInput = systemTokens * model.cachedDiscount
    freshInput  = historyTokens
    return (cachedInput + freshInput) * model.inputPricePerToken
         + outputTokens * model.outputPricePerToken

function routeToModel(intent: Intent, npcImportance: Importance) -> Model
    if intent == "small_talk" and npcImportance < 3
        return MODEL_CHEAP
    if intent == "critical_dialogue" or npcImportance > 7
        return MODEL_TOP
    return MODEL_MEDIUM

function maybeSummarizeHistory(history: List<Message>, threshold: Int = 8) -> List<Message>
    # Cuando el history pasa de 2× el umbral, resume la mitad antigua
    if history.length < threshold * 2
        return history
    toSummarize = history[0 : threshold]
    summary = llmCheap.summarize(toSummarize)
    return [systemMessage("Resumen previo: " + summary)] + history[threshold:]

function classifyIntent(userMessage: String) -> Intent
    # Llamada barata con JSON mode al modelo small
    return llmCheap.classify(userMessage, schema=IntentSchema)

Las cuatro funciones cubren las cuatro palancas: estimar antes de mandar, enrutar al modelo correcto, mantener el history acotado, y clasificar barato para decidir.

4. Implementación en Unity / C#

CostTracker agrega tokens y dólares por NPC. LlmRouter decide qué modelo usar según la importancia del NPC y la intención clasificada del mensaje. Precios hardcoded como ejemplo; en producción los lees de un ScriptableObject que actualizas cuando el proveedor cambia.

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public struct ModelPricing {
    public string id;
    public float inputUsdPerMillion;
    public float outputUsdPerMillion;
    [Range(0f, 1f)] public float cachedDiscount; // 0.1 = paga 10% sobre tokens cacheados
}

public class CostTracker : MonoBehaviour {
    readonly Dictionary<string, float> costByNpc = new();
    readonly Dictionary<string, int> tokensByNpc = new();

    public void Record(string npcId, ModelPricing model, int input, int cached, int output) {
        var freshInput = Mathf.Max(0, input - cached);
        var cost = (freshInput * model.inputUsdPerMillion / 1_000_000f)
                 + (cached    * model.inputUsdPerMillion * model.cachedDiscount / 1_000_000f)
                 + (output    * model.outputUsdPerMillion / 1_000_000f);

        costByNpc.TryGetValue(npcId, out var prev);
        costByNpc[npcId] = prev + cost;
        tokensByNpc.TryGetValue(npcId, out var prevTok);
        tokensByNpc[npcId] = prevTok + input + output;

        if (Debug.isDebugBuild)
            Debug.Log($"[LLM] {npcId} {model.id}: in={input} (cached={cached}) out={output} cost=${cost:F4} total=${costByNpc[npcId]:F3}");
    }

    public float TotalUsd() { float s = 0; foreach (var v in costByNpc.Values) s += v; return s; }
}

public enum Intent { SmallTalk, NormalDialogue, CriticalDialogue }

public class LlmRouter : MonoBehaviour {
    public ModelPricing cheap;   // p. ej. gpt-4o-mini o claude-haiku
    public ModelPricing medium;  // gpt-4o o claude-sonnet
    public ModelPricing top;     // gpt-4-turbo o claude-opus

    public ModelPricing Choose(Intent intent, int npcImportance) {
        if (intent == Intent.SmallTalk && npcImportance < 3) return cheap;
        if (intent == Intent.CriticalDialogue || npcImportance > 7) return top;
        return medium;
    }
}

5. En otros engines

  • Godot: idéntico patrón. CostTracker como autoload (singleton); LlmRouter como Resource. La aritmética de tokens es engine-agnóstica.
  • Unreal: UCostTracker como UGameInstanceSubsystem para que persista durante la sesión. El routing va en tu UAIController o equivalente.
  • JavaScript / Node backend: los SDKs oficiales de OpenAI y Anthropic devuelven el usage directamente. Trackea en tu backend (que es donde debe vivir la API key) y expone solo métricas agregadas al cliente.

Lo importante es que el tracking y el routing vivan en el lado donde tienes la API key, no en el cliente del jugador.

6. Quiz

Pon a prueba lo que entendiste

Responde una por una. La explicación aparece al elegir, correcta o no.

  1. Tu factura de OpenAI se triplicó este mes y el número de jugadores apenas cambió. ¿Dónde miras primero?

  2. Quieres reducir un 70% del costo de tu NPC sin cambiar de modelo. ¿Qué activas primero?

  3. ¿Cuándo Ollama local NO baja el costo total respecto a la API cloud?

  4. Tienes 5 tipos de NPCs: guardias genéricos, mercaderes, jefe de gremio, compañero de aventura, antagonista único. ¿Cómo enrutas modelos?