Skip to content

chernykhmark/recruiter_bot

Repository files navigation

Архитектурный документ: сервис оценки кандидатов (Recruiter Bot)

1. Назначение

Сервис автоматически собирает отклики на вакансии с рекрутингового сайта (hh.ru), с помощью LLM (OpenAI) оценивает соответствие каждого кандидата вакансии, отсеивает нерелевантных и присылает рекрутеру в Telegram топ подходящих кандидатов с обоснованием, кому стоит позвонить и почему.

Проблема, которую решаем: ручной просмотр десятков откликов по каждой вакансии занимает много времени. Простые алгоритмы (TF-IDF, эмбеддинги) не понимают смысл и дают плохое качество. LLM оценивает по сути, а не по совпадению слов.


2. Основные требования

Тип Требование
Функц. Собрать вакансии и отклики (резюме) с hh.ru
Функц. Оценить релевантность каждого резюме через LLM
Функц. Отсеять явный мусор, сформировать топ-20 релевантных
Функц. Прислать топ в Telegram с оценкой, вердиктом, минусами, ссылкой
Функц. Не обрабатывать повторно уже оценённые пары «вакансия+резюме»
Нефункц. ~100 вакансий/день, ~25 резюме на вакансию (~2500 резюме/день)
Нефункц. Стоимость LLM в пределах нескольких $/день
Нефункц. Источник данных, анализатор и уведомления заменяемы без переписывания системы
Нефункц. Устойчивость к ошибкам отдельных резюме/вакансий (не падать целиком)

3. Ключевой архитектурный принцип

Разделение на слои через абстрактные интерфейсы. Три независимых слоя общаются только через единый формат данных (модели), не зная о внутренней реализации друг друга.

┌─────────────┐    ┌──────────────┐    ┌───────────────┐
│  Collector  │ →  │   Analyzer   │ →  │   Notifier    │
│  ОТКУДА     │    │  ОЦЕНКА AI   │    │    КУДА        │
└─────────────┘    └──────────────┘    └───────────────┘
      │                   │                    │
 ChromeDriver          OpenAI             Telegram
 (сейчас)              (сейчас)           (сейчас)
      ↓                   ↓                    ↓
 API / Playwright    др. LLM / локальная   Email / CRM / Web
 (в будущем)         (в будущем)           (в будущем)

Это позволяет менять любой слой заменой одной строки в оркестраторе.


4. Структура проекта

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

5. Модели данных (контракт)

Единый формат, которым обмениваются слои. Определяются в 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 наружу. Только эти модели.


6. Описание слоёв

6.1. Collector (сбор данных)

Интерфейс 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-запросов сайта.
  • Другие сайты — новые коллекторы по тому же интерфейсу.

6.2. Analyzer (оценка через LLM)

Интерфейс 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 только для финала).

6.3. Notifier (уведомления)

Интерфейс BaseNotifier:

  • send(vacancy, top) — отправить результат.

Реализация TelegramNotifier:

  • Один дайджест на вакансию (не спамить по каждому кандидату).
  • Формат сообщения на кандидата: номер, оценка, вердикт, минусы, ссылка.
  • Разбивка длинных сообщений на части (лимит Telegram — 4096 символов).

Будущие реализации: Email, запись в CRM, веб-дашборд, интерактивные кнопки.

6.4. Storage (кэш и история)

Назначение: не жечь токены на повторной оценке. Один кандидат может откликаться на разные вакансии, скрипт может перезапускаться.

Интерфейс:

  • is_processed(vacancy_id, resume_id) -> bool
  • mark_processed(vacancy_id, resume_id)
  • (на будущее) save_evaluation(evaluation) — история оценок.

Реализация: SQLite, ключ = пара vacancy_id + resume_id (оценка зависит от вакансии).

6.5. Оркестратор (main.py)

Единственное место, где слои собираются вместе. Вся высокоуровневая логика — здесь.

Алгоритм:

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.


7. Конфигурация (config.py)

Все настройки в одном месте:

  • API-ключи (OpenAI), Telegram token + chat_id.
  • Модель LLM, размер батча фильтра, число воркеров, top_n.
  • Лимит резюме на вакансию, задержки парсера.
  • Настройки Chrome: путь к сессии (chrome_session_path), основная версия браузера (chrome_version_main), стартовый URL списка вакансий (vacancies_url).
  • Путь к БД.

Секреты — через переменные окружения / .env, не в коде. У всех ключей есть дефолты-заглушки, чтобы проект запускался без полного .env на ранних этапах.


8. Обработка ошибок и устойчивость

Место Стратегия
Парсинг одной страницы try/except, лог, пропуск — не валить всю вакансию
Одна вакансия try/except, лог, переход к следующей
Запрос к LLM retry с backoff, при финальном сбое — пропуск резюме
Отправка в Telegram retry, лог
Освобождение ресурсов collector.close() в finally оркестратора
Прерывание скрипта storage хранит прогресс — при перезапуске не дублируем работу

Логирование на всех этапах.


9. Этапы реализации (порядок разработки)

Этап 1 — Каркас

  • models.py (Vacancy, Resume, Evaluation).
  • Абстрактные базовые классы всех трёх слоёв.
  • main.py с логикой оркестратора.

Этап 2 — Сбор данных

  • Обёрнут Selenium-код в HHSeleniumCollector.
  • Вывод приведён к моделям. Проверка на реальных вакансиях.

Этап 3 — Анализ

  • OpenAIAnalyzer: сначала этап 2 (детальная оценка), проверить качество.
  • Добавить этап 1 (фильтр батчами) для экономии.
  • Настроить и отладить промпты.

Этап 4 — Уведомления

  • TelegramNotifier, формат дайджеста, разбивка сообщений.

Этап 5 — Кэш

  • Storage на SQLite, интеграция в оркестратор.

Этап 6 — Интеграция и прогон

  • Собрать всё в main.py, прогнать на реальных вакансиях.
  • Замерить стоимость и время, отладить параллельность.

Этап 7 — Планировщик (опц.)

  • Автозапуск по расписанию.

10. Возможные расширения (roadmap)

Направление Что даёт Что менять
Источник → API / Playwright скорость, надёжность, обход блокировок новый Collector
Другая / локальная LLM стоимость, независимость новый Analyzer
Гибридный анализ (эмбеддинги + LLM) ещё дешевле фильтр доработка Analyzer
Планировщик (cron/APScheduler) автономность обёртка над main
Очередь задач (Celery/RQ) параллельная обработка при росте объёма инфраструктурный слой
Обратная связь из Telegram (звонил/отказ) накопление данных, улучшение промптов Notifier + Storage
Веб-дашборд просмотр истории, аналитика слой над Storage
Мультисайтовость другие площадки новые Collector'ы
CRM-интеграция автоматизация найма новый Notifier

11. Открытые вопросы / риски

  • Доступ к данным hh: проверить, не блокирует ли сайт при большом объёме (100 вакансий/день). При блокировках — ускорить переход на API/Playwright.
  • Хрупкость селекторов: HHSeleniumCollector завязан на CSS-классы и data-qa hh.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 может быть узким местом; кандидат на оптимизацию.
  • Юридические/этические аспекты обработки персональных данных кандидатов — учитывать при хранении.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages