Загрузка 0
ПОДЕЛИТЬСЯ

Мой блог

Листай вниз

Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC

Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC

Когда в день вам присылают 400 резюме на одну вакансию, «посмотреть всех» физически не получится. Зато это выглядит прямо как задача под автоматизацию!

Итак, у меня есть вакансия и огромная пачка PDF-файлов с резюме. Надо понять, в каких из них действительно подтверждаются нужные компетенции, где информации не хватает и кого стоит посмотреть в первую очередь. Мы не будем автоматизировать сам найм или отправку отказов. В кейсе ниже LLM будет составлять техническую выжимку и помогать разобрать очередь. Финальное решение останется за человеком. По идее можно было бы открыть любую ИИшницу, загрузить туда пачку резюме и собрать результаты. Но это не автоматизация, а ерунда. Поэтому мне нужен API, который можно подключить и встроить в собственный процесс проверки резюме. Для меня тут важен именно уровень абстракции. Я не хочу искать GPU, скачивать веса модели, подбирать версию CUDA, поднимать vLLM, рассчитывать, влезет ли модель в видеопамять, настраивать масштабирование, мониторить отдельный ML-зоопарк и выяснять, почему после обновления драйвера все снова упало. Статью написал Роман Шубин, CTO и автор Telegram-канала Bash Days.

Идея и подготовка

Да, у меня есть домашний LLM-стек, собранный до всех этих кризисов с памятью, но это решение мне не подошло. Оно требует постоянно держать эту махину включенной, а я частенько нахожусь в перелетах и сильно тревожусь, если оставил дома «включенный утюг». Поэтому выбор пал на FMC. Все работает в одном из московских дата-центров Selectel, не потребляет мое электричество и доступно в любое время и в любом месте. То что нужно!

Реклама

Интеграция модели в мой проект и бизнес-логика остаются на моей стороне, сервис разделяет эти зоны. То есть Selectel дает мне работающий эндпоинт, а я уже решаю, как использовать его в своем коде. На момент моего тестирования, FMC находится на стадии public preview. Инференс-сервисы работают синхронно, а загрузка собственных моделей в каталог пока не поддерживается.

Архитектурная схема пайплайна с LLM посередине
LLM занимает центральное положение в конвейере, выполняя исключительно рутинные задачи по текстовой выжимке.
Инструменты автоматизации и принятые решения
Схема распределения зон ответственности между кодом и нейросетью.
Сравнение результатов матчинга в виде диаграммы
Наглядное распределение статусов match, manual_review и no_match.
Проверка стабильности ответов на повторных прогонах
Убеждаемся, что разброс оценок минимален и система работает стабильно.
Метрика задержки для каждого PDF-файла
Фиксация времени обработки каждого документа в секундах.
Сводка преимуществ подхода без использования tools
Все критические операции выполняются в Python, минимизируя риски ошибок.
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC
Как автоматизировать разбор 400 резюме с помощью LLM, Python и FMC

Наша задача — взять описание конкретной вакансии, извлечь текст из резюме, проверить наличие подтвержденного опыта по заранее заданным критериям и собрать понятный отчет для HR. Важно — не улучшать резюме, не оценивать человека, не анализировать фотографию, возраст или семейное положение. А именно сопоставить профессиональный опыт из документа с требованиями конкретной вакансии.

В результате вместо папки с 400 резюме я хочу получить таблицу:

  • resume-001.pdf — 88/100 (соответствует профилю)
  • resume-002.pdf — 61/100 (требуется ручная проверка)
  • resume-003.pdf — 34/100 (недостаточно подтверждений)
  • resume-004.pdf — 42/100 (много ключевых слов, мало доказательств)
  • resume-005.pdf — 84/100 (соответствует профилю)

При этом по каждому баллу должны быть видны доказательства из исходного текста:

Критерий: CI/CD
Уровень: 4 из 4
Доказательства:

  • Разработал GitLab CI pipeline для сборки, тестирования и развертывания 18 сервисов.
  • Сократил время деплоя с 35 до 12 минут.

Если доказательства нет, модель должна честно написать: «В тексте резюме подтверждение не найдено», а не додумывать опыт кандидата. Для анализа резюме подойдет обычная text-to-text-модель. На вход отправляем вакансию, критерии и извлеченный текст, на выходе ожидаем структурированный JSON. Никакие model tools для этого не потребуются. Последовательностью действий управляет Python.

Выбор инференс-сервиса в панели управления FMC
Интерфейс облачной панели управления для запуска языковой модели.

Модель не должна сама открывать файл, выполнять команды или менять статус кандидата в кадровой системе. Она решает только одну задачу — делает структурированную оценку по заданным критериям на основе текста из резюме. Для эксперимента я взял t-tech/T-pro-it-2.1. Модель доступна в актуальном каталоге FMC вместе с T-lite, Qwen, Gemma и другими text-to-text-моделями.

