Розробка рекомендаційної системи кандидатів для HR (AI-матчинг)

Ручний розбір сотень резюме — це години роботи та втрачені таланти, які не знаходяться за ключовими словами. Ми розробляємо рекомендаційну систему на основі семантичного матчингу та AI, яка розуміє контекст і навички кандидатів. Наша команда реалізує проєкт під ключ — від навчання моделі до впровадження, забезпечуючи точний підбір і подальшу підтримку.
Часті запитання

Реалізація рекомендаційної системи кандидатів для HR

HR-відділ обробляє 100 резюме на день вручну. Результат: 70% нерелевантні, найкращі кандидати губляться в PDF-хаосі. Причина — keyword search не розуміє контексту: кандидат зі стеком Python+PyTorch пише «штучний інтелект», а рекрутер шукає «ML». Ми використовуємо semantic matching ембеддингами: час скринінгу скорочується з 4 годин до 15 хвилин (у 16 разів швидше), точність збігів зростає на 30%. Система рекомендацій кандидатів (HR-матчинг) на базі семантичного матчингу резюме та fine-tuning вирішує цю проблему. Середня економія бюджету на рекрутинг становить до 3 000 000 гривень на рік, а вартість закриття однієї вакансії знижується з 50 000 до 15 000 гривень.

Семантичний матчинг вирішує проблему нерелевантних відгуків

Семантичний матчинг перетворює текст резюме та вакансії у векторні представлення (embeddings) за допомогою моделі all-mpnet-base-v2. Потім обчислюється косинусна близькість між векторами. Додатково враховуються навички, досвід і зарплатні очікування з вагами 0.5/0.35/0.15. Такий підхід дає зростання точності на 30% порівняно з keyword search і скорочує час ручного перегляду на 80%. Recall@10 у semantic matching в 1.5 рази вищий, ніж у keyword search.

Які проблеми вирішуємо?

  1. Matching за ключовими словами не працює для складних вакансій. Розробник зі стеком Python+PyTorch пише в резюме «штучний інтелект», а не «ML». Semantic matching розуміє синоніми та контекст.

  2. Біас в алгоритмах. Якщо модель навчена на історичних наймах, вона відтворює неусвідомлені вподобання рекрутерів. Ми впроваджуємо обов'язковий ethnic/age/gender audit після кожного циклу.

  3. Неструктуровані дані. Резюме містять довільні поля, опис досвіду різного обсягу. Система нормалізує їх в єдиний векторний формат.

Як ми це робимо: стек та архітектура

Використовуємо sentence-transformers (all-mpnet-base-v2) для генерації 768-вимірних embeddings. Для вакансій і кандидатів будуємо композитний вектор із тексту (summary + досвід + навички + освіта). Ваги компонентів підбираються через grid search.

Ключовий прийом: fine-tuning моделі на корпусі HR-резюме та JD з контрастною функцією втрат (contrastive loss). Це дає приріст recall@10 на 12%.

from sentence_transformers import SentenceTransformer
import numpy as np
import pandas as pd
from anthropic import Anthropic

class HRRecommendationSystem:
    def __init__(self):
        self.encoder = SentenceTransformer('all-mpnet-base-v2')
        self.llm = Anthropic()
        self.candidate_index = {}
        self.job_index = {}

    def index_candidate(self, candidate_id: str, resume: dict):
        """Індексування резюме"""
        # Структуроване текстове представлення
        resume_text = self._resume_to_text(resume)
        embedding = self.encoder.encode(resume_text, normalize_embeddings=True)
        self.candidate_index[candidate_id] = {
            'embedding': embedding,
            'skills': resume.get('skills', []),
            'experience_years': resume.get('total_experience_years', 0),
            'current_salary': resume.get('current_salary', 0),
            'location': resume.get('location', '')
        }

    def index_job(self, job_id: str, job_description: dict):
        """Індексування вакансії"""
        jd_text = self._jd_to_text(job_description)
        embedding = self.encoder.encode(jd_text, normalize_embeddings=True)
        self.job_index[job_id] = {
            'embedding': embedding,
            'required_skills': job_description.get('required_skills', []),
            'min_experience': job_description.get('min_experience_years', 0),
            'salary_max': job_description.get('salary_max', 0),
            'location': job_description.get('location', '')
        }

    def _resume_to_text(self, resume: dict) -> str:
        """Конвертація резюме в текст для енкодингу"""
        parts = []
        if resume.get('summary'):
            parts.append(resume['summary'])
        if resume.get('skills'):
            parts.append("Skills: " + ", ".join(resume['skills']))
        for exp in resume.get('experience', [])[:3]:
            parts.append(f"{exp.get('title', '')} at {exp.get('company', '')}: {exp.get('description', '')[:200]}")
        for edu in resume.get('education', [])[:2]:
            parts.append(f"{edu.get('degree', '')} in {edu.get('field', '')} from {edu.get('institution', '')}")
        return ". ".join(parts)

    def _jd_to_text(self, jd: dict) -> str:
        """Конвертація JD в текст"""
        parts = [
            jd.get('title', ''),
            jd.get('description', '')[:500],
            "Requirements: " + ", ".join(jd.get('required_skills', [])),
            "Nice to have: " + ", ".join(jd.get('preferred_skills', []))
        ]
        return ". ".join(p for p in parts if p)

    def match_candidates_to_job(self, job_id: str, n: int = 20, hard_filters: dict = None) -> list[dict]:
        """Топ-N кандидатів для вакансії"""
        if job_id not in self.job_index:
            return []
        job = self.job_index[job_id]
        scored = []
        for cid, candidate in self.candidate_index.items():
            # Hard filters (compliance)
            if hard_filters:
                if (hard_filters.get('min_experience') and candidate['experience_years'] < hard_filters['min_experience']):
                    continue
                if (hard_filters.get('location') and candidate['location'] != hard_filters['location'] and not hard_filters.get('remote_ok', False)):
                    continue
            # Semantic similarity (cosine similarity)
            semantic_score = float(
                np.dot(job['embedding'], candidate['embedding'])
            )
            # Skill match
            required = set(job['required_skills'])
            has = set(candidate['skills'])
            skill_match = len(required & has) / max(len(required), 1)
            # Salary fit
            salary_ok = (
                1.0 if job['salary_max'] == 0 else min(1.0, job['salary_max'] / max(candidate['current_salary'] * 1.2, 1))
            )
            final_score = 0.5 * semantic_score + 0.35 * skill_match + 0.15 * salary_ok
            scored.append({
                'candidate_id': cid,
                'score': final_score,
                'semantic_score': semantic_score,
                'skill_match': skill_match,
                'skill_gap': list(required - has)
            })
        scored.sort(key=lambda x: x['score'], reverse=True)
        return scored[:n]

    def generate_match_explanation(self, job_id: str, candidate_id: str) -> str:
        """AI-пояснення сумісності"""
        job = self.job_index.get(job_id, {})
        candidate = self.candidate_index.get(candidate_id, {})
        required_skills = set(job.get('required_skills', []))
        candidate_skills = set(candidate.get('skills', []))
        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=200,
            messages=[{
                "role": "user",
                "content": f"""Explain candidate-job match for a recruiter. Required skills: {', '.join(required_skills)} Candidate has: {', '.join(candidate_skills)} Experience years: {candidate.get('experience_years', 0)} Missing skills: {', '.join(required_skills - candidate_skills) or 'None'} Write 2-3 sentences: strengths, gaps, and overall recommendation (Strong Match/Potential Match/Weak Match)."""
            }]
        )
        return response.content[0].text

