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

4. Few-Shot Avanzado: Example Engineering

Descripción de la cápsula

Example engineering va más allá de elegir ejemplos manualmente: incluye generar ejemplos sintéticos con LLMs cuando no tienes datos etiquetados, selección dinámica con embeddings (dynamic few-shot), bancos de ejemplos por dominio, y optimización del balance costo/calidad. Estas técnicas escalan el few-shot a escenarios de producción reales.

En esta cápsula aprenderás cuándo invertir en cada técnica, cómo implementarlas en código Python, y cómo medir si la inversión extra (costo de embeddings, tiempo de curación de sintéticos) justifica la mejora de accuracy. El resultado es un toolkit que puedes llevar directamente al Few-Shot Classification System del proyecto final.

Por qué importa: En producción, rara vez tienes exactamente los ejemplos correctos disponibles. Saber generar sintéticos válidos puede bootstrap un sistema de clasificación en horas en lugar de días esperando datos etiquetados reales.


Ejemplos Sintéticos Generados por LLM

Por qué generarlos

Cuando empiezas un nuevo dominio de clasificación, raramente tienes ejemplos etiquetados listos. Generarlos manualmente es lento (necesitas expertos de dominio). Los LLMs pueden generar ejemplos realistas para muchas tareas en minutos.

Caso de uso: Construir un banco inicial de ejemplos para bootstrapping, luego reemplazar progresivamente con ejemplos reales.

Generador básico

from openai import OpenAI
import json
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