Почему именно она? Потому что входные данные у меня русскоязычные. При этом в резюме встречаются английские названия технологий, команды, аббревиатуры и описание технических проектов. От модели мне нужен не творческий текст, а строгое следование инструкции:

Настройка контекста и конфигурации инстанса
Параметры контекста и масштабирования создаваемого сервиса.
  • обработать заранее заданный список критериев,
  • не придумывать отсутствующий опыт,
  • привести доказательства,
  • вернуть валидный JSON,
  • не рассчитывать итоговый балл самостоятельно.

Последний пункт тут очень важен. Модель определяет уровень подтвержденного опыта, итоговую математику выполняет Python. Иначе одинаковые оценки критериев однажды могут превратиться в 78 баллов, а в другой раз — в 83. Модели, конечно, виднее, но меня такой гидрометцентр не устраивает.

Параметры графических ускорителей для модели
Выделение вычислительных ресурсов для работы нейросети.

По архитектуре получилась следующая схема: LLM находится примерно в середине конвейера. Она не управляет процессом, не принимает кадровое решение и не формирует итоговый балл. Все опасные операции остаются в обычном коде. Роботу отдаем только самую рутину, все остальное реализуем самостоятельно.

Окно создания сервиса в облаке Selectel
Финальный этап создания сервиса, требующий немного времени на инициализацию.

Для эксперимента я придумал вакансию DevOps-инженера уровня middle. Так мне будет проще проверить модель самостоятельно, если она вдруг решит, что человек с четырьмя годами Django и одним домашним Docker Compose идеально подходит на эксплуатацию кластера Kubernetes.

Пропуск настройки WebUI за ненадобностью
Для программной интеграции графический интерфейс не требуется.

Сумма всех весов в моем тестовом файле вакансии составляет ровно 100. Каждый критерий модель оценивает от 0 до 4. Python переводит уровень в баллы по формуле: баллы критерия = вес × уровень / 4.

Реклама
Выпуск API-ключей для доступа к сервису
Система автоматически выпускает первый ключ доступа для каждого инференс-сервиса.

Почему нельзя просто искать ключевые слова? Изначально я хотел решить это регулярными выражениями вроде if "kubernetes" in resume.lower(): score += 10. Но тогда возникнет неувязочка с кандидатом, у которого есть огромный список технологий, скопированный из интернета.

Список ключей доступа в панели управления FMC
Раздел генерации и отзыва ключей для безопасного обращения к эндпоинту.

Он всех обманет и получит максимальный балл, хотя из текста вообще не ясно, использовал ли он эти инструменты в работе, настраивал ли что-нибудь самостоятельно или просто скопировал список. С другой стороны, сильный SRE может не использовать Ansible, но несколько лет управлять конфигурацией через SaltStack. Поиск по ключевым словам скажет: «Ansible не найден — ноль баллов». А нормальный анализ должен выявить, что SaltStack решает тот же класс задач. Вот здесь модель будет прям полезна и в тему.

Для статьи я не стал брать настоящие резюме. Вместо этого подготовил пять синтетических PDF с заранее известными особенностями. Так я сразу проверю работу пайплайна без слива персональных данных и буду понимать, что моя система должна найти.

Создаем сервис в облаке

Теперь надо создать инференс-сервис. Идем в панель управления → Продукты → Foundation Models Catalog и выбираем нужные параметры конфигурации. Для теста я выбрал t-pro-it-2.1, один инстанс, фиксированное масштабирование, четыре GPU NVIDIA L4 (по 24 ГБ) и контекст 16 384.

Конфигурацию после создания изменить нельзя, а количество инстансов можно масштабировать потом отдельно. Создание сервиса может занимать около 15 минут, так что наберитесь терпения и попейте кофейку. Кстати, дополнительно можно взгромоздить WebUI. Но в моей задаче оно не требуется, поэтому этот шаг пропускаю.

После запуска сервис выдал креды. Для каждого инференс-сервиса используются отдельные API-ключи, причем первый ключ при создании выпускается автоматически. Создаем .env-файл и заполняем его полученными данными, сразу добавляя его в .gitignore, так как API-ключу в репозитории делать нечего.

Перед написанием приложения отправляем банальный запрос через curl, чтобы сразу все проверить и не городить огород с багами. Тестовый запрос проходит успешно, после чего можно сделать дополнительную проверку с обработкой через jq.

Пример успешного ответа API в формате OpenAI
FMC поддерживает стандартные эндпоинты и формат ответов OpenAI API.

FMC поддерживает Chat API по адресу /v1/chat/completions, Bearer-авторизацию и ответы в формате OpenAI API. Эндпоинт, API-ключ и имя модели можно скопировать из панели управления. Если получаете ошибку 401 Unauthorized, проверяйте учетные данные.

