Datasets sintéticos: el LLM se inventa preguntas
El atajo seductor
Sección titulada «El atajo seductor»Tu golden set tiene 12 entries. Querés 100 para tener más señal estadística. ¿Las escribís vos a mano? Te lleva una tarde.
Otra opción: que el LLM las invente. Es rápido (5 minutos), escalable (mañana querés 500, las pedís) y no requiere cerebro humano.
Spoiler: tiene un costo. Los datasets sintéticos meten un sesgo específico que infla las métricas falsamente. Vamos a generarlos, ver el sesgo en vivo, y decidir cuándo se usan y cuándo no.
Cómo se generan: el pipeline
Sección titulada «Cómo se generan: el pipeline»La idea es simple:
- Cargás el corpus.
- Lo chunkeás (con el mismo chunker que usás en producción).
- Para cada chunk, mandás al LLM: “leé este chunk y escribí una pregunta cuya respuesta esté contenida acá. Devolvé también la respuesta extraída del chunk.”
- Parseás el JSON y armás un
EvalCaseconexpected_filenamesapuntando al archivo de origen del chunk. - Repetís hasta tener N entries.
El comando del curso:
pnpm --filter @rag-lab/06-evals run synth -- --n 20Tarda ~3 minutos para 20 entries (1 LLM call por entry). Output en packages/06-evals/golden/synthetic.json.
El prompt que hace el trabajo
Sección titulada «El prompt que hace el trabajo»Vive en packages/06-evals/src/synth.ts:
const PROMPT_TEMPLATE = `Generás pares de pregunta + respuesta sintéticos para evaluar un sistema RAG.
Te paso un fragmento (CHUNK) de un documento técnico en español. Tenés que escribirUNA pregunta cuya respuesta esté contenida en ese chunk, y la respuesta tomada del chunk.
Respondé EXACTAMENTE con este JSON (sin texto adicional, sin bloques de código):{"question": "...", "answer": "..."}
La pregunta:- en español- específica al contenido del chunk (no genérica)- no incluye frases como "según el texto" o "en este chunk"
La respuesta:- en español- extracto fiel o paráfrasis del chunk- 1 a 3 oraciones, un solo párrafo
CHUNK:`;Tres detalles del prompt que importan:
específica al contenido del chunk (no genérica): sin esto, el LLM tiende a inventar preguntas tipo “¿qué se discute en este texto?” — inútiles.no incluye frases como "según el texto": el modelo es vago por defecto. Eliminamos las muletillas que hacen las preguntas no-naturales.- JSON cerrado, no markdown: si el LLM devuelve
```json ... ```, el parser falla. El prompt explícito pide JSON pelado.
El parser: defensivo
Sección titulada «El parser: defensivo»const match = data.response.match(/\{[\s\S]*\}/);if (!match) return null;try { const parsed = JSON.parse(match[0]) as Partial<SynthResult>; if (typeof parsed.question === 'string' && typeof parsed.answer === 'string') { return { question: parsed.question, answer: parsed.answer }; } return null;} catch { return null;}Tres cosas que pasan en producción y por eso el parser es así:
- El LLM mete prosa antes o después del JSON. El regex captura el primer
{...}y descarta el resto. - El JSON viene mal formado (comilla suelta, comma trailing). El
try/catchlo maneja silencioso — la entry se descarta y el script sigue. - Faltan campos. El check explícito de tipos rechaza la entry.
El script imprime cuántas se descartaron al final. Para n=20 esperá perder 1-3 entries por parseo fallido. Si perdés más de la mitad, algo anda mal con el modelo o el prompt.
El sesgo: vocabulario espejado
Sección titulada «El sesgo: vocabulario espejado»Acá está la parte que importa entender. Mirá un par de ejemplos generados sobre el corpus del curso:
{ "id": "synth-clean-architecture-2", "question": "¿Qué establece la regla de dependencia respecto a las direcciones permitidas para las dependencias del código fuente?", "expected_answer": "La regla de dependencia establece que las dependencias del código fuente sólo pueden apuntar hacia adentro.", "expected_filenames": ["clean-architecture.md"]}Y el chunk de origen:
La regla de dependencia establece que las dependencias del código fuente sólo pueden apuntar hacia adentro. Nada en una capa interior puede saber absolutamente nada sobre algo en una capa exterior…
¿Notás algo? La pregunta usa el mismo vocabulario que el chunk. “Regla de dependencia”, “dependencias del código fuente”, “direcciones permitidas” — todo aparece literal o casi literal.
Cuando un usuario real hace esa pregunta, no la formula así. Diría algo como:
“¿En qué dirección apuntan las dependencias en Clean Architecture?”
Diferentes palabras, mismo significado. Y los embeddings densos son mejores capturando significado idéntico que parafraseado. Para una pregunta sintética, el embedding va a matchear el chunk de origen casi perfecto. Score altísimo. Retrieval@k = 1.0 trivialmente.
Tu retriever no es así de bueno — es que el dataset está sesgado a su favor.
Cuándo usar sintéticos (sí, hay casos)
Sección titulada «Cuándo usar sintéticos (sí, hay casos)»A pesar del sesgo, tienen lugar:
- Smoke tests post-deploy. Despues de un cambio de infra (docker rebuild, dependency upgrade), querés saber si el RAG sigue respondiendo coherente. Un eval de 50 sintéticos en 5 minutos es ideal — si el promedio cae mucho, algo se rompió.
- Tests de regresión. Cambiaste el chunker. ¿Las respuestas cambiaron de forma sospechosa? Un sintético constante (con
--seedfijo para reproducibilidad) te da una baseline barata. - Coverage check. ¿Tu retriever encuentra TODOS los archivos del corpus o hay alguno “muerto”? Generá 1 sintético por chunk y mirá si retrieval@k = 1.0 para todos. Si alguno está sistemáticamente bajo, ese archivo tiene problemas (encoding, idioma, formato).
Cuándo NO usar sintéticos
Sección titulada «Cuándo NO usar sintéticos»- Benchmark publicado. Si ponés un número en un blog post o comparación, querés golden curado.
- Decisión de tuning. Si vas a cambiar
TOP_Kde 4 a 6 basado en métricas, las métricas tienen que venir del golden, no del sintético. - A/B test contra otra config. Mismo argumento.
Generá 20 sintéticos:
pnpm --filter @rag-lab/06-evals run synth -- --n 20 --seed 42Después, corré dos evals:
# Con el golden curado a manopnpm --filter @rag-lab/06-evals run eval -- --only vercel-ai-sdk
# Con los sintéticospnpm --filter @rag-lab/06-evals run eval -- --only vercel-ai-sdk \ --dataset packages/06-evals/golden/synthetic.jsonLo que viene
Sección titulada «Lo que viene»Ya sabés medir. Sabés cuál métrica significa qué. Sabés cuándo confiar en los números y cuándo no.
Ahora viene la parte que hace que valga la pena haber armado todo esto: comparar configuraciones. ¿Qué pasa cuando bajás TOP_K de 4 a 2? ¿Cuando subís el chunk size? ¿Cuando cambiás el modelo de embeddings? Eso es el último capítulo del Nivel 2.