Módulo 1: Fundamentos de Prompt Engineering

6. Comparación: Prompt Casual vs Prompt Engineered

Descripción de la cápsula

En esta cápsula verás side-by-side los resultados de prompts naive versus prompts diseñados con los principios de este módulo. Medirás la diferencia con datos concretos: consistencia de formato, parseabilidad por código, tokens consumidos y costo estimado. Sin esta evidencia, es fácil subestimar el valor del prompt engineering — parece un esfuerzo extra para algo que "ya funciona".

El objetivo no es que abandones prompts simples para siempre, sino que entiendas en qué contextos la diferencia es crítica y en qué contextos no importa. Aprenderás a elegir el nivel de ingeniería apropiado para cada situación y a medir la mejora de forma objetiva, no por intuición.

Por qué importa: En el Módulo 7 aprenderás a evaluar prompts sistemáticamente. Esta cápsula es el puente entre "sé que mejoré" y "tengo datos que demuestran cuánto mejoré". El framework de comparación que verás aquí es la versión simplificada de lo que usarás con métricas formales en producción.


Fundamentos: Por Qué los Prompts Casuales Son Inconsistentes

Un prompt casual falla porque no especifica ninguno de estos elementos:

  1. Sin system prompt: El modelo usa su comportamiento default, que varía por modelo y versión
  2. Sin formato de salida: El modelo elige cómo responder (puede ser una palabra o tres párrafos)
  3. Sin temperatura controlada: El default varía entre proveedores (0.7-1.0 típicamente), añadiendo variabilidad
  4. Sin restricciones: El modelo puede añadir explicaciones, advertencias, sugerencias no pedidas
  5. Sin manejo de edge cases: Inputs atípicos producen outputs impredecibles

Analogía: Un prompt casual es como una función sin type hints, sin validación de inputs, y sin valor de retorno definido. Funciona a veces, falla silenciosamente otras.


Experimento 1: Clasificación de Sentimiento

Setup del experimento

from openai import OpenAI
import json

client = OpenAI()

test_inputs = [
    "El servicio fue terrible, no volveré.",
    "El producto llegó en perfectas condiciones, muy contento.",
    "Más o menos, podría mejorar pero tampoco está mal.",
    "INCREÍBLE! Mejor compra que hice este año!!",
    "No funciona como esperaba. Decepcionante."
]

Prompt casual — ejecución y análisis

def clasificar_casual(texto: str) -> str:
    """Prompt sin estructura, sin system, temperature default."""
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "user", "content": f"¿Este texto es positivo o negativo? \"{texto}\""}
        ]
        # Sin temperature (default del modelo)
        # Sin max_tokens
        # Sin system prompt
    )
    return r.choices[0].message.content.strip()

print("=== Resultados Prompt Casual ===")
resultados_casual = []
for texto in test_inputs:
    resultado = clasificar_casual(texto)
    resultados_casual.append(resultado)
    print(f"Input: {texto[:50]}")
    print(f"Output: {resultado}\n")

Output típico (varía entre ejecuciones):

Input: El servicio fue terrible, no volveré.
Output: Este texto es claramente negativo. El autor expresa insatisfacción con el servicio y declara que no regresará.

Input: El producto llegó en perfectas condiciones, muy contento.
Output: Positivo

Input: Más o menos, podría mejorar pero tampoco está mal.
Output: El texto tiene un tono neutral o mixto. No es claramente positivo ni negativo.

Input: INCREÍBLE! Mejor compra que hice este año!!
Output: Es positivo.

Input: No funciona como esperaba. Decepcionante.
Output: Negativo. El texto expresa decepción.

Problema: Cinco formatos diferentes en cinco respuestas. Imposible de parsear con código.

Prompt engineered — ejecución y análisis

SYSTEM_CLASIFICADOR = """
Eres un clasificador de sentimiento de texto.

TAREA: Clasifica el sentimiento del texto en exactamente una de estas categorías:
- POSITIVO
- NEGATIVO
- NEUTRO

REGLAS:
- Responde ÚNICAMENTE con la categoría (una palabra en mayúsculas)
- Sin explicaciones, sin puntuación, sin texto adicional
- Para textos mixtos, elige el sentimiento DOMINANTE
"""

