Todos los equipos de datos maestros tienen el mismo atasco: cuentas que son la misma empresa escrita dos veces. «Hotel Royal SL», «HOTEL ROYAL S.L.» y «Royal Hotel (Madrid)» con tres ids, tres responsables y tres condiciones de pago distintas. La herramienta de MDM caza las fáciles. Lo que queda es la pila que nadie quiere: miles de pares que quizá son duplicados y en los que alguien tiene que mirar dos registros y decidir.
Así convertí esa pila en una cola que una persona puede sacar adelante, con Python haciendo la búsqueda y un modelo de lenguaje en local haciendo el juicio.
La forma del problema
La detección de duplicados (record linkage, entity resolution, elige el nombre) tiene dos mitades que piden herramientas muy distintas:
- Encontrar pares candidatos. Con n registros hay n²/2 pares posibles. No puedes mirarlos todos y no hace falta: la mayoría son obviamente distintos. Esta mitad va de exhaustividad (recall): que ningún duplicado real se escape.
- Decidir sobre cada candidato. Dados dos registros parecidos, ¿son la misma entidad? Esta mitad va de precisión y de explicar la decisión, porque deshacer una fusión en MDM es caro.
Las reglas son excelentes en la primera mitad y mediocres en la segunda. Los modelos de lenguaje, al revés. Así que el pipeline se parte justo por ahí.
Primera mitad: las reglas generan los candidatos
Python a secas, sin modelo:
- Normalizar cada campo una vez: minúsculas, sin acentos, espacios colapsados, formas jurídicas fuera (
S.L.,SLU,GmbH,Ltd), abreviaturas habituales expandidas (hotel/htl,avenida/avda). - Bloquear para comparar solo registros que podrían coincidir: mismo país y prefijo de código postal, o misma raíz de NIF, o mismo primer token del nombre normalizado. Cada bloque es pequeño; la unión de bloques es el conjunto de candidatos.
- Puntuar cada candidato con similitud barata (solapamiento de tokens en el nombre, distancia de cadenas en la dirección, coincidencia exacta de NIF, teléfono, dominio del email). Los pares por encima de un umbral alto se marcan como duplicados; los que están por debajo de un umbral bajo se descartan. Todo lo de en medio —la zona gris— pasa a la segunda mitad.
Este paso, por sí solo, elimina más del 95 % de los pares. Y es completamente determinista, que importa cuando alguien pregunta «¿por qué ha salido este par?».
Segunda mitad: un LLM local decide, con motivo
Para cada par de la zona gris, el modelo recibe los dos registros lado a lado y una instrucción estricta: responder con un objeto JSON y nada más.
{
"same_entity": true,
"confidence": "high",
"reason": "Misma raíz de NIF y código postal; el nombre solo difiere en la forma jurídica y el orden de palabras."
}
Lo que hizo que funcionara:
- Salida estructurada y validada. La respuesta se parsea como JSON y se comprueba contra un esquema. Si no parsea, el par se reintenta una vez y después va a una persona. Al modelo nunca se le permite «explicarse en prosa».
- Un prompt corto y explícito. Los modelos pequeños siguen mejor un prompt de sistema de diez líneas que una página de indicaciones. Las reglas del juego (qué cuenta como la misma empresa, qué es una filial, qué hacer con un campo vacío) van en viñetas.
- El motivo es el producto. El campo
reasones lo primero que lee el revisor. Una decisión errónea con un motivo claro se corrige en segundos; una acertada sin motivo se sigue comprobando. - Lotes, no chat. Los pares pasan en lotes con un prompt fijo, así los resultados son reproducibles y el rendimiento, predecible.
Por qué un modelo en local
Los registros son datos de clientes. Mandarlos a una API alojada implicaba una revisión de tratamiento de datos, un contrato y una línea de presupuesto, para un pipeline que corre unas pocas veces al mes. Un modelo local vía Ollama (un qwen2.5 pequeño) se saltó todo eso: los datos nunca salieron del portátil y el coste fue cero.
La limitación era el hardware: un portátil de trabajo con 32 GB de RAM y sin GPU. Eso descarta modelos grandes, pero un modelo de clase 3B en CPU responde a un par en pocos segundos, y la zona gris tras el bloqueo es lo bastante pequeña como para cubrirla en una ejecución nocturna. Las reglas son lo que hace que un modelo pequeño baste: el modelo nunca ve los pares fáciles, solo aquellos en los que hace falta criterio.
La cola de revisión
Nada se fusiona automáticamente. La salida es una cola ordenada por confianza e impacto (cuántas transacciones cuelgan de las cuentas), con los dos registros, la puntuación, la decisión del modelo y su motivo. Un steward acepta o rechaza; la fusión se hace en la herramienta de MDM, que es donde toca. Los rechazos se guardan: son el mejor conjunto de prueba para la siguiente versión del prompt y de los umbrales.
Lo que sale mal
- Filiales y sucursales. «Hotel Royal Madrid» y «Hotel Royal Barcelona» comparten propietario, raíz de NIF y nombre. Si son una cuenta o dos es una regla de negocio, no una cuestión de similitud. Está escrita en el prompt y en las reglas de bloqueo, y sigue siendo la principal fuente de correcciones humanas.
- Homónimos. Dos empresas realmente distintas con el mismo nombre común en ciudades distintas. El bloqueo geográfico caza la mayoría; el motivo del modelo caza el resto («mismo nombre, NIF y ciudades distintos»).
- Campos vacíos. Un registro sin NIF ni dirección no se puede emparejar con confianza. Al modelo se le indica que diga
confidence: lowen vez de adivinar, y esos pares van al principio de la cola.
Lo que le diría a quien empiece
- Dedica la primera semana a normalizar y bloquear. Ahí vive el recall, y ningún modelo arregla unos candidatos malos.
- Exige salida estructurada desde el primer día y valídala. El texto libre de un modelo no es un dato.
- Mantén a una persona en el circuito y guarda sus decisiones. El registro de revisión es tu conjunto de evaluación.
- Un modelo pequeño en local con buenas reglas gana a un modelo grande remoto sin reglas, y además es la versión que te dejan ejecutar sobre datos de clientes.