Módulo 2: Zero-Shot y Few-Shot Prompting

3. Few-Shot: Selección de Ejemplos

Descripción de la cápsula

La calidad del few-shot prompting depende críticamente de qué ejemplos eliges, cuántos usas, y en qué orden los presentas. Ejemplos mal elegidos pueden empeorar los resultados respecto a zero-shot. Ejemplos redundantes desperdician tokens sin mejorar accuracy. Ejemplos sesgados enseñan al modelo patrones incorrectos que luego son difíciles de diagnosticar.

En esta cápsula aprenderás el sweet spot de 3-5 ejemplos (por qué funciona), cómo seleccionar por diversidad y representatividad, el impacto del orden, y cuándo usar ejemplos negativos para cubrir edge cases. Todo con código Python ejecutable y métricas para verificar tus decisiones.

Por qué importa: En el Few-Shot Classification System del proyecto final, la calidad de los ejemplos en el banco determina directamente la accuracy del sistema. Si entiendes los principios de selección, puedes construir un banco que funcione bien desde el primer día en lugar de iterar a ciegas.


Cuántos Ejemplos Usar: El Sweet Spot

La curva de rendimiento

La investigación empírica muestra que la mayoría de tareas de clasificación siguen esta curva:

Accuracy
  │
  │          ●────────────────── plateau
  │        ●
  │      ●
  │    ●
  │  ●
  │●
  └─────────────────────────
  0    2    4    6    8   10
           # de ejemplos
  • 0 ejemplos (zero-shot): Accuracy base. Varía según familiaridad de la tarea.
  • 1-2 ejemplos: Mejora notable. El modelo "entiende" el formato.
  • 3-5 ejemplos: Sweet spot para la mayoría de tareas. Mejora máxima por token.
  • 6-10 ejemplos: Rendimientos decrecientes. Costo crece, accuracy se estabiliza.
  • 10+ ejemplos: Poco beneficio adicional. Considera fine-tuning si necesitas más.

Experimento: midiendo el impacto del número de ejemplos

from openai import OpenAI
import time

client = OpenAI()

# Banco completo de 8 ejemplos representativos
BANCO_COMPLETO = [
    ("Error 500 al cargar la página", "TÉCNICO"),
    ("No puedo iniciar sesión", "TÉCNICO"),
    ("Mi factura tiene un cargo incorrecto", "FACTURACIÓN"),
    ("¿Puedo cambiar mi plan mensual a anual?", "FACTURACIÓN"),
    ("Quiero cancelar mi cuenta permanentemente", "CUENTA"),
    ("¿Cómo actualizo mi email de contacto?", "CUENTA"),
    ("¿Cuándo es el próximo mantenimiento?", "INFORMACIÓN"),
    ("La app se cierra al abrir adjuntos", "TÉCNICO"),
]

# Ground truth para evaluar accuracy
TEST_CASES = [
    ("La plataforma no carga en Firefox", "TÉCNICO"),
    ("Cobro duplicado en mi tarjeta", "FACTURACIÓN"),
    ("Borrar todos mis datos de la cuenta", "CUENTA"),
    ("Error al exportar reportes", "TÉCNICO"),
    ("¿Tienen soporte en fin de semana?", "INFORMACIÓN"),
]

def clasificar_con_n_ejemplos(consulta: str, n: int) -> tuple[str, float]:
    """
    Clasifica usando n ejemplos del banco.
    Returns: (clasificación, latencia_ms)
    """
    ejemplos = BANCO_COMPLETO[:n]
    
    texto_ejemplos = "\n".join([
        f"Input: {inp}\nOutput: {out}" for inp, out in ejemplos
    ])
    
    prompt = f"""Clasifica consultas de soporte. Categorías: TÉCNICO, FACTURACIÓN, CUENTA, INFORMACIÓN.

Ejemplos:
{texto_ejemplos}

Clasifica (solo la categoría):
Input: {consulta}
Output:"""
    
    start = time.time()
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=20
    )
    latencia = (time.time() - start) * 1000
    
    return response.choices[0].message.content.strip(), latencia

# Benchmark para distintos N
print(f"{'N':>3} | {'Accuracy':>8} | {'Latencia avg':>12} | {'Tokens input':>12}")
print("-" * 45)

