Módulo 8: Prompt Engineering en Producción

3. Cost Optimization

Descripción

Estrategias para reducir el costo de LLM en producción: token reduction, context compression, prompt caching (OpenAI y Anthropic), model routing inteligente, y budget tracking con alertas. Cómo reducir costo 50-80% sin sacrificar calidad.


El Problema del Costo en Producción

El costo de LLM en producción puede sorprender:

# Estimación rápida de costo sin optimización:
requests_dia = 10_000
tokens_input_avg = 800   # Prompt largo
tokens_output_avg = 150  # Respuesta
precio_input_4o_mini = 0.15 / 1_000_000   # $0.15/1M tokens
precio_output_4o_mini = 0.60 / 1_000_000  # $0.60/1M tokens

costo_diario = (
    requests_dia * tokens_input_avg * precio_input_4o_mini +
    requests_dia * tokens_output_avg * precio_output_4o_mini
)
costo_mensual = costo_diario * 30

print(f"Sin optimización: ${costo_mensual:.2f}/mes")
# → Sin optimización: $36.00/mes

# A 100,000 requests/día:
# → $360/mes sin optimización
# → Con 3 técnicas bien aplicadas: $72/mes (80% reducción)

Las 5 Técnicas de Optimización de Costo

Técnica 1: Token Reduction       → Menos tokens por request
Técnica 2: Prompt Caching        → Reutilizar prefijos comunes
Técnica 3: Model Routing         → Usar modelo barato cuando es suficiente
Técnica 4: Context Compression   → Resumir/comprimir contextos largos
Técnica 5: Output Constraints    → Limitar tokens del output

Técnica 1: Token Reduction

Principio: Cada token tiene costo

from openai import OpenAI
import tiktoken

client = OpenAI()
encoder = tiktoken.encoding_for_model("gpt-4o-mini")

def contar_tokens(texto: str) -> int:
    return len(encoder.encode(texto))

def costo_estimado(
    prompt_tokens: int,
    completion_tokens: int,
    modelo: str = "gpt-4o-mini"
) -> float:
    """Estima el costo de un request en USD."""
    precios = {
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "gpt-4o": {"input": 2.50, "output": 10.00},
    }
    
    p = precios.get(modelo, precios["gpt-4o-mini"])
    return (prompt_tokens * p["input"] + completion_tokens * p["output"]) / 1_000_000

Identificar Tokens Innecesarios

# ANTES — prompt verboso (148 tokens):
PROMPT_VERBOSO = """
Eres un asistente de clasificación de sentimiento muy útil y experimentado.
Tu misión principal es analizar el sentimiento del texto que se te proporcione
y clasificarlo en una de las siguientes tres categorías principales:
POSITIVO, NEGATIVO o NEUTRO.
Es muy importante que SOLAMENTE respondas con la categoría y absolutamente
nada más, sin explicaciones adicionales ni texto extra.

Texto para analizar: {input}

Tu clasificación:
"""

# DESPUÉS — prompt compacto (31 tokens):
PROMPT_COMPACTO = """Clasifica como POSITIVO, NEGATIVO o NEUTRO. Solo la categoría.

Texto: {input}
Categoría:"""

# Diferencia:
tokens_verboso = contar_tokens(PROMPT_VERBOSO.format(input="texto de prueba"))
tokens_compacto = contar_tokens(PROMPT_COMPACTO.format(input="texto de prueba"))

print(f"Verboso: {tokens_verboso} tokens")
print(f"Compacto: {tokens_compacto} tokens")
print(f"Reducción: {(1 - tokens_compacto/tokens_verboso):.0%}")
# Verboso: ~148 tokens
# Compacto: ~31 tokens
# Reducción: ~79%

Guía de Reducción de Tokens

