«HIREFLOW» — AI-сервис поиска работы: анализ резюме, скоринг вакансий с hh.ru и генерация сопроводительных писем
Веб-сервис, который помогает соискателю: принимает текст резюме, разбирает его через LLM в структурированный профиль, находит подходящие вакансии на hh.ru, оценивает каждую по 100-балльной шкале на соответствие резюме и по требованию генерирует персональное сопроводительное письмо. Всё — в многопользовательском кабинете с изоляцией данных, базой знаний о пользователе и учётом стоимости AI-вызовов.

Слайд 1 из 8
ОПИСАНИЕ
Full-stack веб-приложение на Python 3.11 (FastAPI, полностью асинхронный стек) поверх PostgreSQL. Сервер отдаёт server-rendered интерфейс на Jinja2 (без фронтенд-сборки) и одновременно является полноценным JSON-API. Данные лежат в PostgreSQL через SQLAlchemy 2.x (async, драйвер asyncpg), схема ведётся миграциями Alembic. Конфигурация и все секреты — только в окружении (.env) через pydantic-settings; хардкод ключей в коде запрещён на уровне принципа проекта.
Сквозная идея — не «ещё один парсер вакансий», а фундамент карьерной ОС: MVP реализует два первых модуля (поиск/скоринг и письма), но архитектурные «швы» уже заложены так, чтобы продукт наращивался эволюционно, без переписывания работающего ядра.
Рабочий пайплайн: резюме → анализ → поиск → скоринг → письмо
Это главный инвариант продукта — он должен оставаться функциональным на каждом шаге развития. Пользователь вставляет текст резюме; LLM превращает его в структурированный ResumeProfile (навыки, роли, ключевые слова, ожидания); по профилю строится поисковый план и собираются вакансии hh.ru; вакансии дедуплицируются и сохраняются; затем проходит скоринг соответствия резюме; на выходе — список матчей с баллами и обоснованиями, из которого по одной кнопке генерируется черновик сопроводительного письма.
LLM за единым провайдер-агностичным интерфейсом
Весь доступ к моделям спрятан за абстракцией LLMProvider с двумя реализациями — OpenAI (по умолчанию) и Anthropic Claude. Провайдер выбирается настройкой из .env; ключ и модель тоже из окружения и никогда не вводятся пользователем и не хранятся в БД (это отдельный инвариант безопасности). Клиент каждого провайдера кэшируется как синглтон на процесс (один httpx-пул, без пересоздания на каждый запрос), у него выставлен таймаут одного вызова и принудительно отключены встроенные ретраи SDK — вместо слепых повторов устойчивость обеспечивается на уровне сервисов. Для structured-задач (анализ резюме, скоринг, письма) используется нативный JSON-режим OpenAI, а разбор ответа дополнительно защищён собственным экстрактором JSON — если модель вернёт мусор или обрамит ответ текстом, парсер извлечёт валидный объект, а не уронит запрос. Предусмотрен и режим работы через прокси (socks5/http) — чтобы сервер без прямого доступа к OpenAI всё равно мог обращаться к LLM.
Устойчивость к «плохому» ответу модели
Каждый structured-сервис проектировался из допущения, что LLM может ответить невалидно. Анализ резюме и черновик письма делают одну повторную попытку, а если и она не проходит валидацию Pydantic — ошибка приводится к единому LLMError, который роуты отдают как честный 502, а не как необработанный 500. Скоринг ещё мягче: если оценка конкретной вакансии не удалась, подставляется безопасный дефолт (score=0, verdict «weak»), и весь батч не рушится из-за одной проблемной вакансии.
Скоринг вакансий: дешёвый пре-фильтр + параллельный LLM
Прежде чем тратить платные вызовы, вакансии проходят пре-фильтр без LLM: отсев уже оценённых и чёрный список по названию/компании. Оставшиеся оцениваются LLM параллельно с ограничением конкурентности (семафор на 5 одновременных вызовов) — быстро, но без перегрузки провайдера. Модель возвращает балл 0–100, вердикт и список причин; результат сохраняется идемпотентным UPSERT (ON CONFLICT по паре резюме+вакансия), поэтому повторный скоринг обновляет оценку, а не плодит дубли. Есть настраиваемый «акцент» — подсказка о приоритете критериев (навыки / опыт / культура / зарплата / формат), которая подмешивается в системный промпт и смещает оценку в нужную пользователю сторону. Матчи фильтруются по порогу MIN_SCORE и отдаются с обоснованием — почему именно эта вакансия подходит.
Генерация писем с опциональным «критиком»
Письмо создаётся строго как черновик (статус draft) и никогда не отправляется автоматически — это ещё один сознательный инвариант. Базовый режим — черновик по профилю и вакансии; при включённом флаге LETTER_CRITIC_ENABLED добавляется второй проход: отдельный промпт-критик оценивает черновик и возвращает правки, после чего письмо переписывается. Пользователь может и вручную дать инструкцию на переписывание («сделай короче», «добавь про опыт с X») — сервис перегенерирует текст, сохраняя факты и деловой тон. Каждый вариант письма сохраняется (версионируется), ничего не затирается.
Многопользовательность и изоляция по Workspace
Приложение изначально многопользовательское, но контейнером данных выступает не пользователь, а Workspace — под будущих карьерных консультантов и HR-команды. При регистрации создаётся пользователь + личный Workspace + членство (owner). Любая пользовательская сущность (резюме, оценки, письма, документы, статистика) scoped к workspace_id, а доступ идёт только через FastAPI-зависимость «текущий Workspace + проверка членства» — данные одного Workspace физически не видны другому.
Безопасность аутентификации
Пароли хешируются argon2id (argon2). Сессии — серверные: в cookie кладётся случайный токен (secrets.token_urlsafe), а в БД хранится только его sha256-хеш, поэтому утечка таблицы сессий не даёт готовых токенов. Cookie помечена HttpOnly и SameSite=Lax, а флаг Secure включается в проде (HTTPS). Все небезопасные методы защищены CSRF: токен привязан к сессии, принимается из form-поля или заголовка X-CSRF-Token и сверяется в постоянном времени (hmac.compare_digest). Проверка пароля не падает на битом хеше — возвращает False, а не исключение.
Knowledge Base — централизованное знание о пользователе
Отдельный модуль /knowledge: резюме здесь — лишь один из типов документа (kind), рядом с ним портфолио, проекты, опыт, сертификаты, рекомендации, письма-образцы, заметки и ссылки (список типов легко расширяется). Файлы (PDF/DOCX/TXT) загружаются drag&drop с тройной проверкой — расширение, MIME и размер (≤5 МБ); имена файлов генерируются, а сами файлы хранятся вне web-root (UPLOAD_DIR) и отдаются только под аутентификацией. Из PDF/DOCX извлекается текст (pypdf / python-docx). Каждый документ версионируется: вся история хранится, актуальная версия — с максимальным номером, а «восстановление» старой версии создаёт новую из неё, не теряя историю. По тексту документа LLM извлекает структурированный анализ (summary, навыки, технологии, проекты, опыт), который сохраняется отдельно с привязкой к версии.
AI-движок как отдельный слой платформы
Помимо пайплайна есть общий AI-модуль: профиль пользователя как контекст для генераций, реестр моделей/провайдеров, настройки поиска и стиля писем на уровне Workspace, а также журнал генераций. Все вызовы LLM пишутся в таблицу llm_calls с моделью и числом токенов — эндпоинт /usage отдаёт сводку затрат (сколько вызовов и токенов «съел» сервис), что делает стоимость AI прозрачной, а не скрытой.
Клиентский сбор выдачи hh.ru (ключевое инженерное решение)
hh.ru активно защищается от серверного парсинга (может отдавать 403 по IP/гео), поэтому сбор страниц вынесен на сторону пользователя: сервер строит план (по резюме — список URL страниц поиска и, затем, страниц вакансий), а браузер пользователя скачивает эти страницы со своим IP и cookies и присылает «сырой» HTML обратно. Всю «голову» держит сервер — анализ резюме, разбор HTML, дедуп и сохранение, скоринг; браузер выступает только курьером. Схема потока: plan (сервер) → скачать выдачу (клиент) → search-разбор (сервер) → скачать вакансии (клиент) → details-разбор (сервер) → score (сервер) → результаты. Благодаря этому сервер ни разу не ходит в hh.ru — ни бана по IP, ни нагрузки парсинга на бэкенде. Все шаги требуют аутентификации, а CSRF пробрасывается через заголовок X-CSRF-Token. Клиентская часть намеренно «тонкая» и безопасная: она может скачивать только https://hh.ru/* (жёсткий allow-list, чтобы канал нельзя было использовать как произвольный прокси), общается со страницей строго по проверке origin и работает только на доменах самого сервиса.
Job Sources как плагины
В коде нет жёсткой привязки к HeadHunter вне его адаптера: источник вакансий — это реализация общего контракта (search / get_details → нормализованная вакансия), а идентичность вакансии задаётся парой (источник, внешний ID). Это тот шов, по которому позже добавляются новые площадки (LinkedIn, Habr Career и т.д.) без переделки ядра.
Интерфейс
Server-rendered Jinja2 + современный CSS без сборщика: дизайн-система на CSS-токенах, компонентный CSS (кнопки, карточки, формы, таблицы, вкладки, тосты, оверлеи) и отдельный слой адаптива и «моушена». Есть «мастер поиска» (пошаговый визард), страница матчей, экран письма с правками, история действий, дашборд-рабочий стол из виджетов (чтобы добавлять модули без переделки), форма обратной связи и брендированный раздел /docs. Клиентский JavaScript — ванильный, разбит по задачам (визард, оркестрация клиентского поиска, мост к браузерному сборщику, панель выбора модели, тосты и т.п.).
Сквозные решения и «честные» ограничения MVP
Единое логирование; таймаут на каждый LLM-вызов, чтобы зависший вызов не держал воркер; аддитивные миграции (новые поля nullable → бэкфилл → enforce), чтобы не ломать существующие таблицы. Ограничения текущего MVP описаны прямо в документации как осознанный техдолг: пайплайн выполняется синхронно в рамках запроса (вынос в фоновую очередь — отдельная задача), поиск берёт одну страницу выдачи, а Knowledge Base пока не подключена к самому поиску. Проект намеренно ведёт объёмную инженерную документацию (VISION, дизайн-система, принципы разработки, правила доступности и деплоя) — как каркас для дальнейшего роста. Прод развёрнут на VPS (systemd), обновление — git pull + alembic upgrade + рестарт сервиса.
ИСПОЛЬЗУЕМЫЕ ИНСТРУМЕНТЫ
- Python 3.11
- FastAPI (async)
- Uvicorn
- PostgreSQL
- SQLAlchemy 2.x (async ORM)
- asyncpg
- Alembic
- Pydantic v2 / pydantic-settings
- OpenAI Chat Completions (JSON-режим)
- Anthropic Claude Messages API
- httpx (пул клиентов
- socks5/http-прокси)
- asyncio (семафор конкурентности)
- argon2id (argon2-cffi)
- Серверные сессии (secrets + sha256)
- CSRF (hmac.compare_digest)
- Jinja2 (server-rendered UI)
- HTML5 / CSS3 (дизайн-токены
- компонентный CSS)
- Ванильный JavaScript
- Браузерное расширение Manifest V3
- pypdf / python-docx
- HH API / разбор HTML hh.ru
- ruff
- pytest
- Docker Compose (PostgreSQL)
- systemd (VPS)
- Git
РЕЗУЛЬТАТ
- Спроектирован и реализован AI-сервис поиска работы на асинхронном стеке FastAPI + PostgreSQL (SQLAlchemy 2.x async, Alembic) со сквозным пайплайном «анализ резюме → поиск вакансий → скоринг → сопроводительное письмо».
- LLM спрятан за провайдер-агностичным интерфейсом с адаптерами OpenAI и Claude: выбор провайдера/модели и ключ — только из .env, клиенты кэшируются, таймаут на вызов, SDK-ретраи отключены в пользу устойчивости на уровне сервисов.
- Обеспечена устойчивость к невалидному ответу модели: нативный JSON-режим + собственный экстрактор JSON, повторная попытка с валидацией Pydantic, единый LLMError→502 и безопасные дефолты (скоринг не рушится из-за одной вакансии).
- Реализован скоринг соответствия 0–100: дешёвый пре-фильтр (уже оценённые + чёрный список) → параллельный LLM с семафором конкурентности → идемпотентный UPSERT оценок, настраиваемый «акцент» критериев и порог отбора матчей.
- Сделана генерация сопроводительных писем строго как черновиков (draft, без автоотправки) с опциональным проходом «критика» и переписыванием по инструкциям пользователя; каждый вариант версионируется.
- Построена многопользовательская модель с изоляцией по Workspace: любая сущность scoped к workspace_id, доступ — только через зависимость «Workspace + членство», данные одного Workspace недоступны другому.
- Реализована безопасная аутентификация: пароли argon2id, серверные сессии (в cookie случайный токен, в БД его sha256-хеш), cookie HttpOnly/SameSite=Lax/ Secure в проде, CSRF на всех небезопасных методах со сверкой в постоянном времени.
- Построена Knowledge Base: типизированные документы (резюме — частный вид), загрузка PDF/DOCX/TXT с проверкой расширения/MIME/размера и хранением вне web-root, извлечение текста (pypdf/python-docx), версии с восстановлением и структурированный AI-анализ документа.
- Вынесен AI-движок платформы: контекст-профиль пользователя, реестр моделей, журнал генераций и учёт стоимости — все LLM-вызовы логируются с токенами, сводка затрат доступна через /usage.
- Реализован клиентский сбор выдачи hh.ru через браузер пользователя (Manifest V3 расширение как «курьер»): сервер строит план и разбирает HTML, скачивание идёт с IP/cookies пользователя — бэкенд ни разу не ходит в hh.ru, что снимает баны по IP и нагрузку парсинга; канал ограничен allow-list только на hh.ru.
- Заложены архитектурные швы платформы: Job Sources как плагины (нет привязки к hh.ru вне адаптера, идентичность вакансии = источник+внешний ID), аддитивные миграции и версионирование — под эволюционное развитие без переписывания ядра.
- Сделан server-rendered интерфейс на Jinja2 без сборки: дизайн-система на CSS-токенах, компонентный CSS с адаптивом, «мастер поиска», страницы матчей и письма, история, дашборд-виджеты, обратная связь и брендированный /docs.
- Проект доведён до работающего MVP и развёрнут на VPS (systemd) с процедурой обновления git pull + alembic upgrade + рестарт; ведётся объёмная инженерная документация (VISION, дизайн-система, принципы) как каркас для дальнейшего роста.
AI АССИСТЕНТ
Задать вопрос по этой работе