Volver al blog
Una sala de archivo en penumbra con miles de documentos suspendidos en el aire como puntos de luz cálida, agrupados en constelaciones con nombre propio. En el centro, un pliego cae despacio hacia uno de esos grupos: es el documento nuevo encontrando su sitio por proximidad, sin que nadie lo haya etiquetado.

Clasificar documentos en 77 categorías sin entrenar un modelo

Caso realKonectTaxonomíaIA aplicada

Una empresa de gestión documental recibe cada día decenas de correos con documentos adjuntos: actas de reunión, facturas de mantenimiento, pólizas, presupuestos de proveedores, requerimientos oficiales. Entran todos por el mismo sitio y ninguno viene etiquetado. Cuando alguien necesita el acta de la reunión extraordinaria de hace dos años, empieza la búsqueda a mano.

La plataforma ya transcribía las llamadas de ese cliente y analizaba sus conversaciones. Los adjuntos entraban, se les extraía el texto y ahí se quedaban. Clasificarlos era el paso que faltaba, y la decisión de fondo con la que se resolvió va a contracorriente: no se entrenó ningún modelo.

El categorizador es trabajo del equipo de producto en Irontec, donde trabajé como arquitecto de software y arquitecto de plataformas. Lo cuento por el diseño, que me parece de lo más sensato que he visto para este tipo de problema.

La opción obvia, y por qué no

Lo natural sería entrenar un clasificador. La taxonomía tiene 77 subcategorías repartidas en 15 familias —actas, contratos, finanzas, tributación, seguros, mantenimiento, personal…—, así que hablamos de un problema multiclase con muchas clases y muy desequilibrado.

Ahí es donde se rompe. Entrenar exige un corpus etiquetado con suficientes ejemplos de cada una de las 77, y la cola larga nunca los tiene: de «contratos de suministro» o de «seguridad y prevención» llegan cuatro documentos al año. Y cada vez que la empresa quiera partir una categoría en dos, a reentrenar y a reetiquetar. El coste no está en el entrenamiento inicial, está en mantenerlo vivo.

Prototipos en vez de pesos

El módulo mueve el problema de sitio. En lugar de aprender una frontera entre clases, coloca cada categoría como un punto en el espacio de embeddings y mide distancias. Un documento nuevo se convierte en un vector y gana la categoría cuyo punto esté más cerca.

Cada subcategoría tiene dos anclas, no una: el centroide de sus documentos de ejemplo y el embedding de su descripción escrita. La primera captura cómo son los documentos de verdad; la segunda, qué dice la definición. Pesan lo mismo, mitad y mitad. Cuando una categoría tiene cuatro ejemplos, la descripción sostiene el ancla; cuando la descripción es vaga, mandan los documentos. Es un reparto de riesgo entre dos fuentes que fallan por motivos distintos.

77subcategorías en 15 familias
1llamada de embedding por documento
0,88umbral por debajo del cual entra el LLM
0modelos entrenados

El problema de los hubs

Aquí está el detalle que separa esto de una búsqueda por similitud del montón. En espacios de muchas dimensiones aparecen vectores que salen como vecinos de casi todo: los hubs. Una categoría genérica —«comunicaciones, otros»— acaba colocada en mitad de la nube y gana comparaciones que no debería ganar, no porque encaje mejor, sino porque está cerca de todo. Si clasificas por coseno y ya está, la cola larga se te va entera al cajón de sastre.

La corrección se llama CSLS y consiste en restar esa popularidad por los dos lados:

sims_c = protos.centroids @ doc_vec                # coseno contra los 77 centroides
sims_d = protos.description_embeddings @ doc_vec   # y contra las 77 descripciones

# cuánto de "cerca de todo" está este documento (media de sus 10 mejores)
rd_c = np.sort(sims_c)[-k:].mean()
rd_d = np.sort(sims_d)[-k:].mean()

# rc_* viene precalculado: cuánto de "cerca de todo" está cada categoría
csls_c = 2.0 * sims_c - rd_c - protos.rc_centroid
csls_d = 2.0 * sims_d - rd_d - protos.rc_description

mix = 0.5 * csls_c + 0.5 * csls_d

