Saltar al contenido principal

Costes y optimización

Usar LLMs a escala puede volverse muy costoso rápidamente. Una aplicación con 10.000 usuarios activos puede generar millones de tokens al día.

La optimización no es solo sobre dinero: también impacta en latencia y experiencia de usuario.

Cómo se calculan los costes

Los proveedores no cobran por palabra o por carácter, sino por token. Un token es una unidad básica de procesamiento que equivale aproximadamente a 4 caracteres o a una sílaba en español.

Es fundamental entender que los modelos suelen tener precios diferentes para los tokens de entrada (el prompt que envías) y los de salida (la respuesta generada), siendo estos últimos generalmente mucho más caros debido al esfuerzo computacional que requieren.

Coste total = (tokens_entrada × precio_entrada) + (tokens_salida × precio_salida)

Precios aproximados (referencia, verificar en cada proveedor)

ModeloEntrada (por 1M tokens)Salida (por 1M tokens)
GPT-4o$2.50$10.00
GPT-4o-mini$0.15$0.60
Claude Opus 4$15.00$75.00
Claude Sonnet 4$3.00$15.00
Claude Haiku 4$0.80$4.00
Gemini 2.0 Flash$0.075$0.30
Ollama (local)$0$0

Estimar tokens antes de llamar

Para evitar sorpresas en la factura o latencias inesperadas, es una buena práctica contar los tokens antes de realizar la petición. Herramientas como tiktoken (de OpenAI) permiten calcular de forma exacta cuántos tokens ocupa un texto para un modelo concreto.

import { encoding_for_model } from 'tiktoken'; // npm install tiktoken

function contarTokens(texto, modelo = 'gpt-4o-mini') {
const enc = encoding_for_model(modelo);
const tokens = enc.encode(texto);
enc.free();
return tokens.length;
}

function estimarCoste(prompt, respuestaEstimada, modelo = 'gpt-4o-mini') {
const precios = {
'gpt-4o-mini': { entrada: 0.00015, salida: 0.0006 } // por 1K tokens
};
const p = precios[modelo];

const tokensEntrada = contarTokens(prompt);
const tokensSalida = respuestaEstimada;

return (tokensEntrada * p.entrada + tokensSalida * p.salida) / 1000;
}

console.log(`Coste estimado: $${estimarCoste('¿Qué es Node.js?', 200).toFixed(6)}`);

Estrategia 1 — Seleccionar el modelo adecuado

No todas las tareas requieren el modelo más potente del mercado. Una de las formas más efectivas de ahorrar (hasta un 90-99%) es usar el modelo más pequeño y barato que sea capaz de cumplir con la tarea.

Por regla general:

  • Modelos "Mini" o "Flash": Ideales para clasificación, extracción de datos síncrona y resúmenes sencillos.
  • Modelos "Pro" o "Large": Reservados para razonamiento complejo, análisis de documentos legales o generación de código avanzado.
function seleccionarModelo(tipo) {
const modelos = {
// Tareas simples y rápidas
clasificacion: 'gpt-4o-mini',
extraccion_datos: 'gpt-4o-mini',
resumen_corto: 'gpt-4o-mini',
respuesta_faq: 'gpt-4o-mini',

// Tareas de complejidad media
generacion_codigo: 'gpt-4o',
analisis_documento: 'gpt-4o',
redaccion_profesional: 'claude-sonnet-4-5',

// Tareas muy complejas
razonamiento_complejo: 'claude-opus-4-5',
analisis_legal: 'claude-opus-4-5',

// Siempre gratis con hardware propio
desarrollo_local: 'llama3.1:8b' // Ollama
};

return modelos[tipo] ?? 'gpt-4o-mini';
}

Estrategia 2 — Caché de respuestas

Si tu aplicación recibe preguntas repetitivas (por ejemplo, en un FAQ o un asistente predecible), no tiene sentido pagar por procesar la misma información una y otra vez. La caché nos permite devolver una respuesta guardada previamente, eliminando el coste y reduciendo la latencia a milisegundos.