for n in [1, 2, 3, 5, 8]:
    correcto = 0
    latencias = []
    
    for consulta, ground_truth in TEST_CASES:
        resultado, lat = clasificar_con_n_ejemplos(consulta, n)
        if resultado == ground_truth:
            correcto += 1
        latencias.append(lat)
    
    accuracy = correcto / len(TEST_CASES)
    # Tokens aproximados: ~15 tokens por ejemplo + overhead
    tokens_est = n * 15 + 50
    
    print(f"{n:>3} | {accuracy:>8.0%} | {sum(latencias)/len(latencias):>12.0f}ms | ~{tokens_est:>10} tkns")

Output típico:

  N | Accuracy | Latencia avg | Tokens input
---------------------------------------------
  1 |      60% |         320ms | ~        65 tkns
  2 |      80% |         340ms | ~        80 tkns
  3 |      80% |         350ms | ~        95 tkns
  5 |     100% |         380ms | ~       125 tkns
  8 |     100% |         410ms | ~       170 tkns

Conclusión: De 3 a 5 ejemplos ya alcanzas el máximo. De 5 a 8, mismo accuracy, más tokens.


Selección por Diversidad

Por qué diversidad importa más que cantidad

Si tienes 5 ejemplos pero son variaciones del mismo input, el modelo aprende un patrón estrecho. Cinco ejemplos diversos enseñan al modelo el rango completo de la categoría.

# ❌ Ejemplos redundantes (todos son variaciones del mismo tipo)
EJEMPLOS_REDUNDANTES = [
    ("Error 404 en la página", "TÉCNICO"),
    ("Error 500 al cargar", "TÉCNICO"),
    ("Error al abrir la app", "TÉCNICO"),
    ("Pantalla de error al iniciar", "TÉCNICO"),
    ("El sistema da error", "TÉCNICO"),
]

# ✅ Ejemplos diversos (cubren distintos tipos de TÉCNICO y otras categorías)
EJEMPLOS_DIVERSOS = [
    ("Error 500 al cargar la página", "TÉCNICO"),        # Error de servidor
    ("La app se congela al subir archivos", "TÉCNICO"),   # Performance
    ("Mi factura tiene cobro duplicado", "FACTURACIÓN"),  # Billing
    ("Quiero cancelar mi cuenta", "CUENTA"),               # Account
    ("¿Cómo exporto mis datos?", "INFORMACIÓN"),           # Info request
]

Métrica de diversidad: Para verificar que tus ejemplos son diversos, mide la similitud promedio entre pares:

def medir_diversidad(ejemplos: list[tuple[str, str]]) -> float:
    """
    Calcula diversidad como 1 - similitud_promedio_entre_pares.
    Más cercano a 1.0 = más diverso.
    """
    def jaccard(a: str, b: str) -> float:
        wa = set(a.lower().split())
        wb = set(b.lower().split())
        if not wa or not wb:
            return 0
        return len(wa & wb) / len(wa | wb)
    
    if len(ejemplos) < 2:
        return 1.0
    
    similitudes = []
    for i in range(len(ejemplos)):
        for j in range(i + 1, len(ejemplos)):
            sim = jaccard(ejemplos[i][0], ejemplos[j][0])
            similitudes.append(sim)
    
    similitud_avg = sum(similitudes) / len(similitudes)
    return 1 - similitud_avg

# Test
div_redundantes = medir_diversidad(EJEMPLOS_REDUNDANTES)
div_diversos = medir_diversidad(EJEMPLOS_DIVERSOS)

print(f"Diversidad ejemplos redundantes: {div_redundantes:.2f}")  # ~0.60
print(f"Diversidad ejemplos diversos:    {div_diversos:.2f}")     # ~0.90

Regla práctica: Apunta a diversidad > 0.80. Si está por debajo, reemplaza los ejemplos más similares entre sí.


Selección por Representatividad

Cuándo la representatividad importa más que la diversidad

Si tu dominio tiene una distribución muy sesgada (ej: 70% de tickets son TÉCNICO, 15% FACTURACIÓN, 15% OTRO), tus ejemplos deben reflejar eso — o al menos no ignorarlo completamente.

# Distribución real del dominio
DISTRIBUCION_REAL = {
    "TÉCNICO": 0.65,
    "FACTURACIÓN": 0.20,
    "CUENTA": 0.10,
    "INFORMACIÓN": 0.05,
}

