GenAI: Lecciones aprendidas en la creación de un microservicio de mapeo automático(Parte II)

En el mundo del análisis de datos, uno de los desafíos más persistentes y costosos es la integración de datos heterogéneos. Cada nuevo cliente o fuente de datos llega con su propio esquema, sus propias convenciones de nombrado y su propia estructura. Mapear manualmente cientos de columnas como `DNI`, `ID_Cliente` o `Nro_Documento` al campo estándar que espera una librería de datos o un modelo de machine learning es una tarea tediosa, propensa a errores y consume un tiempo valioso que podría dedicarse al análisis.

En nuestro equipo, nos enfrentamos a este problema a gran escala. Necesitábamos una solución que fuera no solo automática e inteligente, sino también robusta, escalable y, sobre todo, fiable para un entorno de producción. Así nació el proyecto **AutoMapper**, una librería Python que ha evolucionado desde un simple clasificador basado en LLM hasta una sofisticada herramienta con una arquitectura híbrida.

Este es un resumen técnico de mi viaje, los desafíos que encontré y las soluciones de diseño que implementé.

El Problema Inicial: El Mapeo Manual No Escala

El punto de partida era claro: el mapeo manual de columnas de DataFrames a nuestro esquema canónico (QA0) era un cuello de botella. El proceso era:

  1. Recepción de Datos: Un analista recibe un fichero CSV o Excel de un cliente.
  2. Análisis Manual: El analista debe revisar cada nombre de columna (ej. `nombre_y_apellidos`) y cada muestra de datos (ej. «Juan Pérez») para deducir a qué campo estándar de nuestro sistema corresponde (ej. `fullname`).
  3. Implementación: Se escribe un script de transformación para renombrar y ajustar las columnas.

Este flujo de trabajo, aunque funcional para casos aislados, presentaba problemas críticos:

  • Lentitud: Días o incluso semanas para integrar una nueva fuente de datos.
  • Subjetividad: La calidad del mapeo dependía de la experiencia y el criterio del analista.
  • Fragilidad: Un pequeño cambio en el fichero de origen podía afectar el resultado del proceso de marchine learning que le continuaba.

La Primera Solución: Dejar que el LLM Decida (v1.0)

Nuestra primera incursión en la automatización fue utilizar el poder de los Modelos de Lenguaje Grandes (LLMs), específicamente la familia Gemini de Google Cloud. La idea era simple pero potente: si un humano puede deducir el mapeo a partir del nombre y los datos de una columna, un LLM también debería poder hacerlo.

Estrategia: *Controlled Generation*

Para evitar que el LLM devolviera respuestas en texto libre, utilizamos una de las características más potentes de la API de VertexAI: la Generación Controlada (Controlled Generation). En lugar de pedirle una descripción, le proporcionamos un esquema JSON que define la estructura exacta de la respuesta que esperamos.

El flujo se definió de la siguiente manera:

  1. Selección de Contexto: No podíamos enviar ficheros de millones de filas al LLM. Creé un componente, el `DataSelector`, que implementa una estrategia `rich_data` para seleccionar un pequeño número de filas (ej. 20) que contengan la mayor cantidad de datos no nulos, asumiendo que estas son las más representativas.
  2. Generación del Prompt: Con la muestra de datos (`df_sample`) y la lista de columnas, se construía un prompt que le pedía al LLM que actuara como un «experto en mapeo de datos». Hubo una intensa búsqueda y ajuste de prompt durante todo el desarrollo, casi merece un artículo en si mismo que dejo para otra ocasión.
  3. Llamada a la API: Se enviaba el prompt al LLM junto con el esquema JSON de nuestro modelo de datos. El LLM se veía forzado a devolver un JSON donde cada clave era un campo de nuestro esquema y cada valor era el nombre de la columna del DataFrame que mejor correspondía (o `null` si no encontraba correspondencia).

Esta primera versión fue un éxito rotundo. Redujo el tiempo de mapeo de días a segundos y proporcionó una base sólida.

El Desafío de la Realidad: Ambigüedad y la Necesidad de Corrección

El mapeo automático era preciso en un 80-90% de los casos, pero el 10-20% restante era problemático. El LLM a veces fallaba en casos ambiguos, o el usuario simplemente tenía un conocimiento del negocio que el modelo no poseía. Necesitábamos una forma de que el usuario pudiera corregir el mapeo inicial y validarlo.