def clasificar_engineered(texto: str) -> str:
    """Prompt estructurado, system prompt, temperature=0."""
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_CLASIFICADOR},
            {"role": "user", "content": texto}
        ],
        temperature=0,       # Determinístico
        max_tokens=10        # Solo necesitas una palabra
    )
    return r.choices[0].message.content.strip()

print("=== Resultados Prompt Engineered ===")
resultados_eng = []
for texto in test_inputs:
    resultado = clasificar_engineered(texto)
    resultados_eng.append(resultado)
    print(f"Input: {texto[:50]}")
    print(f"Output: {resultado}\n")

Output consistente:

Input: El servicio fue terrible, no volveré.
Output: NEGATIVO

Input: El producto llegó en perfectas condiciones, muy contento.
Output: POSITIVO

Input: Más o menos, podría mejorar pero tampoco está mal.
Output: NEUTRO

Input: INCREÍBLE! Mejor compra que hice este año!!
Output: POSITIVO

Input: No funciona como esperaba. Decepcionante.
Output: NEGATIVO

Cinco respuestas, cinco formatos idénticos. Parseable directamente: resultado in ["POSITIVO", "NEGATIVO", "NEUTRO"].


Métricas Cuantitativas: Comparación Directa

def medir_consistencia_formato(resultados: list[str], 
                                formatos_validos: set[str]) -> float:
    """Qué % de respuestas tiene formato válido y parseable."""
    validos = sum(1 for r in resultados if r in formatos_validos)
    return validos / len(resultados)

def medir_tokens_promedio(resultados: list[str]) -> float:
    """Tokens promedio por respuesta (aproximado)."""
    return sum(len(r.split()) for r in resultados) / len(resultados)

formatos_validos = {"POSITIVO", "NEGATIVO", "NEUTRO"}

consistencia_casual = medir_consistencia_formato(resultados_casual, formatos_validos)
consistencia_eng = medir_consistencia_formato(resultados_eng, formatos_validos)

tokens_casual = medir_tokens_promedio(resultados_casual)
tokens_eng = medir_tokens_promedio(resultados_eng)

print("=== Comparación de Métricas ===")
print(f"Consistencia casual:    {consistencia_casual:.0%}")
print(f"Consistencia engineered:{consistencia_eng:.0%}")
print(f"Tokens/respuesta casual: {tokens_casual:.1f}")
print(f"Tokens/respuesta eng:    {tokens_eng:.1f}")

Output esperado:

=== Comparación de Métricas ===
Consistencia casual:    20%
Consistencia engineered:100%
Tokens/respuesta casual: 18.4
Tokens/respuesta eng:    1.0

Tabla de métricas completa

MétricaPrompt casualPrompt engineeredMejora
Consistencia de formato~20% (1/5)100% (5/5)+5x
Parseable directamenteNo (requiere regex/NLP)Sí (in valid_set)
Tokens por respuesta15-501-2~10-25x menos
Costo relativo por call10-15x mayorBase-90%
Latencia relativaMayorMenor~20-30% más rápido
ReproducibilidadVariableDeterminístico (temp=0)

Experimento 2: Extracción de Datos Estructurados

La diferencia es aún más pronunciada cuando el output debe ser estructurado.

Prompt casual para extracción

def extraer_casual(texto: str) -> str:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"Extrae el nombre y email de este texto: \"{texto}\""
        }]
    )
    return r.choices[0].message.content.strip()

textos = [
    "Contactar a María García en mgarcia@empresa.com para la reunión",
    "El responsable es Juan (juan.perez@gmail.com) y su backup Carlos (carlos@mail.com)",
    "No hay información de contacto en este mensaje"
]

print("=== Extracción Casual ===")
for t in textos:
    print(f"Input: {t[:60]}")
    print(f"Output: {extraer_casual(t)}\n")

Output típico (problemático):

Input: Contactar a María García en mgarcia@empresa.com para la reunión
Output: Nombre: María García
        Email: mgarcia@empresa.com

Input: El responsable es Juan (juan.perez@gmail.com) y su backup Carlos (carlos@mail.com)
Output: En el texto se mencionan dos personas con sus correos:
        1. Juan - juan.perez@gmail.com
        2. Carlos - carlos@mail.com

