DEV Community

Moon Robert
Moon Robert

Posted on • Originally published at blog.rebalai.com

Prompt Engineering Avanzado: Lo que aprendí después de dos semanas rompiendo producción

Few-Shot: No es solo "dar ejemplos", es dar los ejemplos correctos

El concepto es simple: en lugar de describir la tarea, se la muestras al modelo con ejemplos entrada-salida. Pero hay una diferencia enorme entre poner ejemplos al azar y construir un conjunto de ejemplos que realmente orienten al modelo.

Cuando arreglé el clasificador de tickets, mi primer intento fue esto:

# ❌ Primera versión — ejemplos demasiado simples, no cubren casos límite
prompt = """Clasifica el ticket de soporte en una de estas categorías:
- Bug técnico
- Consulta de facturación  
- Solicitud de función
- Otro

Ejemplo 1:
Ticket: "No puedo iniciar sesión"
Categoría: Bug técnico

Ejemplo 2:
Ticket: "¿Cuánto cuesta el plan Pro?"
Categoría: Consulta de facturación

Ticket: "{ticket}"
Categoría:"""
Enter fullscreen mode Exit fullscreen mode

Funcionó mejor que antes, pero seguía fallando en casos ambiguos. "No puedo acceder a mi factura" se clasificaba como bug técnico porque la palabra "acceder" resonaba más con el primer ejemplo que con el segundo.

El problema era que mis ejemplos cubrían los casos fáciles. Lo que realmente necesitaba era cubrir los casos límite — los que confunden al modelo.

# ✓ Segunda versión — ejemplos estratégicos que cubren ambigüedades reales
EJEMPLOS = [
    # Casos que parecen bugs pero no lo son
    ("No puedo ver mi historial de pagos", "Consulta de facturación"),
    ("La aplicación dice que mi suscripción expiró pero pagué ayer", "Consulta de facturación"),

    # Casos que parecen consultas pero son bugs
    ("El botón de descarga no hace nada al hacer clic", "Bug técnico"),
    ("Cada vez que guardo, la página se recarga y pierdo todo", "Bug técnico"),

    # Casos genuinamente ambiguos — aquí decides la política de tu producto
    ("No puedo exportar mis datos", "Bug técnico"),  # decidimos que exportación es feature crítica
]

def construir_prompt(ticket: str) -> str:
    ejemplos_formateados = "\n\n".join(
        f"Ticket: \"{t}\"\nCategoría: {c}" 
        for t, c in EJEMPLOS
    )

    return f"""Clasifica el ticket de soporte. Categorías posibles:
- Bug técnico: algo que debería funcionar, no funciona
- Consulta de facturación: preguntas sobre pagos, suscripciones, facturas
- Solicitud de función: quieren algo nuevo
- Otro: no encaja en las anteriores

{ejemplos_formateados}

Ticket: "{ticket}"
Categoría:"""
Enter fullscreen mode Exit fullscreen mode

La precisión pasó de ~70% a ~91% en nuestro set de validación. No fue magia — fue elegir ejemplos que cubrieran las fronteras entre categorías.

Una cosa que noté: más de 8-10 ejemplos rara vez ayuda y a veces empeora las cosas. El modelo empieza a sobre-indexar en patrones superficiales del texto de ejemplo más que en entender la tarea. Yo me quedo en 5-7 ejemplos bien elegidos.


Chain-of-Thought: Forzar al modelo a "pensar en voz alta"

CoT surgió de un paper de Google en 2022 (Wei et al.) y la idea es que si le pides al modelo que muestre su razonamiento paso a paso antes de dar la respuesta final, la precisión en tareas complejas sube notablemente. No tengo certeza absoluta de por qué funciona a nivel de arquitectura — hay debates interesantes sobre si el modelo realmente "razona" o solo imita el patrón de razonamiento — pero empíricamente funciona, y eso me importa más ahora mismo.