El requerimiento era volver a usar el LLM: «El usuario ha dicho ‘esto no es el email, es el nombre de usuario’, actualiza el mapeo». Esto nos llevó a un problema fundamental: la ambigüedad. Dependiendo de cómo se expresara el usuario, el LLM a veces actualizaba el campo, a veces se confundía, a veces inventaba campos nuevos… No era fiable.

La Solución: Arquitectura Híbrida (LLM interpreta, Código ejecuta)

Aquí es donde el diseño dio un salto cualitativo e innovador. Luego de muchas pruebas y prototipado finalmente adoptamos un enfoque híbrido que separa la interpretación del modelo, de la ejecución:

  1. Interpretación (LLM): La única tarea del LLM es interpretar la instrucción del usuario y traducirla a una acción estructurada. Creé un nuevo esquema (`user_correction.json`) y un nuevo prompt que le pedía al modelo que devolviera una de cuatro acciones: `assign`, `unassign`, `reassign`, `sugest` o `no_op`, junto con el campo objetivo (`target_field`).
  2. Ejecución Python: Creé una nueva clase, cuyo único trabajo es recibir esa acción estructurada y aplicarla al estado del mapeo de forma **100% determinista**. Esta clase no tiene contacto con el LLM; es puro código Python que aplica una lógica de negocio predecible.

Este enfoque nos dio lo mejor de ambos mundos: la flexibilidad del LLM para entender el lenguaje natural y la fiabilidad del código programático para modificar el estado sin errores ni ambigüedades y al mismo tiempo un efoque innovador, en vez de utilizar un function calling para llamar a estas funciones programaticas, invertimos la lógica para capturar el rasonamiento/interpretación y pasarlo al sistema en un JSON.

La Evolución hacia la Escalabilidad: El Refactor a Stateless

La arquitectura híbrida funcionaba, pero introdujo un nuevo problema: el estado. Para permitir correcciones sucesivas, la instancia de `AutoMapper` necesitaba «recordar» el mapeo original y el mapeo actual. Esto la convertía en una clase stateful, lo que es un gran problema en entornos web concurrentes. Si una única instancia de `AutoMapper` era compartida por múltiples usuarios, sus correcciones se mezclarían. La solución temporal era crear una instancia por cada usuario, lo cual no era escalable.

La versión 3.1.0 abordó esta deuda técnica con una refactorización completa a una arquitectura **stateless**.

Principio Clave: El Cliente Gestiona el Estado

El cambio fue conceptual: la librería `AutoMapper` ya no es responsable de gestionar el estado. Ahora es una herramienta de procesamiento puro.

  • `map columns`: Devuelve el mapeo inicial, pero también el `pure_mapping` (el estado canónico) y el `df_sample` utilizado para hacer ese mapeo y los guarda en una base de datos.
  • `user correction`: Ahora requiere que el cliente le pase el `current_mapping` y las columnas del df como parámetros. La función calcula la corrección, devuelve el nuevo estado y «olvida» todo inmediatamente.

Con este cambio, la responsabilidad de persistir el estado entre llamadas (por ejemplo, en una base de datos asociada a una sesión de usuario) recae enteramente en la aplicación cliente.

Esto nos proporcionó enormes ventajas:

  • Seguridad en Entornos Concurrentes (Thread-Safe): Una única instancia de `AutoMapper` puede ser compartida por toda la aplicación sin riesgo.
  • Escalabilidad: Se eliminó la necesidad de crear una instancia por usuario.
  • Simplicidad en la API: Se eliminaron todos los métodos relacionados con la gestión de estado, resultando en una interfaz más limpia.

Conclusiones

El viaje del AutoMapper ha sido un microcosmos del desarrollo de software moderno basado en IA: empezamos con una idea centrada en el poder del LLM, nos enfrentamos a los límites de la fiabilidad y el determinismo, evolucionamos hacia una arquitectura híbrida más robusta, y finalmente refactorizamos para alcanzar la escalabilidad y la pureza arquitectónica.

La solución actual nos proporciona un sistema que es a la vez inteligente, fiable, predecible y escalable, resolviendo un problema de negocio real de una manera técnicamente sólida.

Scroll al inicio