Введение: почему мы решили искать альтернативу Claude Sonnet
Наша задача - структурирование вакансий. На вход подаётся сырой текст с HeadHunter, LinkedIn или корпоративного сайта, на выходе - строгий JSON с полями: должность, зарплатная вилка, требования, условия. Объём - несколько тысяч запросов в месяц. Первая версия пайплайна работала на Claude Sonnet 3.5 через API Anthropic. Качество было близко к эталонному: модель чётко следовала схеме, не путала валюты, корректно разделяла годовой и месячный доход. Проблема - счёт. При среднем объёме в 5000 вакансий и расходе ~40 000 токенов на один запрос (промпт плюс ответ) месячный прогноз упирался в $2000. Масштабировать такой пайплайн на 20 000 вакансий означало бюджет в $8000 ежемесячно - неприемлемо для задачи, которая не генерирует прямую выручку, а обслуживает внутренний продукт.
Мы поставили цель: найти модель, которая сохранит приемлемое качество структурирования при радикальном снижении стоимости. Это не была простая замена одной LLM на другую. Потребовалась инженерная работа: методология сравнения, слой защиты от типовых ошибок дешёвых моделей, пересмотр контракта с провайдером. Итог: переход на Qwen 3.5 Flash через OpenRouter сократил расходы до $30 в месяц. В этой статье - полный разбор процесса, от эвала до продакшен-решений.
Методология выбора: как мы сравнивали модели без иллюзий
Базовое правило: бенчмарки общего назначения вроде MMLU или HumanEval не предсказывают качество на узкой задаче. Мы собрали собственный датасет из 50 реальных вакансий, покрывающих основные паттерны: IT-вакансии с вилкой в рублях и долларах, позиции с бонусами, вакансии без указания зарплаты, описания с избыточной информацией о компании. Для каждой вакансии вручную подготовили эталонный структурированный JSON.
Кандидатов было пять: Claude Sonnet (базовый уровень), GPT-4o mini, Gemini 1.5 Flash, Qwen 2.5 72B и Qwen 3.5 Flash. Каждая модель получала идентичный промпт с JSON-схемой и инструкцией. Ответы прогонялись через слепого судью - Claude Opus, который не знал, какая модель сгенерировала конкретный результат. Opus оценивал три параметра: точность заполнения полей (совпадение с эталоном), полнота (все ли значимые атрибуты извлечены), соответствие схеме (валидный JSON, корректные типы).
Почему 50 примеров достаточно для первого решения
Выборка из 50 вакансий - компромисс между скоростью принятия решения и статистической значимостью. Мы не проводили академическое исследование, нам нужен был рабочий инструмент для выбора модели за 2-3 дня. Разнообразие вакансий в выборке перекрывало 90% типовых кейсов из нашего потока. Для задач с более высокой вариативностью входных данных размер выборки стоит увеличить до 100-200 примеров. Но для первого раунда отсева 50 хватило: разрыв в качестве между лидерами и аутсайдерами был очевиден уже после 30 примеров.
Слепой судья Opus: почему мы доверили оценку другой LLM
Использование LLM в роли оценщика вызывает скепсис. Мы провели предварительную калибровку: три инженера независимо оценили 20 ответов, затем сравнили их оценки с вердиктом Opus. Корреляция составила 0.89 - достаточно для практических целей. Промпт для судьи требовал попарного сравнения ответа модели с эталоном по каждому полю, с жёстким требованием аргументировать любое расхождение. Opus склонен к излишней строгости в оценке креативных формулировок, но для задачи с жёсткой схемой этот недостаток не критичен.
Результаты эвала:
| Модель | Точность полей | Полнота | Соответствие схеме | Цена за 1M токенов (вход/выход) |
|---|---|---|---|---|
| Claude Sonnet 3.5 | 94% | 91% | 99% | $3 / $15 |
| GPT-4o mini | 89% | 87% | 96% | $0.15 / $0.6 |
| Gemini 1.5 Flash | 85% | 82% | 93% | $0.075 / $0.3 |
| Qwen 2.5 72B | 88% | 85% | 91% | $0.9 / $0.9 |
| Qwen 3.5 Flash | 87% | 84% | 90% | $0.02 / $0.06 |
Qwen 3.5 Flash показала точность на 7 процентных пунктов ниже Sonnet, но при этом стоила в 150 раз дешевле по входным токенам и в 250 раз - по выходным. Разрыв в качестве был достаточно мал, чтобы попробовать компенсировать его инженерными решениями. Мы выбрали Qwen 3.5 Flash.
Скрытые грабли дешёвых моделей: что пошло не так после миграции
Первая неделя в продакшене принесла отрезвление. Эвал на 50 примерах не выявил целый класс проблем, которые проявились на реальном потоке из тысяч запросов. Дешёвые модели страдают от слабой выравненности (alignment) и нестабильного следования инструкциям. Вот что мы обнаружили в логах.
Пустые ответы при статусе 200: когда API врёт
Самый опасный режим отказа. Модель возвращает HTTP 200 и пустое тело ответа - ни символа. Наш пайплайн, рассчитанный на то, что 200 означает успех, пытался распарсить пустую строку как JSON и падал с невнятной ошибкой. Частота: примерно 3 запроса из 100. Причина не в сетевых проблемах, а в отказе модели генерировать ответ - вероятно, срабатывают внутренние фильтры безопасности на некоторые формулировки в вакансиях. Решение: проверка длины ответа до парсинга JSON, при пустом ответе - немедленный ретрай.
Опечатки в полях схемы: почему строгая типизация обязательна
Модель выдавала валидный JSON, но с ошибками в названиях ключей: salary вместо salary, expirience вместо experience, requirments вместо requirements. Частота: 8-10% ответов содержали хотя бы одну опечатку в ключах. Наш downstream-сервис, ожидавший строго определённые поля, молча игнорировал такие ключи, и в базу попадали записи с пустыми полями. Решение: слой fuzzy matching на стороне постпроцессинга - расстояние Левенштейна между фактическим ключом и ожидаемым, с порогом в 2 символа. Плюс явный маппинг для частых ошибок.
Путаница годовых и месячных зарплат: бизнес-логика под угрозой
Семантическая ошибка, которая чуть не исказила аналитику по рынку труда. Модель извлекала из текста «от 200 000 рублей» и помечала это как месячную зарплату, хотя в контексте вакансии речь шла о годовом доходе. Выявили по статистическим выбросам: медианная зарплата по выборке внезапно выросла в 12 раз. Решение: эвристика - если числовое значение зарплаты превышает 500 000 рублей, принудительно проверяем контекст на наличие маркеров «годовая», «в год», «annual». При неоднозначности - помечаем поле флагом salary_period_ambiguous: true и отправляем на ручную верификацию.
Проблемы на стороне агрегатора: OpenRouter и его сюрпризы
Мы использовали OpenRouter как единый шлюз к разным провайдерам моделей. Удобно: один API-ключ, единый биллинг, fallback между провайдерами. Но агрегатор добавляет свой слой нестабильности.
Маскировка мёртвого ключа под 502/529: как мы теряли часы на дебаг
Ситуация: наш API-ключ OpenRouter был случайно отозван из-за сбоя в биллинге на их стороне. Вместо HTTP 401 (Unauthorized) или 403 (Forbidden) мы получали 502 Bad Gateway и 529 Too Many Requests. Логика была такой: 502 - проблема на стороне провайдера, ретрай с экспоненциальной задержкой. 529 - превышение лимита, увеличиваем задержку. Мы потратили четыре часа, пытаясь диагностировать несуществующую проблему с сетью, пока не проверили ключ напрямую через curl. Решение: на старте каждого воркера выполнять проверочный запрос к API с минимальным количеством токенов. Если возвращается 401 или 403 - алерт и остановка пула. Отдельный мониторинг кодов ответа с группировкой по провайдерам.
Биллинг: когда счёт приходит не вовремя
OpenRouter использует постоплатную модель с пополнением баланса. Задержка в отображении расходов достигала 48 часов. Мы настроили дневной лимит в $5, но из-за лага в биллинге фактический расход за выходные перевалил за $20 - система не успела заблокировать ключ. Решение: дублировать учёт расходов на своей стороне через подсчёт токенов в ответах API. Настроить алерт при достижении 80% дневного лимита по собственным метрикам, а не по данным OpenRouter.
Архитектурные решения для надёжной интеграции дешёвых LLM
После двух недель сбора ошибок мы спроектировали защитный слой, который компенсирует нестабильность Qwen 3.5 Flash. Три компонента работают последовательно в постпроцессинге.
Слой коэрции данных: последний рубеж обороны
Коэрция - это принудительное приведение ответа модели к ожидаемой схеме, даже если модель ошиблась в типах или названиях полей. Логика работы:
- Парсинг JSON с обработкой исключений. Если ответ не парсится - попытка извлечь JSON из текста через регулярное выражение.
- Fuzzy matching ключей: для каждого ожидаемого ключа ищем ближайший фактический ключ с расстоянием Левенштейна не более 2.
- Приведение типов: строку "50000" превращаем в число 50000, null в обязательном поле заменяем на дефолтное значение.
- Нормализация зарплат: если поле salary содержит строку "50 000 руб.", извлекаем число и валюту, приводим к единому формату {value: 50000, currency: "RUB"}.
Этот слой не исправляет семантические ошибки вроде путаницы год/месяц, но устраняет 70% синтаксических проблем.
Ретраи с умом: когда и как переспрашивать модель
Стратегия повторных запросов напрямую влияет на стоимость. Мы настроили три условия для ретрая:
- Пустой ответ или ответ не является JSON.
- Ошибка валидации после коэрции: отсутствуют обязательные поля, типы не приводятся.
- Сработала эвристика подозрительной зарплаты (значение > 1 000 000 рублей без маркера «годовая»).
Максимум 3 попытки с задержкой 1, 3 и 9 секунд. На каждом ретрае немного варьируем промпт: добавляем «Убедись, что зарплата указана в месяц, если не сказано иное» или «Проверь названия полей JSON по схеме». После трёх неудач - отказ. Среднее количество ретраев на 1000 запросов: 120 первых попыток, 40 вторых, 15 третьих. Дополнительные расходы на ретраи - около 15% от базовой стоимости, что всё ещё укладывается в $30/мес.
Fail-closed: почему лучше отказать, чем навредить
Для действий от имени пользователя или данных, которые попадают в публичный доступ, стратегия «пропустить как есть» неприемлема. Если после трёх ретраев ответ не прошёл валидацию, мы не публикуем частично структурированную вакансию. Запись помечается флагом status: "failed" и попадает в очередь на ручную обработку. Это создаёт операционную нагрузку на команду, но предотвращает показ пользователям вакансий с зарплатой $500 000 в месяц из-за ошибки модели. За месяц работы очередь ручной обработки составила 2.3% от общего потока - приемлемый уровень.
Результаты: что мы получили за $30 в месяц
Итоговые метрики после трёх месяцев работы в продакшене:
| Показатель | Claude Sonnet (прогноз) | Qwen 3.5 Flash (факт) |
|---|---|---|
| Месячный бюджет | $2000 | $30 |
| Стоимость обработки одной вакансии | $0.40 | $0.006 |
| Процент успешных структурирований | 98.5% | 95.7% |
| Ошибки в зарплатах (год/месяц) | 0.2% | 1.1% |
| Записи, требующие ручной обработки | 1.5% | 2.3% |
Качество снизилось на 2.8 процентных пункта по успешным структурированиям. Это осознанная плата за снижение стоимости в 66 раз. Для нашего продукта такое соотношение приемлемо: ручная обработка дополнительных 0.8% записей обходится дешевле, чем $1970 экономии. Семантические ошибки в зарплатах выросли с 0.2% до 1.1%, но эвристики и ручная верификация не позволяют им попасть в прод.
Ограничения подхода и когда не стоит повторять этот кейс
Успех миграции обеспечили три фактора, специфичных для нашей задачи. Первый: структурирование - это извлечение информации, а не генерация нового контента. Метрики качества объективны и измеримы. Второй: у нас есть возможность валидации ответа - JSON-схема и проверка типов отсекают большую часть ошибок. Третий: бизнес-логика допускает небольшой процент отказов с передачей на ручную обработку.
Для каких задач дешёвые модели не подойдут:
- Сложные многошаговые рассуждения - модели уровня Qwen 3.5 Flash проваливаются на задачах, требующих цепочки логических выводов из 5+ шагов. Здесь по-прежнему нужны Claude Sonnet или GPT-4o.
- Креативные тексты, где качество оценивается субъективно - маркетинговые описания, слоганы, сценарии. Разрыв с дорогими моделями слишком велик.
- Задачи с жёсткими требованиями к безопасности и отсутствием права на ошибку - медицинские или юридические данные. Fail-closed не спасёт, если цена даже одной ошибки неприемлема.
- Сценарии с длинным контекстом - Qwen 3.5 Flash имеет окно 32K токенов, но качество следования инструкциям падает после 8-10K.
Рынок моделей меняется стремительно. То, что верно для Qwen 3.5 Flash в июле 2026 года, может устареть через месяц. 20B Looping Model уже показывает 10-кратную эффективность обучения, а техники дистилляции вроде A.L.F.R.E.D. позволяют малым моделям обходить гигантов. Принципиальный подход - эвал на своих данных, слой коэрции, мониторинг - остаётся неизменным.
Заключение: дешёвые LLM - это реально, но требует инженерной культуры
Миграция с Claude Sonnet на Qwen 3.5 Flash сократила наши расходы в 66 раз при падении качества на 2.8 п.п. Это не drop-in замена, а системная инженерная работа. Ключевые выводы:
- Эвал на собственных данных с чёткими метриками - единственный способ выбрать модель. Бенчмарки общего назначения не релевантны для узких задач.
- Слой коэрции данных обязателен. Дешёвые модели ошибаются в типах, названиях полей и форматах, и это нужно исправлять автоматически.
- Ретраи на уровне бизнес-логики с вариацией промпта повышают итоговое качество ценой 15% дополнительных токенов.
- Fail-closed для критичных данных: лучше отправить запись на ручную обработку, чем показать пользователю неверную информацию.
- Агрегаторы вроде OpenRouter экономят время на интеграции, но добавляют риски: маскировка ошибок аутентификации, задержки биллинга. Нужен собственный мониторинг.
Тестируйте модели на своих данных. Не бойтесь мигрировать, если цифры сходятся. Но закладывайте инженерный ресурс на защитный слой - он окупается. Оптимизация расходов на LLM в продакшене возможна, и этот кейс - прямое тому подтверждение.