def audit_prompt_tokens(prompt: str) -> dict:
    """
    Analiza un prompt e identifica oportunidades de reducción de tokens.
    """
    tokens = contar_tokens(prompt)
    problemas = []
    
    # Detectar patrones verbosos comunes
    patrones_verbosos = [
        ("Como un experto en", "Reduce a instrucción directa"),
        ("Es muy importante que", "Eliminar — instrucciones directas son más efectivas"),
        ("Por favor, asegúrate de", "Eliminar — usar imperativo directo"),
        ("Tu misión es", "Reducir a 'Tu tarea:'"),
        ("absolutamente", "Eliminar — redundante"),
        ("sin ninguna duda", "Eliminar — redundante"),
        ("Proporciona únicamente", "Simplificar a 'Solo:'"),
    ]
    
    for patron, sugerencia in patrones_verbosos:
        if patron.lower() in prompt.lower():
            problemas.append(f"'{patron}' → {sugerencia}")
    
    return {
        "tokens_actuales": tokens,
        "costo_por_1000_requests": f"${costo_estimado(tokens, 50) * 1000:.4f}",
        "problemas_detectados": problemas,
        "tiene_oportunidades": len(problemas) > 0
    }

Few-Shot: Optimizar el Número de Ejemplos

def optimizar_few_shot(tarea: str, ejemplos: list[dict]) -> dict:
    """
    Encuentra el mínimo número de ejemplos con accuracy satisfactoria.
    Esto reduce tokens sin sacrificar calidad.
    """
    resultados = {}
    
    golden_set = [
        {"input": "Excelente producto", "expected": "POSITIVO"},
        {"input": "Terrible servicio", "expected": "NEGATIVO"},
        {"input": "El paquete llegó ayer", "expected": "NEUTRO"},
        # ... más ejemplos
    ]
    
    for n_ejemplos in [0, 1, 2, 3, 5]:
        ejemplos_subset = ejemplos[:n_ejemplos]
        
        # Construir prompt con N ejemplos
        if ejemplos_subset:
            ejemplos_str = "\n".join(
                f"- '{e['input']}' → {e['output']}"
                for e in ejemplos_subset
            )
            prompt = f"Clasifica. Ejemplos:\n{ejemplos_str}\n\nTexto: {{input}}\nCategoría:"
        else:
            prompt = "Clasifica como POSITIVO, NEGATIVO o NEUTRO:\n\nTexto: {input}\nCategoría:"
        
        tokens = contar_tokens(prompt.format(input="texto de prueba"))
        
        # Evaluar accuracy (simplificado)
        outputs = []
        for ej in golden_set[:10]:
            r = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": prompt.format(input=ej["input"])}],
                temperature=0
            )
            outputs.append(r.choices[0].message.content.strip())
        
        accuracy = sum(o.lower() == e["expected"].lower() for o, e in zip(outputs, golden_set[:10])) / 10
        
        resultados[n_ejemplos] = {
            "tokens": tokens,
            "accuracy": accuracy,
            "costo_relativo": tokens / contar_tokens(prompt.format(input="texto"))
        }
    
    return resultados

Técnica 2: Prompt Caching

El caching de prompts es la técnica de mayor ROI cuando tienes prompts con prefijos largos comunes.

OpenAI Prompt Caching (Automático)

OpenAI cachea automáticamente los primeros 1,024+ tokens de prompts que se repiten:

from openai import OpenAI

client = OpenAI()

# El system prompt largo se cachea automáticamente después del primer request
SYSTEM_PROMPT_LARGO = """
Eres un asistente de análisis legal especializado en contratos de software.
Tu función es analizar contratos y responder preguntas específicas.

REGLAS:
1. Solo analiza el contrato proporcionado en el mensaje del usuario
2. Cita el número de cláusula cuando hagas referencia al contrato
3. Si no está en el contrato, di explícitamente que no está
4. No des opiniones legales, solo análisis descriptivo
5. Responde en español

FORMATO DE RESPUESTA:
- Respuesta directa a la pregunta
- Cita de cláusula relevante (si aplica)
- Notas adicionales (si relevante)

[... 500+ tokens más de instrucciones ...]
"""  # 800+ tokens — se cachea automáticamente

