SEXTANTEcursos técnicos de IA
métodobackward-design
árbitroel dato
Entrar
N1 · Error analysis/L5

Datos sintéticos con criterio

Objetivo de maestría

decidirás cuándo generar datos sintéticos para tu dataset y cuándo no, y los producirás por el proceso de dos pasos (dimensiones → naturalización). Sin criterio, generar sintéticos llena tu dataset de ruido que degrada la señal en lugar de cubrir el gap real.


5.1El caso raro que casi no tienes

Acabas de etiquetar el dataset de Aurora —la tienda online ficticia que es tu banco de pruebas todo el curso—. Tienes la taxonomía priorizada y los casos con etiqueta binaria del checkpoint anterior.

Repasas la cobertura por tipo de consulta. Casi todo cuadra. Pero hay un hueco que te incomoda: la categoría "devolución de un producto frágil enviado a Canarias con seguro". Es un caso raro. Y es caro: si el agente lo resuelve mal, hay una reclamación con coste real detrás.

Buscas cuántas trazas reales tienes de ese caso. Dos. Solo dos.

Con dos ejemplos no puedes medir nada con confianza. La tentación es directa: abres el SDK de Anthropic y le pides al modelo "genera 50 conversaciones de devoluciones para Aurora".

Lo haces. Lees las 50. Y el resultado te deja un mal sabor: 47 son variaciones genéricas de "quiero devolver mi pedido", educadas y planas. Ninguna toca el caso frágil-Canarias-con-seguro que de verdad te faltaba. Has generado volumen, no cobertura. Y peor: esas 47 variaciones genéricas ahora pesan en tu dataset y emborronan la señal del caso que importaba.

No te faltaban datos. Te faltaba criterio sobre qué datos generar y cómo.


5.2Qué vas a poder hacer

Al terminar esta lección sabrás:

  • Decidir, ante un gap del dataset, si la respuesta correcta es generar sintéticos, arreglarlo en el prompt, o conseguir más datos reales.
  • Generar datos sintéticos por el proceso de dos pasos: tuplas de dimensiones primero, naturalización a lenguaje natural después.
  • Nombrar el riesgo de abusar de los sintéticos —model collapse— y la mitigación que lo acota.

Necesitas saber antes:

  • Qué es un caso con etiqueta binaria y por qué necesitas balance entre casos que pasan y que fallan (de N1·L4).
  • Por qué el error analysis sobre datos reales va primero (de N1·L1).
  • La firma de messages.create() del SDK de Anthropic, que ya usaste para instrumentar al agente en N0.

Esta lección no te enseña a generar sintéticos a lo bruto. Te enseña a decidir cuándo merecen la pena y a producirlos sin contaminar el dataset que tanto te costó etiquetar.


5.3Recupera

Antes de seguir, responde mentalmente. No mires lo de abajo hasta tener una respuesta.

  1. En N1·L4 separaste tu dataset en casos que pasan y casos que fallan un criterio. ¿Por qué necesitabas los dos, y no solo los que fallan?
  2. En N1·L1 viste que el error analysis sobre conversaciones reales precede a montar métricas. ¿Por qué el orden importa: datos reales primero?
  3. Aurora tiene dos trazas reales del caso frágil-Canarias. ¿Por qué dos no bastan para medir si el agente lo maneja bien?

La respuesta a la 1: sin casos que pasan, un evaluador no aprende la frontera entre bien y mal; solo ve fallos y no sabe qué distingue un acierto. La 2: las métricas genéricas no correlacionan con los fallos reales de tu producto; mirar datos reales te dice qué medir antes de medirlo. La 3: con dos ejemplos, un acierto o un fallo mueve la tasa del 0% al 50% al 100%. La medición es pura suerte muestral.


5.4El concepto: sintéticos como expansión, no como atajo

Empecemos por el orden, porque ahí se equivoca casi todo el mundo.

La regla de orden: reales primero, sintéticos después

Los datos sintéticos son ejemplos de evaluación generados por un modelo, no recogidos de tráfico real. Sirven para una cosa concreta: expandir la cobertura de tu dataset en dimensiones que tu error analysis ya identificó (Hamel Husain, erroranalysis office hours, dic 2024).

Lee esa frase otra vez: dimensiones que tu error analysis ya identificó. Los sintéticos llegan después del análisis con datos reales, nunca antes. El error analysis te dice dónde tienes huecos; los sintéticos los rellenan. Si generas antes de mirar, generas a ciegas: produces lo que el modelo cree que pasa, no lo que de verdad le pasa a Aurora.