Input: No hay información de contacto en este mensaje
Output: No se encontró nombre ni email en el texto proporcionado.

Problema: Tres formatos completamente diferentes. ¿Cómo parseas esto con código?

Prompt engineered para extracción

SYSTEM_EXTRACTOR = """
Extrae nombres y emails de textos. 

FORMATO DE RESPUESTA (JSON estricto, sin nada más):
{"nombres": ["nombre1", "nombre2"], "emails": ["email1", "email2"]}

REGLAS:
- Si hay múltiples personas, incluye todos
- Si no hay nombres: {"nombres": [], "emails": [...]}
- Si no hay emails: {"nombres": [...], "emails": []}
- Si no hay nada: {"nombres": [], "emails": []}
- Solo JSON válido. Sin texto adicional.
"""

def extraer_engineered(texto: str) -> dict:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_EXTRACTOR},
            {"role": "user", "content": texto}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(r.choices[0].message.content)

print("=== Extracción Engineered ===")
for t in textos:
    resultado = extraer_engineered(t)
    print(f"Input: {t[:60]}")
    print(f"Output: {resultado}")
    # Verificar que es parseable
    assert isinstance(resultado["nombres"], list)
    assert isinstance(resultado["emails"], list)
    print("  ✅ Parseable y válido\n")

Output consistente:

Input: Contactar a María García en mgarcia@empresa.com para la reunión
Output: {'nombres': ['María García'], 'emails': ['mgarcia@empresa.com']}
  ✅ Parseable y válido

Input: El responsable es Juan (juan.perez@gmail.com) y su backup Carlos (carlos@mail.com)
Output: {'nombres': ['Juan', 'Carlos'], 'emails': ['juan.perez@gmail.com', 'carlos@mail.com']}
  ✅ Parseable y válido

Input: No hay información de contacto en este mensaje
Output: {'nombres': [], 'emails': []}
  ✅ Parseable y válido

Cuándo Basta el Prompt Casual

No siempre necesitas ingeniería completa. La decisión depende del contexto de uso:

Basta con prompt casual cuando:

  1. Exploración personal: Estás probando ideas, generando borradores, explorando posibilidades. El output va a tus ojos, no a un sistema.

  2. One-off manual: Ejecutas el prompt una sola vez, tú revisas el output manualmente, y no lo repetirás.

  3. Prototipado rápido: Estás validando si un concepto funciona antes de invertir tiempo en diseño. Ciclo de validación de 30 minutos.

  4. Tarea con resultado humano: El output pasa por revisión humana antes de usarse. La persona puede corregir inconsistencias.

  5. Tarea trivialmente simple: "Traduce 'hello' al español." No hay ambigüedad posible; el formato natural del modelo ya es el correcto.

Necesitas prompt engineering cuando:

  1. Producción: El output va a un sistema, a usuarios reales, o a código que lo procesa. Los fallos tienen consecuencia.

  2. Output parseable: Tu código espera JSON, una categoría específica, un número, un formato concreto. Un output mal formateado rompe el sistema.

  3. Consistencia requerida: El mismo input debe producir el mismo output (o un rango predecible). Multi-user, multi-region, multi-proveedor.

  4. Evaluación y mejora: Necesitas comparar versiones del prompt con métricas objetivas. Sin formato consistente, no puedes medir.

  5. Escala: El prompt se ejecuta 1,000+ veces al día. Cada token de más es costo real. Cada formato inconsistente es un fallo de parsing.

  6. Multi-proveedor: Tu prompt debe funcionar en OpenAI, Anthropic o cualquier otro modelo. Sin estructura explícita, el comportamiento varía.

# Regla de decisión práctica
def necesita_engineering(contexto: dict) -> tuple[bool, str]:
    """
    contexto: dict con flags del caso de uso.
    Retorna (necesita, razón).
    """
    if contexto.get("produccion"):
        return True, "Producción: consistencia crítica"
    if contexto.get("output_parseable"):
        return True, "Output parseable: formato estricto requerido"
    if contexto.get("llamadas_por_dia", 0) >= 100:
        return True, f"Escala: {contexto['llamadas_por_dia']} llamadas/día → optimizar tokens"
    if contexto.get("multi_proveedor"):
        return True, "Multi-proveedor: necesita instrucciones explícitas portables"
    if contexto.get("necesita_evaluacion"):
        return True, "Evaluación: sin formato consistente no puedes medir mejoras"
    return False, "Exploración/one-off: prompt casual suficiente"

