AI Hiring Matcher
Um matcher de currículos para vagas que audita o próprio rótulo de treinamento antes de confiar nele
O problema
Isso começou como um envio para um Datathon: um classificador simples sobre sete campos categóricos, sem nenhum sinal de texto. Esta é a reconstrução. Um matcher real baseado em recuperação substitui o classificador categórico cego, e os dados de treinamento passam por uma auditoria honesta antes de qualquer coisa ser treinada sobre eles.
Arquitetura
Auditoria de fairness
Matching
O rótulo é enviesado por gênero
Antes de confiar no rótulo Best Match do dataset como alvo de treinamento, uma auditoria de fairness verifica sua taxa de seleção por grupo demográfico, direto nos dados brutos, sem nenhum modelo envolvido. Best Match é 1 para 61,3% das linhas de homens contra 35,4% das linhas de mulheres no geral. Por cargo, a diferença é bem mais extrema e não uniforme. Alguns cargos favorecem homens, outros favorecem mulheres, com diferenças de até 89 pontos percentuais (Jornalista: 6,2% mulheres contra 95,6% homens).
Entre os 102 grupos de cargo e gênero, as taxas são fortemente bimodais: 53 grupos ficam em 20% ou menos, 49 em 80% ou mais, e nenhum fica no meio. Essa é a assinatura de um rótulo amostrado por grupo com uma probabilidade fixa perto de 0,1 ou 0,9, não algo que reflete qualificação real. Idade, raça, etnia, nível de experiência e certificações não mostram um sinal comparável. Esse viés é específico de gênero e específico de cargo.
A consequência direta no design: o classificador nunca recebe gênero, raça ou etnia como feature, mesmo gênero sendo de longe o sinal mais forte para o rótulo. Treinar com um atributo protegido para prever um match construído sobre esse atributo simplesmente reproduziria o viés que a auditoria existe para pegar.
Como o matcher funciona
Currículos e descrições de vaga são transformados em embedding com sentence-transformers (all-MiniLM-L6-v2, local, sem chave de API) e ranqueados por similaridade de cosseno contra um catálogo de 51 vagas. Avaliado como recuperação (o currículo recupera seu próprio cargo entre as 51 vagas conhecidas), o resultado é 63,1% de Recall@1, 88,2% de Recall@5 e um MRR de 0,745. É um problema de conjunto fechado: ranquear entre 51 vagas conhecidas, não generalizar para vagas que o modelo nunca viu.
Exemplo de requisição
Saída real, gerada pelo modelo treinado neste repositório.
curl -X POST http://localhost:8000/match \
-H "Content-Type: application/json" \
-d '{"resume": "Proficient in Python, SQL, Machine Learning, with senior-level experience in the field. Holds a master's degree. Skilled in delivering results and adapting to dynamic environments.", "top_n": 3}'{
"matches": [
{"job_role": "Software Engineer", "similarity": 0.564, "skill_overlap": 0.0, "best_match_proba": 0.480},
{"job_role": "AI Specialist", "similarity": 0.530, "skill_overlap": 0.25, "best_match_proba": 0.484},
{"job_role": "Machine Learning Engineer", "similarity": 0.484, "skill_overlap": 0.125, "best_match_proba": 0.487}
]
}Os limites honestos do classificador
Uma regressão logística separada prevê o Best Match a partir da similaridade de cosseno e da sobreposição de skills. O F1 dela na classe positiva é cerca de 0,08, pouco acima do acaso. Isso não é um bug para corrigir ajustando hiperparâmetros. É o resultado esperado de deliberadamente não fornecer o atributo (gênero) que de fato guia o rótulo. Melhorar esse F1 significaria alimentar o modelo com o atributo protegido que a auditoria existe para pegar, o que anula o propósito.
Monitoramento de drift
Uma checagem separada compara requisições reais registradas contra uma referência construída da mesma forma que uma requisição ao vivo é pontuada. Ela roda um teste estatístico por coluna e se recusa a rodar abaixo de 100 requisições registradas, já que testes de drift não são confiáveis em amostras pequenas.
Testes
28 testes cobrem a auditoria de fairness, o ranking e as métricas de recuperação do matcher, e o pipeline de predição, rodados com pytest. Ruff e mypy passam sem erros. Não há CI configurado, então tudo isso roda localmente em vez de a cada push.
Limitações
- ▸Apenas recuperação em conjunto fechado. O matcher ranqueia entre as 51 vagas vistas no treinamento. Não generaliza para vagas que nunca viu.
- ▸O classificador Best Match é fraco por design, como visto acima. É uma escolha deliberada, não um descuido, mas significa que o classificador não é realmente útil para ranquear além do que a etapa de recuperação já faz.
- ▸Alertas de drift precisam de tráfego real. Abaixo de 100 requisições registradas, a checagem simplesmente não roda.
- ▸Sem CI. Testes e lint rodam localmente, não automaticamente a cada push.