La analogía: los sintéticos son como un suplemento alimenticio. Complementan una dieta real ya analizada; no la sustituyen. Esta analogía falla si la llevas lejos —un dataset no "absorbe nutrientes"—, pero captura lo esencial: primero comes comida de verdad, luego suplementas el déficit que mediste.

El antipatrón: no generes lo que se arregla en el prompt

Aquí está la trampa más común. Es habitual confundir "el dataset no cubre este comportamiento" con "necesito generar datos para este comportamiento". No son lo mismo.

La regla del corpus es tajante: no generes sintéticos para lo que se arregla directamente en el prompt (Hamel Husain, evals-faq, ene 2026).

Un ejemplo de Aurora. Detectas que el agente saluda con mayúsculas raras: "HoLa, ¿En Qué PuEdo AyUdaRTE?". Es un fallo real, pero su causa está en una instrucción del prompt del sistema. Generar 30 conversaciones sintéticas de saludos no te da nada: no necesitas medir este fallo en un dataset, necesitas corregir una línea del prompt y comprobar que desapareció. Generar sintéticos aquí es trabajo desperdiciado que además infla el dataset.

El criterio para decidir, ante un gap, se reduce a tres opciones:

  • Arréglalo en el prompt — si la causa es una instrucción y la corrección es directa. No generes nada.
  • Consigue más datos reales — si el caso ocurre de verdad con frecuencia y solo necesitas esperar/muestrear más tráfico.
  • Genera sintéticos — si el caso es raro en producción pero importa (poco frecuente, alto impacto) y no puedes esperar a acumular reales.

El caso frágil-Canarias de Aurora cae en la tercera casilla: raro, caro, y no vas a esperar meses a que lleguen 50 trazas reales.

Por qué no generar "a pelo": el proceso de dos pasos

Recuerda las 47 variaciones genéricas del §5.1. No fue mala suerte. La generación directa con un LLM —pedirle "genera 50 conversaciones"— tiende a producir outputs genéricos y a omitir los escenarios raros (Hamel Husain, evals-faq, ene 2026). El modelo gravita hacia lo típico: justo lo que no te faltaba.

La solución es separar la generación en dos pasos:

  • Paso 1 — dimensiones. Defines los ejes que caracterizan el caso y enumeras tuplas estructuradas combinándolos. Para Aurora: producto (frágil / no-frágil) × región (Canarias / península) × tono (tranquilo / enfadado). Cada tupla es una celda de esa rejilla. Aquí controlas tú la cobertura: si quieres el caso frágil-Canarias-enfadado, lo pones explícitamente como tupla.
  • Paso 2 — naturalización. Conviertes cada tupla, por separado, en una conversación realista en lenguaje natural. El modelo solo "viste" la tupla de palabras; no decide qué casos existen.

Separar los pasos te devuelve el control sobre la distribución. En el paso 1 decides qué combinaciones cubrir —incluidas las raras—; en el paso 2 las redactas. Pedirlo todo de una vez deja la distribución en manos del modelo, que la sesgará hacia lo común.

El riesgo de abusar: model collapse

Una advertencia honesta antes del código. Imagina que alimentas un sistema con sus propios outputs sintéticos de forma recursiva: sintéticos sobre sintéticos sobre sintéticos. Aparece el model collapse. La distribución se degrada vuelta tras vuelta, las colas raras desaparecen y el sistema converge hacia un promedio empobrecido (arXiv:2503.14023, mar 2025).

La mitigación que el paper documenta: acumular los sintéticos junto a los datos reales, no reemplazarlos. Mantener los reales en la mezcla acota el error y evita la espiral.

Un matiz que te debo: ese paper estudia entrenamiento de modelos, no datasets de evaluación. La aplicabilidad directa a evals queda por verificar. Aun así, la intuición transfiere y la regla práctica es la misma que ya conoces: los sintéticos complementan a los reales, nunca los sustituyen. Por eso los etiquetarás como sintéticos —lo verás en el código— para no perder de vista cuánto de tu señal es generado.


5.5Míralo funcionar: el caso frágil-Canarias en dos pasos

Vamos a generar, de punta a punta, los casos sintéticos del gap de Aurora. El código corre con el SDK de Anthropic que ya usaste en N0.

Una advertencia antes de leer: este bloque encadena dos llamadas a la API con un paso de parseo JSON en medio. Lee el código entero una vez para ver la forma general; después lo recorremos por tramos. El aviso de la doc oficial es importante —temperature no se soporta en los modelos Opus 4.7 y posteriores, así que para generación sintética usamos claude-sonnet-4-6—.