# Ejemplos
casos = [
    {"descripcion": "Chatbot de soporte productivo", "produccion": True, "output_parseable": True},
    {"descripcion": "Script one-off para traducir 10 títulos", "llamadas_por_dia": 1},
    {"descripcion": "Extractor de datos que corre 500 veces/día", "llamadas_por_dia": 500, "output_parseable": True},
    {"descripcion": "Prototipo rápido para demo interna", "llamadas_por_dia": 5}
]

for caso in casos:
    necesita, razon = necesita_engineering(caso)
    tipo = "ENGINEERED" if necesita else "CASUAL"
    print(f"{caso['descripcion']}: [{tipo}] — {razon}")

Output:

Chatbot de soporte productivo: [ENGINEERED] — Producción: consistencia crítica
Script one-off para traducir 10 títulos: [CASUAL] — Exploración/one-off: prompt casual suficiente
Extractor de datos que corre 500 veces/día: [ENGINEERED] — Escala: 500 llamadas/día → optimizar tokens
Prototipo rápido para demo interna: [CASUAL] — Exploración/one-off: prompt casual suficiente

Framework de Comparación de Prompts

Cuando quieras medir si una versión nueva de un prompt es mejor que la anterior, necesitas un benchmark básico:

from typing import Callable
import time

def benchmark_prompts(
    fn_casual: Callable[[str], str],
    fn_engineered: Callable[[str], str],
    test_inputs: list[str],
    ground_truth: list[str],
    valid_formats: set[str]
) -> dict:
    """
    Compara dos versiones de prompt con métricas cuantitativas.
    
    Returns:
        dict con métricas de cada versión
    """
    results = {"casual": {}, "engineered": {}}
    
    for name, fn in [("casual", fn_casual), ("engineered", fn_engineered)]:
        outputs = []
        times = []
        
        for inp in test_inputs:
            start = time.time()
            output = fn(inp)
            elapsed = time.time() - start
            outputs.append(output)
            times.append(elapsed)
        
        # Métricas
        formato_valido = sum(1 for o in outputs if o in valid_formats)
        tokens_promedio = sum(len(o.split()) for o in outputs) / len(outputs)
        
        # Accuracy vs ground truth (si aplica)
        accuracy = None
        if ground_truth:
            correcto = sum(1 for o, g in zip(outputs, ground_truth) if o == g)
            accuracy = correcto / len(ground_truth)
        
        results[name] = {
            "consistencia_formato": formato_valido / len(test_inputs),
            "tokens_promedio": tokens_promedio,
            "latencia_promedio_s": sum(times) / len(times),
            "accuracy": accuracy,
            "outputs": outputs
        }
    
    return results

# Uso
ground_truth = ["NEGATIVO", "POSITIVO", "NEUTRO", "POSITIVO", "NEGATIVO"]

metricas = benchmark_prompts(
    fn_casual=clasificar_casual,
    fn_engineered=clasificar_engineered,
    test_inputs=test_inputs,
    ground_truth=ground_truth,
    valid_formats={"POSITIVO", "NEGATIVO", "NEUTRO"}
)

print("=== Benchmark Resultados ===\n")
for version, m in metricas.items():
    print(f"[{version.upper()}]")
    print(f"  Consistencia de formato: {m['consistencia_formato']:.0%}")
    print(f"  Tokens promedio:         {m['tokens_promedio']:.1f}")
    print(f"  Latencia promedio:       {m['latencia_promedio_s']:.2f}s")
    if m['accuracy'] is not None:
        print(f"  Accuracy vs ground truth: {m['accuracy']:.0%}")
    print()

Output esperado:

=== Benchmark Resultados ===

[CASUAL]
  Consistencia de formato: 20%
  Tokens promedio:         18.4
  Latencia promedio:       0.38s
  Accuracy vs ground truth: 60%

