11 min read ai-guides

Локальный ИИ-агент для семантической фильтрации и сопоставления вакансий

Техническое руководство по созданию автономного ИИ-агента на базе эмбеддингов, векторного поиска (Qdrant) и локальных LLM для семантического скоринга вакансий.

Локальный ИИ-агент для семантической фильтрации и сопоставления вакансий
D

Dim

AI tools writer for job seekers

Каждый, кто пробовал настроить автоматический поиск вакансий по ключевым словам на популярных площадках (LinkedIn, Indeed, HeadHunter), знает о боли неточного совпадения. Обычный текстовый поиск по ключевому слову “SRE” выдает как вакансииSenior SRE-архитекторов, так и вакансии системных администраторов поддержки 24/7 с мизерной зарплатой, или вообще вакансии менеджеров по продажам в SRE-компаниях.

Ключевые слова не отражают контекст, уровень грейда, реальные технологические ожидания и культурный код компании. Решить эту проблему на качественно новом уровне позволяет семантический поиск (Semantic Search) с использованием векторных эмбеддингов и автономных ИИ-агентов.

В этой статье мы с нуля спроектируем и реализуем локального ИИ-агента на Python. Он будет автоматически получать вакансии, преобразовывать их в многомерные векторы, сопоставлять с вашим детальным профилем кандидата в векторной базе данных Qdrant и принимать окончательное решение о релевантности с помощью локальной большой языковой модели (например, Llama 3 через Ollama).


1. Архитектура решения

Наш агент состоит из трех ключевых модулей:

  1. Embedding Engine (Векторизатор): Преобразует текстовое описание вакансии в вектор (обычно размерностью 384, 768 или 1024) с помощью локальной модели эмбеддингов. Мы будем использовать модель mxbai-embed-large или all-MiniLM-L6-v2.
  2. Vector Database (Векторное хранилище): База данных Qdrant, где хранится векторный слепок вашего идеального профиля (желаемый опыт, технологии, примеры проектов). Мы выполняем поиск по косинусному сходству (Cosine Similarity), чтобы мгновенно отсечь 90% нерелевантного мусора.
  3. LLM Decision Core (Локальный ИИ-аналитик): Если косинусное сходство превышает пороговое значение (например, 0.75), полный текст вакансии и ваше резюме передаются локальной LLM для детальной аргументации и принятия решения: “Подходит / Не подходит / Подходит с натяжкой”.

2. Шаг 1: Подготовка окружения и запуск инфраструктуры

Мы будем запускать векторную базу Qdrant и локальную LLM в Docker-контейнерах для обеспечения полной изоляции и безопасности данных.

Запустим Qdrant:

docker run -d -p 6333:6333 -p 6334:6334 \
    -v qdrant_storage:/qdrant/storage \
    qdrant/qdrant:latest

Запустим Ollama с моделью Llama 3 и моделью эмбеддингов:

# Запуск контейнера Ollama (с поддержкой GPU, если есть)
docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

# Загрузка моделей
docker exec -it ollama ollama run llama3:8b-instruct-q4_K_M
docker exec -it ollama ollama pull mxbai-embed-large

3. Шаг 2: Реализация векторизации и семантического сравнения на Python

Установим необходимые зависимости на хосте:

pip install qdrant-client sentence-transformers requests

Напишем модуль работы с Qdrant и генерации векторов с помощью sentence-transformers:

from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer

class VectorSearchEngine:
    def __init__(self, collection_name="candidate_profiles"):
        # Инициализируем локальный клиент Qdrant
        self.client = QdrantClient("http://localhost:6333")
        self.collection_name = collection_name
        
        # Загружаем легковесную и быструю локальную модель эмбеддингов
        # Она преобразует текст в вектор размерностью 384
        self.model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
        
        # Создаем коллекцию в Qdrant, если её нет
        self._ensure_collection()

    def _ensure_collection(self):
        collections = self.client.get_collections().collections
        exists = any(c.name == self.collection_name for c in collections)
        
        if not exists:
            self.client.create_collection(
                collection_name=self.collection_name,
                vectors_config=models.VectorParams(
                    size=384, # Размерность all-MiniLM-L6-v2
                    distance=models.Distance.COSINE # Косинусное расстояние
                )
            )
            print(f"[+] Создана коллекция Qdrant: {self.collection_name}")

    def index_profile(self, profile_id, profile_text, metadata=None):
        """
        Индексирует ваш профиль (резюме + пожелания) в базу данных.
        """
        vector = self.model.encode(profile_text).tolist()
        self.client.upsert(
            collection_name=self.collection_name,
            points=[
                models.PointStruct(
                    id=profile_id,
                    vector=vector,
                    payload={"text": profile_text, **(metadata or {})}
                )
            ]
        )
        print(f"[+] Профиль #{profile_id} успешно векторизован и сохранен.")

    def search_similar_jobs(self, vacancy_text, limit=1):
        """
        Ищет совпадение вакансии с сохраненным профилем кандидата.
        """
        query_vector = self.model.encode(vacancy_text).tolist()
        
        results = self.client.search(
            collection_name=self.collection_name,
            query_vector=query_vector,
            limit=limit,
            with_payload=True
        )
        return results

4. Шаг 3: Реализация локального LLM фильтра решений

Если векторный поиск Qdrant возвращает оценку сходства (Similarity Score) выше порога (например, 0.40 для этой компактной модели эмбеддингов), мы отправляем текст вакансии локальному ИИ-агенту для финальной проверки на “соответствие здравому смыслу”.

Создадим файл agent_matcher.py:

import requests
import json
from agent_vector import VectorSearchEngine

OLLAMA_URL = "http://localhost:11434/api/generate"

SYSTEM_PROMPT = """
Вы — умный ИИ-фильтр вакансий. Ваша задача — сравнить профиль кандидата и текст вакансии и дать строгий вердикт.

Выведите результат в формате JSON со следующими полями:
1. "match_status": Строка, принимающая значения "MATCH" (подходит), "PARTIAL" (подходит частично, есть спорные моменты), "NO_MATCH" (не подходит абсолютно).
2. "reasoning": Краткое техническое обоснование вашего решения (2-3 предложения).
3. "missing_skills": Список критически важных навыков или требований из вакансии, которых нет в профиле кандидата.

Будьте строги и реалистичны. Не делайте ложных допущений.
"""

def evaluate_vacancy_with_llm(candidate_profile, vacancy_text):
    prompt = f"""
    ### ПРОФИЛЬ КАНДИДАТА:
    {candidate_profile}

    ### ОПИСАНИЕ ВАКАНСИИ:
    {vacancy_text}

    Пожалуйста, проведи глубокий анализ и предоставь JSON по инструкции.
    """

    payload = {
        "model": "llama3",
        "prompt": prompt,
        "system": SYSTEM_PROMPT,
        "stream": False,
        "format": "json" # Просим Ollama сразу вернуть валидный JSON
    }

    try:
        response = requests.post(OLLAMA_URL, json=payload, timeout=60)
        response_data = response.json()
        return json.loads(response_data['response'])
    except Exception as e:
        print(f"[-] Ошибка при запросе к локальной LLM: {e}")
        return None

def process_new_vacancy(engine, vacancy_title, vacancy_text):
    print(f"\n[*] Анализ новой вакансии: {vacancy_title}...")
    
    # 1. Семантический поиск по вектору
    search_results = engine.search_similar_jobs(vacancy_text)
    
    if not search_results:
        print("[-] Профиль кандидата не найден в базе данных.")
        return

    best_match = search_results[0]
    score = best_match.score
    candidate_profile_text = best_match.payload["text"]

    print(f"[+] Семантическое сходство (Cosine Similarity Score): {score:.4f}")

    # Задаем эмпирический порог семантического совпадения
    THRESHOLD = 0.38
    if score < THRESHOLD:
        print("[-] Отклонено: Слишком низкое векторное соответствие.")
        return

    # 2. Глубокий анализ локальной LLM
    print("[*] Высокое сходство! Запуск локального LLM-валидатора...")
    verdict = evaluate_vacancy_with_llm(candidate_profile_text, vacancy_text)

    if verdict:
        print("\n=== ВЕРДИКТ ИИ-АГЕНТА ===")
        print(f"Статус: {verdict.get('match_status')}")
        print(f"Обоснование: {verdict.get('reasoning')}")
        print(f"Отсутствующие навыки: {', '.join(verdict.get('missing_skills', []))}")
        print("=========================")
    else:
        print("[-] Не удалось получить вердикт.")

# Демонстрационный запуск
if __name__ == "__main__":
    engine = VectorSearchEngine()

    # Сначала заиндексируем наш профиль
    my_profile = """
    Опыт: Senior DevOps/SRE-инженер, более 6 лет опыта.
    Ключевые навыки: Kubernetes, Terraform, Ansible, Docker, Prometheus, VictoriaMetrics, CI/CD (GitLab, GitHub Actions).
    Языки программирования: Python (скрипты, FastAPI), Go (базовый уровень, написание микросервисов).
    Интересы: Оптимизация производительности инфраструктуры, построение систем мониторинга и алертинга.
    Не интересует: Поддержка legacy Windows-серверов, ручной деплой на виртуальные машины без автоматизации.
    """
    engine.index_profile(profile_id=1, profile_text=my_profile)

    # Тестируем хорошую вакансию
    good_job = """
    Мы ищем Senior SRE в амбициозный проект. 
    Вам предстоит заниматься масштабированием наших Kubernetes кластеров в AWS, писать Infrastructure as Code на Terraform, 
    а также настраивать мониторинг микросервисов на базе Prometheus и VictoriaMetrics.
    Опыт написания автоматизации на Python приветствуется.
    """
    process_new_vacancy(engine, "Senior SRE (Kubernetes/AWS)", good_job)

    # Тестируем плохую вакансию
    bad_job = """
    Требуется специалист технической поддержки 1-й линии.
    Обязанности: прием звонков от клиентов, заведение тикетов в Jira, ручная перезагрузка Windows-серверов по RDP.
    Уверенный пользователь ПК.
    """
    process_new_vacancy(engine, "Support Engineer L1", bad_job)

5. Преимущества локального подхода для соискателя

  1. Абсолютная приватность (Zero Data Leak): Тексты ваших резюме, личные пожелания и данные просмотренных вакансий не отправляются на сервера OpenAI или Anthropic. Весь цикл обработки происходит на вашем компьютере.
  2. Нулевая стоимость инференса (Free of Charge): Вы можете анализировать десятки тысяч вакансий в день, не тратя деньги на API-токены. Единственный ресурс — электроэнергия и вычислительная мощность вашего ПК/сервера.
  3. Гибкость интеграции: Вы можете интегрировать этого агента в качестве подписчика на очередь сообщений NATS в рамках нашей распределенной платформы (JobHunterBot) и обрабатывать поток вакансий в реальном времени.

Заключение

Создание семантического агента выводит автоматизацию поиска работы на принципиально новый, интеллектуальный уровень. Комбинация быстрого векторного поиска для грубой фильтрации и глубокого анализа локальной языковой модели позволяет создать персональный высокоточный радар вакансий, работающий автономно 24/7.

Read more AI for job search guides on the MindFire blog.