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.loadslo acepta, sijson_repairlo 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_basede GPT-4o.
Resultados
| Fallo | Casos | Reparados e idénticos al original |
|---|---|---|
| Coma final | 167 | 167 |
| Bloque markdown con frase | 167 | 167 |
| Comillas simples | 167 | 167 |
| Claves sin comillas | 166 | 166 |
| Comentario | 166 | 166 |
| Respuesta cortada | 167 | 0 |
| Medida | Resultado |
|---|---|
Aceptados por json.loads sin reparar | 0 |
| Reparados a un JSON válido | 1000 (100 %) |
| Idénticos al original | 833 (83,3 %) |
| Tiempo por reparación, mediana | 0,122 ms |
| Tiempo por reparación, P95 | 0,425 ms |
| Tokens de una llamada normal, mediana | 550,5 |
| Tokens de un reintento, mediana | 745 |
| Reintento frente a llamada original | 137,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
- Segunda parte: structured outputs con 1200 salidas reales
- Agentes de navegador: Accessibility Tree vs capturas de pantalla
- Durable execution en agentes: reintentos sin efectos duplicados
- Decision models: enrutar agentes por coste y latencia
¿Tus agentes queman tokens en reintentos? Medimos dónde se van y cómo cortarlo. Encárganos el experimento.