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écnica | Antes | Después | Reducción |
|---|---|---|---|
| Token reduction | 800 tokens/req | 200 tokens/req | 75% |
| Prompt caching | Sin caché | 60% cache hit | 30% en esos requests |
| Model routing | GPT-4o siempre | GPT-4o-mini (70%) | 70% menos en enrutados |
| Context compression | 4,000 tokens doc | 500 tokens resumen | 87% |
| max_tokens | 500 default | 50 para clasificación | 90% 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
- OpenAI Pricing — Precios actuales por modelo
- OpenAI Prompt Caching — Documentación oficial
- Anthropic Prompt Caching — Anthropic caching
- tiktoken — Contador de tokens de OpenAI
- LLM Cost Calculator — Calculadora de costos