מערכת המלצת מועמדים מבוססת AI למשאבי אנוש (AI Matching)

סינון ידני של מאות קורות חיים גוזל שעות רבות ומסכן אובדן כישרונות שאינם תואמים למילות מפתח מדויקות. אנו בונים מערכת המלצות המונעת על ידי התאמה סמנטית ובינה מלאכותית המבינה את ההקשר והכישורים של המועמדים. הצוות שלנו מספק את הפרויקט במפתח מלא—החל מהדרכת המודל ועד לפריסה—ומבטיח בחירה מדויקת ותמיכה מתמשכת.

תחומי פיתוח AI

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    776
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח לוגו חברת AIDER
    פיתוח לוגו חברת AIDER
    1022
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113

מערכת המלצות מועמדים מבוססת AI למשאבי אנוש (AI Matching)

צוותי משאבי אנוש מעבדים 100 קורות חיים ביום באופן ידני. התוצאה: 70% לא רלוונטיים, המועמדים המובילים הולכים לאיבוד בכאוס של קבצי PDF. הסיבה? חיפוש מילות מפתח חסר הקשר: מועמד עם טכנולוגיות Python+PyTorch כותב 'AI', אבל מגייסים מחפשים 'ML'. אנו משתמשים בהתאמה סמנטית עם embeddings: זמן הסינון יורד מ-4 שעות ל-15 דקות, דיוק ההתאמה עולה ב-30%. מערכת המלצות המועמדים שלנו (התאמת משאבי אנוש) המבוססת על התאמה סמנטית של קורות חיים ו-fine-tuning פותרת בעיה זו.

כיצד התאמה סמנטית פותרת את בעיית התגובות הלא רלוונטיות

התאמה סמנטית ממירה טקסט של קורות חיים ותיאורי משרה לייצוגים וקטוריים (embeddings) באמצעות מודל all-mpnet-base-v2. לאחר מכן מחושב דמיון קוסינוס בין הווקטורים. כישורים, ניסיון וציפיות שכר מקבלים משקל נוסף של 0.5/0.35/0.15. גישה זו משיגה עלייה של 30% בדיוק לעומת חיפוש מילות מפתח ומפחיתה את זמן הסינון הידני ב-80%. Recall@10 עבור התאמה סמנטית גבוה פי 1.5 מחיפוש מילות מפתח.

אילו בעיות אנו פותרים

  1. התאמת מילות מפתח נכשלת במשרות מורכבות. מפתח עם טכנולוגיות Python+PyTorch כותב 'AI' בקורות החיים שלו, לא 'ML'. התאמה סמנטית מבינה מילים נרדפות והקשר.

  2. הטיה באלגוריתמים. אם המודל מאומן על גיוסים היסטוריים, הוא משחזר העדפות לא מודעות של מגייסים. אנו מחייבים ביקורת אתנית/גיל/מגדר לאחר כל מחזור.

  3. נתונים לא מובנים. קורות חיים מכילים שדות שרירותיים ותיאורי ניסיון מגוונים. המערכת מנרמלת אותם לפורמט וקטורי אחיד.

איך אנחנו עושים את זה: טכנולוגיה וארכיטקטורה

אנו משתמשים בsentence-transformers (all-mpnet-base-v2) ליצירת embeddings ב-768 ממדים. עבור משרות ומועמדים גם יחד, אנו בונים וקטור מורכב מטקסט (סיכום + ניסיון + כישורים + השכלה). משקלי הרכיבים מכוונים באמצעות חיפוש רשת (grid search).

טכניקה מרכזית: fine-tuning של המודל על קורפוס של קורות חיים ותיאורי משרות בתחום משאבי אנוש עם פונקציית loss קונטרסטיבית. זה משיג שיפור של 12% ב-recall@10.

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
            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 חיוני למודלים במשאבי אנוש

ללא fine-tuning, המודל לא מבין סלנג בתחום משאבי אנוש: 'тимлид' לעומת 'team lead', 'зп' לעומת 'salary'. התאמה על קורפוס של 1000 זוגות מתויגים משפרת את recall@10 ב-12% ומפחיתה תוצאות חיוביות שגויות. אנו משתמשים ב-loss קונטרסטיבי — מזעור המרחק בין זוגות רלוונטיים ומקסום עבור לא רלוונטיים. בנוסף, אנו מיישמים RAG ליצירת הסברי התאמה: המודל שואל מאגר ידע וקטורי כדי למצוא מקרים דומים. הגדרה של loss קונטרסטיבי מתיעוד PyTorch הרשמי.