[ENGINEERED]
  Consistencia de formato: 100%
  Tokens promedio:         1.0
  Latencia promedio:       0.31s
  Accuracy vs ground truth: 100%

Impacto a Escala

Las diferencias parecen pequeñas en un solo prompt. A escala, cambian el costo operativo:

def calcular_costo_escala(
    tokens_promedio: float,
    llamadas_por_dia: int,
    precio_por_1k_tokens: float = 0.00015  # gpt-4o-mini output
) -> dict:
    """Calcula costo mensual estimado."""
    tokens_diarios = tokens_promedio * llamadas_por_dia
    costo_diario = (tokens_diarios / 1000) * precio_por_1k_tokens
    costo_mensual = costo_diario * 30
    
    return {
        "tokens_diarios": tokens_diarios,
        "costo_diario_usd": costo_diario,
        "costo_mensual_usd": costo_mensual
    }

llamadas_por_dia = 10_000  # Sistema real mediano

costo_casual = calcular_costo_escala(18.4, llamadas_por_dia)
costo_eng = calcular_costo_escala(1.0, llamadas_por_dia)

print(f"=== Impacto de tokens a {llamadas_por_dia:,} llamadas/día ===\n")
print(f"Prompt casual:     ${costo_casual['costo_mensual_usd']:.2f}/mes")
print(f"Prompt engineered: ${costo_eng['costo_mensual_usd']:.2f}/mes")
ahorro = costo_casual['costo_mensual_usd'] - costo_eng['costo_mensual_usd']
print(f"Ahorro mensual:    ${ahorro:.2f} (-{ahorro/costo_casual['costo_mensual_usd']:.0%})")

Output:

=== Impacto de tokens a 10,000 llamadas/día ===

Prompt casual:     $0.83/mes
Prompt engineered: $0.05/mes
Ahorro mensual:    $0.78 (-95%)

Nota: con gpt-4o-mini los costos son bajos, pero con gpt-4o o con modelos de pago los valores se multiplican 10-20x. El principio de minimizar tokens innecesarios aplica en todos los casos.


Conexión con el Proyecto

En el Prompt Analyzer (cápsula 08) implementarás una función que detecta si un prompt tiene características de "casual" o "engineered" según:

  • Presencia de system prompt (sí/no)
  • Componentes explícitos detectados (instrucción, formato, restricciones)
  • Temperatura especificada (sí/no)
  • Manejo de edge cases documentado

El score resultante es una de las "sugerencias de mejora" que generará el analyzer: "Este prompt es casual (2/5 características engineered). Añade system prompt y formato explícito para aumentar consistencia."


Troubleshooting

Problema 1: El prompt engineered a veces también falla

Causa: Edge cases no cubiertos en el diseño. Ningún prompt es 100% perfecto al primer intento.

Solución: Documenta los inputs que fallan, identifica el patrón, y añade al prompt:

  • En Insight: regla específica para ese tipo de input
  • En Experiment: ejemplo del edge case con su output correcto
# Agregar edge case al Experiment
EXPERIMENT_MEJORADO = """
...ejemplos previos...

Edge cases:
- Input vacío o solo espacios → NEUTRO
- Input con solo emojis positivos (😊👍) → POSITIVO
- Input en otro idioma → clasifica igualmente (el sentiment es universal)
"""

Problema 2: No sé qué métricas usar para comparar

Causa: Falta definición de "éxito" antes de comparar.

Solución: Define antes de ejecutar:

  • Para clasificación: accuracy vs ground truth (labels verificados por humanos)
  • Para extracción: precision y recall de campos (¿extrae todos? ¿solo los correctos?)
  • Para formato: % de respuestas que parsean sin error
  • Para costo: tokens promedio por llamada exitosa

Problema 3: Mi "ground truth" es difícil de construir

Causa: No tienes ejemplos etiquetados para medir accuracy.

Solución: Empieza con un conjunto pequeño pero representativo (20-50 ejemplos):

  1. Selecciona casos que cubran happy path, edge cases, y casos ambiguos
  2. Etiqueta manualmente (o con un modelo de alta calidad como "juez")
  3. Usa esos mismos casos en cada versión del prompt

Problema 4: La comparación no muestra diferencia notable