# ❌ Ejemplos desbalanceados (ignoran distribución real)
# 5 ejemplos con 1 por categoría = sobrerepresenta categorías raras
EJEMPLOS_BALANCEADOS_FORZADO = [
    ("Error al cargar", "TÉCNICO"),      # 1/5 = 20%
    ("Factura mal", "FACTURACIÓN"),      # 1/5 = 20%
    ("Cancelar cuenta", "CUENTA"),        # 1/5 = 20%
    ("¿Horario soporte?", "INFORMACIÓN"),# 1/5 = 20%
    ("App crashea", "TÉCNICO"),          # 1/5 = 20%
]

# ✅ Ejemplos que reflejan distribución (con 5 ejemplos, 3-2-1-0 o 3-1-1-0)
EJEMPLOS_REPRESENTATIVOS = [
    ("Error 500 al guardar", "TÉCNICO"),          # TÉCNICO dominante
    ("La app se cierra sola", "TÉCNICO"),           # TÉCNICO dominante
    ("App no responde después de update", "TÉCNICO"), # TÉCNICO dominante
    ("Mi factura tiene un cobro doble", "FACTURACIÓN"),# Segundo más común
    ("Quiero cambiar mi email", "CUENTA"),           # Tercer más común
    # INFORMACIÓN: tan poco frecuente que puede depender de zero-shot
]
CriterioDiversidadRepresentatividad
ObjetivoCubrir tipos distintos de inputsReflejar distribución real del dominio
Cuándo priorizarCategorías balanceadasDistribución fuertemente sesgada
Riesgo si ignorasFalla en inputs poco comunesSesgo hacia categorías sobrerepresentadas
Cómo medirDiversidad > 0.80Distribución de ejemplos ≈ distribución real

Regla práctica: Si todas las categorías tienen distribución similar → prioriza diversidad. Si una categoría domina (>60%) → incluye al menos 2 ejemplos de ella y asegura 1 de las demás.


El Impacto del Orden

Por qué el orden importa

Los LLMs tienen un sesgo conocido: tienden a imitar los ejemplos más recientes (recency bias). Si el último ejemplo antes del input real es de categoría A, el modelo tiene más probabilidad de clasificar el input como A, especialmente en casos ambiguos.

def clasificar_con_orden(consulta: str, ejemplos: list[tuple[str, str]]) -> str:
    """Clasifica con los ejemplos en el orden dado."""
    texto_ejs = "\n".join([f"Input: {i}\nOutput: {o}" for i, o in ejemplos])
    prompt = f"Clasifica. Categorías: TÉCNICO, FACTURACIÓN, CUENTA.\n\n{texto_ejs}\n\nInput: {consulta}\nOutput:"
    
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=15
    )
    return r.choices[0].message.content.strip()

# Test: mismo input, diferentes órdenes
consulta_ambigua = "Tengo un problema con mi cuenta y no puedo pagar"
# Esta consulta podría ser TÉCNICO, FACTURACIÓN o CUENTA

ejemplos_base = [
    ("Error al cargar la página", "TÉCNICO"),
    ("Mi factura tiene error", "FACTURACIÓN"),
    ("Quiero cambiar mis datos personales", "CUENTA"),
]

# Orden 1: termina con CUENTA
orden_1 = ejemplos_base.copy()

# Orden 2: termina con FACTURACIÓN
orden_2 = [ejemplos_base[0], ejemplos_base[2], ejemplos_base[1]]

# Orden 3: termina con TÉCNICO
orden_3 = [ejemplos_base[1], ejemplos_base[2], ejemplos_base[0]]

print(f"Consulta: '{consulta_ambigua}'\n")
print(f"Orden 1 (último=CUENTA):      {clasificar_con_orden(consulta_ambigua, orden_1)}")
print(f"Orden 2 (último=FACTURACIÓN): {clasificar_con_orden(consulta_ambigua, orden_2)}")
print(f"Orden 3 (último=TÉCNICO):     {clasificar_con_orden(consulta_ambigua, orden_3)}")

Resultado típico: La clasificación puede variar según el orden, especialmente en inputs ambiguos.