def analizar_contrato(contrato: str, pregunta: str) -> dict:
    """
    Primera llamada: cache miss — costo normal
    Llamadas siguientes con mismo system prompt: cache hit — 50% descuento en input
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT_LARGO},
            {"role": "user", "content": f"Contrato:\n{contrato}\n\nPregunta: {pregunta}"}
        ],
        temperature=0
    )
    
    # En la respuesta puedes ver si hubo cache hit:
    usage = response.usage
    cached_tokens = getattr(usage, 'prompt_tokens_details', {})
    
    return {
        "respuesta": response.choices[0].message.content,
        "tokens_usados": usage.total_tokens,
        "cached_tokens": getattr(cached_tokens, 'cached_tokens', 0) if cached_tokens else 0
    }

Anthropic Prompt Caching (Explícito)

Anthropic permite marcar explícitamente qué cachear:

import anthropic

client_anthropic = anthropic.Anthropic()

SYSTEM_PROMPT_LARGO = "..."  # 1000+ tokens

def consultar_con_cache(pregunta: str) -> str:
    """
    Marca el system prompt con cache_control para caching explícito.
    Costo: Primera llamada = normal, siguientes = 90% descuento en tokens cacheados.
    """
    response = client_anthropic.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1024,
        system=[
            {
                "type": "text",
                "text": SYSTEM_PROMPT_LARGO,
                "cache_control": {"type": "ephemeral"}  # Cachear este bloque
            }
        ],
        messages=[
            {"role": "user", "content": pregunta}
        ]
    )
    
    # Verificar cache usage
    cache_creation = response.usage.cache_creation_input_tokens
    cache_read = response.usage.cache_read_input_tokens
    
    print(f"Cache creation: {cache_creation}, Cache read: {cache_read}")
    # Primera llamada: cache_creation > 0, cache_read = 0
    # Siguientes: cache_creation = 0, cache_read > 0 (90% descuento)
    
    return response.content[0].text


# Multi-turn con documentos en caché:
DOCUMENTO_LARGO = "... 50,000 tokens de documento ..."

def qa_sobre_documento(preguntas: list[str]) -> list[str]:
    """
    Cachea el documento largo, hace múltiples preguntas sobre él.
    Sin cache: 50K tokens × N preguntas
    Con cache: 50K tokens (creación) + N × 100 tokens (lectura)
    """
    respuestas = []
    
    for i, pregunta in enumerate(preguntas):
        response = client_anthropic.messages.create(
            model="claude-3-haiku-20240307",
            max_tokens=500,
            messages=[
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": DOCUMENTO_LARGO,
                            "cache_control": {"type": "ephemeral"}
                        },
                        {
                            "type": "text",
                            "text": f"Pregunta: {pregunta}"
                        }
                    ]
                }
            ]
        )
        respuestas.append(response.content[0].text)
        
        if i == 0:
            print(f"Primera llamada (cache miss): {response.usage.cache_creation_input_tokens} tokens cacheados")
        else:
            print(f"Llamada {i+1} (cache hit): {response.usage.cache_read_input_tokens} tokens leídos de caché")
    
    return respuestas

Técnica 3: Model Routing

No todos los requests necesitan el modelo más potente:

from openai import OpenAI

client = OpenAI()

# Precios aproximados (por 1M tokens de input)
PRECIOS_MODELOS = {
    "gpt-4o-mini": 0.15,    # Barato, rápido
    "gpt-4o": 2.50,          # Potente, caro (16.7x más caro)
}

class ModelRouter:
    """
    Router inteligente que selecciona el modelo según la complejidad del request.
    
    Estrategias:
    1. Clasificación de complejidad por keywords
    2. Longitud del input
    3. Tipo de tarea
    4. Resultado de evaluación previa
    """
    
    def __init__(
        self,
        modelo_economico: str = "gpt-4o-mini",
        modelo_potente: str = "gpt-4o",
        threshold_tokens: int = 2000  # Requests largos → modelo potente
    ):
        self.modelo_economico = modelo_economico
        self.modelo_potente = modelo_potente
        self.threshold_tokens = threshold_tokens
        
        # Patrones que indican alta complejidad
        self.patrones_complejos = [
            "razona paso a paso",
            "analiza en detalle",
            "código complejo",
            "matemáticas",
            "legal",
            "médico",
            "múltiples factores",
            "pros y contras"
        ]
    
    def seleccionar_modelo(
        self,
        input_text: str,
        tipo_tarea: str = "general"
    ) -> tuple[str, str]:
        """
        Selecciona el modelo apropiado.
        
        Returns: (modelo, razon)
        """
        # Tarea simple siempre → modelo económico
        tareas_simples = ["clasificacion", "extraccion_simple", "resumen_corto"]
        if tipo_tarea in tareas_simples:
            return self.modelo_economico, "tarea_simple"
        
        # Input largo → modelo potente
        tokens = len(input_text.split())  # Aproximación simple
        if tokens > self.threshold_tokens:
            return self.modelo_potente, "input_largo"
        
        # Detectar complejidad por keywords
        input_lower = input_text.lower()
        for patron in self.patrones_complejos:
            if patron in input_lower:
                return self.modelo_potente, f"patron_complejo: {patron}"
        
        # Default: modelo económico
        return self.modelo_economico, "default"
    
    def run(
        self,
        prompt: str,
        input_text: str,
        tipo_tarea: str = "general",
        **kwargs
    ) -> dict:
        """Ejecuta el request con el modelo apropiado."""
        modelo, razon = self.seleccionar_modelo(input_text, tipo_tarea)
        
        response = client.chat.completions.create(
            model=modelo,
            messages=[{"role": "user", "content": prompt.format(input=input_text)}],
            temperature=0,
            **kwargs
        )
        
        costo = costo_estimado(
            response.usage.prompt_tokens,
            response.usage.completion_tokens,
            modelo
        )
        
        return {
            "output": response.choices[0].message.content,
            "modelo_usado": modelo,
            "razon_routing": razon,
            "costo": costo,
            "tokens": response.usage.total_tokens
        }


# Uso:
router = ModelRouter()

# Request simple → mini automáticamente
result = router.run(
    prompt="Clasifica como POSITIVO/NEGATIVO/NEUTRO: {input}",
    input_text="Me encanta el producto",
    tipo_tarea="clasificacion"
)
print(f"Modelo: {result['modelo_usado']} ({result['razon_routing']})")
# Modelo: gpt-4o-mini (tarea_simple)

# Request complejo → gpt-4o
result = router.run(
    prompt="Analiza en detalle este contrato legal: {input}",
    input_text="Contrato de software..." * 100,
    tipo_tarea="analisis_legal"
)
print(f"Modelo: {result['modelo_usado']} ({result['razon_routing']})")
# Modelo: gpt-4o (input_largo)

Routing con Clasificador Previo

def clasificar_complejidad(texto: str) -> str:
    """
    Usa un modelo pequeño para clasificar la complejidad,
    antes de llamar al modelo principal.
    
    Costo: ~10 tokens para clasificar vs 500 tokens de prompt complejo
    ROI: Si el 70% son simples, ahorra 16x en esos requests.
    """
    prompt_clasificador = """¿Esta tarea requiere razonamiento complejo o es simple?