Causa: Los inputs de test son demasiado simples (no prueban edge cases).

Solución: Añade inputs que fuercen al modelo a elegir:

  • Textos mixtos (positivo y negativo en la misma oración)
  • Inputs con ironía o sarcasmo
  • Inputs muy cortos ("OK", "no")
  • Inputs con typos o informalidad

Ejercicios

Ejercicio 1: Diseñar tu comparación (Fácil)

Elige una tarea de tu preferencia (ej: extraer URLs de un texto, clasificar tipo de email, detectar idioma) y escribe (a) prompt casual y (b) prompt engineered. Ejecuta ambos 5 veces con el mismo input y compara consistencia de formato.

Ver solución
from openai import OpenAI
import json

client = OpenAI()

# Tarea: Extraer URLs de un texto

# (a) Prompt casual
def casual_urls(texto: str) -> str:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"Extrae las URLs de este texto: {texto}"}]
    )
    return r.choices[0].message.content

# (b) Prompt engineered
SYSTEM_URLS = """
Extrae todas las URLs de un texto.
Responde SOLO con JSON: {"urls": ["url1", "url2"]}
Si no hay URLs: {"urls": []}
Sin texto adicional.
"""

def engineered_urls(texto: str) -> dict:
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_URLS},
            {"role": "user", "content": texto}
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    return json.loads(r.choices[0].message.content)

# Test
texto = "Visita https://openai.com y también https://anthropic.com para más info"

print("Casual (5 ejecuciones):")
for _ in range(5):
    print(f"  {casual_urls(texto)[:80]}")

print("\nEngineered (5 ejecuciones):")
for _ in range(5):
    result = engineered_urls(texto)
    print(f"  {result}")
    # Verificar parseabilidad
    assert isinstance(result["urls"], list)

Métrica clave: ¿Cuántas de las 5 respuestas casuales puedes procesar directamente con código? ¿Cuántas de las engineered?


Ejercicio 2: Decidir el nivel apropiado (Fácil)

Para cada escenario, decide si prompt casual basta o si necesitas ingeniería, y justifica:

a) Herramienta interna para que un dev extraiga datos de logs manualmente
b) Chatbot de soporte que clasifica intención antes de rutear a equipo
c) Script one-off para traducir 10 títulos de marketing
d) API pública que procesa texto de usuarios y devuelve análisis JSON

Ver solución
  • (a) Herramienta interna — DEPENDE. Si el dev revisa manualmente: casual basta. Si el output alimenta otro sistema automáticamente: engineered.

  • (b) Chatbot de soporte — ENGINEERED. Criterios: producción, output parseable para routing, escala (muchas llamadas), consistencia crítica para UX. Fallar el routing afecta directamente al cliente.

  • (c) Script one-off — CASUAL. Criterios: una sola ejecución, revisión humana antes de usar, resultado no critico. Invertir en diseño aquí sería overkill.

  • (d) API pública — ENGINEERED. Criterios: producción, output JSON que usuarios parsearan en su código, multi-usuario, SLA de consistencia implícito, necesidad de evaluación y versioning.


Ejercicio 3: Construir el benchmark básico (Medio)

Extiende el benchmark_prompts del ejemplo para añadir una métrica de "tokens de input" (no solo output). ¿Por qué es relevante medir también los tokens del prompt en sí?

Ver solución
def benchmark_con_input_tokens(
    fn_casual: Callable,
    fn_engineered: Callable,
    system_casual: str,
    system_eng: str,
    test_inputs: list[str]
) -> dict:
    """
    Incluye tokens de input (system + user) en la comparación.
    Los tokens de input también se cobran y afectan el costo total.
    """
    import tiktoken
    enc = tiktoken.encoding_for_model("gpt-4o-mini")
    
    results = {}
    for name, fn, system in [
        ("casual", fn_casual, system_casual),
        ("engineered", fn_engineered, system_eng)
    ]:
        outputs = [fn(inp) for inp in test_inputs]
        
        # Tokens de input = sistema + cada mensaje de usuario
        tokens_input_total = 0
        for inp in test_inputs:
            tokens_input_total += len(enc.encode(system + inp))
        
        tokens_output_total = sum(len(enc.encode(o)) for o in outputs)
        tokens_totales_promedio = (tokens_input_total + tokens_output_total) / len(test_inputs)
        
        results[name] = {
            "tokens_input_promedio": tokens_input_total / len(test_inputs),
            "tokens_output_promedio": tokens_output_total / len(test_inputs),
            "tokens_totales_promedio": tokens_totales_promedio
        }
    
    return results