A la similitud bruta se le descuenta lo popular que es el documento y lo popular que es la categoría. Lo que queda es afinidad específica. El vector de popularidad de cada categoría se calcula una vez, al construir los artefactos, y viaja con ellos en un JSON: en tiempo de clasificación no hay más cálculo que un producto de matrices.

Sin la corrección de hubness, clasificar por similitud en 1024 dimensiones favorece sistemáticamente a las categorías genéricas. Es el fallo silencioso de medio RAG que hay montado por ahí.

El LLM entra el último, y solo si hace falta

Con las puntuaciones ordenadas hay que decidir si el resultado es fiable. El sistema mira la similitud coseno cruda de la primera candidata —no la puntuación CSLS, ni la confianza— y la compara con 0,88.

La distinción importa. CSLS reordena bien pero deforma la escala, así que su número no significa nada en absoluto. Y la confianza es un softmax sobre las cinco finalistas: mide cuánto destaca la primera sobre las otras cuatro, no si la primera es buena. Cinco candidatas malas y parecidas dan una confianza baja; cinco candidatas malas de las que una destaca dan una confianza alta y una respuesta igual de mala. Para decidir «esto lo tengo claro» hace falta una medida absoluta, y la única que hay es el coseno.

Por encima del umbral, el documento se resuelve sin llamar a nadie. Por debajo, las cinco mejores van a un modelo de lenguaje con sus descripciones, el texto del documento y el asunto del correo como contexto, y elige una. Si la que elige es distinta, se sustituye y queda marcado que lo decidió el modelo.

Loading diagram...

Lo que hace falta para que aguante en producción

El resto del módulo son decisiones pequeñas que solo se le ocurren a quien ya se ha dado el golpe.

  • Documentos largos. Un acta de reunión no cabe en una llamada de embedding. Se parte en trozos de 6.000 tokens con 200 de solape y se promedian sus vectores, pero con peso decreciente: el primer trozo pesa más que el segundo, y así sucesivamente. La cabecera de un acta dice mucho más sobre qué es ese documento que su última página de firmas.
  • Texto insuficiente. Por debajo de 50 caracteres extraídos, el adjunto se omite sin error y los demás siguen clasificándose. Un PDF escaneado del que no sale texto no rompe el correo entero.
  • Integridad. Cada adjunto se contrasta con el SHA-256 que se guardó al recibirlo. Si no cuadra, la categorización del correo falla en vez de clasificar un contenido que ya no es el que llegó.
  • Modelos por alias. Ni el modelo de embeddings ni el reranker nombran a un proveedor: piden un alias a la pasarela —uno de embeddings, otro de reranking— y ella decide qué hay detrás. Es el mismo patrón que describí en el post de LLMOps, y aquí se nota: cambiar el modelo de reranking no toca el módulo.

El humano cierra el círculo

Toda clasificación nace marcada como no revisada. En la pestaña de análisis de la conversación se ve qué se le asignó a cada adjunto, y quien tenga acceso puede corregirlo eligiendo entre las cinco candidatas o buscando en la taxonomía entera. Al corregir, la categoría padre se deriva sola —no se puede dejar una subcategoría colgando de una familia que no le corresponde— y, esto es lo importante, se conserva la predicción original.

Ese detalle es lo que convierte la revisión en algo más que un arreglo puntual. Con la predicción y la corrección guardadas juntas, un responsable puede exportar el conjunto filtrando por fechas, categoría o solo lo revisado por personas, y sale un JSON con el texto del documento, lo que predijo el sistema y lo que dijo el humano. Cada corrección es una etiqueta gratis para la siguiente versión de los prototipos.

Cuándo copiaría este diseño

Siempre que haya muchas clases, pocos ejemplos por clase y una taxonomía que aún se está moviendo. En esas tres condiciones, entrenar es comprometerse con una foto del problema que va a quedar vieja; los prototipos se regeneran en minutos y la definición escrita de una categoría vale casi tanto como sus ejemplos.

Lo que más me gusta es dónde queda el gasto. La parte cara —preguntar a un modelo de lenguaje— es la excepción, no el camino habitual: solo paga quien lo necesita, y el umbral que lo decide es un número visible que se puede discutir, medir y mover. Es lo contrario a meter un LLM en el centro del sistema y descubrir la factura a fin de mes.

Más casos reales