Simple: clasificación, extracción directa, resumen de 1-2 oraciones
Complejo: análisis multi-etapa, código, matemáticas, síntesis de información

Responde SOLO: SIMPLE o COMPLEJO

Tarea: """ + texto[:200]
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt_clasificador}],
        temperature=0,
        max_tokens=5
    )
    
    resultado = response.choices[0].message.content.strip().upper()
    return "COMPLEJO" if "COMPLEJO" in resultado else "SIMPLE"


# Con dos llamadas pero mayor ahorro:
def run_con_routing_inteligente(tarea: str, input_text: str) -> dict:
    complejidad = clasificar_complejidad(tarea + " " + input_text)
    
    modelo = "gpt-4o" if complejidad == "COMPLEJO" else "gpt-4o-mini"
    
    response = client.chat.completions.create(
        model=modelo,
        messages=[{"role": "user", "content": f"{tarea}\n\n{input_text}"}],
        temperature=0
    )
    
    return {
        "output": response.choices[0].message.content,
        "complejidad": complejidad,
        "modelo": modelo
    }

Técnica 4: Context Compression

Cuando el contexto es largo (documentos, historial de conversación), comprimir antes de enviar:

def comprimir_historial_conversacion(
    historial: list[dict],
    max_tokens: int = 500
) -> list[dict]:
    """
    Comprime el historial de conversación manteniendo lo importante.
    
    Estrategia: Resumir mensajes antiguos, mantener los N más recientes intactos.
    """
    if not historial:
        return []
    
    # Mantener los últimos 4 mensajes intactos (2 turnos)
    mensajes_recientes = historial[-4:] if len(historial) >= 4 else historial
    mensajes_antiguos = historial[:-4] if len(historial) >= 4 else []
    
    if not mensajes_antiguos:
        return mensajes_recientes
    
    # Resumir mensajes antiguos
    texto_antiguo = "\n".join(
        f"{m['role'].upper()}: {m['content'][:200]}"
        for m in mensajes_antiguos
    )
    
    prompt_resumen = f"""Resume esta conversación en máximo 3 oraciones, manteniendo los puntos clave y decisiones tomadas:

{texto_antiguo}

Resumen:"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt_resumen}],
        temperature=0,
        max_tokens=150
    )
    
    resumen = response.choices[0].message.content.strip()
    
    # Combinar: resumen + mensajes recientes
    return [
        {"role": "system", "content": f"Resumen de conversación previa: {resumen}"}
    ] + mensajes_recientes


def comprimir_documento(
    documento: str,
    max_tokens: int = 500,
    enfoque: str = ""
) -> str:
    """
    Comprime un documento largo en un resumen enfocado.
    
    enfoque: Qué aspectos son más relevantes para la tarea.
    """
    tokens_doc = contar_tokens(documento)
    
    if tokens_doc <= max_tokens:
        return documento  # No necesita compresión
    
    enfoque_str = f" enfocándote en: {enfoque}" if enfoque else ""
    
    prompt = f"""Resume el siguiente documento en máximo {max_tokens//4} palabras{enfoque_str}.
Preserva datos específicos, números y fechas importantes.

DOCUMENTO:
{documento[:8000]}  # Límite de seguridad

RESUMEN:"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=max_tokens
    )
    
    resumen = response.choices[0].message.content.strip()
    
    print(f"Documento: {tokens_doc} tokens → Resumen: {contar_tokens(resumen)} tokens")
    return resumen

Técnica 5: Output Constraints

def run_con_output_controlado(
    prompt: str,
    input_text: str,
    max_tokens_output: int = 50,
    usar_structured: bool = True
) -> dict:
    """
    Controla el tamaño del output para reducir costo.
    
    Para clasificación/extracción: max_tokens=10-50 es suficiente.
    Para resumen: max_tokens=100-200.
    """
    kwargs = {
        "model": "gpt-4o-mini",
        "messages": [{"role": "user", "content": prompt.format(input=input_text)}],
        "temperature": 0,
        "max_tokens": max_tokens_output  # Clave: limitar output tokens
    }
    
    # Para structured output: siempre es más eficiente que texto libre
    if usar_structured:
        kwargs["response_format"] = {"type": "json_object"}
    
    response = client.chat.completions.create(**kwargs)
    
    return {
        "output": response.choices[0].message.content,
        "prompt_tokens": response.usage.prompt_tokens,
        "completion_tokens": response.usage.completion_tokens,
        "costo": costo_estimado(response.usage.prompt_tokens, response.usage.completion_tokens)
    }


# Ejemplo: clasificación con max_tokens=5 (suficiente para "POSITIVO")
result = run_con_output_controlado(
    prompt="Clasifica como POSITIVO, NEGATIVO o NEUTRO. Solo la categoría.\n\nTexto: {input}",
    input_text="Me encanta este producto",
    max_tokens_output=5  # "POSITIVO" = 1-2 tokens
)
print(f"Output: '{result['output']}', completion_tokens: {result['completion_tokens']}")

Budget Tracking y Alertas

import json
from datetime import datetime, date
from pathlib import Path


class BudgetTracker:
    """
    Trackea el costo de las llamadas a la API y alerta cuando se acerca al límite.
    """
    
    def __init__(
        self,
        budget_diario: float = 10.0,
        budget_mensual: float = 200.0,
        storage_path: str = "cost_tracking.json"
    ):
        self.budget_diario = budget_diario
        self.budget_mensual = budget_mensual
        self.storage_path = Path(storage_path)
        self._data = self._cargar()
    
    def _cargar(self) -> dict:
        if self.storage_path.exists():
            with open(self.storage_path) as f:
                return json.load(f)
        return {"daily": {}, "monthly": {}, "total": 0.0}
    
    def _guardar(self) -> None:
        with open(self.storage_path, "w") as f:
            json.dump(self._data, f, indent=2)
    
    def registrar(self, respuesta_api) -> dict:
        """
        Registra el costo de una respuesta de la API.
        
        respuesta_api: Objeto de respuesta de OpenAI.
        """
        usage = respuesta_api.usage
        
        # Calcular costo (GPT-4o-mini por defecto)
        costo = (
            usage.prompt_tokens * 0.15 / 1_000_000 +
            usage.completion_tokens * 0.60 / 1_000_000
        )
        
        hoy = date.today().isoformat()
        mes = date.today().strftime("%Y-%m")
        
        # Actualizar tracking
        self._data["daily"][hoy] = self._data["daily"].get(hoy, 0) + costo
        self._data["monthly"][mes] = self._data["monthly"].get(mes, 0) + costo
        self._data["total"] += costo
        
        self._guardar()
        
        # Verificar alertas
        alertas = []
        costo_hoy = self._data["daily"][hoy]
        costo_mes = self._data["monthly"][mes]
        
        if costo_hoy >= self.budget_diario * 0.8:
            alertas.append(f"⚠️ 80% del budget diario alcanzado: ${costo_hoy:.4f}/${self.budget_diario}")
        if costo_hoy >= self.budget_diario:
            alertas.append(f"🚨 Budget diario EXCEDIDO: ${costo_hoy:.4f}/${self.budget_diario}")
        if costo_mes >= self.budget_mensual * 0.8:
            alertas.append(f"⚠️ 80% del budget mensual alcanzado: ${costo_mes:.2f}/${self.budget_mensual}")
        
        return {
            "costo_request": costo,
            "costo_hoy": costo_hoy,
            "costo_mes": costo_mes,
            "alertas": alertas
        }
    
    def resumen(self) -> dict:
        """Retorna resumen de costos actuales."""
        hoy = date.today().isoformat()
        mes = date.today().strftime("%Y-%m")
        
        costo_hoy = self._data["daily"].get(hoy, 0)
        costo_mes = self._data["monthly"].get(mes, 0)
        
        return {
            "costo_hoy": f"${costo_hoy:.4f}",
            "pct_budget_diario": f"{costo_hoy/self.budget_diario*100:.1f}%",
            "costo_mes": f"${costo_mes:.2f}",
            "pct_budget_mensual": f"{costo_mes/self.budget_mensual*100:.1f}%",
            "total_historico": f"${self._data['total']:.2f}"
        }
    
    def proyeccion_mensual(self) -> float:
        """Proyecta el costo mensual basado en los últimos 7 días."""
        ultimos_7 = []
        for i in range(7):
            from datetime import timedelta
            dia = (date.today() - timedelta(days=i)).isoformat()
            costo = self._data["daily"].get(dia, 0)
            ultimos_7.append(costo)
        
        promedio_diario = sum(ultimos_7) / len([c for c in ultimos_7 if c > 0] or [1])
        return promedio_diario * 30


# Wrapper integrado con tracking:
budget = BudgetTracker(budget_diario=5.0, budget_mensual=100.0)

def call_with_budget(prompt: str, input_text: str) -> str:
    """Llamada a la API con tracking de budget automático."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt.format(input=input_text)}],
        temperature=0
    )
    
    tracking = budget.registrar(response)
    
    for alerta in tracking["alertas"]:
        print(alerta)
    
    return response.choices[0].message.content

