Моё приложение пишет сопроводительные письма к вакансиям на hh либо локальной моделью через Ollama, либо облачным API. Акцент в разработке я хотел сделать на локальную модель, потому что это просто-напросто бесплатно. Один вопрос портит красивую картину: а маленькая локальная модель вообще умеет писать так, чтобы не стыдно отправить работодателю?

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

Как я мерил

Метрику взял максимально приземлённую: что бы человек сделал с письмом перед отправкой работодателю. Три корзины по нарастанию возни:

  • «как есть»: слал бы не глядя;

  • «мелкая правка»: поменять пару слов, подчистить обращение, и годится;

  • «переписать»: проще снести и написать заново.

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

отправляемо, % = (как есть + мелкая правка) ÷ N × 100

Если под каждое письмо надо садиться и переписывать с нуля, автогенерация работу не сделала.

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

И сразу оговорюсь, чтобы не выдавать это за науку: один оценщик, малые выборки - зчистый энтузиазм, а не диссертация.

Почему именно эти модели

Локальные модели я отбирал по одному жёсткому критерию: они должны запускаться на обычном ноутбуке, без дискретной видеокарты. Для Ollama это небольшие квантованные веса, которые целиком влезают в оперативку. Из того, что при этом сносно пишет по-русски, лесенка вышла короткая: семейства qwen и gemma в размерах от долей миллиарда до 4B параметров. Совсем крохотные (0.8B) интересовали меня как нижняя граница «запустится вообще на чём угодно», 4B как верх того, что комфортно живёт на слабой машине.

Облачные модели (сначала llama-70b на Groq, позже GigaChat и Claude Opus 4.8) я держал не кандидатами на дефолт приложения, а верхней планкой, с которой сравнивать локалки. GigaChat отдельно интересен тем, что бесплатен для России и не требует VPN, а Opus это уже честный фронтир, потолок качества как таковой.

Плохо у всех

Первый прогон. Три претендента: qwen2.5:3b (маленькая локалка), qwen2.5:7b (побольше) и llama-3.3-70b на Groq в роли облачного потолка. Десять пар «вакансия и резюме», вакансии живые, прямо из моей рабочей базы hh. Открываю ключ, свожу цифры и не верю глазам.

Модель

Отправляемо

qwen2.5:3b (локаль)

20%

qwen2.5:7b (локаль)

30%

llama-3.3-70b (облако)

25%

Один общий комментарии: «Давал поблажки твоим письмам, сам бы лучше написал».

Облако. 25 процентов. Мой хвалёный потолок писал ровно так же посредственно, как всё остальное, а местами и хуже локальной 7b.

Пошёл смотреть, на чём письма сыпались, в ожидании увидеть «маленькая модель тупит». А дефекты у всех оказались одинаковые: незаполненные заглушки [Ваше имя] и [Компания] прямо в тексте, выдуманный опыт (модель уверенно приписывала кандидату фрезеровку, которой в резюме нет), кривые обращения непонятно к кому. Вишенка: китайские иероглифы посреди русского письма. И кластеризовались они не на «глупой маленькой локалке», а на облачной llama-70b, четыре письма из десяти. У 3b такое вылезло ровно раз.

Промпт я со счетов не сбрасывал, важность инструкции понимал. Но ставку делал на модель: дай железу мощности, и текст сам подтянется. Поэтому промпт держал общим и сухим, лишь бы работал, а силы вкладывал в выбор модели. Прогон показал, что приоритеты я расставил задом наперёд. Мой сухой промпт (к тому же написанный по-английски с просьбой отвечать по-русски, отсюда и дрейф в иероглифы, и без единого запрета на заглушки с подписью) ровно тянул вниз всех троих. Большая модель просто аккуратнее воспроизводила ту же серость, вот и вся разница. Слабыми у меня были не модели, а инструкция, которую я им дал.

Охота за промптом

Раз главный подозреваемый промпт, надо было перестать гадать и мерить его всерьёз. Я собрал отдельный стенд: набор реалистичных вакансий, собранных по настоящим объявлениям с hh, и резюме под них (белые воротнички: IT, финансы, маркетинг, продажи), фиксированная temperature = 0 ради воспроизводимости, и две метрики вместо одной. Локальный набор заодно обновил на более свежие мелкие модели, gemma 3 4B и qwen 3.5, всё той же логики «влезает в ноутбук».