python
1import anthropic
2import json
3
4client = anthropic.Anthropic()  # lee ANTHROPIC_API_KEY del entorno
5
6# ── PASO 1: generar tuplas estructuradas de dimensiones ───────────────────────
7# El gap del error analysis: "devolución de producto frágil enviado a Canarias".
8# Dimensiones: producto (frágil/no-frágil) × región (Canarias/península) × tono.
9
10resp_paso1 = client.messages.create(
11    model="claude-sonnet-4-6",   # ajusta al modelo de tu cuenta
12    max_tokens=1024,
13    system=(
14        "Eres un generador de casos de prueba para el agente de soporte de Aurora "
15        "(e-commerce). Devuelve SOLO JSON válido con la estructura pedida."
16    ),
17    messages=[
18        {
19            "role": "user",
20            "content": (
21                "Genera 3 tuplas para el gap: 'devolución de producto frágil'. "
22                "Formato JSON: [{\"producto\": str, \"region\": str, "
23                "\"tono\": str, \"fallo_esperado\": str}]. "
24                "Varía: producto (fragil/no-fragil), region (canarias/peninsula), "
25                "tono (tranquilo/enfadado). Solo JSON, sin explicacion."
26            ),
27        }
28    ],
29)
30
31# Verifica el stop_reason ANTES de parsear: si se cortó por max_tokens,
32# el JSON estará truncado y json.loads() lanzará excepción.
33assert resp_paso1.stop_reason == "end_turn", "respuesta incompleta, sube max_tokens"
34tuplas: list[dict] = json.loads(resp_paso1.content[0].text)
35
36# ── PASO 2: naturalizar cada tupla por separado ───────────────────────────────
37casos_sinteticos = []
38for t in tuplas:
39    resp_paso2 = client.messages.create(
40        model="claude-sonnet-4-6",
41        max_tokens=256,
42        system=(
43            "Eres un cliente real de Aurora. Escribe el mensaje exactamente como lo "
44            "escribiria un usuario en un chat de soporte: informal, directo, sin estructura."
45        ),
46        messages=[
47            {
48                "role": "user",
49                "content": (
50                    f"Convierte esta tupla en un mensaje de usuario real:\n"
51                    f"{json.dumps(t, ensure_ascii=False)}"
52                ),
53            }
54        ],
55    )
56    casos_sinteticos.append({
57        "query": resp_paso2.content[0].text.strip(),
58        "label": "fail",              # el fallo esperado: este caso DEBE fallar el criterio
59        "fallo_tipo": t["fallo_esperado"],
60        "es_sintetico": True,         # marca explícita: sabrás cuánto de tu señal es generado
61    })

Ahora la pregunta de auto-explicación. Antes de leer el análisis, responde: ¿por qué el paso 1 y el paso 2 son dos llamadas separadas, en vez de pedir "genera 3 conversaciones del caso frágil" de una sola vez?

Porque cada paso controla una cosa distinta:

  • El paso 1 controla la distribución. Tú decides las tuplas: si quieres frágil-Canarias-enfadado, lo escribes. El modelo no elige qué casos existen, solo los rellena. Así garantizas que el caso raro aparece.
  • El paso 2 controla el realismo. Convierte una tupla seca en lenguaje de cliente real, sin tocar qué caso es. Su único trabajo es redactar, no decidir cobertura.

Si lo pidieras todo de una vez, el modelo gravitaría hacia lo común —las 47 variaciones planas del §5.1— y el caso raro se diluiría. La separación es lo que te devuelve el control.

Fíjate también en dos detalles defensivos del código:

  • El assert resp_paso1.stop_reason == "end_turn" antes del json.loads(). Si la respuesta se cortó por max_tokens, el JSON está truncado y el parseo revienta. Verificas el stop_reason para fallar con un mensaje claro, no con un JSONDecodeError críptico.
  • El campo "es_sintetico": True en cada caso. Es tu rastro: en cualquier momento sabes qué fracción del dataset es generada. Eso es lo que te deja "acumular junto a reales, no reemplazar" de forma comprobable.

Y el contraejemplo, en paralelo. El fallo del saludo en mayúsculas raras —"HoLa, ¿En Qué PuEdo AyUdaRTE?"— no pasa por este código. Su causa vive en el prompt del sistema. Lo arreglas ahí y compruebas que desapareció. Cero sintéticos generados.


5.6Hazlo tú

Ejercicio 1 — andamiaje parcial: naturaliza dos tuplas

