Ir al contenido
SealMetrics
IA

Nuestra IA se equivocó en producción — y lo cazó nuestro propio test

6 min de lecturaPor Rafa Jiménez

Alguien pidió una comparativa. El modelo respondió correctamente y después adjuntó un gráfico cuya clave del eje Y era una lista en lugar de un string. La validación del esquema rechazó el gráfico y toda la respuesta se cayó con un 500: una buena respuesta tirada a la basura por culpa de un adorno.

Conclusiones clave

  • Una pregunta de comparativa hizo que el modelo emitiera dos series de datos en un campo que el esquema declaraba como un único string. La validación estricta lo rechazó y toda la respuesta del chat devolvió un HTTP 500.
  • Lo encontramos como 1 error de transporte en 162 consultas reales del benchmark: lo bastante raro como para sobrevivir a las pruebas manuales, lo bastante frecuente como para llegar a los usuarios.
  • El arreglo tiene dos capas: convertir en la frontera de parseo las malformaciones recuperables, y un airbag que descarta con un warning cualquier elemento que siga siendo inválido en lugar de tumbar la respuesta.
  • La regla general: trata cada campo de la salida estructurada de un LLM como entrada no confiable, y no dejes nunca que un elemento cosmético decida si el usuario recibe una respuesta.

Construimos Seal AI dando por hecho que seremos nosotros quienes encontremos sus bugs. Este lo encontramos nosotros, en el producto tal y como se entrega, y enseña más que cualquier caso de éxito que pudiéramos publicar.

Qué pasó

El asistente responde preguntas de analítica en lenguaje natural y, cuando un gráfico ayuda, lo devuelve junto al texto: un objeto estructurado que describe qué pintar, validado contra un esquema estricto antes de llegar al navegador.

Al pedirle una comparativa, el modelo hizo algo perfectamente razonable desde su punto de vista: produjo un gráfico con dos series. Y para expresarlo, puso como clave del eje Y una lista con dos nombres de campo. Nuestro esquema decía que esa clave es un string.

La validación rechazó el objeto. El rechazo se propagó. Toda la respuesta del chat devolvió un HTTP 500.

Esa es la parte que conviene digerir con calma. La respuesta de texto ya estaba generada. Era correcta, estaba anclada en datos reales y era útil. El usuario no recibió nada de eso, porque un elemento decorativo no pasó una comprobación de tipos.

Cómo lo encontramos

Apareció como un único error de transporte en las 162 consultas reales de nuestro benchmark interno, que pilota el endpoint de producción real en lugar de un mock. Un error en 162 es una frecuencia incómoda: demasiado raro para que alguien lo reproduzca a mano durante una revisión, demasiado común para quedarse en lo teórico cuando un producto responde miles de preguntas a la semana.

Estábamos ejecutando ese benchmark para comparar modelos. Lo que sacamos fue un defecto de producción, que es el argumento más fuerte que conocemos a favor de hacer el benchmark contra tu stack real en lugar de contra una simulación. Un mock habría validado nuestro esquema contra nuestras propias suposiciones y habría pasado.

Por qué este tipo de bug es inevitable

Es tentador archivar esto como «el modelo se equivocó». En realidad no. Entendió la petición — una comparativa necesita dos series — y expresó esa comprensión con una forma que nuestro contrato no había previsto.

Esa es la condición permanente de la salida estructurada de un LLM: un generador probabilístico a un lado, un esquema estricto al otro. El prompt reduce la tasa de desajuste. Nunca llega a cero, y menos en los bordes: comparativas, peticiones multimétrica, periodos raros, otros idiomas. Si tu arquitectura da por hecho que esa tasa es cero, no has construido una funcionalidad: has construido un cara o cruz con buenas probabilidades.

Nuestro error no fue que el esquema fuera estricto. Estricto está bien. Nuestro error fue darle a un validador estricto la autoridad de tumbar una respuesta entera por un elemento opcional.

El arreglo en dos capas

No parcheamos el prompt y dimos el tema por cerrado. Los retoques de prompt no son una frontera: son una esperanza. Cambiamos la frontera misma.

  1. Conversión en la frontera de parseo. Cuando la clave del eje Y llega como lista, cogemos su primera serie y seguimos. La malformación es recuperable sin ambigüedad, así que el gráfico sobrevive — degradado a una sola serie en vez de perdido del todo, que es un resultado muchísimo mejor que nada.
  2. Un airbag detrás. Cualquier gráfico que siga siendo inválido tras la conversión se descarta, se registra un warning y la respuesta continúa sin él. Un gráfico malo ya no puede tumbar una respuesta. Nunca.

Después aplicamos el mismo tipo de blindaje a la ruta de carga del historial de conversaciones, donde un único elemento almacenado mal formado podría haber envenenado igualmente la carga de una conversación pasada entera. Y añadimos tests de regresión para esa malformación exacta, porque la misma forma volverá: los modelos cambian, y esta era una cosa sensata que emitir.

Tres reglas para quien publique salida estructurada de un LLM

  1. Trata cada campo como entrada no confiable. La salida de un modelo no es una estructura de datos tuya que va y vuelve. Es una carga útil de un sistema que no controlas, y merece el mismo escepticismo que el cuerpo de una petición llegada de internet: comprobación de tipos, límites y una decisión explícita sobre qué pasa cuando viene mal.
  2. Clasifica los elementos por cuánto los necesita el usuario. Decide, elemento a elemento, si un fallo de validación es fatal. La respuesta que el usuario ha pedido es fatal. Un gráfico, una pregunta de seguimiento sugerida, un icono, un resaltado: ninguno lo es. Tumbar la respuesta entera por una parte opcional convierte un fallo cosmético en una caída de servicio.
  3. Recupera lo que puedas, descarta lo que no y registra todo. Convierte las malformaciones que tengan una lectura correcta evidente, descarta el resto con un warning para no perder visibilidad, y escribe un test de regresión por cada forma que hayas observado de verdad en producción. La recuperación silenciosa y sin logs es la manera de dejar de aprender de tu propio modelo.

Por qué te lo contamos

Porque «nuestra IA funciona» no es una afirmación que nadie debería aceptar por fe, tampoco viniendo de nosotros. Lo que sí es comprobable es si un proveedor está mirando: si exprime su propio producto lo suficiente como para encontrar el fallo de uno entre cien, y si cuenta lo que ha encontrado.

Este nos costó una respuesta visible para el usuario en un test. Arreglado, nos costó una función de conversión, un airbag y un puñado de tests de regresión. Sin detectar, habría sido un 500 intermitente que nadie sabría reproducir, en una funcionalidad de la que la gente empezaba a depender.

El benchmark que lo cazó, con su metodología y las tandas que descartamos, está publicado en nuestro informe del benchmark interno.

Lecturas relacionadas