Comparación: Antes vs Después de Optimización

TécnicaAntesDespuésReducción
Token reduction800 tokens/req200 tokens/req75%
Prompt cachingSin caché60% cache hit30% en esos requests
Model routingGPT-4o siempreGPT-4o-mini (70%)70% menos en enrutados
Context compression4,000 tokens doc500 tokens resumen87%
max_tokens500 default50 para clasificación90% output tokens

Combinadas: En un sistema real con todas las técnicas → 50-80% reducción de costo


Troubleshooting

Problema 1: Reducir tokens rompe la calidad

Síntoma: El prompt comprimido tiene accuracy 15% menor.

Causa: Eliminaste instrucciones necesarias, no solo relleno.

Solución:

# Proceso correcto de optimización de tokens:
# 1. Establecer accuracy baseline con prompt original
# 2. Eliminar elementos uno a uno
# 3. Medir accuracy después de cada eliminación
# 4. Retener si no hay caída > 2% en accuracy

def optimizar_prompt_iterativamente(
    prompt_original: str,
    golden_set: list[dict],
    max_degradacion: float = 0.02
) -> str:
    """Elimina tokens de forma segura verificando accuracy."""
    
    def evaluar_accuracy(prompt_template):
        correctos = 0
        for ej in golden_set[:20]:  # subset para rapidez
            r = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": prompt_template.format(input=ej["input"])}],
                temperature=0
            )
            if r.choices[0].message.content.strip().lower() == str(ej["expected_output"]).lower():
                correctos += 1
        return correctos / min(20, len(golden_set))
    
    baseline = evaluar_accuracy(prompt_original)
    prompt_actual = prompt_original
    
    # Intentar eliminar líneas redundantes
    lineas = prompt_original.split("\n")
    for i, linea in enumerate(lineas):
        if len(linea.strip()) < 5:  # Skip líneas vacías
            continue
        
        # Probar sin esta línea
        prompt_sin_linea = "\n".join(lineas[:i] + lineas[i+1:])
        accuracy_sin = evaluar_accuracy(prompt_sin_linea)
        
        if accuracy_sin >= baseline - max_degradacion:
            # La línea no era necesaria
            lineas[i] = ""
            prompt_actual = "\n".join([l for l in lineas if l])
            print(f"Eliminada: '{linea[:40]}...' (accuracy: {accuracy_sin:.2%})")
    
    return prompt_actual

