Skip to content

English · Español

Break 00 — Desactivar la máscara JSON Schema en una tarea de salida estructurada

Pedimos a Mini-GPT que devuelva el past simple español de work en JSON. Con la máscara JSON Schema activa, la salida es siempre parseable. Sin ella, alternamos entre prosa, markdown, frases incompletas, y JSON con comas finales. Contamos el porcentaje de parses fallidos sobre 50 muestras — esa cifra es la lección.

Este ejercicio /break apunta a la invariancia mecánica que aporta la generación estructurada. El bug es un booleano; el modo de fallo es una tasa de fallo de parseo medible.

Anclajes: theory/01-jsonmode-vs-grammar.md, theory/04-cfg-vs-regex-vs-jsonschema.md, .claude/commands/break.md.


Hipótesis

El aprendiz predice: "Cuando le pido a Mini-GPT 'dame el past simple español de work en JSON', con la máscara de esquema activa, cada salida parsea como JSON válido que coincide con el esquema. Sin la máscara, el modelo — entrenado en un corpus minúsculo de lenguaje natural, no en uno cargado de JSON — emite una mezcla de prosa, JSON parcial y JSON con comas finales. La tasa de fallo de parseo salta del 0% al ~40-70%."

El break

En src/minimask/conjugate_cli.py:

 def conjugate(verb: str, tense: str, person: str) -> dict:
     prompt = build_prompt(verb, tense, person)
-    output = generate_with_mask(model, prompt, schema=CONJUGATE_SCHEMA)
+    output = generate_freeform(model, prompt)   # /break: sin máscara de esquema
     return json.loads(output)        # lanzará excepción en la mayoría de salidas

Un swap: generate_with_mask → generate_freeform. La llamada json.loads se queda — y ahí es donde aflorarán los fallos.

Predecir, luego ejecutar

La plantilla de prompt:

Return ONLY JSON: {"verb": "work", "tense": "past simple", "person": "1st singular",
                   "english": "<form>", "spanish": "<form>"}

Verb: work
Tense: past simple
Person: 1st singular
JSON:

La respuesta de Mini-GPT con la máscara: siempre {"verb": "work", "tense": "past simple", "person": "1st singular", "english": "worked", "spanish": "trabajé"}. Parseable en cada muestra.

Sin la máscara, comportamientos esperados sobre 50 muestras (temperature 0,7):

  • ~10 muestras: JSON válido, correcto (el modelo conoce algo de sintaxis JSON por exposición mínima en el corpus).
  • ~15 muestras: JSON válido, pero falta un campo o sobran campos ("spanish" mal escrito como "español").
  • ~10 muestras: JSON con coma al final (...,}) — inválido por json.loads pero recuperable.
  • ~10 muestras: explicación en prosa / markdown en vez de JSON ("The past simple of 'work' is 'worked' (yo trabajé).").
  • ~5 muestras: JSON truncado ({"verb": "work").

Predicciones

  • Tasa de fallo de parseo (json.loads crudo): ≈ 60-70%.
  • Tasa de fallo de validación de esquema (JSON válido pero claves equivocadas): además de los fallos de parseo, un ~10% adicional.
  • Total de muestras útiles: ~10-20%.
  • Con la máscara: fallos de parseo 0% por construcción; fallos de validación de esquema 0% por construcción.

Escribe tus predicciones en learners/borja/phase-30/notes/breaks.md antes de ejecutar.

Observa

Ejecuta la CLI de conjugación 50 veces con cada modo:

# Roto (sin máscara)
for i in $(seq 1 50); do just exp 30-conjugate --mode freeform --seed $i; done \
    > experiments/30-freeform/results.jsonl
# Baseline (máscara activa)
for i in $(seq 1 50); do just exp 30-conjugate --mode masked --seed $i; done \
    > experiments/30-masked/results.jsonl

Diagnósticos:

  1. Gráfico de barras: parse_failures / 50 para cada modo. Esperado: 30/50 freeform vs 0/50 masked.
  2. Categoriza los 30 fallos freeform: coma final, prosa, campo faltante, truncamiento. Barra apilada.
  3. Lado a lado las primeras 10 salidas de cada modo. El contraste es la lección.

Síntoma que Borja verá

  • json.JSONDecodeError lanzado en la mayoría de muestras freeform.
  • Las salidas bonitas (cuando parsean) a menudo tienen drift en nombres de campo: spanish se vuelve español o es.
  • Las salidas enmascaradas son aburridamente idénticas — justo el punto: estructura determinista, el contenido aún puede variar en los campos english/spanish.

Causa oculta (una frase)

El decoder ya no está restringido por la máscara JSON Schema, así que la distribución natural del modelo sobre tokens de gramática inglesa (mayoritariamente prosa) compite con el objetivo de sintaxis JSON, y la prosa gana ~60% de las veces.

Cascada de pistas

  1. Imprime las primeras 5 salidas crudas de generate_freeform. ¿Cuántas son JSON válido?
  2. Compara con generate_with_mask. ¿Qué es estructuralmente invariante entre ellas?
  3. Relee theory/04-cfg-vs-regex-vs-jsonschema.md §"La elección correcta para el tutor de gramática §A13". ¿Por qué la máscara de esquema es el componente que carga el peso aquí?

Diff de arreglo

 def conjugate(verb: str, tense: str, person: str) -> dict:
     prompt = build_prompt(verb, tense, person)
-    output = generate_freeform(model, prompt)
+    output = generate_with_mask(model, prompt, schema=CONJUGATE_SCHEMA)
     return json.loads(output)

Restaurar la ruta enmascarada. El arreglo también es un cambio de una línea.

Por qué esto enseña el concepto

El pitch entero de la generación estructurada: "no confías en que el modelo siga el formato — lo haces mecánicamente imposible desviarse." Este break hace que esa afirmación cargue peso en un modelo real. Con la máscara, el contrato JSON es un sistema de tipos — no puede ser violado. Sin la máscara, el contrato es una plegaria — el modelo normalmente lo respeta pero lo hace a una tasa catastrófica para parsers downstream que esperan entrada estructurada.

El impacto downstream: cualquier agente de tool-calling (Fase 31) que dependa de salida JSON del LLM es frágil sin una máscara de esquema. Los agentes de producción o bien usan constrained decoding por esquema, o se apoyan en bucles retry-y-valida a posteriori (que añaden latencia y coste). El tutor de gramática de la Fase 32 usa la ruta de máscara; este break muestra por qué.

Referencia

  • Willard & Louf, Outlines (arXiv:2307.09702) — cuantifica la tasa de fallo JSON sobre modelos base comunes sin restricciones.
  • OpenAI, documentación de Structured Outputs — la API de producción expone constrained decoding por esquema exactamente por esta razón.

Siguiente: restaura la ruta de máscara y ejecuta lab/02-end-to-end-conjugate.md para medir el sobrecoste de latencia del decoder enmascarado.