Локальный ИИ-агент для семантической фильтрации и сопоставления вакансий
Техническое руководство по созданию автономного ИИ-агента на базе эмбеддингов, векторного поиска (Qdrant) и локальных LLM для семантического скоринга вакансий.
Dim
AI tools writer for job seekers
Каждый, кто пробовал настроить автоматический поиск вакансий по ключевым словам на популярных площадках (LinkedIn, Indeed, HeadHunter), знает о боли неточного совпадения. Обычный текстовый поиск по ключевому слову “SRE” выдает как вакансииSenior SRE-архитекторов, так и вакансии системных администраторов поддержки 24/7 с мизерной зарплатой, или вообще вакансии менеджеров по продажам в SRE-компаниях.
Ключевые слова не отражают контекст, уровень грейда, реальные технологические ожидания и культурный код компании. Решить эту проблему на качественно новом уровне позволяет семантический поиск (Semantic Search) с использованием векторных эмбеддингов и автономных ИИ-агентов.
В этой статье мы с нуля спроектируем и реализуем локального ИИ-агента на Python. Он будет автоматически получать вакансии, преобразовывать их в многомерные векторы, сопоставлять с вашим детальным профилем кандидата в векторной базе данных Qdrant и принимать окончательное решение о релевантности с помощью локальной большой языковой модели (например, Llama 3 через Ollama).
1. Архитектура решения
Наш агент состоит из трех ключевых модулей:
- Embedding Engine (Векторизатор): Преобразует текстовое описание вакансии в вектор (обычно размерностью 384, 768 или 1024) с помощью локальной модели эмбеддингов. Мы будем использовать модель
mxbai-embed-largeилиall-MiniLM-L6-v2. - Vector Database (Векторное хранилище): База данных Qdrant, где хранится векторный слепок вашего идеального профиля (желаемый опыт, технологии, примеры проектов). Мы выполняем поиск по косинусному сходству (Cosine Similarity), чтобы мгновенно отсечь 90% нерелевантного мусора.
- 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. Преимущества локального подхода для соискателя
- Абсолютная приватность (Zero Data Leak): Тексты ваших резюме, личные пожелания и данные просмотренных вакансий не отправляются на сервера OpenAI или Anthropic. Весь цикл обработки происходит на вашем компьютере.
- Нулевая стоимость инференса (Free of Charge): Вы можете анализировать десятки тысяч вакансий в день, не тратя деньги на API-токены. Единственный ресурс — электроэнергия и вычислительная мощность вашего ПК/сервера.
- Гибкость интеграции: Вы можете интегрировать этого агента в качестве подписчика на очередь сообщений NATS в рамках нашей распределенной платформы (JobHunterBot) и обрабатывать поток вакансий в реальном времени.
Заключение
Создание семантического агента выводит автоматизацию поиска работы на принципиально новый, интеллектуальный уровень. Комбинация быстрого векторного поиска для грубой фильтрации и глубокого анализа локальной языковой модели позволяет создать персональный высокоточный радар вакансий, работающий автономно 24/7.
Read more AI for job search guides on the MindFire blog.