Me he subido al carro de la IA, he caído en el hype del momento, pero lo hago a mi manera, usándola en el mundo del dato. Sobra que os hable de cómo han irrumpido los modelos de IA generativa con sus LLMs, MoEs, las guerras entre proveedores, la batalla entre Estados Unidos y China por dominar este mercado y todo lo demás; ya hay mucho contenido acerca de eso.
Cuando pensamos en modelos de IA Generativa, imaginamos modelos capaces de conversar, razonar, generar; modelos que nos generen informes, que nos resuman textos, que analicen datos directamente, incluso ya han aparecido herramientas para generar dashboards a través de lenguaje natural. Sin embargo, una parte de los casos de uso reales con los que nos encontramos en ingeniería de datos no necesita que el modelo escriba nada, sino que que decida: decidir si un texto contiene datos personales, qué categoría debe asociarse una entrada, etc.
En las últimas semanas le ha salido competencia a los modelos de LLM generativos con la irrupción del modelo Jev. Este nuevo modelo se engloba dentro de lo que se ha llamado modelos de decisión. Estos modelos resuelven justo las tareas de toma de decisión de las que hablaba un poco más arriba. En lugar de pedirle al modelo una respuesta en lenguaje natural, le damos un texto y una serie de preguntas tipadas y el modelo nos devuelve la respuesta elegida junto con la probabilidad de cada opción.
Estoy montando poco a poco un stack para trabajar con IA local, por el momento me apoyo en Ollama para servir los modelos, por eso de no complicarme mucho la vida. En septiembre de este año, la gente de Ollama incorporó soporte para este tipo de modelos basado en la API Jev, está disponible desde la versión 0.35. Con él llegaron tres modelos:
- nimble:9B de Bespoke labs.
- tev1:4B y tev1:0.8B de Together AI.
En este artículo os cuento un ejemplo con el que he estado jugando para probar Nimble y daros un poco mi punto de vista de cómo podemos utilizar este tipo de modelos para enriquecer nuestros datos. En concreto, lo he usado para asignar categorías a los datos. He tomado un ejemplo muy sencillo, he generado datos fake simulando movimientos bancarios de una empresa con sus ingresos, sus pagos a proveedores, pago de suministros, etc.
Podéis encontrar el código del ejemplo dentro de este repositorio. En él se incluye también un código para generar un archivo CSV con datos sintéticos.
¿ Qué es nimble ?
Antes de seguir, toca contaros un poco qué es este modelo. Nimble es un modelo de decisión de 9 mil millones de parámetros desarrollado por Bespoke Labs a partir de Qwen3.5-9B. Ocupa algo más de 9 GB en disco y solo trabaja con texto.
Lo que lo hace distinto a un modelo de chat es cómo responde. Un LLM generativo produce la respuesta token a token, con un paso de razonamiento previo si el modelo lo incluye. Nimble no razona, lee el prompt una vez por cada pregunta y puntúa directamente los tokens de las respuestas permitidas. De ahí salen las probabilidades de cada opción, y de ahí viene también su velocidad.
La API
La petición es una llamada POST con estos campos:
| Campo | Descripción |
| model | El modelo a usar (nimble:9b en este caso. |
| state | El texto a juzgar. |
| questions | De 1 a 64 preguntas con nombre. |
| Keep_alive | Opcional: cuánto tiempo se mantiene el modelo cargado tras la petición. |
Cada pregunta lleva:
- Un type (tipo): el tipo de pregunta (choice, noul o score) que define cómo responde el modelo.
- Unas instructions (instrucciones): la pregunta en lenguaje natural.
- Unos criteria (criterios: las respuestas posibles y lo que significa cada una.
Tipos de pregunta
| Tipo | Para qué sirve | Criterios | Qué devuelve |
| choice | Elegir una opción de una lista (clasificar, enrutar) | Un objeto que mapea cada opción a su descripción | Choice, probabilities y confidence * |
| noul | Verdadero o falso | Opcional: describe cada lado | Noul: la probabildad de que la respuesta sea verdadera. |
| score | Puntuar en una escala de niveles, del más bajo al más alto. Por ejemplo el nivel de urgencia de un ticket, con tres niveles que se numeran desde 0: 0: Rutinario 1: Pronto 2: Urgente | Un array con la descripción de cada nivel, del más bajo al más alto | Nivel ponderado por probabilidad incluyendo: legend, probabilities y confidence |
* En el apartado de Límites se explican los conceptos de probabilities y de confidence. Choice es la categoría elegida.
Para el caso que os muestro en esta entrada se usará un tipo choice, cada categoría es una opción y su descripción es el criterio que lee el modelo. Así es la forma de la pregunta que genera el código de ejemplo para cada movimiento bancario a categorizar:
{
"model": "nimble:9b",
"state": "Cobro soporte mensual - factura nº20264500092",
"questions": {
"tipo_movimiento": {
"type": "choice",
"instructions": "Elige la categoría que mejor describe el movimiento bancario.",
"criteria": {
"soporte": "Cobro de soporte técnico o mantenimiento recurrente de aplicaciones.",
"proveedor": "Operaciones con un proveedor identificado por su razón social.",
"otros": "Cualquier movimiento que no encaje claramente en otra categoría."
}
}
}
} La respuesta que nos devolverá el modelo tendrá la siguiente forma:
{
"model": "nimble",
"answers": {
"tipo_movimiento": {
"type": "choice",
"choice": "soporte",
"probabilities": {"soporte": 0.97, "proveedor": 0.02, "otros": 0.01},
"confidence": 0.9
}
}
} Límites
Conviene conocer los límites de este modelo porque condicionan el planteamiento a la hora de diseñar las categorías que se quieran crear.
- Soporta entre 2 y 26 opciones por pregunta de tipo choice o score. Si nuestro catálogo de categorías creace más allá de 26, hay que pensar en realizar un árbol de categorías con categorías principales y subcategorías para poder crecer.
- Permite de 1 a 64 preguntas por petición, cada pregunta se puntúa de forma independiente.
- El prompt completo debe caber en 8.192 tokens, cada pregunta se puntúa con el texto y el conjunto completo de preguntas en el prompt, por lo que las descripciones de las opciones también cuentan.
- Solo texto y solo puede elegir entre las respuestas que le damos, no redacta explicaciones, ni JSON anidado, ni extrae citas del texto.
- La probabilidad no es la tasa de acierto, que una opción tenga un 0.9 no significa que acierte al 90% de las veces con nuestros datos, sino que es la puntuación que da el modelo a la hora de decidirse por una de las opciones. Y confidence solo mide cuán concentradas están las probabilidades, un valor igual a 1 indica que la probabilidad recae en una sola opción de las que se han pasado; por el contrario, un valor igual a 0 es que el modelo reparte de manera uniforme la probabilidad entre las opciones.
- Si puede darse el caso de que alguna entrada no pueda categorizarse, se debe indicar en las opciones pasando una opción que sea de tipo no match. Yo he usado la categoría otros como categoría para todas aquellas entradas que no son categorizadas. Algo que se podrá usar para revisar la necesidad de crear nuevas categorías, o dejarlo así por no ser entradas habituales y categorizables.
- Requiere Ollama 0.35 o superior si usáis esta herramienta para servir el modelo.
Vamos al lío
Venga, ya os he roto la cabeza con teoría y ahora toca lo bueno, a lo que hemos venido, a ver cómo usar este tipo de modelos para enriquecer nuestros datos. Para ello parto de algo sencillo, tal y como os avanzaba, voy a enriquecer una información de movimientos bancarios de una empresa creando dos tablas calculadas nuevas:
- tipo_operacion: que simplemente indicará si es un movimiento de tipo pago o de tipo ingreso, esto no se hará usando el modelo, aunque se podría, es algo muy simple, al final es decidir en función de si es un número positivo o negativo.
- tipo_movimiento: para este campo sí se hará uso del modelo, ya que la idea es categorizar el tipo de movimiento que se ha realizado.
Estas columnas permitirán hacer mayores agrupaciones para mejorar la analítica de datos de los movimientos de la empresa, unos buenos roscos en los cuadros de mando con el porcentaje de movimientos por tipo de operación y por categoría.
Parto de un CSV con datos sintéticos, en el repositorio del ejemplo tenéis un código para generarlo, en las referencias del artículo tenéis el enlace al repositorio y también está mencionado en la parte introductoria del artículo. Este archivo ofrece cuatro columnas:
- Fecha del movimiento.
- Concepto del movimiento.
- Movimiento con detalle de la operación
- E importe con el valor de la transacción.
Para no complicar las cosas, el motor de base de datos sobre el que vuelco los datos finales es DuckDB con persistencia a un archivo de datos. Ya hacía tiempo que no usaba DuckDB en una demo.
El flujo del script es:
- Carga el CSV en una tabla sobre DuckDB, la tabla movimientos.
- Calcula tipo_operación con SQL teniendo en cuenta si el importe es un valor positivo o negativo.
- Lee el catálogo de categorías de un fichero YAML.
- Para cada movimiento pendiente, envía a Nimble el texto formado por los campos concepto y movimiento y guarda la categoría elegida y su probabilidad.
En los siguientes puntos os cuento los detalles de cada uno de los pasos del script
Carga de datos
Como ya he adelantado, genero un archivo CSV con datos sintéticos. Este archivo con el detalle de cada movimiento se carga en una tabla en DuckDB trabajando con la librería de DuckDB para Python y persistiendo la tabla en un archivo. La carga se hace con las facilidades que nos da la librería para este tipo de operaciones:
CREATE OR REPLACE TABLE movimientos AS
SELECT
row_number() OVER () AS id,
Fecha AS fecha, Concepto AS concepto,
Movimiento AS movimiento, Importe AS importe,
CAST(NULL AS VARCHAR) AS tipo_operacion,
CAST(NULL AS VARCHAR) AS tipo_movimiento,
CAST(NULL AS DOUBLE) AS confianza_movimiento
FROM read_csv(
?, delim = ';', header = true, encoding = 'utf-8',
decimal_separator = ',', dateformat = '%d/%m/%Y',
columns = {
'Fecha': 'DATE', 'Concepto': 'VARCHAR',
'Movimiento': 'VARCHAR', 'Importe': 'DECIMAL(15,2)'
}
); Se añade un identificador numérico como Id para tener una referencia de la fila dentro del CSV, y también se incluye una columna adiciona, confianza_movimiento, con el valor de la probabilidad que devuelve el modelo.
Hay movimientos del tipo `Plan <número>` que no dicen nada sobre su naturaleza. Se ha creado así para derivar entradas a la categoría otros.
Tipo de operación
Este campo, tal y como os he explicado, me niego a hacerlo usando IA, es un campo calculado muy sencillo así que tiré de SQL de toda la vida:
UPDATE movimientos
SET tipo_operacion = CASE
WHEN importe > 0 THEN 'ingreso'
WHEN importe < 0 THEN 'pago'
END; El catálogo de categorías
A partir de los datos sintéticos generados para crear el CSV de base, le he pedido a mi amigo Claude que generará la configuración de las categorías que detectaba en el CSV, no había ganas de trabajar ese día, y ha detectado unas 21 categorías e incluí la categoría otros para asociar aquellas entradas que no tuviesen una categoría clara. Todo ello se define dentro del archivo de configuración categorias.yaml que tiene el siguiente formato para cada una de las categorías definidas:
- nombre: soporte
descripcion: Cobro de soporte técnico o mantenimiento recurrente de aplicaciones a clientes.
tipo_operacion: ingreso
ejemplos: Cobro soporte mensual; Cobro mantenimiento aplicacion En donde se indica el nombre de la categoría, la descripción para que nimble pueda determinar si un texto se debe o no asociar a esta categoría y ejemplos de entradas que se asociarían a esta categoría, texto que usará el modelo para puntuar si debe o no asociarse a esta categoría cada texto.
El campo tipo_operación tiene una funcionalidad importante para reducir el tamaño de las peticiones al modelo y también mejorar su estimación, básicamente, no se le pasa en cada petición todas las categorías, sino que se pasan las categorías asociadas al tipo de operación vinculada al movimiento en sí, es decir, para un movimiento que ha sido un ingreso, no se le pasará al modelo las categorías con un tipo de operación que se un pago. Habrá algunas categorías que, por su definición, puedan ser ingresos y pagos, y se marcan con tipo_operación con valor ambos.
El script ya valida que no se superen las 26 categorías máximas que soporta Nimble y también que exista la categoría otros.
Trabajando con Nimble
La idea ahora es pasar el detalle de los movimientos para que el modelo nos dé su decisión e incluir la información de categoría para cada movimiento. En este punto comentar varias decisiones que he tomado para reducir el tamaño de las peticiones e incluso el número de peticiones, alguna ya os la he adelantado:
- Para cada entrada en la tabla de movimientos se concatenan los valores de las columnas concepto y movimiento para pasar ese combo como texto al modelo. Esto es necesario porque hay detalles para la toma de decisión que puede estar en uno de los dos campos, así que, en caso de duda, mandamos los dos y nos curamos en salud.
- He definido un umbral mínimo para la probabilidad que devuelve el modelo, por defecto es 0.5. Esto lo hago porque para casos en donde la probabilidad es inferior a esa cifra, considero que hay demasiada «duda» en el modelo a la hora de categorizar un movimiento y prefiero forzar que se categorice como otros.
- Se presentan solo las categorías que tienen sentido, lo que os avanzaba en la parte de definición de categorías, en caso de un movimiento de tipo ingreso, se enviarán las categorías asociadas a este tipo de operativa y las categorías que pueden reflejar tanto ingresos como pagos. No tiene sentido para un ingreso pasar categorías de pagos y viceversa.
- Muchos movimientos solo se diferencian en un número de factura, y ese número no cambia la categoría que asignar. Antes de consultar al modelo, el script sustituye los dígitos por una marca y usa el resultado como clave de caché. Así `factura nº20264500092` y `factura nº20264500187` del mismo concepto se consultan una sola vez. Con cientos de filas repetitivas, el ahorro de peticiones es considerable.
- Es reanudable, solo se clasifican las filas con tipo_movimiento con valor NULL. Si algo falla a mitad del procesado, se vuelve a lanzar y continúa en donde lo dejó. También se puede forzar una reclasificación poniendo este campo a NULL para todas las entradas; por si jugáis a crear, modificar o borrar categorías y queréis ver cómo las aplica.
- Comprueba la versión de Ollama al arrancar. Si no responde se corta rápido, así nos evitamos que lance peticiones fallidas en bucle hasta procesar todas las entradas.
- Proceso de calentamiento del modelo, no queremos que se nos lesione. Cargar el modelo en una primera petición lleva su tiempo y las primeras peticiones pueden dar timeout o un error 5xx mientras tanto. Antes de empezar, el script lanza una primera petición y espera, reintentando cada pocos segundos hasta que el modelo responde.
Lo que he hecho en el ejemplo que os presento al final es lo de siempre en un modelo de datos, añadir columnas derivadas que aportan detalle a la tabla base y permiten más analíticas de datos. La novedad es que una de las columnas la calcula un modelo que interpreta texto, en lugar de reglas escritas a mano y que permite añadir más categorías de forma sencilla.
Los modelos de decisión que implementan la API de Jev nos facilitan el tratamiento de la respuestas. Los modelos LLM generativos los hay que acotar tratando de forzar un formato de salida, y que no alucinen y devuelvan categorías que no existen. La limitación de los modelos de decisión a decidir solo entre las categorías que se les pasa, y en devolver siempre un formato tipado, es su principal ventaja para este tipo de tareas, asegurándonos el tener el control completo de la salida recibida.
Y como veis no es necesario mucho recurso para sacarles partido, el modelo con el que he trabajado corre en un equipo en local. Para los curiosos os pongo la configuración:
- Se trata de un mini pc con un AMD Ryzen 9 8945HS con una gráfica integrada Radeon 780m.
- 32 GB de memoria RAM LPDDR5 a 5600MHz
- Configurada en la BIOS para que la gráfica pueda usar hasta 16GB de memoria compratida del equipo.
- El sistema operativo es PopOS 24.04 LTS.
- Y versión 0.35 de Ollama.
Espero que este post y el código de ejemplo os sea útil y os haya abierto una nueva vía de explotar lso nuevos modelos de IA que se están desarrollando. Ya me contaréis qué se os ocurre hacer con modelos de decisión en el mundo del dato.
Referencias
Nimble en Ollama: https://ollama.com/library/nimble- Anuncio del soporte de modelos de decisión en Ollama: https://ollama.com/blog/ollama-now-supports-decision-models
- Repositorio de Nimble (Bespoke Labs): https://github.com/bespokelabsai/nimble
- Modelo en Hugging Face: https://huggingface.co/bespokelabs/Bespoke-Nimble-9B
- Benchmarks públicos de Nimble: https://github.com/bespokelabsai/nimble/blob/main/docs/PUBLIC_BENCHMARKS.md
- TypeSafe (API Jev): https://typesafe.ai
- Repositorio con el código de ejemplo: https://github.com/tblproject/tbl_ai.git