Первая метрика механическая, её считает скрипт: объективный брак, который отправлять нельзя ни при каком вкусе. Шесть ворот на письмо: пусто, не по-русски, письмо не от кандидата (модель иногда пишет так, будто это HR отвечает соискателю), скобочные заглушки, хвост-подпись, иероглифы. Вторая метрика человеческая: та самая отправляемость вслепую.

Первый же замер отрезвил. Боевой промпт, тот самый переписанный по-русски, механически чистый на 79%. А по-человечески друг не отправил бы ни одного письма. Ноль из шести. Механика ловит мусор, но не ловит скуку.

И вот тут друг, отсматривая письма пачками, сформулировал то, что я сам не додумал. Письмо не читают построчно. Его просматривают по диагонали за несколько секунд: взгляд скачет по началам предложений и по цифрам, остальное пропускается. Если беглый взгляд раз за разом попадает в клише без конкретики, письмо кончилось, дальше его никто не читает. Значит, оптимизировать надо не «качество текста» вообще, а плотность крючков в первых словах фразы. Отсюда и метрика «отправил бы»: гладких пустых писем модель штампует сколько угодно.

Дальше началась собственно охота. Десяток версий промпта, и почти каждая научила чему-то, чего я не ждал.

Чёрный список фраз бьёт по своим. Логичный ход: перечислить в промпте штампы и запретить их дословно. Я так и сделал. Слабая модель (0.8B) после этого выдала запрещённую фразу в семи письмах из восьми. Назвать фразу означает подсказать её. Правила пришлось переписать структурно: не «не пиши вот это», а «начинай предложение с глагола действия».

Пример в промпте копируется как содержание. В следующей версии я дал живой образец первой строки: «Например: „Пять лет пишу на Java в финтехе…"». Модель поняла буквально. В тринадцати открывашках из шестнадцати письмо начиналось со слов «Пять лет», а на вакансию маркетолога gemma честно приписала кандидату пять лет разработки платёжного бэкенда. Иллюстрация в промпте сама породила выдумку, ровно ту, что промпт в соседнем абзаце запрещал. Пример заменил схемой со слотами, без готового текста.

У модели есть тики, которых в промпте нет. Конструкция «X, а не Y» всплывала в письмах десятками раз. В промпте её не было ни разу, это собственная привычка модели. Друг отметил её как раздражающую уже на третьем письме. Против такого промпт бессилен напрямую, можно только не подкидывать модели поводов.

К десятой версии картина устоялась. Промпт объясняет модели, как письмо читают по диагонали, требует первую строку-факт (срок опыта, роль, цифра), просит короткое живое приветствие вместо казённого «Уважаемый HR-менеджер», и держит отдельный абзац попадания: взять задачу из вакансии, показать, где кандидат уже решал такую, назвать, что он возьмёт на себя здесь. Вот что происходило с механикой по дороге, три локальные модели, 24 письма на версию:

промпт

механически чистых

v1 (англоязычный)

25%

v2 (боевой на старте)

79%

v4 (позитивные примеры)

100%

v10 (финал)

100%

Механический потолок берётся уже к четвёртой версии. Дальше скрипт версии не различает, тут нужен человек. А человек на слепой оценке показал главное: боевой промпт v2 это 0% отправляемых, финальный v10 это около 88%. Одна и та же модель, одни и те же вакансии, температура ноль. Разница целиком в тексте инструкции.

А как же ATS-фильтры?

Тут въедливый читатель меня перебьёт: письма читают не только люди. На той стороне часто стоит ATS, робот, который парсит отклики и ранжирует их по ключевым словам. Не надо ли напихать в письмо термины прямо из вакансии, чтобы пройти этот фильтр?

Я проверил и это. Сначала матчасть. ATS (в России это Huntflow, Talantix, за рубежом Greenhouse, Workday и подобные) сопроводительные действительно парсят и подмешивают их текст в поиск рекрутёра по ключевикам. Но первичный отсев почти всегда идёт по резюме, письмо тут вторично. И совпадение робот ищет буквальное: точное слово, а не синоним.

Звучит как руководство к действию, и я вписал в промпт правило: называй навык ровно тем словом, что стоит в вакансии. Результат меня уже не удивил. Модель принялась штамповать «мои навыки и опыт полностью соответствуют требованиям вакансии» и прочий канцелярит под робота, а на слепой оценке эта версия просела. Живой человек такие фразы бракует мгновенно. Ровно тот же эффект, что с чёрным списком: явную инструкцию модель понимает слишком буквально и ломает себе живость.