La forma más simple es Zero-Shot CoT: agregas "Piensa paso a paso" al final del prompt. Y honestamente, esa frase tan tonta funciona sorprendentemente bien para problemas matemáticos o lógicos simples.

Pero donde realmente brilla es en la versión Few-Shot CoT, donde tus ejemplos incluyen el razonamiento explícito:

# Ejemplo de Few-Shot CoT para análisis de código
PROMPT_ANALISIS = """Analiza si este fragmento de código tiene problemas de seguridad.
Razona paso a paso antes de dar tu veredicto final.

---
Código:
Enter fullscreen mode Exit fullscreen mode


python
def get_user(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
return db.execute(query)

Análisis:
1. El parámetro `user_id` se inserta directamente en la query SQL sin sanitización
2. Si user_id viene del usuario (ej: de una URL), un atacante puede enviar: `1 OR 1=1`
3. Eso convertiría la query en `SELECT * FROM users WHERE id = 1 OR 1=1`, retornando todos los usuarios
4. Esto es una vulnerabilidad de SQL Injection clásica
Veredicto: INSEGURO — SQL Injection en línea 2

---
Código:
Enter fullscreen mode Exit fullscreen mode


python
def get_user(user_id):
query = "SELECT * FROM users WHERE id = ?"
return db.execute(query, (user_id,))

Análisis:
1. La query usa un placeholder `?` en lugar de f-string
2. El valor de `user_id` se pasa como parámetro separado al driver de la DB
3. El driver sanitiza automáticamente el valor antes de construir la query
4. No hay forma de inyectar SQL a través del parámetro
Veredicto: SEGURO — usa prepared statements correctamente

---
Código:
{codigo}
Análisis:"""
Enter fullscreen mode Exit fullscreen mode

El truco está en que el modelo "aprende" el formato del razonamiento — no solo la respuesta. En mis pruebas con Claude sonnet-4-6, este patrón redujo falsos negativos en análisis de seguridad de ~25% a ~8% comparado con un prompt simple que pedía directamente el veredicto.


El error que cometí con Self-Consistency (y cómo lo resolví)

Self-consistency es una extensión de CoT donde generas múltiples razonamientos independientes y tomas la respuesta por mayoría. El paper original (Wang et al., 2022) muestra mejoras de ~5-10 puntos porcentuales en benchmarks matemáticos.

Sonaba prometedor. Lo implementé así en mi primer intento:

# ❌ Mi error: temperatura demasiado baja, respuestas casi idénticas
respuestas = []
for _ in range(5):
    respuesta = llm.completar(
        prompt=mi_prompt,
        temperatura=0.1  # ← el error
    )
    respuestas.append(respuesta)

# Las 5 respuestas eran prácticamente iguales — votar entre ellas no aportaba nada
Enter fullscreen mode Exit fullscreen mode

El problema era obvio en retrospectiva: con temperatura baja, el modelo es casi determinista. Estaba votando entre 5 copias del mismo razonamiento. Para que self-consistency funcione, necesitas diversidad real en los caminos de razonamiento:

# ✓ Temperatura más alta para generar razonamientos genuinamente distintos
from collections import Counter

def self_consistency(prompt: str, n: int = 5) -> str:
    respuestas = []

    for _ in range(n):
        respuesta = llm.completar(
            prompt=prompt,
            temperatura=0.7,  # suficiente variación para razonamientos distintos
            max_tokens=500
        )
        # Extraer solo la respuesta final (no el razonamiento)
        veredicto = extraer_veredicto(respuesta)
        respuestas.append(veredicto)

    # Mayoría simple
    conteo = Counter(respuestas)
    return conteo.most_common(1)[0][0]
Enter fullscreen mode Exit fullscreen mode

Funciona. Pero tiene un costo real: 5 llamadas al API por consulta. En producción lo uso solo para decisiones de alto impacto donde el error es caro — análisis de contratos, clasificaciones que afectan el billing, ese tipo de cosas. Para el clasificador de tickets del principio, el costo no justificaba la mejora marginal.

No estoy 100% seguro de que esto escale bien más allá de tareas con respuestas discretas (clasificación, sí/no, verdadero/falso). Para tareas abiertas como redacción, "votar por mayoría" no tiene mucho sentido.


Combinando técnicas: lo que realmente uso en producción

Después de experimentar con esto un par de meses, mi patrón estándar para tareas complejas quedó así:

  1. Few-shot con ejemplos de borde para darle al modelo el formato y los casos difíciles
  2. CoT explícito en los ejemplos para tareas que requieren razonamiento (análisis, diagnóstico, evaluación)
  3. Self-consistency solo cuando el costo del error justifica el costo del API

Hay una técnica más que vale mencionar: Chain-of-Thought estructurado donde defines explícitamente los pasos del razonamiento. En lugar de dejar que el modelo decida cómo pensar, le das una plantilla:

PROMPT_DIAGNOSTICO = """Diagnostica el problema descrito en el reporte de error.

Sigue exactamente este proceso:
1. SÍNTOMA: Describe qué está fallando en una oración
2. CAUSAS POSIBLES: Lista 2-3 causas técnicas que podrían producir este síntoma
3. EVIDENCIA: Para cada causa, identifica qué información del reporte la soporta o descarta
4. DIAGNÓSTICO: Basándote en la evidencia, indica la causa más probable
5. PRÓXIMO PASO: Recomienda una sola acción concreta para confirmar o resolver

Reporte de error:
{reporte}

Diagnóstico:
1. SÍNTOMA:"""
Enter fullscreen mode Exit fullscreen mode

Esto es más rígido que el CoT libre, pero en contextos de ingeniería donde el proceso importa tanto como la respuesta (auditorías, debugging colaborativo, reportes) la estructura ayuda a que la salida sea parseable y consistente.

Una cosa que aprendí a las malas: no mezcles demasiado. Un prompt con few-shot + CoT + instrucciones de formato + restricciones largas se vuelve frágil. El modelo empieza a ignorar partes. Mi regla ahora es: si el prompt supera ~800 tokens de instrucciones, algo está mal en el diseño — probablemente debería dividir la tarea.


Mi recomendación real

Si tuvieras que empezar desde cero mañana:

Para clasificación y extracción: Few-shot con 5-7 ejemplos bien elegidos que cubran los casos límite. No necesitas CoT aquí. Es más barato y más rápido.

Para razonamiento, diagnóstico o análisis: Few-shot CoT — incluye el razonamiento en los ejemplos. La mejora en precisión vale el costo de tokens extra.

Para decisiones de alto impacto con respuestas discretas: Self-consistency sobre CoT, temperatura 0.6-0.8, 3-5 muestras. Caro, pero duermes mejor.

Para tareas de múltiples pasos complejas: Chain-of-Thought estructurado donde defines los pasos explícitamente.

Lo que no haría: usar estas técnicas ciegamente en producción sin un set de validación propio. Los benchmarks de los papers son bonitos, pero tu dominio específico puede comportarse diferente. Arma aunque sea 50-100 ejemplos etiquetados a mano y mide. En mi experiencia, invertir un día en construir ese set de validación te ahorra semanas de debugging de prompts.

El clasificador de tickets ahora corre en producción con 94% de precisión en nuestro set de validación. Sigue fallando ocasionalmente en casos realmente ambiguos — pero eso ya no es un problema de prompt engineering, es un problema de definición del producto.

Top comments (1)

Collapse
 
nyrok profile image
Hamza KONTE

Dos semanas rompiendo producción es exactamente el tipo de experiencia que cambia cómo piensas en los prompts para siempre. La parte sobre consistencia es clave — es mucho más fácil depurar cuando el prompt tiene bloques separados (rol, restricciones, formato de salida) en lugar de un párrafo mezclado.

Construí una herramienta gratuita que hace exactamente eso: descompone cualquier prompt en 12 bloques semánticos y los recompila en XML estructurado. flompt.dev / github.com/Nyrok/flompt