Por qué medir tokens de input: Un system prompt de 500 tokens se cobra en CADA llamada. Si el prompt casual tiene 10 tokens de system y el engineered tiene 200 tokens de system, el engineered puede ser más caro en input aunque sea más barato en output. El costo total = input tokens + output tokens.

La optimización real es: minimizar output tokens (están bien) + mantener system prompt lo más conciso posible sin perder precisión.


Ejercicio 4: Medir impacto de temperature en consistencia (Medio)

Ejecuta el mismo prompt casual con temperature 0, 0.5, y 1.0 (5 veces cada uno). ¿Cómo afecta temperature a la consistencia del formato?

Ver solución
from openai import OpenAI
client = OpenAI()

texto = "El producto es bueno pero el precio es muy alto"
prompt_simple = "¿Este texto es positivo o negativo?"

print("=== Impacto de temperature en consistencia ===\n")
for temp in [0.0, 0.5, 1.0]:
    outputs = []
    for _ in range(5):
        r = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"{prompt_simple}\n\n{texto}"}],
            temperature=temp
        )
        outputs.append(r.choices[0].message.content.strip()[:60])
    
    formatos_unicos = len(set(outputs))
    print(f"temperature={temp}:")
    for o in outputs:
        print(f"  '{o}'")
    print(f"  Formatos únicos: {formatos_unicos}/5 (menor = más consistente)\n")

Resultado típico:

  • temperature=0: 1-2 formatos únicos (alta consistencia)
  • temperature=0.5: 3-4 formatos únicos (media consistencia)
  • temperature=1.0: 4-5 formatos únicos (baja consistencia, máxima variabilidad)

Conclusión: Para clasificación en producción → temperature=0. Para generación creativa → temperature=0.7-1.0. El system prompt mejora la consistencia en todos los casos, pero temperature es multiplicador de variabilidad.


Resumen

En esta cápsula aprendiste:

  • Prompt casual: Sin system prompt, sin formato explícito, sin temperatura controlada → resultados variables, difícilmente parseables, costosos a escala
  • Prompt engineered: System prompt + formato explícito + temperature=0 + max_tokens controlado → resultados consistentes, parseables directamente, optimizados en costo
  • Métricas de comparación: Consistencia de formato (%), tokens promedio, accuracy vs ground truth, latencia, parseabilidad directa
  • Cuándo basta lo casual: Exploración personal, one-off con revisión humana, prototipado rápido
  • Cuándo necesitas ingeniería: Producción, output parseable por código, escala, evaluación sistemática, multi-proveedor
  • Impacto a escala: 18 tokens promedio vs 1 token = diferencia de 95% en costo a 10k llamadas/día
  • Framework de benchmark: benchmark_prompts() mide consistencia, tokens, latencia y accuracy — la base del Módulo 7 (Evaluation)

Próxima cápsula: Diferencias de comportamiento entre proveedores — por qué el prompt que funciona en OpenAI puede necesitar ajustes en Anthropic, y cómo diseñar prompts portables.


Recursos adicionales

  1. OpenAI Evals — Framework oficial para evaluar prompts con datasets y métricas, base para lo que harás en el Módulo 7
  2. OpenAI Pricing — Precios actuales por token para calcular impacto real a escala
  3. Prompt Engineering Best Practices — OpenAI — Guía oficial con estrategias para mejorar consistencia y reducir tokens
  4. tiktoken — Librería de OpenAI para contar tokens exactamente antes de ejecutar (evita sorpresas de costo)
  5. Anthropic Prompt Caching — Técnica para reducir costos en prompts con system prompts largos repetidos (preview del Módulo 8)
  6. BLEU, ROUGE y métricas de evaluación — Hugging Face — Métricas más formales que usarás en el Módulo 7 para evaluar calidad de outputs de texto libre