Далее устанавливаем зависимости и необходимые пакеты: pymupdf, requests, pydantic, python-dotenv, pyyaml и jinja2. Набор пакетов выбран неслучайно: PyMuPDF извлекает текст из PDF, requests обращается к FMC, pydantic проверяет ответ модели, pyyaml читает описание вакансии, jinja2 формирует HTML-отчет, а python-dotenv загружает настройки.

Дальше будет много кода. На роль Python-разработчика я не претендую, поэтому для профильного специалиста код может выглядеть как лапша. В любом случае код рабочий и меня он вполне устраивает.

Установка необходимых Python-пакетов
Установка библиотек для работы с PDF, запросами, YAML и шаблонами.

Обезличиваем документ

В настоящем резюме содержатся персональные данные: имя, телефон, почта, адрес, дата рождения и ссылки на соцсети. Для проверки профессионального соответствия они не нужны, да и трудовое законодательство запрещает ограничения и преимущества по обстоятельствам, не связанным с деловыми качествами кандидата.

Оговорюсь, что здесь у меня демонстрационный фильтр, а не продакшен-система обезличивания. Фильтр удалит очевидный телефон и email, но может не распознать имя или необычно записанный адрес. В продакшене такой этап придется усиливать и отдельно проверять с юристами и специалистами по ИБ. Но для синтетических документов этого вполне достаточно.

Описываем ожидаемый ответ

Библиотека pydantic проверяет наличие всех полей, типы значений, диапазон уровня от 0 до 4, допустимые статусы, максимальное количество доказательств и вопросов. Если модель вернет некорректный формат данных или неожиданный тип, проверка сразу завершится ошибкой.

Создаем промпт

Теперь пришло время создать промпт. Создаем файл в каталоге проекта и сначала пишем строгую системную инструкцию. В ней прописываем правила оценки, запрет на учет персональных данных, требование приводить дословные цитаты и использовать эквивалентные инструменты вроде SaltStack или Argo CD.

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

Реклама

Запускаем FMC

Параметр temperature я устанавливаю в ноль, чтобы снизить случайность ответа. Но даже при нулевой температуре полная повторяемость не гарантируется, поэтому позже отдельно проверим стабильность на нескольких запусках.

Не доверяем цитатам модели

В ответе модель должна приводить доказательства из резюме. Но что мешает ей меня обмануть или вообще придумать красивую цитату? Чтобы избежать этого, создаем специальную функцию проверки подстроки.

Если модель вернула несуществующую в исходнике строку, доказательство помечается как неподтвержденное. Это не идеальная проверка: модель может убрать знак препинания или слегка сократить цитату. Но для первого варианта я специально сделал дословные фрагменты, чтобы результат можно было проконтролировать.

Считаем итоговый балл

Расчет очков и итогового вердикта вынесен в отдельный модуль. Возможные результаты трактуются следующим образом:

  • match — в резюме достаточно подтверждений, чтобы посмотреть кандидата в приоритетном порядке.
  • manual_review — есть спорные места, проблемы с извлечением текста или неполное покрытие требований. Такое резюме нужно проверить вручную.
  • no_match — в документе недостаточно подтверждений по текущему профилю вакансии.

В общем, вердикт no_match не должен автоматически отправлять кандидату отказ. Это результат сопоставления текста с конкретной рубрикой, который HR может проверить и изменить.

Загружаем вакансию

На этапе загрузки вакансии обязательно проверяется сумма весов всех критериев. Проверка суммы весов спасет от ситуации, когда после добавления нового критерия максимальный результат становится 115 из 100. Нам такое, естественно, не подходит, поэтому тут лучше переборщить со внимательностью к деталям.

Собираем HTML-отчет

Для генерации красивых отчетов используется шаблонизатор Jinja2 и кастомный шаблон с темной темой оформления, карточками результатов, таблицами критериев и блоками для вопросов на интервью.

Рендерим отчет

Специальная функция отвечает за компиляцию шаблона, передачу данных по конкретному кандидату, расчету времени задержки и сохранение готового HTML-документа на диск.

Собираем все в один запуск

Финальный скрипт объединяет парсинг аргументов командной строки, загрузку вакансии, последовательный обход всех PDF-файлов в папке, вызов FMC-клиента, расчет очков, генерацию JSON-отчетов, HTML-страниц и сводной CSV-таблицы.

Запускаем сервис

Запускаем написанный скрипт через консоль и наблюдаем за процессом. Спустя пару минут в папке результатов появляются новые файлы и итоговые логи выполнения.

Можно сказать, это успех. Оно работает! Теперь можно посмотреть красивые отчеты по каждому резюме. Не стал бы утверждать, что это прям готовая ATS, которую можно внедрять к себе в CRM и пачками отсеивать соискателей.