Reglas de ordenamiento

  1. Casos claros primero: Empieza con ejemplos inequívocos que anclan el patrón
  2. Edge cases al final: Si hay ejemplos ambiguos, ponlos al final — pero antes del input real
  3. Rotar si hay sesgo: Si detectas que el último ejemplo sesga el resultado, rota el banco entre llamadas
  4. Consistencia entre categorías: Si tienes 2+ ejemplos de una categoría, ponlos no consecutivos
# Orden recomendado para 5 ejemplos
ORDEN_OPTIMO = [
    ("Error 500 al cargar", "TÉCNICO"),       # 1. Claro, primer tipo
    ("Factura incorrecta este mes", "FACTURACIÓN"),  # 2. Claro, segundo tipo
    ("Cancelar mi suscripción hoy", "CUENTA"), # 3. Claro, tercer tipo
    ("La app crashea en iOS 17", "TÉCNICO"),   # 4. Segundo de TÉCNICO (más común)
    ("¿Cómo funciona el módulo X?", "INFORMACIÓN"),  # 5. Último: menos común
    # Input real viene aquí
]

# ❌ Orden problemático
ORDEN_MALO = [
    ("No sé qué hacer", "OTRO"),           # Empieza con caso vago
    ("Error 500 al cargar", "TÉCNICO"),
    ("No sé bien, creo que es un error", "TÉCNICO"),  # Ejemplo débil
    ("Factura incorrecta", "FACTURACIÓN"),
    ("No puedo entrar ni pagar", "TÉCNICO"), # Ambiguo al final → sesga
    # Input real viene aquí
]

Ejemplos Negativos

Cuándo usarlos

Los ejemplos negativos muestran explícitamente qué NO hacer. Son especialmente útiles para:

  • Edge cases que el modelo clasifica mal consistentemente
  • Categorías que parecen similares pero no lo son
  • Evitar sobre-clasificación hacia la categoría más común
from openai import OpenAI

client = OpenAI()

def clasificar_con_negativos(consulta: str) -> str:
    """
    Usa ejemplos negativos para clarificar el límite entre categorías similares.
    """
    prompt = """
Clasifica la consulta en: TÉCNICO, FACTURACIÓN, CUENTA, INFORMACIÓN.

## Ejemplos positivos (qué hacer)
Input: "El módulo de reportes no carga" → TÉCNICO
Input: "Mi factura de octubre tiene un cobro doble" → FACTURACIÓN
Input: "Quiero dar de baja mi cuenta" → CUENTA
Input: "¿Tienen soporte en español?" → INFORMACIÓN

## Ejemplos negativos (errores comunes a evitar)
Input: "No puedo acceder y tampoco veo mi factura"
  NO CLASIFICAR como: TÉCNICO (aunque menciona acceso)
  CLASIFICAR como: FACTURACIÓN (el problema principal es la factura)

Input: "¿Cómo cambio el plan para ver si sale más barato?"
  NO CLASIFICAR como: INFORMACIÓN (aunque hace una pregunta)
  CLASIFICAR como: FACTURACIÓN (el intent es sobre precios/planes)

## Clasifica:
"""
    r = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": prompt},
            {"role": "user", "content": consulta}
        ],
        temperature=0,
        max_tokens=15
    )
    return r.choices[0].message.content.strip()

# Test con casos que suelen confundirse
casos_ambiguos = [
    "No veo mi última factura y el sistema dice que tengo deuda",
    "¿Cuánto me costaría pasar al plan Pro?",
    "Error al pagar con mi tarjeta guardada"
]

for caso in casos_ambiguos:
    resultado = clasificar_con_negativos(caso)
    print(f"'{caso[:50]}' → {resultado}")

Regla: Añade un ejemplo negativo cuando veas que el modelo clasifica mal consistentemente el mismo tipo de input. Un ejemplo negativo bien colocado puede eliminar un error sistemático.


ExampleBank: Clase Reutilizable

from typing import Optional
import json