El paso 1 ya te dio estas dos tuplas para el gap frágil-Canarias de Aurora:

python
1tuplas = [
2    {"producto": "fragil", "region": "canarias", "tono": "enfadado",
3     "fallo_esperado": "no aplica recargo de devolución de Canarias"},
4    {"producto": "fragil", "region": "peninsula", "tono": "tranquilo",
5     "fallo_esperado": "no menciona embalaje especial para frágiles"},
6]

Tu tarea: escribe a mano (sin llamar a la API) cómo quedaría el campo query de cada caso tras el paso 2. Es decir, redacta el mensaje informal de cliente que naturalizaría cada tupla. Una o dos frases por tupla.

Recuerda el rol del paso 2: redactas el mensaje del cliente; no inventas un caso distinto del que dice la tupla.

Ejercicio 2 — autónomo: decide para tres gaps

El error analysis de Aurora dejó tres gaps en el dataset. Para cada uno, decide la acción correcta —sintético / arréglalo en prompt / consigue más reales— y justifica en una frase:

  • Gap A: el agente, cuando el cliente escribe en gallego, responde siempre en castellano e ignora el idioma. Causa: el prompt no instruye nada sobre idioma.
  • Gap B: el caso "cambio de talla de una prenda comprada en oferta flash" es raro (ocurre poco) pero genera muchas reclamaciones cuando el agente lo maneja mal. Tienes 1 traza real.
  • Gap C: las consultas de "estado de mi pedido" son el 40% del tráfico real, pero tu dataset solo tiene 4 etiquetadas porque etiquetaste poco esa categoría.

Antes de seguir, una pregunta de interrogación elaborativa. Respóndela tú primero: ¿por qué un gap de alto volumen real (como el Gap C) NO es candidato a sintéticos, aunque tu dataset esté escaso de él?

Porque los datos reales ya existen y son más fieles que cualquier generación. Si el caso es el 40% del tráfico, etiquetar más trazas reales te da señal de mejor calidad a coste casi nulo. Generar sintéticos para algo abundante en producción introduce ruido evitable y arriesga sesgar la distribución. Los sintéticos se reservan para lo que es raro en la realidad y no puedes muestrear, no para lo que solo etiquetaste poco.


5.7Comprueba

Sin pistas. Aquí tienes cuatro situaciones del dataset de Aurora. Clasifica cada una como "sintético justificado" o "antipatrón". Para las de antipatrón, di si es "arreglable en prompt" o "sin gap real". Y nombra qué riesgo corres si abusas de los sintéticos.

  1. El agente firma todas sus respuestas con "Atentamente, el equipo" cuando la marca pide tono cercano sin firma. Ocurre siempre.
  2. Falta cobertura del caso "reembolso de un pedido internacional retenido en aduanas con pago en otra divisa": ocurre raras veces al año, pero cada uno escala a un humano caro. Tienes 1 traza real.
  3. Tienes 3 trazas reales de "queja por retraso" y quieres generar 200 sintéticas más de queja por retraso, aunque ese caso es el 30% de tu tráfico real diario.
  4. El gap "devolución de electrónica fuera de plazo de garantía" es raro en producción, alto impacto, y tras esperar dos meses sigues con solo 2 trazas reales.
Ver la respuesta razonada

1 → antipatrón (arreglable en prompt). La firma "Atentamente, el equipo" sale de una instrucción del prompt del sistema. Se corrige cambiando esa línea y comprobando que desaparece. Generar sintéticos de firmas no mide nada útil; infla el dataset sin cubrir ningún gap real.

2 → sintético justificado. Caso raro en producción, alto impacto (escala a humano caro), y solo 1 traza real que no puedes multiplicar esperando tráfico. Es el patrón canónico: expandir cobertura por dos pasos en una dimensión que el análisis identificó.

3 → antipatrón (sin gap real / consigue más reales). El caso es el 30% del tráfico diario: los datos reales abundan. Etiquetar más trazas reales da mejor señal a coste casi nulo. Generar 200 sintéticas aquí introduce ruido evitable y sesga la distribución hacia lo generado.

4 → sintético justificado. Raro, alto impacto, y ya intentaste la vía de los reales —esperaste dos meses— sin éxito. Es la tercera casilla del §5.4: no puedes acumular reales, el caso importa. Generas por dos pasos.

Riesgo de abusar: model collapse. Si dejas que los sintéticos dominen el dataset y reemplacen a los reales, la distribución se degrada y las colas raras —justo lo que querías cubrir— desaparecen. Mitigación: acumular junto a reales, no sustituir; marcar cada caso con es_sintetico para saber cuánta señal es generada.