Итого по результатам тестирования на пяти синтетических резюме мы получаем два совпадения из пяти (40%). Два кандидата получили статус match, два отправлены на ручную проверку и один получил no_match. При этом модель не спрятала потенциально подходящих кандидатов в no_match, выбрав для спорных случаев более осторожный режим manual_review.

Проверяем стабильность

Одно и то же резюме я прогнал пять раз подряд, чтобы оценить разброс показателей. Сравниваю итоговый балл, вердикт, уровни критериев, доказательства и вопросы для интервью. Результат полностью стабилен: разброс составил 0 баллов, совпадение вердиктов — 5 из 5, а уровни критериев и цитаты совпали дословно.

Структура файлов проекта для анализа резюме
Организация каталогов и исходного кода приложения на Python.

Тут важно понимать, что если балл гуляет на два-три пункта, с этим еще можно жить. Если один запуск говорит match, a другой no_match, такой инструмент использовать нельзя, пока не будет достигнута стабильность.

Что еще можно измерить

Метрики оценки работы сервиса позволяют понять его надежность и эффективность перед массовым запуском на сотни резюме.

Результаты работы скрипта в терминале
Консольный вывод подтверждает корректную работу пайплайна.

Валидность JSON

Метрика валидности рассчитывается как отношение успешно проверенных ответов к общему числу запросов. Например, при 49 валидных ответах из 50 показатель составит 98%.

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

Достоверность доказательств

Доля достоверных доказательств определяется как отношение дословно найденных в тексте цитат ко всем цитатам, выданным моделью. Если модель выдумала фрагмент, результат отправляется на ручную проверку.

Рекламный блок ИИ-роутера в источнике
Доступ к большому количеству моделей через единый OpenAI-совместимый шлюз.

Время обработки

Для каждого PDF-файла фиксируется время выполнения запроса latency_seconds.

Оценка времени для 400 резюме

При последовательной обработке общее время будет равно среднему времени на проверку одного резюме, умноженному на 400. То есть если одно резюме обрабатывается 12 секунд, то суммарно потребуется около 80 минут. Но это пока только арифметическая оценка.

Консольные логи пакетной обработки файлов
Процесс последовательного прогона всех PDF-документов через систему.

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

Сводный JSON-отчет по оценке кандидата
Результат оценки модели, содержащий баллы и детальные комментарии.

Ложные отрицательные результаты

Главный вопрос для такой системы — не спрятали ли мы сильного кандидата в хвост выдачи. В этом сценарии лучше показать человеку несколько лишних резюме, чем потерять одного подходящего специалиста. Поэтому пороги лучше настраивать в сторону ручной проверки, а не агрессивного отказа.

Детализированная оценка критериев в JSON
Разбор каждого требования с подтвержденными уровнями и весами.

Опасения насчет раздувания бюджета на инфраструктуру здесь напрасны, так как FMC использует модель оплаты по потреблению облачных ресурсов, а не за токены. Средства списываются за мощности инференс-сервера, а количество токенов отдельно не тарифицируется.

Сгенерированные вопросы для интервью с кандидатом
Модель формирует список уточняющих вопросов на основе резюме.

Итоговый балл считается обычным кодом, LLM не занимается математикой и не определяет пороги. Уровни критериев всегда дают одинаковый результат, а инструменты (tools) для выполнения внешних команд модели вовсе не требуются.

Где начались неожиданности

PDF — это не всегда чистый текст. Красивое резюме может извлекаться в странном порядке, таблица навыков иногда перемешивается с опытом, а сканированные документы вообще не содержат текстового слоя. Поэтому я сохраняю извлеченный текст, чтобы самостоятельно проверять, что именно уходит на вход модели.

Таблица сравнения результатов по кандидатам
Итоговое сопоставление претендентов по уровню соответствия вакансии.

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

Запуск многократных тестов стабильности
Повторный запуск пайплайна для проверки повторяемости результатов.
Метрики производительности и времени ответа
Сбор метрик задержки для каждого обработанного файла.

Еще модель иногда переформулирует доказательства, из-за чего простая проверка подстроки может счесть цитату неподтвержденной. Можно перейти к нечеткому сравнению, но для начала лучше оставить строгий режим: лучше получить лишнее предупреждение, чем принять сгенерированную цитату за чистую монету.

Порог в 75 баллов не является универсальной истиной, реальные пороги нужно подбирать на размеченном наборе резюме для каждой конкретной вакансии. Резюме не описывает человека полностью, поэтому отсутствие упоминания технологии не означает, что специалист с ней незнаком.

Описанный сервис не отправляет отказы, не пишет кандидатам, не назначает встречи и не принимает финальных решений о найме. Моя автоматизация лишь помогает быстрее добраться до сильных кандидатов, оставляя человека главным принимающим лицом.

Реклама
01.