class ExampleBank:
    """
    Banco de ejemplos con selección dinámica y persistencia.
    """
    
    def __init__(self, categorias: list[str], max_por_prompt: int = 5):
        self.categorias = categorias
        self.max_por_prompt = max_por_prompt
        self.ejemplos: list[dict] = []
    
    def add(self, input_text: str, output: str, notas: str = "") -> None:
        """Añade ejemplo al banco con validación."""
        if output not in self.categorias:
            raise ValueError(f"Categoría '{output}' no válida. Válidas: {self.categorias}")
        if not input_text.strip():
            raise ValueError("Input no puede estar vacío")
        
        self.ejemplos.append({
            "input": input_text.strip(),
            "output": output,
            "notas": notas,
            "negativo": notas.startswith("NO:")  # Marca ejemplos negativos
        })
    
    def seleccionar(self, 
                    nuevo_input: str,
                    k: Optional[int] = None,
                    balancear_categorias: bool = True) -> list[dict]:
        """
        Selecciona los mejores ejemplos para el nuevo input.
        
        Args:
            nuevo_input: El input que se va a clasificar
            k: Número de ejemplos. Default: self.max_por_prompt
            balancear_categorias: Si True, incluye al menos 1 ejemplo por categoría si hay espacio
        """
        k = k or self.max_por_prompt
        if not self.ejemplos:
            return []
        
        # Calcular similitud
        def jaccard(a: str, b: str) -> float:
            wa = set(a.lower().split())
            wb = set(b.lower().split())
            if not wa or not wb:
                return 0
            return len(wa & wb) / len(wa | wb)
        
        # Score = similitud (más similar = más relevante)
        scored = [(jaccard(ej["input"], nuevo_input), ej) for ej in self.ejemplos]
        scored.sort(key=lambda x: x[0], reverse=True)
        
        if not balancear_categorias:
            return [ej for _, ej in scored[:k]]
        
        # Asegurar al menos 1 ejemplo por categoría
        seleccionados = []
        cats_cubiertas = set()
        
        # Primera pasada: 1 ejemplo por categoría (el más similar)
        for _, ej in scored:
            if ej["output"] not in cats_cubiertas and len(cats_cubiertas) < len(self.categorias):
                seleccionados.append(ej)
                cats_cubiertas.add(ej["output"])
        
        # Segunda pasada: rellenar hasta k con los más similares
        for _, ej in scored:
            if len(seleccionados) >= k:
                break
            if ej not in seleccionados:
                seleccionados.append(ej)
        
        return seleccionados[:k]
    
    def build_prompt(self, tarea: str, nuevo_input: str) -> str:
        """Construye el prompt few-shot completo."""
        ejemplos = self.seleccionar(nuevo_input)
        
        lineas = [tarea, ""]
        for i, ej in enumerate(ejemplos, 1):
            if ej.get("notas"):
                lineas.append(f"# {ej['notas']}")
            lineas.append(f"Input: {ej['input']}")
            lineas.append(f"Output: {ej['output']}")
            lineas.append("")
        
        lineas.append(f"Input: {nuevo_input}")
        lineas.append("Output:")
        
        return "\n".join(lineas)
    
    def save(self, path: str) -> None:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"categorias": self.categorias, "ejemplos": self.ejemplos}, f, 
                     ensure_ascii=False, indent=2)
    
    @classmethod
    def load(cls, path: str) -> "ExampleBank":
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
        bank = cls(data["categorias"])
        bank.ejemplos = data["ejemplos"]
        return bank

# Uso completo
bank = ExampleBank(
    categorias=["TÉCNICO", "FACTURACIÓN", "CUENTA", "INFORMACIÓN"],
    max_por_prompt=4
)

bank.add("Error 500 al cargar el dashboard", "TÉCNICO")
bank.add("Mi factura de enero tiene un cobro doble", "FACTURACIÓN")
bank.add("Quiero cancelar mi suscripción", "CUENTA")
bank.add("¿Tienen API disponible para desarrolladores?", "INFORMACIÓN")
bank.add("La app crashea al abrir notificaciones", "TÉCNICO")

# Construir prompt para nuevo input
nuevo = "El módulo de reportes no responde desde ayer"
prompt = bank.build_prompt(
    "Clasifica la consulta de soporte. Solo la categoría.",
    nuevo
)
print(prompt)

Troubleshooting

Problema 1: Más ejemplos empeoran el resultado

Causa: Ejemplos redundantes o contradictorios diluyen la señal. El modelo promedia patrones contradictorios.

Solución: Reduce a 3-4 ejemplos bien elegidos. Mide diversidad con medir_diversidad() — debe ser > 0.80. Elimina ejemplos con diversidad < 0.3 respecto a otro del banco.