Разрешилось всё просто. Главное, что нужно ATS от письма, нормальное письмо и так даёт: чистый текст без таблиц и картинок, точное название должности, лексика вакансии там, где резюме её честно подтверждает. Конкретное письмо несёт это само собой, без набивки ключевиков. А keyword-stuffing роботу помогает на копейку, а живого читателя отталкивает сразу. Так что правило я выкинул, а вывод оставил: пиши под человека, робот будет доволен как побочный эффект.

Обратно к моделям, на хорошем промпте

С нормальным промптом можно наконец честно сравнить модели. Взял финальный v10 и прогнал через четыре яруса на свежих вакансиях, которых друг раньше не видел: локальная gemma 3 4B, локальная qwen 3.5 4B, облачный GigaChat-2-Pro и фронтир Claude Opus 4.8. По два письма с модели, вслепую.

модель

ярус

отправляемо

как читается

Claude Opus 4.8

фронтир

100%

«письмо дышит, будто поток мыслей живого человека»

GigaChat-2-Pro

облако РФ

100%

«есть энергетика, затягивает живостью»

gemma 3 4B

локаль (ноутбук)

100%

«нормальное, но суховатое»

qwen 3.5 4B

локаль (ноутбук)

50%

один раз сорвался в грамматику и в письмо от лица рекрутёра

Промпт закрывает пол. Семь писем из восьми отправимы даже на локальных 4B, которые крутятся на обычном ноутбуке. Тот же v10, что вытащил боевой промпт с нуля, доводит до «отправил бы» на любом железе. А модель поднимает потолок. Разницу человек всё-таки чувствует, но не в браке, а в живости: фронтир и сильное облако «дышат», локальные 4B аккуратные, но суховатые. Цитаты друга в таблице выше это ровно та грань, которую цифра «100% отправимо» не показывает.

Практический смысл для приложения простой. На слабом железе локальная 4B уже даёт письма, которые не стыдно отправить. Кому нужна искра, а поднимать VPN неохота, тому GigaChat-2-Pro вплотную подходит к фронтиру и стоит ноль рублей.

А отличит ли человек робота?

Раз письма читаются живо, возник спортивный вопрос: а поймает ли человек подделку? Я собрал тест Тьюринга. Восемь генераций v10 против восьми настоящих человеческих писем.

С человеческой половиной пришлось повозиться честно. Брать сегодняшние примеры из интернета нельзя: любой текст после конца 2022 года мог быть написан нейросетью, и тогда я сравнивал бы робота с роботом. Поэтому я взял письма из карьерных статей, опубликованных до появления ChatGPT, и дату каждого источника проверил по снимку в веб-архиве: убедился, что текст лежал там ещё в 2021-м и не переписан задним числом. Обе половины привёл к одному формату, чтобы подпись или её отсутствие не выдавали автора.

Друг принял шесть генераций из восьми за письма живых людей. А единственное настоящее письмо, которое он записал в «машину», оказалось корпоративным шаблоном из статьи-совета. Болванка остаётся болванкой, кто бы её ни набирал.

Что в итоге

Три захода, и картина сложилась такая.

По железу: есть видеокарта или мощный процессор, ставь локальную 4B, качество вровень с облаком по отправляемости, бесплатно и приватно. Железо слабое, а искра нужна, бери GigaChat-2-Pro. Совсем крохотные модели вроде 0.8B в дефолт не годятся: на длинном промпте теряют нить, путают род, обрывают письмо на полуслове.

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

Оговорки держу те же: один оценщик, малые выборки, облачные модели не строго детерминированы даже при нулевой температуре.

Причём тут спам

Сейчас кто-нибудь в комментариях напишет, что я штампую спам и заваливаю HR мусором. Возражение честное, отвечу так же. Вся вторая половина статьи про то, как я гонял промпт, чтобы модель не выдумывала кандидату опыт и не лепила отписки. Инструмент, который прямым текстом запрещает модели врать про человека, это не спам-пушка. Встроенный генератор писем hh я обгонять и не пытался: он и так раздаётся бесплатно в PRO (Только купи подписку PRO). Отклики упираются во встроенные лимиты платформы, а не льются потоком.

Приложение Otklik. Оно само парсит вакансии по фильтру на hh, пишет под каждую сопроводительное на основе вашего резюме, а с включённым автооткликом - откликается. Открытое, лежит на гитхабе, ставится локально.