Привет, Хабр.

Думаю, у каждого, кто искал работу, есть этот "любимый" запрос на hh. Вбиваешь "Python Developer", ставишь фильтр "нет опыта", а тебе вываливается 500 вакансий "Senior Analyst", где в требованиях "базовое знание SQL, Python будет плюсом".

Ручной разбор этой каши убивает время и мотивацию. Мой друг Роман (он IT-рекрутер и карьерный консультант) постоянно рассказывает, как кандидаты выгорают еще до первого собеса именно на этом этапе.

Я решил, что хватит. Пора автоматизировать рутину. Казалось бы, 30-минутная задача: дернул API, отфильтровал, откликнулся. Как же я ошибался. Сегодня расскажу, на какие грабли наступил, пока пилил «Аврору» - свой инструмент для автоматизации этого ада.

Грабли №1: "Умный" поиск - это не text=...

Наивный старт: эндпоинт /vacancies с параметром text="Python". Сразу нет.

Окей, усложняем. ("QA" OR "Тестировщик") NOT ("Junior" OR "Стажер"). Уже лучше, но составлять руками - боль.

Решение (частичное): Эндпоинт /resumes/{resume_id}/similar_vacancies. HH сам подбирает похожее. Чище, чем ручной поиск.

Новая проблема: А если резюме "на коленке"? Или человек хочет сменить профиль? similar_vacancies не сработает. Роман подтвердил: таких случаев полно, и простой поиск по резюме их отсекает.

Пришлось городить гибрид:

  1. Сначала similar_vacancies + кастомный query пользователя как доп. фильтр.

  2. Когда там все кончается, бесшовно переключаемся на /vacancies со всеми фильтрами и кастомным запросом.

В коде (hh_api_client.py) это async def search_and_apply_generator, который меняет search_stage с 'similar' на 'general'. Это позволяет Авроре копать глубже, когда "идеальные" совпадения закончились.

Грабли №2: Настройка диалога - это конечный автомат

Самое сложное - заставить пользователя дать нужные данные в Telegram. Просто спросить "какой у вас график?" нельзя. Выбор "Офис/Гибрид" требует города, запрещает "Всю Россию", и так далее. Роман тоже говорил, что кандидаты часто путаются в фильтрах, поэтому интерфейс должен быть пуленепробиваемым.

Это - ад. Это - ConversationHandler из python-telegram-bot во всей красе. Машина состояний, где каждый CallbackQueryHandler решает, куда вести диалог дальше, и ловит ошибки через fallbacks.

На отладку этого ушло больше времени, чем на сам поиск.

Грабли №3: Отклик "замораживает" бота

Окей. Поиск есть, настройки есть. Пользователь жмет "Старт". Бот находит 100 вакансий. И начинает откликаться: API Gemini (LLM) за письмом (5-10 сек) -> API HH (/negotiations) с письмом (1-2 сек) -> Повторить 100 раз.

И все это время бот висит. 5 пользователей - сервис можно выключать.

Решение: PostgreSQL как очередь задач. Никаких asyncio.Queue в памяти.

  1. Команды просто кладут (user_id, vacancy_id, resume_id) в таблицу application_queue со статусом pending.

  2. Фоновый asyncio.Task (application_worker) пытается забрать задачу из БД.

  3. Как сделать, чтобы 5 инстансов бота не схватили одну задачу? FOR UPDATE SKIP LOCKED.

Этот SQL (database.py) - сердце воркера:

SQL

-- database.py (функция get_next_pending_job)
UPDATE application_queue SET status = 'processing'
WHERE id = (SELECT id FROM application_queue WHERE status = 'pending' ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED)
RETURNING *;

Транзакция атомарно находит, блокирует, а если заблокировано - пропускает и ищет следующую. Потом меняет статус и возвращает. Воркер спокойно идет в Gemini, в HH API, и только потом (в finally) удаляет job или ставит error. Бот в Telegram при этом "летает".

Зачем все это?

Вся эта сложность - не самоцель. Она нужна, чтобы Аврора могла:

  • Находить вакансии точнее, чем стандартный поиск hh.ru (гибридный подход).

  • Не бесить пользователя сложными настройками (машина состояний).

  • Работать стабильно и быстро для сотен юзеров одновременно (асинхронная очередь).

  • Писать релевантные письма, потому что логику для LLM мы строили как раз на инсайтах Романа о том, что реально цепляет рекрутеров (это тема для отдельной статьи!).

То, что начиналось как скрипт на 50 строк, превратилось в сервис, который пытается немного починить сломанный процесс найма.

Прямо сейчас мы собрали первую фокус-группу (QA, DevOps/SRE, Senior Frontend, QA lead), которая тестирует Аврору. Собираем фидбэк, чтобы сделать ее еще лучше к релизу 7 ноября (да, мы планируем дать доступ первым 100 смельчакам).

Что думаете вы, Хабр? Автоматизация откликов - это необходимый инструмент для выживания на рынке, или мы просто выводим гонку вооружений на новый уровень?

Если сталкивались с похожими проблемами при работе с API HH или просто есть мысли на этот счет - буду рад обсудить в комментариях. Или залетайте в наш небольшой Telegram-канал, где мы делимся прогрессом и спорим о будущем поиска работы: https://t.me/AuroraCareer

Спасибо.

Комментарии (3)


  1. cry_san
    23.10.2025 07:09

    Оркестрирование большой очередью распределенных задач через базу данных - это первое, что сразу приходит в голову. Удивительно, что вы долго к этому шли.


  1. ophil
    23.10.2025 07:09

    А код где ?? Рекламы аж тошнит, вместо кода разговоры, и никаких ссылок в репо


  1. zeroc0de
    23.10.2025 07:09

    Лень было постоянно лазить в hh. Хотел просто получить список вакансий, и пошло-поехало.
    - Написал на PHP получение вакансий по фильтру и сохранение их в БД, с возможностью смены статуса на неактуальные, с просмотром только актуальных вакансий.
    - добавил в процесс фильтр, чтобы неподходящие вакансии сразу помещались в неактуальные.
    - добавил получение своего резюме с hh по запросу и отправка вакансии в чат Qwen для последующего использования.

    - добавил связь с Qwen с установленными настройками. что он сравнивает мое резюме и вакансию, и если в вакансии строго требуется опыт. которого нет в резюме, помечать вакансию как неактуальную.

    Как дойдут руки, хочу добавить условие, если вакансия подходящая, то Qwen напишет сопроводительное письмо, которое учитывает соответствие опыта в резюме и требуемого опыта в вакансии. И откликнется на вакансию этим письмом.
    И повесить все это на крон, хай пасется.
    Делал для себя, поэтому не особо заморачивался качеством и безопасностью кода, главное, чтобы работало.