Сервис автоматически собирает отклики на вакансии с рекрутингового сайта (hh.ru), с помощью LLM (OpenAI) оценивает соответствие каждого кандидата вакансии, отсеивает нерелевантных и присылает рекрутеру в Telegram топ подходящих кандидатов с обоснованием, кому стоит позвонить и почему.
Проблема, которую решаем: ручной просмотр десятков откликов по каждой вакансии занимает много времени. Простые алгоритмы (TF-IDF, эмбеддинги) не понимают смысл и дают плохое качество. LLM оценивает по сути, а не по совпадению слов.
| Тип | Требование |
|---|---|
| Функц. | Собрать вакансии и отклики (резюме) с hh.ru |
| Функц. | Оценить релевантность каждого резюме через LLM |
| Функц. | Отсеять явный мусор, сформировать топ-20 релевантных |
| Функц. | Прислать топ в Telegram с оценкой, вердиктом, минусами, ссылкой |
| Функц. | Не обрабатывать повторно уже оценённые пары «вакансия+резюме» |
| Нефункц. | ~100 вакансий/день, ~25 резюме на вакансию (~2500 резюме/день) |
| Нефункц. | Стоимость LLM в пределах нескольких $/день |
| Нефункц. | Источник данных, анализатор и уведомления заменяемы без переписывания системы |
| Нефункц. | Устойчивость к ошибкам отдельных резюме/вакансий (не падать целиком) |
Разделение на слои через абстрактные интерфейсы. Три независимых слоя общаются только через единый формат данных (модели), не зная о внутренней реализации друг друга.
┌─────────────┐ ┌──────────────┐ ┌───────────────┐
│ Collector │ → │ Analyzer │ → │ Notifier │
│ ОТКУДА │ │ ОЦЕНКА AI │ │ КУДА │
└─────────────┘ └──────────────┘ └───────────────┘
│ │ │
ChromeDriver OpenAI Telegram
(сейчас) (сейчас) (сейчас)
↓ ↓ ↓
API / Playwright др. LLM / локальная Email / CRM / Web
(в будущем) (в будущем) (в будущем)
Это позволяет менять любой слой заменой одной строки в оркестраторе.
recruiter_bot/
├── models.py # dataclasses — контракт между слоями
├── config.py # ключи, настройки, лимиты
│
├── collectors/
│ ├── base.py # BaseCollector (абстрактный)
│ ├── hh_selenium.py # текущая реализация (ChromeDriver)
│ └── hh_api.py # заглушка на будущее
│
├── analyzers/
│ ├── base.py # BaseAnalyzer (абстрактный)
│ └── openai_analyzer.py # двухэтапная оценка через OpenAI
│
├── notifiers/
│ ├── base.py # BaseNotifier (абстрактный)
│ └── telegram.py # отправка в Telegram
│
├── storage.py # кэш обработанного + история (SQLite)
├── main.py # оркестратор (собирает слои вместе)
├── .env # секреты (не в репозитории)
├── chrome_session/ # авторизованная сессия Chrome (не в репозитории)
└── requirements.txt
Единый формат, которым обмениваются слои. Определяются в models.py.
Vacancy:
id: str # уникальный ID вакансии
title: str # заголовок
description: str # полный текст описания
url: str
Resume:
id: str # уникальный ID резюме
url: str
text: str # собранный (структурированный по секциям) текст резюме для анализа
raw: dict # сырые блоки (main_info, опыт, навыки и т.д.) — на будущее
Evaluation:
resume: Resume
score: int # 0–100
verdict: str # почему подходит (1–2 предложения)
red_flags: str # минусы / риски (может быть пусто)
Правило: ни один слой не передаёт HTML, JSON сайта или сырые ответы LLM наружу. Только эти модели.
Интерфейс BaseCollector:
get_vacancies() -> list[Vacancy]— вернуть список активных вакансийget_resumes(vacancy) -> list[Resume]— вернуть отклики на вакансиюclose() -> None— освободить ресурсы (закрыть драйвер/сессию). Вызывается оркестратором вfinally. Реализация по умолчанию может быть пустой (pass) для источников без ресурсов.
Реализация HHSeleniumCollector (текущая):
- Оборачивает существующий код парсера на
undetected_chromedriver. - Внутри: авторизованная сессия (
chrome_session), сбор ссылок на вакансии, переход по откликам, парсинг страниц резюме. - Текст резюме собирается по фиксированному набору секций (контакты, специализации, опыт, навыки, языки, образование, доп. информация, комментарии) с заголовками — для стабильного восприятия LLM. Пустые секции пропускаются.
- URL страниц откликов кэшируется на этапе
get_vacanciesи используется вget_resumes(внутренняя деталь реализации). - Преобразует спарсенные данные в модели
Vacancy/Resume. - Обрабатывает ошибки отдельных страниц (пропуск с логом, не падение).
close()закрывает драйвер.
Будущие реализации:
HHApiCollector— через официальный API работодателя (JSON, без парсинга).PlaywrightCollector— перехват внутренних JSON-запросов сайта.- Другие сайты — новые коллекторы по тому же интерфейсу.
Интерфейс BaseAnalyzer:
rank(vacancy, resumes, top_n) -> list[Evaluation]— вернуть отсортированный топ.
Реализация OpenAIAnalyzer — двухэтапная схема (важно для экономии):
Этап 1 — быстрый отсев мусора (батчами):
- Резюме группируются по 5–10 штук в один запрос.
- Дешёвая модель (
gpt-4o-mini) отвечает по каждому:relevant/trash. - Задача — убрать явно не ту профессию/нет ключевого опыта. Сомнительных оставлять.
- Используются укороченные тексты резюме (экономия токенов).
Этап 2 — детальная оценка выживших (по одному, параллельно):
- Каждое relevant-резюме оценивается отдельным запросом.
- Модель возвращает строгий JSON:
score,verdict,red_flags. - Запросы идут параллельно (
ThreadPoolExecutor, ~5 воркеров). - Оценка по сути: среднее резюме может подойти лучше «красивого» — это заложено в промпт.
Результат: список Evaluation, отсортированный по score, обрезанный до top_n.
Требования к реализации:
temperature=0— стабильность оценок.response_format=json_object— гарантированный парсинг.- Retry при сбоях API.
- Промпты вынесены в константы (легко править — это главный рычаг качества).
Будущие реализации: другая LLM, локальная модель, гибрид (быстрый фильтр эмбеддингами + LLM только для финала).
Интерфейс BaseNotifier:
send(vacancy, top)— отправить результат.
Реализация TelegramNotifier:
- Один дайджест на вакансию (не спамить по каждому кандидату).
- Формат сообщения на кандидата: номер, оценка, вердикт, минусы, ссылка.
- Разбивка длинных сообщений на части (лимит Telegram — 4096 символов).
Будущие реализации: Email, запись в CRM, веб-дашборд, интерактивные кнопки.
Назначение: не жечь токены на повторной оценке. Один кандидат может откликаться на разные вакансии, скрипт может перезапускаться.
Интерфейс:
is_processed(vacancy_id, resume_id) -> boolmark_processed(vacancy_id, resume_id)- (на будущее)
save_evaluation(evaluation)— история оценок.
Реализация: SQLite, ключ = пара vacancy_id + resume_id (оценка зависит от вакансии).
Единственное место, где слои собираются вместе. Вся высокоуровневая логика — здесь.
Алгоритм:
collector, analyzer, notifier, storage = собрать реализации
try:
для каждой вакансии из collector.get_vacancies():
resumes = collector.get_resumes(vacancy)
new = резюме, которых нет в storage
если new пусто → пропустить вакансию
top = analyzer.rank(vacancy, new, top_n=20)
notifier.send(vacancy, top)
отметить обработанные в storage
finally:
collector.close()
Смена реализации любого слоя = замена одного аргумента при создании объектов. Освобождение ресурсов коллектора гарантируется блоком finally.
Все настройки в одном месте:
- API-ключи (OpenAI), Telegram token + chat_id.
- Модель LLM, размер батча фильтра, число воркеров,
top_n. - Лимит резюме на вакансию, задержки парсера.
- Настройки Chrome: путь к сессии (
chrome_session_path), основная версия браузера (chrome_version_main), стартовый URL списка вакансий (vacancies_url). - Путь к БД.
Секреты — через переменные окружения / .env, не в коде. У всех ключей есть дефолты-заглушки, чтобы проект запускался без полного .env на ранних этапах.
| Место | Стратегия |
|---|---|
| Парсинг одной страницы | try/except, лог, пропуск — не валить всю вакансию |
| Одна вакансия | try/except, лог, переход к следующей |
| Запрос к LLM | retry с backoff, при финальном сбое — пропуск резюме |
| Отправка в Telegram | retry, лог |
| Освобождение ресурсов | collector.close() в finally оркестратора |
| Прерывание скрипта | storage хранит прогресс — при перезапуске не дублируем работу |
Логирование на всех этапах.
Этап 1 — Каркас ✅
models.py(Vacancy, Resume, Evaluation).- Абстрактные базовые классы всех трёх слоёв.
main.pyс логикой оркестратора.
Этап 2 — Сбор данных ✅
- Обёрнут Selenium-код в
HHSeleniumCollector. - Вывод приведён к моделям. Проверка на реальных вакансиях.
Этап 3 — Анализ
OpenAIAnalyzer: сначала этап 2 (детальная оценка), проверить качество.- Добавить этап 1 (фильтр батчами) для экономии.
- Настроить и отладить промпты.
Этап 4 — Уведомления
TelegramNotifier, формат дайджеста, разбивка сообщений.
Этап 5 — Кэш
Storageна SQLite, интеграция в оркестратор.
Этап 6 — Интеграция и прогон
- Собрать всё в
main.py, прогнать на реальных вакансиях. - Замерить стоимость и время, отладить параллельность.
Этап 7 — Планировщик (опц.)
- Автозапуск по расписанию.
| Направление | Что даёт | Что менять |
|---|---|---|
| Источник → API / Playwright | скорость, надёжность, обход блокировок | новый Collector |
| Другая / локальная LLM | стоимость, независимость | новый Analyzer |
| Гибридный анализ (эмбеддинги + LLM) | ещё дешевле фильтр | доработка Analyzer |
| Планировщик (cron/APScheduler) | автономность | обёртка над main |
| Очередь задач (Celery/RQ) | параллельная обработка при росте объёма | инфраструктурный слой |
| Обратная связь из Telegram (звонил/отказ) | накопление данных, улучшение промптов | Notifier + Storage |
| Веб-дашборд | просмотр истории, аналитика | слой над Storage |
| Мультисайтовость | другие площадки | новые Collector'ы |
| CRM-интеграция | автоматизация найма | новый Notifier |
- Доступ к данным hh: проверить, не блокирует ли сайт при большом объёме (100 вакансий/день). При блокировках — ускорить переход на API/Playwright.
- Хрупкость селекторов:
HHSeleniumCollectorзавязан на CSS-классы иdata-qahh.ru (в т.ч. динамические имена классов пагинации). При смене вёрстки сайта парсер сломается — селекторы держать в одном месте, при переходе на API проблема снимается. - Совместимость версии Chrome:
chrome_version_mainв конфиге должен соответствовать установленному Chrome, иначеundetected_chromedriverне стартует. - Окружение (macOS / Python 3.12): нужен
setuptools(удалённыйdistutils) и установленные SSL-сертификаты Python (Install Certificates.command) — иначеucне скачает драйвер. Зафиксировать вrequirements.txt/ инструкции по установке. - Качество LLM-оценки: зависит от промптов; нужен ручной контроль на первых прогонах.
- Скорость: последовательный парсинг Selenium может быть узким местом; кандидат на оптимизацию.
- Юридические/этические аспекты обработки персональных данных кандидатов — учитывать при хранении.