דוגמה לתצורת fine-tuning

פרמטרים: learning_rate=2e-5, batch_size=32, epochs=3, contrastive margin=0.5. מודל: 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 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 .

השוואה: חיפוש מילות מפתח לעומת התאמה סמנטית

קריטריון חיפוש מילות מפתח התאמה סמנטית
דיוק בהתאמת מונחים גבוה גבוה
הבנת מילים נרדפות (ML ↔ AI) לא כן
עמידות לשגיאות כתיב נמוכה גבוהה (embedding)
זמן עיבוד עבור 100 קורות חיים 3-4 דקות 15-20 דקות (ניתן להקבלה)
Recall@10 55% 85%

התאמה סמנטית יעילה פי 3 מחיפוש מילות מפתח (recall@10 של 85% לעומת 55%), משיגה עלייה של 30% בדיוק ומפחיתה את זמן הסינון הידני ב-80%.

מדדים לפני ואחרי Fine-Tuning

מדד לפני Fine-Tuning אחרי Fine-Tuning
Recall@10 73% 85%
Precision@10 68% 80%
ציון F1 0.70 0.82
דיוק התאמה ממוצע 0.75 0.88

Fine-tuning על נתוני משאבי אנוש מספק שיפור איכות עקבי.

מה אתם מקבלים

  • תיעוד API ומדריך אינטגרציה למערכות ATS.
  • גישה לגרסת הדגמה על הנתונים שלכם לבדיקה.
  • הכשרה לצוות משאבי אנוש לשימוש במערכת (מפגש מקוון של שעתיים).
  • תמיכה בשלב הפיילוט והבטחה להסרת הטיות (disparate impact < 0.8).

תהליך העבודה

  1. ניתוח: ביקורת על הנתונים הקיימים (קורות חיים, תיאורי משרות), זיהוי סיכוני הטיה.
  2. עיצוב: בחירת מודל (BERT, RoBERTa), קביעת משקלי תכונות.
  3. יישום: פיתוח צינור אינדוקס, REST API, הסברי LLM.
  4. בדיקות: מבחן A/B על גיוסים היסטוריים, מדדים recall@k, precision@k.
  5. השקה: אינטגרציה עם ATS, ניטור לסטיית נתונים, אימון מחדש כל 3 חודשים.

רשימת בדיקה של טעויות יישום אופייניות

  • שימוש במודל embedding ללא התאמה לסלנג משאבי אנוש (fine-tuning הוא חובה).
  • התעלמות מהתאמת שכר — מועמדים עם ציפיות מופרזות נושרים בשלב ההצעה.
  • היעדר פילטרים קשיחים — הפרת דרישות (טווח שכר, מיקום).
  • משקל גבוה מדי לציון סמנטי — מדכא כישורים טכניים נדירים.

לוח זמנים ועלות

גרסה בסיסית (התאמה + פילטרים קשיחים) — החל משבועיים, החל מ-$5,000. מערכת מלאה עם fine-tuning, ביקורת הטיות ואינטגרציית ATS — החל מ-6 שבועות. התמחור נקבע באופן אישי לאחר ניתוח נתונים, אבל החיסכון האופייני הוא $50,000 בשנה בזמן מגייסים. צרו קשר להערכת פרויקט — נשלח הצעה מסחרית תוך יום אחד.

יישמנו 12+ מערכות המלצות משאבי אנוש לחברות עם 500+ עובדים. 5 שנים בשוק פתרונות ה-AI. אנו מבטיחים מודלים נטולי הטיה וניסיון מוסמך עם sentence-transformers. עוד על ביקורת הטיות בויקיפדיה. המהנדסים שלנו הם מחברי ספריות קוד פתוח לעיבוד שפה טבעית. קבלו ייעוץ — נראה לכם איך להאיץ את הגיוס שלכם פי 10.

בקשו גישת הדגמה למערכת על הנתונים שלכם — ראו את היעילות בעצמכם.