Feedback formativo:

  • Si clasificaste bien 1 y 3 como antipatrones y supiste por qué: dominas el filtro que más se falla —distinguir "no cubro este caso" de "necesito generar para él"—. Lo aplicarás en el checkpoint C1 al justificar cada sintético que generes.
  • Si marcaste el 3 como justificado: confundiste "mi dataset tiene pocas" con "la realidad tiene pocas". El caso es el 30% del tráfico: los reales son abundantes y mejores. El siguiente paso: relee el §5.4, la casilla "consigue más datos reales", y el Ejercicio 2 Gap C.
  • Si marcaste el 1 como justificado: confundiste un fallo de prompt con un gap de cobertura. La prueba: ¿se arregla cambiando una instrucción? Si sí, no generes datos, corrige el prompt. Releer el §5.4, "no generes lo que se arregla en el prompt", cierra la brecha.
  • Si no nombraste model collapse: es el riesgo que justifica toda la disciplina de esta lección. Sin él, "genera muchos sintéticos" parece gratis. Vuelve al §5.4 y verbaliza qué le pasa a la distribución cuando los sintéticos se comen a los reales.

5.8Conecta

Vuelve al hueco del §5.1: dos trazas reales del caso frágil-Canarias, y la tentación de pedir "50 conversaciones" a lo bruto.

Ahora tienes el criterio para no caer en esa trampa. Sabes que ese gap merece sintéticos —raro, caro, irreproducible esperando tráfico—. Sabes generarlos por dos pasos para que el caso raro aparezca de verdad, no diluido entre variaciones genéricas. Y sabes marcarlos como sintéticos para acumularlos junto a los reales sin que el model collapse te coma la señal.

Con esto cierras las piezas del Nivel 1:

  • En L2 anotaste las trazas reales de Aurora con open coding.
  • En L3 las agrupaste en una taxonomía priorizada por axial coding.
  • En L4 convertiste el fallo nº1 en un dataset con etiquetas binarias y balance.
  • En L5 —hoy— rellenaste los gaps justificados con sintéticos por dos pasos.

En L6 ensamblas las cuatro piezas en el entregable del checkpoint C1: taxonomía con conteos + dataset etiquetado + justificación de cada sintético. Un equipo real te pedirá exactamente eso: "enséñame por qué generaste cada sintético". Hoy ya sabes responder.

¿Dónde lo aplicarías en tu trabajo? Piensa en cualquier dataset de evaluación que tengas o quieras montar. Mira sus gaps. ¿Cuántos son casos raros-pero-caros que no puedes muestrear (sintéticos), cuántos son fallos de prompt disfrazados de gaps (corrige el prompt), y cuántos son abundantes en producción pero poco etiquetados (etiqueta más reales)?


5.9Reflexiona

Tómate dos minutos. Estas preguntas consolidan más que releer.

  • Con tus palabras: ¿en qué se diferencia un "gap que merece sintéticos" de un "fallo que se arregla en el prompt"?
  • ¿Por qué separar la generación en dos pasos te da más control sobre la distribución que pedirlo todo de una vez?
  • ¿Qué sigue sin estar claro? Anótalo. Si es "cuántos sintéticos son demasiados antes de arriesgar model collapse", es la pregunta correcta —no hay un número mágico; la regla es acumular junto a reales y vigilar la proporción es_sintetico—.

Referencia rápida

  • Regla de orden: sintéticos después del error analysis con datos reales, para expandir cobertura en dimensiones ya identificadas (Hamel Husain, dic 2024).
  • Antipatrón: no generes sintéticos para lo que se arregla en el prompt (Hamel Husain, ene 2026).
  • Tres casillas ante un gap: arréglalo en el prompt (causa = instrucción) · consigue más reales (caso abundante en producción) · genera sintéticos (raro + alto impacto + irreproducible).
  • Por qué no "a pelo": la generación directa tiende a outputs genéricos y omite los escenarios raros (Hamel Husain, ene 2026).
  • Proceso de 2 pasos: (1) tuplas de dimensiones (controlas la distribución), (2) naturalización por separado (controlas el realismo).
  • Defensivo en código: verifica stop_reason == "end_turn" antes de json.loads(); marca cada caso con es_sintetico.
  • Riesgo: model collapse con sintéticos recursivos; mitigación = acumular junto a reales, no reemplazar (arXiv:2503.14023; el paper trata training, aplicabilidad a evals por verificar).