Caché simple en memoria (desarrollo)

const cache = new Map();

async function llmConCache(prompt, opciones = {}) {
const clave = `${opciones.model}:${prompt}`;

if (cache.has(clave)) {
console.log('Cache HIT');
return cache.get(clave);
}

const respuesta = await llamarLLM(prompt, opciones);
cache.set(clave, respuesta);

return respuesta;
}

Caché con Redis (producción)

import { createClient } from 'redis'; // npm install redis
import crypto from 'crypto';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

function hashPrompt(prompt, modelo) {
return crypto
.createHash('sha256')
.update(`${modelo}:${prompt}`)
.digest('hex');
}

async function llmConCacheRedis(messages, modelo = 'gpt-4o-mini', ttl = 3600) {
const prompt = JSON.stringify(messages);
const clave = `llm:${hashPrompt(prompt, modelo)}`;

// Intentar obtener de caché
const cached = await redis.get(clave);
if (cached) {
console.log('🎯 Cache HIT – ahorro de tokens');
return JSON.parse(cached);
}

// Llamar al LLM
const respuesta = await openai.chat.completions.create({ model: modelo, messages });
const texto = respuesta.choices[0].message.content;

// Guardar en caché con TTL
await redis.setEx(clave, ttl, JSON.stringify(texto));

return texto;
}

Prompt Caching (nativo)

Proveedores como Anthropic o DeepSeek ofrecen caché de prefijos de prompt. Esto significa que si envías una documentación muy larga (el sistema de un agente, por ejemplo) y esta no cambia entre peticiones, el proveedor solo te cobrará el coste completo la primera vez. Las siguientes serán drásticamente más baratas.

const response = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
system: [
{
type: 'text',
text: documentacionLarga, // puede ser miles de tokens
cache_control: { type: 'ephemeral' } // cachea este bloque por 5 min
}
],
messages: [{ role: 'user', content: preguntaDelUsuario }]
});

// Las llamadas siguientes con el mismo system prompt son ~90% más baratas

Estrategia 3 — Reducir el tamaño del prompt

Ahorrar en el prompt es ahorrar en cada mensaje. Cada palabra innecesaria en el system prompt se multiplica por cada interacción de cada usuario. Además de ser conciso, en conversaciones largas es vital "podar" el historial para no reenviar mensajes irrelevantes que consumen cuota.

// ❌ Malo: system prompt genérico y verboso
const systemMalo = `
Eres un asistente muy útil y amable que siempre intenta ayudar al usuario
de la mejor manera posible. Debes responder siempre en español y ser muy
detallado en tus respuestas. Nunca digas que no puedes hacer algo...
[200 palabras más de instrucciones genéricas]
`;

// ✅ Bueno: conciso y específico
const systemBueno = `Asistente de soporte técnico para Node.js.
Responde en español. Sé directo y proporciona código cuando sea útil.`;

// ❌ Malo: incluir el historial completo siempre
const historialCompleto = conversacion; // puede ser 50+ mensajes

// ✅ Bueno: solo los últimos N mensajes + resumen del contexto
function trimHistorial(mensajes, maxMensajes = 10) {
if (mensajes.length <= maxMensajes) return mensajes;

const recientes = mensajes.slice(-maxMensajes);
return [
{
role: 'system',
content: `[Contexto previo: ${mensajes.length - maxMensajes} mensajes anteriores omitidos]`
},
...recientes
];
}

Estrategia 4 — Control de tokens de salida

El parámetro max_tokens actúa como un disyuntor de seguridad. Si el modelo entra en un bucle infinito o se vuelve demasiado prolijo, este límite detendrá la generación, protegiendo tu presupuesto de gastos inesperados.

// Limita max_tokens según la tarea
const configuraciones = {
clasificacion: { max_tokens: 50, temperature: 0 },
resumen: { max_tokens: 300, temperature: 0.3 },
respuesta_chat: { max_tokens: 800, temperature: 0.7 },
redaccion: { max_tokens: 2000, temperature: 0.8 }
};