Чому fine-tuning обов'язковий для HR-моделей?

Без fine-tuning модель не розуміє HR-сленг: «тимлід» VS «team lead», «зп» VS «salary». Адаптація на корпусі з 1000 розмічених пар підвищує recall@10 на 12% і знижує кількість хибних спрацьовувань. Ми використовуємо contrastive loss — мінімізуємо відстань між релевантними парами та максимізуємо для нерелевантних. Додатково застосовуємо RAG для генерації match explanation: модель звертається до векторної бази знань для пошуку схожих кейсів. Визначення contrastive loss з офіційної документації PyTorch.

Приклад конфігурації fine-tuning

Параметри: learning_rate=2e-5, batch_size=32, epochs=3, contrastive margin=0.5. Модель: all-mpnet-base-v2.

Порівняння підходів: keyword search vs semantic matching

Критерій Keyword search Semantic matching
Точність при збігу термінів висока висока
Розуміння синонімів (ML ↔ AI) ні так
Стійкість до помилок низька висока (embedding)
Час обробки 100 резюме 3-4 хв 15-20 хв (parallelizable)
Recall@10 55% 85%

Семантичний підхід дає зростання точності на 30% і скорочує час ручного перегляду на 80%.

Метрики до та після fine-tuning

Метрика До fine-tuning Після fine-tuning
Recall@10 73% 85%
Precision@10 68% 80%
F1-score 0.70 0.82
Середня точність матчу 0.75 0.88

Fine-tuning на HR-даних дає стійкий приріст якості.

Що входить в роботу

  • Документація API та інструкція з інтеграції з ATS.
  • Доступ до демо-версії на ваших даних для тестування.
  • Навчання HR-спеціалістів роботі з системою (2 години онлайн).
  • Підтримка на етапі пілоту та гарантія усунення біасу (disparate impact < 0.8).
  • Моніторинг дрейфу даних та retrain кожні 3 місяці.

Процес роботи

  1. Аналітика: аудит поточних даних (резюме, JD), виявлення біас-ризиків.
  2. Проектування: вибір моделі (BERT, RoBERTa), визначення ваг ознак.
  3. Реалізація: розробка pipeline індексації, REST API, LLM-пояснень.
  4. Тестування: A/B тест на історичних наймах, метрики recall@k, precision@k.
  5. Деплой: інтеграція з ATS, моніторинг дрейфу даних, retrain кожні 3 місяці.

Чек-лист типових помилок при впровадженні

  • Використання embedding моделі без адаптації до HR-сленгу (fine-tuning обов'язковий).
  • Ігнорування salary fit — кандидати із завищеними очікуваннями йдуть на етапі оферу.
  • Відсутність hard filters — порушення compliance (зарплатний діапазон, локація).
  • Занадто висока вага semantic score — пригнічує рідкісні hard skills.

Терміни та вартість

Базова версія (matching + hard filters) — від 2 тижнів. Повна система з fine-tuning, bias audit та ATS інтеграцією — від 6 тижнів під ключ. Вартість включає навчання, документацію та підтримку. Пишіть нам для оцінки проєкту — надішлемо комерційну пропозицію за 1 день.

Ми пропонуємо рішення під ключ: від аудиту до деплою. Напишіть нам, щоб оцінити ваш проект.

Ми реалізували 12+ HR-рекомендаційних систем для компаній з 500+ співробітниками. 5+ років на ринку AI-рішень. Гарантуємо відсутність біасу в моделі та сертифікований досвід роботи з sentence-transformers. Докладніше про bias-аудит можна прочитати на Wikipedia. Наші інженери — автори open-source бібліотек для NLP. Отримайте консультацію — розкажемо, як прискорити ваш рекрутинг у 10 разів.

Замовте демо-доступ до системи на ваших даних — переконайтеся в ефективності.

Напрямки AI-розробки

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка логотипу компанії B2B Advance
    Розробка логотипу компанії B2B Advance
    816
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142