Saltar al contenido
← Cuaderno

Agentes & IA

Structured outputs en agentes de IA: reparar el JSON en local o reintentar (BAML, Outlines y retries)

Medido en 1000 JSON rotos: un parser tolerante repara el 100 % en 0,12 ms y deja idéntico el 83,3 %. Un reintento cuesta un 137,3 % de la llamada original en tokens.

Fecha
20 oct 2026
Lectura
7 min
Resultado
100 % reparados en local • 0,12 ms por reparación • Un reintento = 137,3 % de la llamada

Antes de pedirle al modelo que repita un JSON roto, repáralo en local: un parser tolerante arregló el 100 % de 1000 salidas rotas en 0,12 ms de mediana y dejó el 83,3 % idéntico al original. Un reintento, en cambio, cuesta en tokens un 137,3 % de la llamada original. La única excepción es la respuesta cortada por límite de tokens: ahí faltan datos y sí hay que reintentar.

Medido el 20 de octubre de 2026 en un MacBook Pro M1 Max con Python y json_repair.

Segunda parte, con salidas reales: repetimos la prueba con 1200 salidas de Llama, Qwen y Gemma para ver qué errores salen de verdad y qué pasa cuando un tipo domina. Lee los resultados.

El problema

Cuando un agente devuelve JSON, a veces lo devuelve mal: una coma de más, comillas simples, el JSON dentro de un bloque de markdown, un comentario o la respuesta cortada. La solución habitual es el bucle de reintentos: max_retries en Pydantic e Instructor, o la opción de corregir el formato automáticamente en n8n. Funciona, pero cada fallo es otra llamada completa al modelo, con su coste y su espera.

Hay tres alternativas:

  • Reparar en el cliente con un parser tolerante (lo que medimos aquí con json_repair; BAML hace algo parecido con su Schema-Aligned Parsing, guiado además por el esquema).
  • Restringir la generación para que el modelo no pueda producir JSON inválido (constrained decoding, como Outlines con máquinas de estados sobre vLLM, o los modos de salida estructurada de los proveedores).
  • Reintentar solo cuando lo anterior no basta.

Montaje

  • 1000 salidas de un agente que extrae pedidos (cliente, líneas con SKU, cantidad y precio, entrega, notas), generadas con semilla fija.
  • Seis fallos, repartidos a partes iguales: coma final, bloque markdown con frase, comillas simples, respuesta cortada, claves sin comillas y comentario.
  • Se mide: si json.loads lo acepta, si json_repair lo convierte en un objeto válido, si queda idéntico al original y cuánto tarda (5 repeticiones).
  • Coste de un reintento: tokens de reenviar instrucciones, esquema, salida rota y mensaje de error, más los de la nueva salida, con el tokenizador o200k_base de GPT-4o.

Resultados

FalloCasosReparados e idénticos al original
Coma final167167
Bloque markdown con frase167167
Comillas simples167167
Claves sin comillas166166
Comentario166166
Respuesta cortada1670
MedidaResultado
Aceptados por json.loads sin reparar0
Reparados a un JSON válido1000 (100 %)
Idénticos al original833 (83,3 %)
Tiempo por reparación, mediana0,122 ms
Tiempo por reparación, P950,425 ms
Tokens de una llamada normal, mediana550,5
Tokens de un reintento, mediana745
Reintento frente a llamada original137,3 %

Qué significa

  • Cinco de los seis fallos típicos se arreglan sin volver a llamar al modelo, en una fracción de milisegundo y sin coste. Pagar otra llamada para quitar una coma es tirar dinero.
  • Un reintento cuesta más que la llamada original (137,3 %), porque además de repetir instrucciones y esquema reenvías la salida rota y el error. Y suma otra espera completa del modelo.
  • La respuesta cortada no se puede reparar bien: el parser devuelve un JSON válido, pero le faltan datos. Detéctalo (por ejemplo, porque el modelo terminó por límite de tokens) y en ese caso reintenta o sube el límite.
  • Valida siempre después de reparar contra el esquema (Pydantic, JSON Schema): un JSON válido no es un JSON correcto.

Cuándo usar cada opción

  • Reparación en el cliente (json_repair, BAML): siempre como primera línea, con cualquier modelo y cualquier proveedor.
  • Constrained decoding (Outlines sobre vLLM, modos de salida estructurada): si controlas la inferencia o tu proveedor lo ofrece, elimina los fallos de sintaxis de raíz.
  • Reintento: solo para respuestas cortadas o cuando la validación del esquema falla por contenido, no por sintaxis.

Limitaciones

  • Fallos sintéticos: reproducimos los seis fallos típicos, no salidas reales de un modelo. La proporción de fallos de cada modelo no se ha medido aquí.
  • No se midió BAML ni Outlines. BAML requiere generar su cliente a partir del esquema y Outlines necesita ejecutar un modelo con restricción de la generación; quedan para un próximo experimento. Sus cifras en el hilo de X vienen de otra ejecución y no están reproducidas aquí.
  • El coste del reintento se calcula en tokens, no en latencia: la espera depende del modelo y del proveedor.
  • Un solo esquema de tamaño medio; con esquemas más grandes, el reintento sale todavía más caro.

Cómo repetirlo

Código, entorno y resultados en bruto: yeswelab-laboratorio/structured-outputs-agentes-ia-baml-outlines-vs-retries en GitHub. Tarda menos de un minuto.

Relacionado

¿Tus agentes queman tokens en reintentos? Medimos dónde se van y cómo cortarlo. Encárganos el experimento.

#json-repair#tiktoken#Python#BAML#Outlines#Pydantic#n8n