// Pide al modelo que sea conciso en el system prompt
const system = `Responde de forma concisa.
Máximo 3 párrafos. Sin introducciones largas ni repetir la pregunta.`;

Estrategia 5 — Rate limiting y control de gasto

Incluso con modelos baratos, un error en el código o un usuario malintencionado podrían disparar el gasto. Implementar límites de velocidad (rate limiting) y cuotas de uso por usuario es obligatorio para cualquier aplicación en producción.

import Bottleneck from 'bottleneck'; // npm install bottleneck

// Limitar peticiones por minuto
const limiter = new Bottleneck({
maxConcurrent: 5, // máximo 5 peticiones simultáneas
minTime: 200, // mínimo 200ms entre peticiones
reservoir: 60, // máximo 60 peticiones
reservoirRefreshAmount: 60,
reservoirRefreshInterval: 60 * 1000 // refresca cada minuto
});

const llamarLLMConLimite = limiter.wrap(async (messages) => {
return await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages
});
});

// Control de gasto por usuario
async function verificarPresupuesto(userId, tokensEstimados) {
const gastoHoy = await redis.incrBy(`gasto:${userId}:${hoy()}`, tokensEstimados);

const LIMITE_DIARIO = 100_000; // tokens
if (gastoHoy > LIMITE_DIARIO) {
throw new Error('Has alcanzado tu límite diario de uso de IA.');
}
}

Estrategia 6 — Embeddings en batch

Generar embeddings para miles de documentos uno a uno es muy ineficiente debido al overhead de las peticiones HTTP. La mayoría de APIs permiten enviar un array de textos (lotes o batches) en una sola llamada, lo que reduce drásticamente el tiempo total de procesamiento.

// ❌ Malo: una llamada por documento
for (const doc of documentos) {
const { data } = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: doc.contenido
});
doc.embedding = data[0].embedding;
}

// ✅ Bueno: hasta 2048 textos en una sola llamada
const BATCH_SIZE = 100;

async function generarEmbeddingsEnBatch(textos) {
const embeddings = [];

for (let i = 0; i < textos.length; i += BATCH_SIZE) {
const lote = textos.slice(i, i + BATCH_SIZE);

const { data } = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: lote
});

embeddings.push(...data.map(d => d.embedding));
console.log(`Procesados ${Math.min(i + BATCH_SIZE, textos.length)}/${textos.length}`);
}

return embeddings;
}

Monitorización y observabilidad

No puedes optimizar lo que no mides. Es crucial registrar el uso de tokens y el coste real de cada petición, idealmente desglosado por usuario o por funcionalidad de la aplicación.

// Middleware para loguear uso y costes
async function llmConMonitorizacion(messages, opciones) {
const inicio = Date.now();

const response = await openai.chat.completions.create({
...opciones,
messages
});

const duracion = Date.now() - inicio;
const uso = response.usage;

// Enviar métricas (Datadog, Prometheus, tu propio sistema...)
await metricas.registrar({
modelo: opciones.model,
tokens_entrada: uso.prompt_tokens,
tokens_salida: uso.completion_tokens,
tokens_total: uso.total_tokens,
duracion_ms: duracion,
coste_usd: calcularCoste(uso, opciones.model)
});

return response;
}

Checklist de optimización

  • ¿Estás usando el modelo más barato que cumple el requisito?
  • ¿Tienes caché para prompts frecuentes?
  • ¿El system prompt es conciso y necesario?
  • ¿Estás limitando max_tokens según la tarea?
  • ¿Tienes rate limiting por usuario?
  • ¿Estás monitorizando el coste por endpoint?
  • ¿Usas Ollama en desarrollo para no gastar tokens?
  • ¿Generas embeddings en batch en lugar de uno a uno?
  • ¿Tienes alertas si el gasto supera un umbral?