Ручной поиск работы — это боль. Особенно отклики
Вы сидите, открываете HH, видите 20 вакансий, подходящих под ваш скилл Python/DevOps. На каждую нужно писать сопроводительное письмо. Через час у вас глаза на лбу, а откликнулись на три. Потом еще неделю ждете ответа — тишина. Знакомо?
Я прошел это в прошлом году и подумал: а почему бы не заставить нейросеть делать это за меня? Не вручную копипастить шаблоны, а по-настоящему интеллектуально анализировать вакансию и генерировать уникальное письмо под каждую. И все это бесплатно, локально, без облака и подписок.
Ранее мы уже разбирали похожие архитектуры в статье про российского локального AI-агента — там использовался похожий стек, но с фокусом на универсальность. Сегодня сделаем специализированного бойца для HH.
Warning: автоматические отклики — это серая зона. HH официально против. Используйте этот гайд только для личного обучения и автоматизации, с осторожностью. Никаких гарантий, что аккаунт не забанят. Ты предупрежден.
Архитектура агента: что под капотом
Нам нужно решить три задачи:
- Парсинг вакансий — Playwright открывает HH, ищет вакансии по фильтрам, собирает данные (заголовок, описание, требования).
- Генерация сопроводительного письма — Ollama с локальной LLM (например, Qwen2.5-7B-Instruct или Mistral Small 22B) получает промпт и генерирует персонализированный текст.
- Отправка отклика и уведомление — Playwright заполняет форму и кликает «Откликнуться», а Telegram-бот пишет вам лог.
Всё на Python, без Docker (хотя для продакшна я бы завернул в контейнер). Связка: playwright → pydantic → ollama → python-telegram-bot.
Шаг 1: Поднимаем Ollama с моделью
Ollama — это, пожалуй, лучший способ запустить LLM локально. На 5 июля 2026 года актуальны модели Llama 4, Qwen3, Mistral Small 22B. Для автооткликов хватит 7-9B параметров — не гонитесь за гигантами, иначе ваш ноутбук расплавится.
Установка (Linux/Mac/WSL):
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen2.5:7b-instruct # или mistral-small:22b
ollama serve
Проверяем, что модель отвечает:
curl http://localhost:11434/api/generate -d '{
"model": "qwen2.5:7b-instruct",
"prompt": "Напиши краткое сопроводительное письмо для Python-разработчика.",
"stream": false
}'
Готово. Если у вас слабое железо, можно использовать внешний API, например AITunnel — он предоставляет стабильный доступ к мощным моделям через один шлюз. Но наша философия — полная бесплатность и локальность.
Шаг 2: Playwright — управляем браузером как бог
Playwright (версия 1.50+) умеет эмулировать реальный браузер, обходить простые антибот-системы. Установка:
pip install playwright
playwright install chromium
Критический момент — сохранение сессии. Каждый раз логиниться через смс/капчу — путь к безумию. Сохраняем куки после первого ручного входа:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://hh.ru/account/login')
# тут вы логинитесь руками через 30 секунд
page.wait_for_timeout(30000)
# сохраняем куки и localStorage
storage = page.context.storage_state(path='hh_auth.json')
browser.close()
При следующем запуске просто загружаем storage_state — сессия восстановится. Без смс, без капчи (если не сменится IP).
Ошибка: Никогда не сохраняйте storage_state в открытом виде в репозитории. Используйте .env и шифрование. Один мой знакомый выложил сессию на GitHub — HH забанил аккаунт за час.
Шаг 3: Парсинг вакансий — не тупи, Playwright
Ищем вакансии по запросу «Python developer» с фильтром «удаленная работа» и зарплатой от 200 000 руб. Селекторы могут меняться, поэтому используем гибкий подход через текст и data-атрибуты. Пишем функцию, которая собирает все карточки с текущей страницы:
def parse_vacancies(page):
page.goto('https://hh.ru/search/vacancy?text=python+developer&schedule=remote&salary_from=200000')
page.wait_for_selector('[data-qa="vacancy-serp__vacancy"]')
vacancies = []
cards = page.query_selector_all('[data-qa="vacancy-serp__vacancy"]')
for card in cards:
title_el = card.query_selector('[data-qa="serp-item__title"]')
link = title_el.get_attribute('href')
title = title_el.inner_text()
# также можно достать компанию, зарплату, описание
vacancies.append({'title': title, 'link': link})
return vacancies
Парсим N страниц (но не больше 5, чтобы не спамить HH).
Шаг 4: Генерация письма — момент истины
Переходим на страницу каждой вакансии, читаем полное описание. Затем отправляем промпт в Ollama:
def generate_cover_letter(vacancy_description, candidate_info):
prompt = f"""Ты — опытный Python-разработчик, откликаешься на вакансию.
Информация о кандидате: {candidate_info}
Описание вакансии: {vacancy_description}
Напиши сопроводительное письмо (3-5 предложений), кратко и по делу.
Упомяни конкретные технологии из описания, покажи опыт.
Не используй шаблонные фразы."""
import requests
response = requests.post('http://localhost:11434/api/generate', json={
'model': 'qwen2.5:7b-instruct',
'prompt': prompt,
'stream': False,
'options': {'temperature': 0.7, 'max_tokens': 512}
})
return response.json()['response'].strip()
Кандидат-инфо — это ваш заранее заготовленный профиль (скиллы, опыт, GitHub, ключевые проекты). Лучше хранить в отдельном файле конфига.
Шаг 5: Отправка отклика через Playwright
На странице вакансии ищем кнопку «Откликнуться» или открываем форму отклика. Поле для сопроводительного (обычно textarea) и кнопка отправки:
def apply_to_vacancy(page, vacancy_link, cover_letter):
page.goto(vacancy_link)
# ждем загрузки
page.wait_for_selector('[data-qa="vacancy-response-link-top"]')
page.click('[data-qa="vacancy-response-link-top"]')
# ждем появления поля для сопроводительного
page.wait_for_selector('[data-qa="vacancy-response-letter"]')
page.fill('[data-qa="vacancy-response-letter"]', cover_letter)
page.click('[data-qa="vacancy-response-submit"]')
# ждем подтверждения
page.wait_for_timeout(2000)
return page.url # если перебросило — отлично
Снова важно: между откликами ставьте случайные задержки (3-15 секунд), иначе HH решит, что вы бот. Забавно, что мы и есть бот, но не надо так явно.
Шаг 6: Telegram-уведомления — вы в курсе
Установите python-telegram-bot (v21+). Создайте бота через @BotFather, получите токен. Отправляйте сообщение после каждого отклика:
import asyncio
from telegram import Bot
TELEGRAM_TOKEN = 'your_token'
CHAT_ID = 'your_chat_id'
async def notify(text):
bot = Bot(token=TELEGRAM_TOKEN)
await bot.send_message(chat_id=CHAT_ID, text=text)
# в основном потоке:
asyncio.run(notify(f'Откликнулся на {vacancy_title}: {vacancy_link}'))
Если вы хотите telegram-бота, который умеет давать команды «старт» и «стоп» — читайте наш подробный гайд Как превратить обычный Telegram-аккаунт в автономного ИИ-агента. Там показана интеграция Pyrogram для обхода ограничений.
Шаг 7: Собираем всё в систему
Агент должен работать циклически: раз в N часов запускать парсинг свежих вакансий и откликаться на непросмотренные. Используем schedule или apscheduler. Запускаем как service через systemd — подробнее в материале AI-агент, который не спит.
Примерный цикл:
def run_agent():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(storage_state='hh_auth.json')
page = context.new_page()
vacancies = parse_vacancies(page)
for v in vacancies:
# читаем описание
description = get_vacancy_description(page, v['link'])
# генерируем письмо
letter = generate_cover_letter(description, CANDIDATE_INFO)
# откликаемся
apply_to_vacancy(page, v['link'], letter)
# задержка + уведомление
time.sleep(random.randint(3, 10))
asyncio.run(notify(f'Отклик на {v["title"]} отправлен'))
browser.close()
Совет: Сделайте файл .env с переменными: HH_COOKIES_PATH, TELEGRAM_TOKEN, CHAT_ID, OLLAMA_URL, CANDIDATE_INFO. Тогда агента можно запускать на любом сервере, подменяя конфиг.
Грабли и нюансы (которые я наступил)
Капча
Time to time HH выкидывает капчу. Решений два: либо вставлять сервис распознавания (например, 2captcha — платно), либо снизить скорость и использовать headful режим с реальным браузером, рандомным user-agent и прокси. В 95% случаев, если не флудить, капча не появляется.
Лимиты на отклики
HH ограничивает число откликов в день (для обычного аккаунта в районе 15-20). Если превысить — аккаунт могут заморозить на сутки. Сделайте счетчик и стоп-кран.
Качество писем
Локальная 7B модель не всегда пишет идеально. Иногда она галлюцинирует технологии. Лучше использовать модель побольше (22B+), но это уже вопрос ресурсов. Мой рекорд — письмо с упоминанием «Django 6.0» которого еще не вышло. Рецепт: добавьте в промпт «проверь, что все указанные технологии существуют и соответствуют описанию».
Playwright и память
Каждый запуск браузера жрет ОЗУ. Если крутить агента каждые 15 минут — утечка неминуема. Используйте browser.close() и перезапускайте скрипт через cron раз в час.
Кстати, похожий проект мы описывали в статье Бесплатный ИИ-агент для HH.ru, но там упор на промпт-инжиниринг, а здесь — полный код.
А что насчет безопасности и этики?
HH — это бизнес, они продают рекрутерам доступ к базе. Массовый спам от автоматических откликов вредит их модели. Поэтому я не советую запускать агента на полную катушку. Лучше использовать его для точечного отклика на вакансии, которые реально интересны, а низкокачественные отклики фильтровать самому.
Если вы хотите сделать агента «невидимым» — эмулируйте поведение человека: скроллите, перемещайте мышь, кликайте с разной задержкой. В нашем гайде Как запустить AI-агента на старом Android-телефоне мы разбирали обход антибот-систем на эмуляторе Android — некоторые техники применимы и тут.
Небольшой прогноз в никуда
Через пару лет такие агенты станут стандартом поиска работы. Но вот парадокс: когда все будут использовать ИИ для откликов, HR-менеджеры перестанут читать сопроводительные. Тогда агент обесценится. Единственный способ оставаться в топе — создавать личный бренд и контент, а не спамить откликами. Агент — это инструмент для первого контакта, а не замена стратегии.
Не рекомендую запускать агента без тестирования на первом десятке вакансий. Проверьте, что письма не содержат бреда, что форма отправляется, что уведомления приходят. И помните: настоящий оффер приходит от живого общения, а не от автоматических откликов.