Problema 2: El modelo ignora los ejemplos y da su propio formato

Causa: Hay demasiado texto entre los ejemplos y el input final, o el formato de los ejemplos no es suficientemente explícito.

Solución:

# ✅ Formato limpio y consistente
# Input: [texto]
# Output: [categoría]

# Añadir al final del prompt:
"Sigue exactamente el formato: una sola palabra en mayúsculas."

Problema 3: Sesgo hacia una categoría

Causa: Demasiados ejemplos de una categoría, o el último ejemplo siempre es de la misma.

Solución: Balancea distribución de ejemplos. Si una categoría tiene más ejemplos, usa el parámetro balancear_categorias=True del ExampleBank.

Problema 4: Edge cases fallan consistentemente

Causa: No hay ejemplos que cubran esos casos.

Solución: Añade ejemplos negativos para los tipos de input que fallan. Un ejemplo negativo bien diseñado puede eliminar un error sistemático.

Problema 5: El banco de ejemplos crece demasiado

Causa: Se van añadiendo ejemplos sin limpiar los redundantes.

Solución: Cada vez que el banco supera 20-30 ejemplos, ejecuta un dedup y elimina los que tienen diversidad < 0.3 respecto a otro ejemplo de la misma categoría.


Ejercicios

Ejercicio 1: Seleccionar los 4 mejores de 10 (Fácil)

Dado este banco de 10 ejemplos para clasificación TÉCNICO/FACTURACIÓN/CUENTA, selecciona los 4 mejores considerando diversidad y representatividad:

1. "Error 404" → TÉCNICO
2. "Error 500" → TÉCNICO
3. "Error al cargar" → TÉCNICO
4. "App crashea" → TÉCNICO
5. "Mi factura tiene error" → FACTURACIÓN
6. "Cobro incorrecto" → FACTURACIÓN
7. "Cancelar cuenta" → CUENTA
8. "Dar de baja servicio" → CUENTA
9. "Login no funciona" → TÉCNICO
10. "Cambiar plan de pago" → FACTURACIÓN
Ver solución

Selección recomendada: 4, 5, 7, 10

Razonamiento:

  • TÉCNICO (2 ejemplos porque es el más común): 4 "App crashea" (client-side) y 9 "Login no funciona" (diferente tipo)
    • Eliminados 1, 2, 3: muy similares a 4/9 (todos son errores del servidor)
  • FACTURACIÓN: 5 "Mi factura tiene error" (claro, específico)
    • Eliminado 6 "Cobro incorrecto": muy similar a 5
    • Eliminado 10 "Cambiar plan de pago": diferente tipo de facturación → también podría ser bueno como diversidad
  • CUENTA: 7 "Cancelar cuenta"
    • Eliminado 8 "Dar de baja servicio": semánticamente idéntico a 7

Diversidad de la selección (4, 9, 5, 7): ~0.85 (alta)


Ejercicio 2: Ordenar ejemplos (Fácil)

Ordena estos 4 ejemplos para maximizar claridad y minimizar sesgo de recencia:

A. "Algo no funciona bien" → TÉCNICO (ambiguo)
B. "Error al procesar mi pago" → FACTURACIÓN (claro)
C. "Quiero exportar mis datos antes de cancelar" → CUENTA (mixto)
D. "La app no abre en mi teléfono nuevo" → TÉCNICO (claro)
Ver solución

Orden óptimo: D, B, C, A

Razonamiento:

  • D primero: TÉCNICO claro, ancla el patrón
  • B segundo: FACTURACIÓN claro, establece segunda categoría
  • C tercero: CUENTA con matiz ("antes de cancelar"), añade complejidad
  • A al último: el más ambiguo — después de establecer el patrón, el modelo tiene más contexto para clasificar "algo no funciona" correctamente

Evitar: Poner A primero crea un ancla débil que puede confundir los siguientes ejemplos.


Ejercicio 3: Diseñar un ejemplo negativo (Medio)

El modelo clasifica consistentemente "¿Puedo devolver el producto si ya lo usé?" como DEVOLUCIÓN en lugar de POLÍTICA. Diseña un ejemplo negativo que corrija esto.

Ver solución
# Ejemplo negativo para clarificar POLÍTICA vs DEVOLUCIÓN
Input: "¿Puedo devolver un producto que ya usé una semana?"
  NO es: DEVOLUCIÓN (no está iniciando una devolución real)
  SÍ es: POLÍTICA (está preguntando sobre las reglas, no ejecutando una acción)

