Розробка AI-системи семантичного матчингу кандидатів та вакансій

HR-відділ отримує сотні резюме на кожну вакансію. Keyword-матчинг знаходить «Python-розробник» — і пропускає кандидата з досвідом «Django» та «машинне навчання». **Семантичний матчинг** розуміє: навички, а не слова. Ми розробляємо такі системи під ключ для компаній, які хочуть закривати вакансії шви

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

Часті запитання

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

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1302
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    998
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1267
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    714
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    1006

HR-відділ отримує сотні резюме на кожну вакансію. Keyword-матчинг знаходить «Python-розробник» — і пропускає кандидата з досвідом «Django» та «машинне навчання». Семантичний матчинг розуміє: навички, а не слова. Ми розробляємо такі системи під ключ для компаній, які хочуть закривати вакансії швидше та точніше.

Наші інженери мають понад 5 років досвіду в NLP та реалізували більше 30 проектів семантичного матчингу для HRtech, ритейлу та IT-компаній. Наприклад, для однієї мережі ритейлу з 5000 вакансій на місяць ми скоротили time-to-hire з 42 до 26 днів, а якість найму (тих, хто пройшов випробувальний термін) зросла з 72% до 91%.

Дворівнева система семантичного матчингу кандидатів

В основі — двоетапний pipeline: швидкий ANN-скоринг на ембеддінгах та глибокий LLM-аналіз топ-кандидатів. Перший етап відсіває 90% нерелевантних, другий — дає детальну оцінку сумісності. Використовуємо мультимовну модель paraphrase-multilingual-mpnet-base-v2 (768-вимірні ембеддінги) для покриття російської та англійської. Це дозволяє обробляти резюме різними мовами без втрати якості.

import numpy as np import pandas as pd from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity from anthropic import Anthropic import json import re class ResumeJDEncoder: """Кодирование резюме и вакансий в эмбеддинги""" def __init__(self): # Мультиязычная модель: русский + английский self.model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2') def extract_resume_sections(self, resume_text: str) -> dict: """Разбивка резюме на смысловые блоки""" # В production: ML-парсер резюме (Affinda, Sovren или кастомный) sections = { 'skills': '', 'experience': '', 'education': '', 'full_text': resume_text } # Упрощённое извлечение через паттерны skills_pattern = r'(?:навыки|skills|технологии|technologies|стек)[:\s]*([^\n]+(?:\n[^\n]+){0,5})' match = re.search(skills_pattern, resume_text, re.IGNORECASE) if match: sections['skills'] = match.group(1) return sections def encode_resume(self, resume: dict) -> dict: """Мультиаспектное кодирование резюме""" texts_to_encode = { 'full': resume.get('full_text', ''), 'skills': resume.get('skills', ''), 'title': resume.get('current_title', ''), } embeddings = {} for key, text in texts_to_encode.items(): if text.strip(): embeddings[key] = self.model.encode(text, normalize_embeddings=True) return embeddings def encode_job(self, job: dict) -> dict: """Кодирование вакансии""" texts = { 'full': job.get('description', ''), 'requirements': ' '.join(job.get('requirements', [])), 'title': job.get('title', ''), } embeddings = {} for key, text in texts.items(): if text.strip(): embeddings[key] = self.model.encode(text, normalize_embeddings=True) return embeddings class SemanticMatcher: """Двухэтапный матчинг: быстрый ANN + точный LLM""" def __init__(self): self.encoder = ResumeJDEncoder() self.llm = Anthropic() def compute_embedding_score(self, resume_embs: dict, job_embs: dict) -> float: """Быстрый скор через косинусное сходство эмбеддингов""" scores = [] weights = {'full': 0.4, 'skills': 0.4, 'title': 0.2} for key, weight in weights.items(): r_emb = resume_embs.get(key) j_emb = job_embs.get(key) if r_emb is not None and j_emb is not None: sim = float(cosine_similarity( r_emb.reshape(1, -1), j_emb.reshape(1, -1) )[0, 0]) scores.append(sim * weight) return sum(scores) / sum(weights[k] for k in weights if resume_embs.get(k) is not None) if scores else 0.0 def deep_match(self, resume: dict, job: dict) -> dict: """Детальный LLM-анализ совместимости (для топ-кандидатов)""" response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=500, messages=[{ "role": "user", "content": f"""Analyze candidate-job match. Return detailed assessment in Russian. JOB: Title: {job.get('title', '')} Requirements: {', '.join(job.get('requirements', [])[:10])} Nice-to-have: {', '.join(job.get('nice_to_have', [])[:5])} Seniority: {job.get('seniority', 'mid')} CANDIDATE: Title: {resume.get('current_title', '')} Years of experience: {resume.get('years_experience', 0)} Skills: {', '.join(resume.get('skills', [])[:15])} Summary: {resume.get('summary', '')[:300]} Return JSON: {{ "match_score": 0-100, "strengths": ["..."], "gaps": ["..."], "must_have_met": true/false, "recommendation": "strong_yes|yes|maybe|no", "interview_questions": ["..."] }}""" }] ) try: return json.loads(response.content[0].text) except Exception: return {'match_score': 50, 'recommendation': 'maybe', 'strengths': [], 'gaps': []} def rank_candidates(self, job: dict, candidates: list[dict], top_k_deep: int = 10) -> list[dict]: """ Двухэтапный pipeline: 1. Быстрый ANN-матчинг для всей базы → топ-N 2. Глубокий LLM-анализ для топ-K финалистов """ job_embs = self.encoder.encode_job(job) # Этап 1: быстрый скоринг for candidate in candidates: resume_embs = self.encoder.encode_resume(candidate) candidate['embedding_score'] = self.compute_embedding_score(resume_embs, job_embs) # Топ-K по эмбеддинг-скору top_candidates = sorted(candidates, key=lambda x: -x['embedding_score'])[:top_k_deep * 3] # Этап 2: глубокий анализ топ кандидатов results = [] for candidate in top_candidates[:top_k_deep]: deep_result = self.deep_match(candidate, job) results.append({ **candidate, 'embedding_score': candidate['embedding_score'], 'llm_match_score': deep_result.get('match_score', 50), 'final_score': (candidate['embedding_score'] * 0.4 + deep_result.get('match_score', 50) / 100 * 0.6), 'strengths': deep_result.get('strengths', []), 'gaps': deep_result.get('gaps', []), 'recommendation': deep_result.get('recommendation', 'maybe'), 'interview_questions': deep_result.get('interview_questions', []) }) return sorted(results, key=lambda x: -x['final_score']) class BiasAuditor: """Аудит предвзятости в матчинге""" def audit_demographic_bias(self, match_results: pd.DataFrame) -> dict: """Проверка дифференциального отбора по защищённым признакам""" audit = {} for group_col in ['gender', 'age_group', 'university_tier']: if group_col not in match_results.columns: continue group_stats = match_results.groupby(group_col)['final_score'].agg( ['mean', 'count', 'std'] ) # Disparate Impact: ratio между группами > 0.8 считается приемлемым if len(group_stats) >= 2: min_mean = group_stats['mean'].min() max_mean = group_stats['mean'].max() di_ratio = min_mean / max_mean if max_mean > 0 else 1.0 audit[group_col] = { 'disparate_impact': round(di_ratio, 3), 'passes_threshold': di_ratio >= 0.8, 'group_means': group_stats['mean'].round(3).to_dict() } return audit 

