Привет, Хабр.
Думаю, у каждого, кто искал работу, есть этот "любимый" запрос на 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 не сработает. Роман подтвердил: таких случаев полно, и простой поиск по резюме их отсекает.
Пришлось городить гибрид:
Сначала
similar_vacancies+ кастомный query пользователя как доп. фильтр.Когда там все кончается, бесшовно переключаемся на
/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 в памяти.
Команды просто кладут
(user_id, vacancy_id, resume_id)в таблицуapplication_queueсо статусомpending.Фоновый
asyncio.Task(application_worker) пытается забрать задачу из БД.Как сделать, чтобы 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)

zeroc0de
23.10.2025 07:09Лень было постоянно лазить в hh. Хотел просто получить список вакансий, и пошло-поехало.
- Написал на PHP получение вакансий по фильтру и сохранение их в БД, с возможностью смены статуса на неактуальные, с просмотром только актуальных вакансий.
- добавил в процесс фильтр, чтобы неподходящие вакансии сразу помещались в неактуальные.
- добавил получение своего резюме с hh по запросу и отправка вакансии в чат Qwen для последующего использования.- добавил связь с Qwen с установленными настройками. что он сравнивает мое резюме и вакансию, и если в вакансии строго требуется опыт. которого нет в резюме, помечать вакансию как неактуальную.
Как дойдут руки, хочу добавить условие, если вакансия подходящая, то Qwen напишет сопроводительное письмо, которое учитывает соответствие опыта в резюме и требуемого опыта в вакансии. И откликнется на вакансию этим письмом.
И повесить все это на крон, хай пасется.
Делал для себя, поэтому не особо заморачивался качеством и безопасностью кода, главное, чтобы работало.
cry_san
Оркестрирование большой очередью распределенных задач через базу данных - это первое, что сразу приходит в голову. Удивительно, что вы долго к этому шли.