Comparar con positivo:
Input: "Quiero devolver el producto que compré ayer" → DEVOLUCIÓN (acción real)
Input: "¿Cuál es la política de devoluciones?" → POLÍTICA (pregunta sobre reglas)
Input: "¿Puedo devolver si ya lo usé?" → POLÍTICA (es una pregunta de política)

Clave: El ejemplo negativo muestra la distinción sutil: preguntar sobre si se puede hacer algo = POLÍTICA. Iniciar el proceso de hacerlo = DEVOLUCIÓN.


Ejercicio 4: Implementar ExampleBank con validación de calidad (Difícil)

Extiende la clase ExampleBank para que al momento de añadir un ejemplo, valide que no sea demasiado similar a uno ya existente (diversidad mínima del 30%).

Ver solución
class ExampleBankValidado(ExampleBank):
    """ExampleBank con validación de calidad al añadir ejemplos."""
    
    def __init__(self, categorias: list[str], max_por_prompt: int = 5, 
                 min_diversidad: float = 0.3):
        super().__init__(categorias, max_por_prompt)
        self.min_diversidad = min_diversidad
    
    def _jaccard(self, a: str, b: str) -> float:
        wa = set(a.lower().split())
        wb = set(b.lower().split())
        if not wa or not wb:
            return 0
        return len(wa & wb) / len(wa | wb)
    
    def add(self, input_text: str, output: str, notas: str = "", 
            forzar: bool = False) -> None:
        """
        Añade ejemplo con validación de diversidad.
        
        Args:
            forzar: Si True, añade aunque viole la diversidad mínima
        """
        if not forzar:
            for ej in self.ejemplos:
                sim = self._jaccard(input_text, ej["input"])
                if sim > (1 - self.min_diversidad):  # Muy similar a uno existente
                    raise ValueError(
                        f"Ejemplo muy similar (sim={sim:.2f}) a: '{ej['input']}'. "
                        f"Usa forzar=True si quieres añadirlo de todos modos."
                    )
        
        super().add(input_text, output, notas)

# Test
bank = ExampleBankValidado(["TÉCNICO", "FACTURACIÓN"], min_diversidad=0.3)
bank.add("Error al cargar la página", "TÉCNICO")

try:
    bank.add("Error al cargar el sistema", "TÉCNICO")  # Muy similar
except ValueError as e:
    print(f"Rechazado: {e}")

bank.add("La app crashea en iOS", "TÉCNICO")  # Diferente — aceptado
print(f"Banco: {len(bank.ejemplos)} ejemplos")

Resumen

En esta cápsula aprendiste:

  • Sweet spot: 3-5 ejemplos para la mayoría de tareas. Más de 5 = rendimientos decrecientes
  • Diversidad: Ejemplos que cubren distintos tipos de inputs > ejemplos que repiten el mismo patrón. Mide con diversidad > 0.80
  • Representatividad: Si una categoría domina el dominio, incluye más ejemplos de ella (no fuerces 1-por-categoría)
  • Orden: Casos claros primero, edge cases al final. Rota si detectas recency bias en inputs ambiguos
  • Negativos: Para eliminar errores sistemáticos en categorías similares o edge cases específicos
  • ExampleBank: Clase reutilizable con selección dinámica, balanceo, persistencia — base del proyecto final

Próxima cápsula: Example engineering avanzado — cómo generar ejemplos sintéticos con LLM, dynamic few-shot con embeddings, y bancos por dominio.


Recursos adicionales

  1. Language Models are Few-Shot Learners (Brown et al., 2020) — Paper original de GPT-3 con análisis del impacto del número de ejemplos y su selección
  2. What Makes Good In-Context Examples for GPT-3? — Análisis empírico de qué características hacen mejores los ejemplos de few-shot
  3. Rethinking the Role of Demonstrations (Min et al., 2022) — Investigación sobre impacto del orden y la distribución de labels en few-shot
  4. OpenAI Few-Shot Best Practices — Guía oficial con recomendaciones prácticas de selección
  5. Anthropic: Using Examples Effectively — Perspectiva de Claude sobre cuándo y cómo usar ejemplos few-shot