Problema 2: Prompt caching no aplica

Síntoma: Llamadas repetidas no muestran descuento de caching.

Causa (OpenAI): El prompt tiene variables que cambian al inicio, rompiendo el cache.

Solución:

# MAL: La variable al inicio rompe el cache
prompt_mal = f"Analiza el documento de {usuario}: {system_prompt_largo}..."

# BIEN: Las partes variables van al final, el prefix largo se cachea
prompt_bien = f"""{system_prompt_largo}  ← Este prefix se cachea

---
Documento del usuario:
{documento_del_usuario}  ← Variable al final
"""

Problema 3: Model routing manda requests complejos al modelo barato

Síntoma: Requests complejos con baja calidad porque llegan a gpt-4o-mini.

Solución:

# Añadir fallback: si la calidad del output es baja, reintentar con modelo potente
def run_con_quality_fallback(prompt: str, input_text: str, quality_threshold: float = 0.8) -> dict:
    # Primer intento: modelo barato
    response_mini = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt.format(input=input_text)}],
        temperature=0
    )
    output_mini = response_mini.choices[0].message.content
    
    # Verificar si el output parece completo y bien formado
    # (heurística simple: longitud mínima, no contiene "no puedo" o "no sé")
    palabras_problema = ["no puedo", "no sé", "no tengo información", "lo siento"]
    output_tiene_problema = any(p in output_mini.lower() for p in palabras_problema)
    output_muy_corto = len(output_mini.split()) < 5
    
    if output_tiene_problema or output_muy_corto:
        # Fallback a modelo potente
        response_4o = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": prompt.format(input=input_text)}],
            temperature=0
        )
        return {
            "output": response_4o.choices[0].message.content,
            "modelo": "gpt-4o",
            "fallback_activado": True
        }
    
    return {"output": output_mini, "modelo": "gpt-4o-mini", "fallback_activado": False}

Ejercicios

Ejercicio 1: Calcular el ROI de token reduction

Tienes un prompt de 600 tokens y quieres reducirlo a 150. Calcula el ahorro a 50,000 requests/día y 30 días:

Ver solución
def calcular_roi_reduccion(
    tokens_antes: int,
    tokens_despues: int,
    requests_dia: int,
    dias: int = 30,
    modelo: str = "gpt-4o-mini"
) -> dict:
    precio_input = {"gpt-4o-mini": 0.15, "gpt-4o": 2.50}[modelo]
    
    costo_antes = tokens_antes * precio_input / 1_000_000 * requests_dia * dias
    costo_despues = tokens_despues * precio_input / 1_000_000 * requests_dia * dias
    ahorro = costo_antes - costo_despues
    
    return {
        "costo_antes": f"${costo_antes:.2f}",
        "costo_despues": f"${costo_despues:.2f}",
        "ahorro": f"${ahorro:.2f}",
        "pct_reduccion": f"{ahorro/costo_antes*100:.0f}%"
    }

resultado = calcular_roi_reduccion(
    tokens_antes=600,
    tokens_despues=150,
    requests_dia=50_000
)
print(resultado)
# {'costo_antes': '$135.00', 'costo_despues': '$33.75', 'ahorro': '$101.25', 'pct_reduccion': '75%'}

Ejercicio 2: Implementar un model router simple

Implementa un router que use gpt-4o para requests con más de 500 palabras, y gpt-4o-mini para el resto:

Ver solución
from openai import OpenAI

client = OpenAI()

def smart_router(prompt_template: str, input_text: str, threshold_palabras: int = 500) -> dict:
    """Router simple por longitud de input."""
    palabras = len(input_text.split())
    modelo = "gpt-4o" if palabras > threshold_palabras else "gpt-4o-mini"
    
    response = client.chat.completions.create(
        model=modelo,
        messages=[{"role": "user", "content": prompt_template.format(input=input_text)}],
        temperature=0
    )
    
    # Precios por token de input
    precio = 2.50 if modelo == "gpt-4o" else 0.15
    costo_estimado = response.usage.prompt_tokens * precio / 1_000_000
    
    return {
        "output": response.choices[0].message.content,
        "modelo_usado": modelo,
        "palabras_input": palabras,
        "costo_estimado": f"${costo_estimado:.6f}"
    }

# Test:
r1 = smart_router("Resume: {input}", "Texto corto")
r2 = smart_router("Resume: {input}", ("Texto largo " * 100))

print(f"Input corto: {r1['modelo_usado']} ({r1['palabras_input']} palabras)")
print(f"Input largo: {r2['modelo_usado']} ({r2['palabras_input']} palabras)")

Resumen

  • Token reduction: La técnica de mayor impacto — prompts verbosos = costo innecesario
  • Prompt caching: Automático en OpenAI, explícito en Anthropic — 50-90% descuento en tokens cacheados
  • Model routing: Usar gpt-4o-mini para 70%+ de requests simples — 16x más barato
  • Context compression: Para conversaciones largas y documentos — resumir antes de enviar
  • max_tokens: Siempre configurar — clasificación necesita 5 tokens, no 500
  • Budget tracking: Monitorear costo diario y mensual con alertas antes de exceder

Recursos adicionales

  1. OpenAI Pricing — Precios actuales por modelo
  2. OpenAI Prompt Caching — Documentación oficial
  3. Anthropic Prompt Caching — Anthropic caching
  4. tiktoken — Contador de tokens de OpenAI
  5. LLM Cost Calculator — Calculadora de costos