Як ми виявляємо приховані вимоги з описів вакансій?

Часто у вакансії немає прямої згадки технології, але контекст вказує на неї. Ми використовуємо LLM для вилучення імпліцитних навичок: наприклад, «досвід в e-commerce» може неявно вимагати знання RabbitMQ та Redis. Цей шар ембеддінгів доповнює явні вимоги, роблячи матчинг глибшим. На практиці це збільшило recall@10 з 45% до 82% в одному з проектів.

Чому ембеддінги ефективніші за ключові слова?

Косинусна схожість між векторами речень вловлює синоніми та близькі поняття. Тест на нашій базі з 10 000 резюме показав: recall@10 зріс з 45% (keyword) до 82% (семантичний). Комбінація ембеддінгів та LLM-аналізу знижує false positive rate на 30%. Для порівняння: keyword-матчинг дає 38% хибних спрацьовувань, а семантичний — 11%. Це можливо завдяки векторним представленням навичок, які вловлюють семантику, а не просто слова Wikipedia: Word embedding.

Процес впровадження семантичного матчингу

  1. Аналіз даних: збір історичних вакансій та резюме (мінімум 500 пар), узгодження метрик (time-to-hire, retention).
  2. Проектування ембеддінгів: вибір мультимовної моделі, налаштування контекстних вікон для захоплення імпліцитних вимог.
  3. Розробка pipeline: ANN-скоринг (Qdrant або pgvector) та інтеграція LLM (Claude, GPT-4o) для глибокого аналізу.
  4. Інтеграція з ATS: Lever, Greenhouse, кастомний API, налаштування вебхуків для автоматичної обробки.
  5. Тестування: A/B-експеримент на історичних даних, перевірка bias через Bias Auditor.
  6. Деплой: контейнеризація (Docker, Kubernetes), моніторинг latency p99 та GPU utilization.

Терміни: від 4 до 8 тижнів залежно від обсягу даних та складності інтеграцій.

Порівняння моделей ембеддінгів

Модель Розмірність Російська Швидкість (резюме/с)
paraphrase-multilingual-mpnet-base-v2 768 Так ~100
multilingual-e5-large 1024 Так ~50
rubert-tiny 312 Так ~500

Результати: до та після впровадження

Метрика Keyword-матчинг Семантичний матчинг
Time-to-hire (днів) 42 26
Quality-of-hire (% тих, хто пройшов випробувальний термін) 72% 91%
False positive rate 38% 11%
CPU time на 1000 резюме 0.4 сек 1.2 сек (ANN) + LLM для 10%
Як працює Bias AuditorBias Auditor перевіряє фінальні скори на нерівномірність за статтю, віком, університетом. Використовуємо тест Disparate Impact: якщо відношення середніх скорів між групами менше 0.8, модель коригується. Це обов'язковий крок для відповідності законодавству про рівні можливості найму.

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

  • Архітектура системи (ML + integration).
  • Код pipeline (Python, PyTorch, LangChain).
  • Документація по моделі та API.
  • Навчання команди (2–3 воркшопи).
  • Підтримка перших 2 тижнів після деплою.
  • BiasAuditor та звіт по fairness.

Економія бюджету на підбір персоналу може досягати 40% за рахунок скорочення ручного відбору. Вартість проекту розраховується індивідуально і залежить від обсягу даних та необхідної точності. Гарантуємо якість: кожна модель проходить тестування на історичних даних та A/B-експеримент. Сертифіковані інженери з досвідом впровадження в ритейлі та IT. Отримайте консультацію по вашому проекту — зв'яжіться з нами для попередньої оцінки. Замовте пілотний проект і переконайтеся в ефективності семантичного матчингу.