def generar_ejemplos_sinteticos(
    descripcion_tarea: str,
    categorias: list[str],
    n_por_categoria: int = 3,
    temperatura: float = 0.7
) -> list[tuple[str, str]]:
    """
    Genera ejemplos sintéticos de clasificación usando un LLM.
    
    Args:
        descripcion_tarea: Qué tipo de texto se clasifica (ej: "consultas de soporte")
        categorias: Lista de categorías con su descripción
        n_por_categoria: Cuántos ejemplos generar por categoría
        temperatura: Mayor = más variedad, menor = más predecible
    
    Returns:
        Lista de (input_texto, categoría)
    """
    cats_str = "\n".join([f"- {c}" for c in categorias])
    
    prompt = f"""
Genera {n_por_categoria} ejemplos REALISTAS de "{descripcion_tarea}" para cada categoría.
Cada ejemplo debe ser corto (1-2 oraciones), natural, y claramente perteneciente a esa categoría.

Categorías:
{cats_str}

Formato de respuesta (JSON):
{{
  "ejemplos": [
    {{"input": "texto del ejemplo", "categoria": "NOMBRE_CATEGORIA"}},
    ...
  ]
}}

Genera exactamente {n_por_categoria * len(categorias)} ejemplos en total.
"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=temperatura,
        response_format={"type": "json_object"}
    )
    
    data = json.loads(response.choices[0].message.content)
    return [(ej["input"], ej["categoria"]) for ej in data.get("ejemplos", [])]

# Uso
categorias = [
    "TÉCNICO: errores de software, fallos de funcionalidad",
    "FACTURACIÓN: cobros, facturas, planes, pagos",
    "CUENTA: datos de perfil, cancelación, contraseña",
    "INFORMACIÓN: preguntas generales, dudas de uso"
]

ejemplos = generar_ejemplos_sinteticos(
    descripcion_tarea="consultas de soporte técnico SaaS",
    categorias=categorias,
    n_por_categoria=3
)

print(f"Generados {len(ejemplos)} ejemplos:\n")
for inp, cat in ejemplos:
    print(f"  [{cat}] {inp}")

Output típico:

Generados 12 ejemplos:

  [TÉCNICO] La app no carga el dashboard desde la actualización de ayer
  [TÉCNICO] Error 403 al intentar acceder a los reportes de Q3
  [TÉCNICO] El módulo de exportación se congela al seleccionar más de 100 registros
  [FACTURACIÓN] ¿Por qué me cobran dos veces el plan Enterprise este mes?
  [FACTURACIÓN] Necesito factura con datos fiscales para presentar al contador
  [FACTURACIÓN] Quiero cambiar de pago mensual a pago anual para obtener descuento
  [CUENTA] Cómo cambio el email asociado a mi cuenta sin perder mis datos
  [CUENTA] Quiero dar de baja mi cuenta y exportar todos mis datos primero
  [CUENTA] Necesito agregar dos administradores adicionales al equipo
  [INFORMACIÓN] ¿Tienen integración nativa con Slack?
  [INFORMACIÓN] ¿Cuánto tiempo tarda en aparecer un nuevo usuario activo en los reportes?
  [INFORMACIÓN] ¿El plan básico incluye acceso a la API?

Validar y filtrar sintéticos

No todos los ejemplos generados son buenos. Valida antes de añadir al banco:

def validar_ejemplo(input_text: str, categoria: str, categorias_validas: set[str]) -> tuple[bool, str]:
    """
    Valida que un ejemplo sea usable para few-shot.
    Returns: (es_valido, razón_si_invalido)
    """
    # Verificar que la categoría es válida
    if categoria not in categorias_validas:
        return False, f"Categoría '{categoria}' no válida"
    
    # Verificar longitud mínima
    palabras = len(input_text.split())
    if palabras < 4:
        return False, f"Muy corto ({palabras} palabras)"
    
    # Verificar longitud máxima (prompts muy largos pueden ser contraproducentes)
    if palabras > 80:
        return False, f"Muy largo ({palabras} palabras) para ejemplo de few-shot"
    
    # Verificar que no contiene instrucciones (no confundir al modelo)
    palabras_problema = ["clasifica", "categoriza", "responde", "ignora", "olvida"]
    lower = input_text.lower()
    for palabra in palabras_problema:
        if palabra in lower:
            return False, f"Contiene instrucción: '{palabra}'"
    
    return True, ""

def filtrar_ejemplos_validos(
    ejemplos: list[tuple[str, str]],
    categorias_validas: set[str]
) -> list[tuple[str, str]]:
    """Filtra ejemplos inválidos e imprime reporte."""
    validos = []
    rechazados = 0
    
    for inp, cat in ejemplos:
        es_valido, razon = validar_ejemplo(inp, cat, categorias_validas)
        if es_valido:
            validos.append((inp, cat))
        else:
            rechazados += 1
            print(f"  ❌ Rechazado: '{inp[:40]}...' — {razon}")
    
    print(f"\nResultado: {len(validos)}/{len(ejemplos)} válidos ({rechazados} rechazados)")
    return validos

# Test
cats_validas = {"TÉCNICO", "FACTURACIÓN", "CUENTA", "INFORMACIÓN"}
ejemplos_filtrados = filtrar_ejemplos_validos(ejemplos, cats_validas)

Flujo completo: bootstrap → validar → usar

def bootstrap_example_bank(
    descripcion: str,
    categorias_con_desc: list[str],
    n_inicial: int = 3,
    validar: bool = True
) -> list[tuple[str, str]]:
    """
    Flujo completo: generar → validar → devolver banco inicial.
    """
    # Extraer nombres de categorías de "NOMBRE: descripción"
    nombres_cats = {c.split(":")[0].strip() for c in categorias_con_desc}
    
    # Generar ejemplos
    print(f"Generando {n_inicial * len(categorias_con_desc)} ejemplos sintéticos...")
    ejemplos = generar_ejemplos_sinteticos(descripcion, categorias_con_desc, n_inicial)
    
    if validar:
        print("Validando ejemplos...")
        ejemplos = filtrar_ejemplos_validos(ejemplos, nombres_cats)
    
    print(f"\nBanco inicial listo: {len(ejemplos)} ejemplos")
    return ejemplos

Dynamic Few-Shot: Selección por Embeddings

Por qué importa la selección dinámica

Con un banco fijo de 5 ejemplos, usas los mismos para todos los inputs. Con selección dinámica, para cada input nuevo seleccionas los K ejemplos más similares semánticamente. Esto mejora accuracy especialmente cuando el banco es grande (20+ ejemplos) y diverso.

Input: "La app crashea en Android 14"
  
  Banco fijo (5 ejemplos):
    → Siempre los mismos 5, aunque 2 sean irrelevantes
    
  Dynamic few-shot (embeddings):
    → Selecciona los 3 más similares semánticamente: 
       "App se cierra en móvil" (0.85 sim)
       "Error en versión nueva" (0.79 sim)
       "Problema al actualizar" (0.72 sim)
    → Ignora ejemplos de FACTURACIÓN que no ayudan aquí

Implementación con OpenAI Embeddings

from openai import OpenAI
import numpy as np
from typing import Optional

client = OpenAI()

def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
    """Obtiene embedding de OpenAI. Costo: ~$0.00002 por 1K tokens."""
    response = client.embeddings.create(
        model=model,
        input=text
    )
    return response.data[0].embedding

def cosine_similarity(a: list[float], b: list[float]) -> float:
    """Similitud coseno entre dos vectores."""
    va = np.array(a)
    vb = np.array(b)
    return float(np.dot(va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb) + 1e-8))

class DynamicExampleBank:
    """
    Banco de ejemplos con selección dinámica por similaridad semántica.
    Pre-computa embeddings para eficiencia en producción.
    """
    
    def __init__(self, embedding_model: str = "text-embedding-3-small"):
        self.embedding_model = embedding_model
        self.ejemplos: list[dict] = []  # {input, output, embedding}
    
    def add(self, input_text: str, output: str) -> None:
        """Añade ejemplo y pre-computa su embedding."""
        embedding = get_embedding(input_text, self.embedding_model)
        self.ejemplos.append({
            "input": input_text,
            "output": output,
            "embedding": embedding
        })
        print(f"  Añadido: [{output}] '{input_text[:50]}'")
    
    def add_batch(self, ejemplos: list[tuple[str, str]]) -> None:
        """Añade múltiples ejemplos (una llamada a embeddings API por ejemplo)."""
        for inp, out in ejemplos:
            self.add(inp, out)
    
    def select(self, nuevo_input: str, k: int = 3) -> list[tuple[str, str]]:
        """
        Selecciona los K ejemplos más similares semánticamente.
        Solo calcula embedding del nuevo_input en runtime.
        """
        if not self.ejemplos:
            return []
        
        # Embedding del input actual (única llamada a la API en runtime)
        nuevo_emb = get_embedding(nuevo_input, self.embedding_model)
        
        # Comparar contra embeddings pre-computados
        scored = []
        for ej in self.ejemplos:
            sim = cosine_similarity(nuevo_emb, ej["embedding"])
            scored.append((sim, ej["input"], ej["output"]))
        
        # Ordenar por similaridad descendente
        scored.sort(key=lambda x: x[0], reverse=True)
        
        return [(inp, out) for _, inp, out in scored[:k]]
    
    def build_prompt(self, task_description: str, nuevo_input: str, k: int = 3) -> str:
        """Construye prompt con los K ejemplos más similares."""
        ejemplos = self.select(nuevo_input, k)
        
        lineas = [task_description, ""]
        for inp, out in ejemplos:
            lineas.append(f"Input: {inp}")
            lineas.append(f"Output: {out}")
            lineas.append("")
        lineas.append(f"Input: {nuevo_input}")
        lineas.append("Output:")
        
        return "\n".join(lineas)

# Uso
print("Construyendo banco dinámico...")
banco = DynamicExampleBank()

banco.add_batch([
    ("Error 500 al guardar datos del formulario", "TÉCNICO"),
    ("La app se congela al subir archivos grandes", "TÉCNICO"),
    ("No puedo hacer login desde el martes", "TÉCNICO"),
    ("Mi factura de octubre tiene un cobro doble", "FACTURACIÓN"),
    ("¿Puedo cambiar de pago mensual a anual?", "FACTURACIÓN"),
    ("Quiero cancelar mi cuenta y pedir reembolso", "CUENTA"),
    ("¿El plan básico incluye acceso a la API?", "INFORMACIÓN"),
])

# Dynamic selection en acción
consultas = [
    "La app crashea al abrir documentos PDF",
    "Cobro incorrecto en mi tarjeta de crédito",
    "¿Tienen descuento para startups?"
]

print("\n=== Dynamic Few-Shot Selection ===\n")
for consulta in consultas:
    ejemplos_seleccionados = banco.select(consulta, k=3)
    print(f"Input: '{consulta}'")
    print("Ejemplos seleccionados:")
    for inp, out in ejemplos_seleccionados:
        print(f"  [{out}] '{inp[:50]}'")
    print()

Output típico:

Input: 'La app crashea al abrir documentos PDF'
Ejemplos seleccionados:
  [TÉCNICO] 'La app se congela al subir archivos grandes'
  [TÉCNICO] 'Error 500 al guardar datos del formulario'
  [TÉCNICO] 'No puedo hacer login desde el martes'

Input: 'Cobro incorrecto en mi tarjeta de crédito'
Ejemplos seleccionados:
  [FACTURACIÓN] 'Mi factura de octubre tiene un cobro doble'
  [FACTURACIÓN] '¿Puedo cambiar de pago mensual a anual?'
  [CUENTA] 'Quiero cancelar mi cuenta y pedir reembolso'

Input: '¿Tienen descuento para startups?'
Ejemplos seleccionados:
  [INFORMACIÓN] '¿El plan básico incluye acceso a la API?'
  [FACTURACIÓN] '¿Puedo cambiar de pago mensual a anual?'
  [TÉCNICO] 'No puedo hacer login desde el martes'

Los ejemplos seleccionados son semánticamente relevantes para cada input.


K-Nearest Examples: Alternativa Sin Embeddings

Cuando el costo de embeddings es prohibitivo o trabajas offline:

def jaccard_similarity(a: str, b: str) -> float:
    """Similitud basada en palabras compartidas. Sin llamadas a API."""
    wa = set(a.lower().split())
    wb = set(b.lower().split())
    if not wa or not wb:
        return 0.0
    return len(wa & wb) / len(wa | wb)

def tfidf_like_similarity(a: str, b: str, vocabulary: Optional[set] = None) -> float:
    """Similitud mejorada con peso por rareza de palabras."""
    # Stopwords comunes en español
    STOPWORDS = {"el", "la", "los", "las", "un", "una", "de", "del", "que", "y", 
                 "en", "por", "para", "con", "es", "se", "al", "mi", "su"}
    
    def words(text: str) -> set:
        return set(w for w in text.lower().split() if w not in STOPWORDS)
    
    wa = words(a)
    wb = words(b)
    
    if not wa or not wb:
        return 0.0
    
    return len(wa & wb) / len(wa | wb)

def k_nearest_static(
    banco: list[tuple[str, str]],
    nuevo_input: str,
    k: int = 3,
    sim_fn = jaccard_similarity
) -> list[tuple[str, str]]:
    """K ejemplos más similares sin embeddings."""
    scored = [(sim_fn(inp, nuevo_input), inp, out) for inp, out in banco]
    scored.sort(key=lambda x: x[0], reverse=True)
    return [(inp, out) for _, inp, out in scored[:k]]

# Comparación de métodos de similaridad
banco_test = [
    ("Error al iniciar sesión", "TÉCNICO"),
    ("La app se cierra sola", "TÉCNICO"),
    ("Factura con cobro doble", "FACTURACIÓN"),
    ("Cambiar método de pago", "FACTURACIÓN"),
    ("Cancelar suscripción", "CUENTA"),
]

nuevo = "No puedo acceder con mi usuario y contraseña"

print("Jaccard similarity:")
for inp, out in k_nearest_static(banco_test, nuevo, k=3, sim_fn=jaccard_similarity):
    print(f"  [{out}] '{inp}'")

print("\nTFIDF-like similarity:")
for inp, out in k_nearest_static(banco_test, nuevo, k=3, sim_fn=tfidf_like_similarity):
    print(f"  [{out}] '{inp}'")
MétodoCostoCalidadCuándo usar
Fijo (siempre mismos)NingunoBuena si banco pequeñoBanco < 10 ejemplos
JaccardNegligible (CPU)Buena para palabras exactasSin API, banco pequeño
TFIDF-likeNegligible (CPU)Mejor que JaccardSin API, texto en español
Embeddings OpenAI~$0.00002/queryExcelente (semántico)Banco grande, producción

Domain-Specific Example Banks

Para sistemas que manejan múltiples dominios, mantén bancos separados y selecciona según el contexto:

from typing import Optional

class MultiDomainExampleBank:
    """
    Banco de ejemplos por dominio.
    Selecciona el banco correcto según señales del contexto.
    """
    
    def __init__(self):
        # Un banco por dominio
        self.bancos: dict[str, list[tuple[str, str]]] = {}
        # Palabras clave por dominio para routing
        self.keywords: dict[str, set[str]] = {}
    
    def add_domain(self, dominio: str, keywords: set[str]) -> None:
        """Registra un dominio con sus palabras clave de routing."""
        self.bancos[dominio] = []
        self.keywords[dominio] = keywords
    
    def add_example(self, dominio: str, input_text: str, output: str) -> None:
        """Añade ejemplo a un dominio específico."""
        if dominio not in self.bancos:
            raise ValueError(f"Dominio '{dominio}' no registrado")
        self.bancos[dominio].append((input_text, output))
    
    def detect_domain(self, texto: str) -> str:
        """
        Detecta el dominio más probable basado en palabras clave.
        Fallback a 'default' si no hay match.
        """
        texto_lower = texto.lower()
        scores = {}
        
        for dominio, keywords in self.keywords.items():
            score = sum(1 for kw in keywords if kw in texto_lower)
            scores[dominio] = score
        
        if not scores or max(scores.values()) == 0:
            return "default"
        
        return max(scores.items(), key=lambda x: x[1])[0]
    
    def get_examples(self, texto: str, k: int = 3) -> list[tuple[str, str]]:
        """Obtiene ejemplos del dominio más relevante."""
        dominio = self.detect_domain(texto)
        banco = self.bancos.get(dominio, self.bancos.get("default", []))
        return banco[:k]

# Configurar bancos por dominio
multi_bank = MultiDomainExampleBank()

multi_bank.add_domain("soporte", {"error", "no funciona", "falla", "problema", "bug"})
multi_bank.add_domain("ventas", {"precio", "costo", "plan", "descuento", "comprar", "upgrade"})
multi_bank.add_domain("legal", {"contrato", "términos", "privacidad", "gdpr", "datos personales"})

multi_bank.add_example("soporte", "Error 500 al cargar", "TÉCNICO")
multi_bank.add_example("soporte", "La app no abre", "TÉCNICO")
multi_bank.add_example("ventas", "¿Cuánto cuesta el plan Pro?", "PRECIO")
multi_bank.add_example("ventas", "¿Tienen descuento anual?", "PROMO")
multi_bank.add_example("legal", "¿Dónde están sus servidores?", "PRIVACIDAD")

# Test
consultas = [
    "Error al cargar el módulo de reportes",
    "¿Puedo negociar el precio del plan Enterprise?",
    "¿Son GDPR compliant?"
]

for c in consultas:
    dominio = multi_bank.detect_domain(c)
    ejemplos = multi_bank.get_examples(c, k=2)
    print(f"'{c[:50]}' → Dominio: {dominio}, Ejemplos: {[out for _, out in ejemplos]}")

Optimización de Costos: Pre-Computar Embeddings

En producción, nunca recalcules embeddings de ejemplos fijos en cada request:

import numpy as np
import json
import os
from pathlib import Path

def precompute_and_cache_embeddings(
    ejemplos: list[tuple[str, str]],
    cache_path: str = "embeddings_cache.json"
) -> list[dict]:
    """
    Pre-computa embeddings y los cachea en disco.
    En runtime, solo necesitas calcular embedding del input nuevo.
    """
    if Path(cache_path).exists():
        print(f"Cargando embeddings desde caché: {cache_path}")
        with open(cache_path) as f:
            return json.load(f)
    
    print(f"Computando {len(ejemplos)} embeddings (se hace una sola vez)...")
    banco_con_embeddings = []
    
    for inp, out in ejemplos:
        emb = get_embedding(inp)
        banco_con_embeddings.append({
            "input": inp,
            "output": out,
            "embedding": emb  # Lista de floats, serializable a JSON
        })
    
    with open(cache_path, "w") as f:
        json.dump(banco_con_embeddings, f)
    
    print(f"Embeddings guardados en {cache_path}")
    return banco_con_embeddings

def select_from_cached(
    banco_cached: list[dict],
    nuevo_input: str,
    k: int = 3
) -> list[tuple[str, str]]:
    """Selecciona k más similares usando embeddings pre-computados."""
    nuevo_emb = np.array(get_embedding(nuevo_input))
    
    scored = []
    for ej in banco_cached:
        emb = np.array(ej["embedding"])
        sim = cosine_similarity(nuevo_emb.tolist(), emb.tolist())
        scored.append((sim, ej["input"], ej["output"]))
    
    scored.sort(reverse=True)
    return [(inp, out) for _, inp, out in scored[:k]]

# Uso en producción
# La primera vez: computa y cachea
banco_cached = precompute_and_cache_embeddings(ejemplos_filtrados, "mi_banco.json")

# En cada request: solo embeddea el input nuevo (1 llamada a API)
resultado = select_from_cached(banco_cached, "El módulo de exportación falla")

Impacto en costo: Con banco de 20 ejemplos, sin pre-computar = 21 llamadas a embeddings/request. Con pre-computar = 1 llamada/request. Reducción del 95%.


Conexión con el Proyecto

En el Few-Shot Classification System (cápsula 08) usarás:

  • DynamicExampleBank para selección por embeddings en el modo few-shot avanzado
  • generar_ejemplos_sinteticos() para bootstrap del banco si no hay datos etiquetados
  • precompute_and_cache_embeddings() para optimizar costo en el evaluador comparativo
  • MultiDomainExampleBank como base para el modo multi-dominio opcional

Troubleshooting

Problema 1: Ejemplos sintéticos no son realistas

Causa: El prompt de generación es vago o la temperature es muy alta.

Solución:

# Sé más específico en el prompt de generación
prompt_mejorado = f"""
Genera {n} consultas REALISTAS para un sistema de soporte de empresa SaaS B2B.
Los usuarios son técnicos o managers, no consumidores finales.
Cada consulta debe sonar como un email de soporte real (1-2 oraciones).
Evita frases genéricas como "No funciona" o "Tengo un problema".
"""

Problema 2: Dynamic few-shot selecciona ejemplos de categoría equivocada

Causa: Similitud semántica puede seleccionar ejemplos de distinta categoría si el input es ambiguo.

Solución: Añadir filtro de diversidad de categorías en la selección:

def select_balanced(banco_cached, nuevo_input, k=3, max_por_cat=2):
    """Selecciona ejemplos equilibrados por categoría."""
    todos = select_from_cached(banco_cached, nuevo_input, k=k*2)  # Pide más
    
    seleccionados = []
    conteo_cat = {}
    
    for inp, out in todos:
        if conteo_cat.get(out, 0) < max_por_cat:
            seleccionados.append((inp, out))
            conteo_cat[out] = conteo_cat.get(out, 0) + 1
        if len(seleccionados) >= k:
            break
    
    return seleccionados

Problema 3: Costo de embeddings demasiado alto

Causa: Calculando embedding del banco en cada request.

Solución: Pre-computa y cachea embeddings del banco. Solo calcula embedding del input nuevo en runtime. El costo pasa de O(banco) a O(1) por request.

Problema 4: El banco sintético tiene bias hacia ciertos ejemplos

Causa: El LLM generador tiene sus propios sesgos y puede sobrerepresentar ciertos tipos de inputs.

Solución: Genera 10+ por categoría y selecciona los más diversos con medir_diversidad(). Añade ejemplos reales progresivamente para reemplazar los sintéticos.


Ejercicios

Ejercicio 1: Bootstrap con sintéticos (Fácil)

Genera un banco de 12 ejemplos (3 por categoría) para un clasificador de intención de emails de marketing: COMPRA, CONSULTA, QUEJA, BAJA.

Ver solución
cats = [
    "COMPRA: usuario quiere comprar o ya compró algo",
    "CONSULTA: usuario tiene pregunta sobre producto/servicio",
    "QUEJA: usuario está insatisfecho o tiene problema con compra",
    "BAJA: usuario quiere darse de baja o cancelar"
]

ejemplos = generar_ejemplos_sinteticos(
    descripcion_tarea="emails de clientes de tienda online",
    categorias=cats,
    n_por_categoria=3
)

# Validar
cats_validas = {"COMPRA", "CONSULTA", "QUEJA", "BAJA"}
ejemplos_ok = filtrar_ejemplos_validos(ejemplos, cats_validas)
print(f"Banco listo: {len(ejemplos_ok)} ejemplos válidos")

Ejercicio 2: Dynamic few-shot con banco pequeño (Medio)

Dado un banco de 8 ejemplos de emails (URGENTE/NORMAL/BAJA_PRIORIDAD), implementa selección dinámica usando Jaccard (sin embeddings). Verifica que para "SISTEMA CAÍDO URGENTE" selecciona ejemplos de URGENTE.

Ver solución
banco = [
    ("Sistema completamente caído, clientes afectados", "URGENTE"),
    ("Error crítico en producción", "URGENTE"),
    ("¿Cuándo estará lista la feature X?", "NORMAL"),
    ("Seguimiento a ticket anterior", "NORMAL"),
    ("Información sobre la próxima versión", "BAJA_PRIORIDAD"),
    ("¿Pueden añadir esta funcionalidad?", "BAJA_PRIORIDAD"),
    ("Timeout en 2 de 100 requests", "NORMAL"),
    ("Error intermitente al exportar", "NORMAL"),
]

nuevo = "SISTEMA CAÍDO URGENTE clientes no pueden acceder"
seleccionados = k_nearest_static(banco, nuevo, k=3, sim_fn=jaccard_similarity)
print("Seleccionados para input de sistema caído:")
for inp, out in seleccionados:
    print(f"  [{out}] {inp}")

# Verificar que URGENTE aparece
assert any(out == "URGENTE" for _, out in seleccionados), "Debería seleccionar ejemplos URGENTE"
print("✅ Correctamente selecciona URGENTE")

Ejercicio 3: Pre-computar y usar caché (Medio)

Implementa el flujo completo: genera 9 ejemplos sintéticos → pre-computa embeddings → guarda en JSON → carga desde JSON → usa para dynamic selection.

Ver solución
CACHE = "banco_cache.json"

# 1. Generar ejemplos
ejemplos = generar_ejemplos_sinteticos(
    "soporte técnico",
    ["TÉCNICO: errores de software", "FACTURACIÓN: cobros y pagos", "CUENTA: perfil de usuario"],
    n_por_categoria=3
)

# 2. Pre-computar y cachear
banco_cached = precompute_and_cache_embeddings(ejemplos, CACHE)
print(f"Caché creado con {len(banco_cached)} ejemplos")

# 3. Verificar que la segunda carga es desde caché
banco_cached_2 = precompute_and_cache_embeddings(ejemplos, CACHE)
assert len(banco_cached) == len(banco_cached_2)
print("✅ Segunda carga desde caché (más rápida, sin llamadas a API)")

# 4. Usar para clasificar
nuevo = "Error al sincronizar con Salesforce"
seleccionados = select_from_cached(banco_cached, nuevo, k=3)
print(f"\nPara '{nuevo}':")
for inp, out in seleccionados:
    print(f"  [{out}] {inp[:50]}")

Resumen

En esta cápsula aprendiste:

  • Ejemplos sintéticos: Genera con LLM cuando no tienes datos etiquetados. Valida antes de usar (longitud, categoría válida, sin instrucciones embebidas)
  • Dynamic few-shot: Selecciona ejemplos según similaridad con el input actual. Mejora accuracy cuando el banco es grande y diverso
  • Embeddings vs Jaccard: Embeddings = mejor calidad semántica, costo de API. Jaccard = sin costo, suficiente para textos cortos en el mismo idioma
  • Pre-computar embeddings: Calcula una vez, guarda en JSON. En runtime solo embeddeas el input nuevo. Reduce costo 90-95%
  • Multi-domain banks: Bancos separados por dominio + routing por keywords. Escalable a muchos dominios sin mezclar ejemplos

Próxima cápsula: Output formatting y parsing — cómo garantizar que el output del LLM sea siempre parseable por código.


Recursos adicionales

  1. OpenAI Embeddings API — Documentación oficial de text-embedding-3-small y text-embedding-3-large, con precios y casos de uso
  2. What Makes Good In-Context Examples? — Análisis de qué características hacen que un ejemplo sea bueno para few-shot
  3. Self-Generated In-Context Learning (SG-ICL) — Paper sobre generación automática de ejemplos para few-shot
  4. Sentence Transformers — Alternativa open-source para embeddings semánticos sin coste de API (para entornos offline)
  5. FAISS (Facebook AI Similarity Search) — Para búsqueda de K-nearest en bancos de ejemplos muy grandes (100K+)
  6. NumPy Dot Product — Referencia para el cálculo de similitud coseno utilizado en el ejemplo