Proyectos

Ixtli — Reclutamiento con IA

Sistema de reclutamiento potenciado por IA que analiza currículums y los compara con los puestos definidos por cada cliente, para encontrar al talento ideal de forma más rápida, objetiva y privada.

Python · FastAPI · PostgreSQL · Ollama · Embeddings · LLM local · Ministral 3 8B

Ixtli — Reclutamiento con IA

Hoy en día el auge de la IA se vive en el día a día, y los modelos pequeños pueden correr localmente, sin usar una supercomputadora. En este proyecto me pude dar cuenta de que los LLM pequeños ya son suficientes para esto.

El problema

Filtrar candidatos es lento y subjetivo: los equipos revisan cientos de CVs a mano, el criterio cambia entre revisores, y los datos sensibles de los postulantes terminan en servicios como OpenAI y terceros. Las empresas necesitan cribar talento rápido y de forma consistente, sin exponer información personal.

Mi rol

Diseño y construcción del sistema completo: backend en FastAPI, el pipeline de IA (extracción, embeddings y LLM), el modelo de puntaje híbrido y la persistencia multi-tenant.

La solución y las decisiones

Resultado e impacto

La matemática detrás del match

El match no es una caja negra: es una cadena de operaciones deterministas combinada con un LLM, con pesos elegidos a propósito. Así funciona por dentro.

1. Embeddings: del texto a un vector

El modelo paraphrase-multilingual-MiniLM-L12-v2 convierte cualquier texto en un vector de 384 dimensiones. Ninguna dimensión significa algo por sí sola; el modelo fue entrenado para que textos con significado parecido apunten en direcciones parecidas. «Desarrollé APIs en Python» y «construí servicios backend con FastAPI» casi no comparten palabras, pero sus vectores quedan casi paralelos. Eso es lo semántico: cercanía de significado, no de palabras coincidentes.

E(texto) → v ∈ ℝ³⁸⁴

2. Similitud del coseno: el ángulo, no las palabras

Dos textos se comparan por el ángulo entre sus vectores: 1.0 = mismo significado, 0 = sin relación. Se usa coseno y no distancia euclidiana a propósito: un CV de 3 páginas produce un vector más «cargado» que una lista de 5 habilidades, y la euclidiana castigaría esa diferencia de tamaño. Al coseno solo le importa hacia dónde apunta el vector, no cuánto mide.

cos(θ) = (A · B) / (‖A‖ · ‖B‖)

3. Fragmentación y el truco del máximo

El modelo trunca su entrada a ~128 tokens, así que el CV se corta en ventanas de 150 palabras con 30 de solape (el solape evita partir una frase clave justo en el borde de dos fragmentos). Para cada componente del puesto, el score es el máximo sobre todos los fragmentos, no el promedio: un CV legítimo tiene secciones irrelevantes (educación, hobbies) que arrastrarían el promedio hacia abajo. La pregunta correcta no es «¿todo el CV habla del puesto?» sino «¿existe alguna parte que matchee fuerte?».

s_componente = máx( cos(E(Fᵢ), E(componente)) )   para i = 1..n

4. Promedio ponderado (con renormalización)

Los tres componentes pesan distinto: funciones 50% (describen el trabajo real del día a día), habilidades 30%, perfil 20% (suele ser texto más abstracto). Si un componente viene vacío se excluye y se divide entre los pesos restantes — sin esa renormalización, un puesto sin «perfil» cargado quedaría injustamente con un techo de 0.8.

S_raw = (0.5·s_func + 0.3·s_hab + 0.2·s_perfil) / Σ pesos válidos

5. Calibración: del coseno crudo a la escala 0–10

Observación empírica clave del proyecto: con este modelo y este esquema de fragmentos, los cosenos reales nunca recorren todo el [0, 1] — viven en ~[0.25, 0.65]. Un CV sin relación da ~0.25 (el «ruido de fondo» del lenguaje: todo texto se parece algo a todo texto); un match fortísimo da ~0.65. Se estira ese rango real a 0–10 con un mapeo lineal recortado. Sin calibrar, todos los candidatos parecerían «mediocres entre 2.5 y 6.5» y el cliente no vería diferencias útiles.

P_sem = clamp( (S_raw − 0.25) / 0.40 , 0, 1 ) · 10

6. El híbrido: dos mediciones ortogonales

El semántico tiene un punto ciego: mide afinidad de rubro, no calidad — un data scientist senior y uno flojo dan cosenos casi iguales porque hablan del mismo tema. Por eso entra el LLM, que sí lee y califica. Las dos señales son casi ortogonales: una responde «¿es del rubro?», la otra «¿qué tan bueno es?». Además, el semántico es el ancla anti-alucinación: es matemática determinista que el LLM no puede inflar, así que si se entusiasma con un CV del rubro equivocado, el 40% semántico lo frena. (Si el LLM no devuelve una puntuación parseable, P_final = P_sem.)

P_final = 0.4 · P_sem + 0.6 · P_llm

7. Ejemplo de punta a punta

Un CV de 390 palabras → 3 fragmentos, contra un puesto con sus 3 componentes. Tomando el máximo por componente (funciones 0.58, habilidades 0.52, perfil 0.44), el LLM leyó el CV y devolvió 8/10. El resultado: 7.6 → «Promedio Alto».

S_raw   = 0.5·0.58 + 0.3·0.52 + 0.2·0.44 = 0.53
P_sem   = (0.53 − 0.25) / 0.40 · 10        = 7.0
P_final = 0.4·7.0 + 0.6·8.0                = 7.6  → «Promedio Alto»

Demo en video

Volver a proyectos