Схему retrieve → rerank не отвергать, но переставить приоритеты. По файлам видно, что retrieval уже даёт тематически чистый верх (в top-20 нет ни одного NO, 15 из 20 прямо про WordPress). Главная потеря не там: 11 из 20 кандидатов остались UNCERTAIN, и готовый cross-encoder эту долю не уменьшит, потому что он обучен на релевантности «запрос–пассаж», а не на обязательных требованиях и отрицаниях.
Сначала починить измерение, потом выбирать модель. Текущий baseline сравнения не ранжирует по тегам, а фактически выдаёт 20 самых свежих вакансий; результат «9 FIT против 1» ничего не говорит о пользе семантики. Все 40 оценок поставил автор прогона, и внутри них есть расхождения в одинаковых случаях.
Рекомендуемый путь: независимый гибрид BM25 + локальный dense по всему допустимому корпусу как кандидатная лента; разметка владельцем профиля (не агентом) в трёх состояниях «откликнусь / уточню / нет»; закрытый набор «блокеров» для данной L2 как отдельный узкий классификатор на уровне предложений; состояния профиля «не умею / не указывал / не хочу» из онбординга. Reranker подключать только если эксперимент покажет, что узкое место именно retrieval.
Не «продолжить»: предложение §8 отвечает на вопрос «как лучше ранжировать похожие тексты», а нерешённый вопрос проекта (§9, первый пункт) звучит иначе: «как отличить тематическую релевантность от соответствия обязательным требованиям». Cross-encoder повышает первое и почти не трогает второе. Подключать его сейчас значит оптимизировать ту часть, где по собственным данным проекта потерь меньше всего.
Не «заменить»: инфраструктура прогона сделана аккуратно: параметры и shortlist заморожены до просмотра, цитаты проверяются побайтно, контроль не читался, ограничения перечислены честно. Эту дисциплину надо сохранить. Меняется состав следующего шага и источник разметки, а не вся линия.
Что меняется: (1) baseline и временные окна в протоколе сравнения; (2) разметчик: владелец профиля вместо агента; (3) цель следующей модели: не «лучше ранжировать», а «найти предложения-блокеры из закрытого списка»; (4) часть неопределённости переносится из модели в профиль через явные состояния.
Все факты ниже проверены по загруженным файлам. Строки с пометкой «не проверено» означают, что файл в среде аудита отсутствовал.
| # | Факт из файла | Влияние | Исправление |
|---|---|---|---|
| 1 | Baseline не ранжирует по тегам. run.py: overlap = |SKILLS ∩ tags| / |tags|. В baseline.jsonl все 200 строк имеют tag_overlap = 1.0; в top-20 у 12 вакансий ровно один тег (JavaScript, HTML5, WordPress). Поэтому порядок задаёт только свежесть: baseline top-20 — это 20 самых новых вакансий от 3–5 октября, у которых все теги попали в список из 14 навыков. | критично Сравнение «9 FIT против 1» измеряет не семантику против тегов, а «лучшее совпадение за месяц» против «самое свежее за два дня». Вывод REPORT.md о превосходстве метода не обоснован этим сравнением. | Baseline должен быть сильным текстовым: BM25 по тем же шести запросам (без эмбеддингов). Если нужен тег-baseline, нормировать на число навыков профиля и требовать ≥2 совпадений. |
| 2 | Разные временные окна. Даты top-20 кандидатов: 7 сентября — 1 октября (ни одной за 2–5 октября). Даты top-20 baseline: 3–5 октября. У вакансии C10 дедлайн «September 12» уже прошёл на момент прогона. | критично Месячный top-20 не моделирует ежедневную ленту; в реальной выдаче пользователь получит устаревшие вакансии. Протокол сам это признаёт (§9), но сравнение всё равно сделано на разных окнах. | Оценивать по окнам: 4 недельных среза корпуса, top-10 на срез для каждой системы. Свежесть должна входить в выдачу как окно, а не как разрешение равенств. |
| 3 | Единственный разметчик — агент, и метки внутри себя расходятся. review.py: reviewer = assistant_internal_author, метки захардкожены списком. C3 и C7 («contact form not sending») = FIT, но C12 (та же задача, в тексте есть слово «SMTP») и B1 («WooCommerce emails not sending») = UNCERTAIN. C18 (Figma → WordPress, 7 страниц, готовый дизайн) = UNCERTAIN из-за строки «SEO plugin configuration». C4 («Keep ElasticSuite», модуль только для Magento, слова WordPress нет) = UNCERTAIN, хотя это чужая платформа. | важно Счёт FIT при той же выдаче колеблется от 8 до 12 в зависимости от строгости; доля UNCERTAIN завышена осторожностью разметчика, а не свойствами вакансий. Разметка не годится как ground truth даже для внутреннего решения. | Разметчик — владелец реального профиля (§4), вслепую, в случайном порядке, тремя метками «откликнусь / уточню (что именно) / нет (причина)». Метки агента оставить вторым проходом и считать согласие. |
| 4 | Семантика ограничена лексическим shortlist и фактически не влияет на состав. Pool = 1 048 из 8 753 допустимых. В pool 265 вакансий с флагами, из них 237 — other_platform (Shopify, Webflow, Wix и т.п.). | важно До 23 % shortlist — вакансии про другие платформы, а WordPress-вакансии вне лексического top-200 по каждому запросу не могут быть найдены ничем. Recall по корпусу неизвестен и этой схемой неизмерим. | Независимый dense-поиск по всем 8 753 (по измеренной скорости прогона 1 048 текстов укладываются в 102 с вместе с индексом; весь допустимый срез — порядка 10–15 мин на той же машине, однократно). Объединять ранги BM25 и dense через RRF до любого shortlist. |
| 5 | Длинные ТЗ усредняются. encode(): чанки по 254 токена без перекрытия, взвешенное среднее. В pool 429 текстов длиннее ~1 000 символов, 117 длиннее 3 000. C5 (3 147 символов) содержит «Senior-Level Experience Required… do not apply» в конце. | важно Критическое условие в хвосте размывается в векторе документа и не влияет на ранг. Это одна из причин, почему UNCERTAIN не ловятся автоматически. | Для совместимости работать на уровне предложений/абзацев, а не документа. Для retrieval можно оставить среднее, но хранить и max-chunk score как диагностический признак. |
| 6 | Regex-флаги одновременно слабы и шумны. В top-20 флагов 0 при 11 UNCERTAIN; в top-100 — 15; в pool — 265, из них 237 срабатывают на одно упоминание платформы. | важно «Нет флага» ничего не значит, что REPORT.md корректно констатирует. Но это также показывает: закрытый список причин несовместимости существует (Divi, ElasticSuite/Magento, SMTP-инфраструктура, senior SQL, дизайн с нуля, SEO как услуга, performance как услуга), просто его нельзя ловить регулярками по упоминанию. | Заменить regex на классификатор предложений с закрытым набором классов-блокеров (см. раздел 4). Упоминание ≠ требование: обучать на предложениях, где слово стоит в позиции требования. |
| 7 | v2.12 обучался на 13 вакансиях. training.json: все головы — 13 jobs, ~200 строк; голова input — 9 положительных, optional — 15, negated — 11. results.json: input даёт 0 TP / 5 FN у tfidf и 1 TP у MiniLM; dev-оценка на 10 документах; интервалы для диагностических голов от −0,6 до +0,5. | важно Именно различие «вход / результат» (§5 задания) нынешними данными не выучивается в принципе. Выводы «улучшение mandatory +13 п.п.» статистически слабы и не переносятся на полный документ. Агент в REPORT.md это сам пишет; но в задании (§6) результат подан мягче, чем он есть. | Не продолжать эту ветку как есть. Либо резко сузить число классов (блокеры), либо добирать сотни документов, что по §3 недоступно. |
| 8 | context-extraction — полноценная онтология. structured-pilot.json: 57 claims на 3 документа; SCHEMA-NOTES.md: связи related нетипизированы, составные объекты не разделены; status.json: 6 блокеров до обучения, модель не выбрана. | отложено верно Это путь к extractor-у общего назначения; он требует сотен размеченных документов и второго разметчика. Решение приостановить правильное. | Переиспользовать только словарь классов требований и уже размеченные предложения как seed для блокер-классификатора. Не завершать 76 разметок. |
| 9 | Запросы — ручные списки ключевых слов для одного профиля. QUERIES в run.py: шесть строк вроде «WordPress custom theme development convert supplied Figma PSD…». Теги профиля SKILLS тоже вручную, с вариантами css 3/css3. | важно Схема не переносится на другой профиль без ручной работы; это противоречит цели «без массового ручного описания». Нормализация тегов ненадёжна (у C2 в тегах React, в тексте нет). | Строить запросы из подтверждённых направлений онбординга автоматически (одно направление = один запрос, текст направления уже есть в profile_positive_text). Теги использовать только как мягкий признак. |
| 10 | Защита контроля: только точные ID/hash. run.py и verify.py читают из защищённого файла лишь job_id и content_hash; near-duplicate не проверяются (сказано в freeze.json/limitations). | принято Для первичного аудита достаточно. Для следующего эксперимента недостаточно: переопубликованные ТЗ на Upwork частое явление. | Перед разметкой исключать near-dup по косинусу MiniLM ≥ 0,95 или MinHash по 5-граммам к защищённым и к 76 документам инвентаря. |
| 11 | Что сделано хорошо (чтобы не потерять): freeze.json пишется до просмотра; verify.py проверяет хеш кода, литеральность цитат, отсутствие контроля, монотонность рангов; REPORT.md прямо отказывается считать 45 % точностью и «нет флага» проверкой. | сохранить | Перенести этот же набор проверок в протокол следующего эксперимента без изменений. |
| 12 | Не проверено: corpus.sqlite (полнота полей, доля WordPress в допустимом срезе), hybrid-embeddings/config.json, review.html (содержимое совпадает с jsonl по коду генерации), compact-matching-v2_3/training-packet/reference.json, закрытый контроль (намеренно). | — Утверждения о recall и о распределении корпуса в этом заключении опираются только на цифры summary.json. | — |
Оценка по четырём осям из задания. «Ручная подготовка» — что нужно от людей до первого результата; «риск» — чем это ломается на ваших данных. Опубликованные характеристики моделей отделены от ожиданий: ни одна из них не измерена на вакансиях.
| Подход | Что решает | Ручная подготовка | Стоимость | Главный риск | Сейчас |
|---|---|---|---|---|---|
| BM25 + dense (MiniLM) по всему срезу, RRF | Retrieval: находит тематически близкое вне лексического top-200, честный baseline | Нет новой разметки; запросы = направления онбординга | Один индекс на всех пользователей; dense для 110 тыс. однократно (часы CPU), далее по приросту | Не отличает «нужно Divi» от «есть Divi»; это и не его задача | делать |
| Cross-encoder rerank (bge-reranker-v2-m3: Apache-2.0, 0,6 B параметров, многоязычный, обучен на query–passage релевантности; лёгкая альтернатива ms-marco-MiniLM-L6-v2: Apache-2.0, 22,7 M, английский, MS MARCO) | Ранжирование внутри shortlist при длинных и расплывчатых запросах | Нет разметки для запуска; нужна разметка, чтобы доказать пользу | Зависит от пары профиль–вакансия: не кешируется на вакансию. Оценка: 1 000 пользователей × 100 пар/день = 100 тыс. пар; 22 M-модель — десятки CPU-минут, 0,6 B — часы CPU или GPU (порядок величины, не измерение) | Поднимает тематически похожее; отрицания в запросе («no React») не гарантируют штрафа. Опубликованные бенчмарки (BEIR, MIRACL) не про требования к исполнителю | после эксперимента |
| Закрытый набор блокеров: классификатор предложений, 8–12 классов для L2 (платформа-назначение, фреймворк обязателен, оплата/доставка, дизайн с нуля, SEO/маркетинг как услуга, builder-специфика Divi/Bricks, серверные/безопасность/БД-операции, email-инфраструктура, дежурство/on-call) | Compatibility: отделяет требование от упоминания, даёт пользователю конкретное «что проверить» | ~300–500 предложений: кандидаты набираются автоматически (слова-триггеры из flags() и v2.12-словарь), человек подтверждает «требование / упоминание / вход»; 2–3 часа | Один прогон на вакансию для всех пользователей одной L2, кешируется | Длинный хвост причин вне списка; проверяется долей NO-причин «другое» в разметке владельца | делать |
| Zero-shot NLI на предложениях (deberta-v3-base-zeroshot-v2.0: MIT, 0,2 B, английский, 512 токенов, гипотеза задаётся текстом) | То же, что блокеры, но без обучения: гипотезы «исполнитель должен владеть Divi» vs «у заказчика есть Divi» | Только формулировки гипотез; разметка для оценки | N предложений × K гипотез на вакансию; кешируется на вакансию | Качество на коротких деловых фразах не опубликовано; английский только; легко переоценить по демо | как запасной вариант к блокерам |
| Learning-to-rank на простых метках (LightGBM LGBMRanker) | Объединяет BM25, dense, свежесть, блокеры, теги в один порядок | Нужны сотни пар с метками «откликнусь/нет» на несколько профилей | Дёшево в inference | На одном профиле переобучится; метки агента нельзя использовать | после 3+ профилей |
| SPLADE / ColBERT | Конкретные потери retrieval на редких терминах или длинных текстах | Нет | Индекс в разы тяжелее dense | Решают проблему, наличие которой ещё не показано | преждевременно |
| Полное извлечение фактов / граф / Two-Tower | Универсальное понимание ТЗ | Сотни документов, два разметчика, типизированные связи | Месяцы | По §3 ресурсов нет; v2.12 показывает, что мелкие роли не выучиваются на малых данных | прекратить |
A + B + E без C и D, где роль блокеров временно играет честно подписанный список «проверьте в ТЗ»: предложения с конструкциями требования (must / required / strong experience with / do not apply if) и термином вне подтверждённых навыков профиля. Это уже лучше нынешних флагов, потому что опирается на позицию требования, а не на упоминание, и не удаляет ничего автоматически. Такой вариант можно показывать как «кандидаты по вашим направлениям» уже сейчас.
Разделение «тематическая релевантность» / «обязательные требования» делается двумя разными механизмами с разными единицами: документ для retrieval, предложение для совместимости. Ручной каталог заменяется коротким закрытым списком классов на L2 (десяток, а не сотни), а остальная неопределённость честно уходит в состояние «уточнить» и в вопрос пользователю, а не в правило.
S0: BM25 (шесть запросов, максимум по направлениям). S1: S0 + dense по всем 8 753, RRF. S2: S1 + bge-reranker-v2-m3 на top-100 каждого направления. Параметры, запросы, версии моделей и хеш кода фиксируются в freeze.json до начала разметки, как в v1.
Разметчик — владелец профиля. Вакансии в случайном порядке, без указания системы и ранга. Три метки: откликнусь / уточню (что именно, одной фразой) / нет (причина из списка классов + «другое»). Оценка объёма: 270–370 вакансий по 20–30 секунд — 2–3 часа, можно в два приёма. Второй проход агентом по тем же вакансиям вслепую; считать согласие (κ) и расхождения публиковать.
Не требует новой разметки. Берутся ≥20 вакансий с меткой «откликнусь»; к каждой добавляется одно предложение-блокер («Strong Divi Theme Builder experience required») либо, в контроле, нейтральное предложение той же длины. Сравнивается сдвиг ранга. Затем обратное: из вакансий «нет» удаляется предложение-причина. Если медианный сдвиг ранга от блокера неотличим от контроля, reranker не чувствителен к обязательным требованиям и в схеме может отвечать только за тематику.
Лента «Кандидаты по вашим направлениям» в недельном или суточном окне, с указанием направления и цитаты, по которой вакансия попала, и блоком «проверьте в ТЗ» с цитатами предложений-требований. Интерфейс пока не должен: показывать процент совпадения или слово «подходит»; обещать, что все требования проверены; молча скрывать вакансии; приписывать пользователю навыки по тегам. Кнопка «не подходит» с выбором причины из закрытого списка начинает копить тот самый шумный сигнал, который потом пригодится для LTR, но сейчас ничего не меняет в ранжировании.
| № | Вопрос | Ответ |
|---|---|---|
| 1 | Верно ли поставлена цель? | Цель «полезная лента, а не предсказание найма» верна. Но в разметке проект уже пытается оценить скрытые способности: UNCERTAIN ставится за SMTP, Core Web Vitals, настройку SEO-плагина, которых в профиле просто нет. Это не задача модели; это вопрос пользователю. Разделить состояния «не умею / не указывал / не хочу» в профиле нужно, и это дешевле любой модели. |
| 2 | Какие выводы обоснованы, а какие чрезмерны? | Обоснованно: retrieval воспроизводим; flags не решают compatibility; классифицировать заданный фрагмент проще, чем найти его. Чрезмерно: «9 FIT против 1» как свидетельство пользы метода (baseline = свежесть); «устойчивый рост MiniLM-голов» (13 обучающих документов, 10 проверочных); разметка 40 оценок как основание для чего-либо кроме демонстрации. |
| 3 | Причины ошибок | Retrieval: неизмерим, shortlist ограничен лексикой, 23 % pool про другие платформы. Ranking: в top-20 тематических ошибок нет; свежесть не учтена. Compatibility: главный источник UNCERTAIN (Divi, ElasticSuite, senior SQL, on-call). Профиль: вторая половина UNCERTAIN (SMTP, SEO-плагин, performance). Разметка: один автор, расхождения в одинаковых случаях. Один энкодер не лечит ни одну из этих причин. |
| 4 | Сравнение альтернатив | Релевантны сейчас: BM25 + dense (retrieval), закрытый набор блокеров (compatibility), zero-shot NLI как запасной вариант. Условно: cross-encoder. Преждевременны: SPLADE/ColBERT, LTR, граф, полное извлечение, Two-Tower. |
| 5 | Готовые модели | Перечислены в разделе 3 с лицензией, размером, языком и назначением по карточкам Hugging Face. Ни у одной нет опубликованных результатов на подборе вакансий; ожидания на ваших данных не заявляются. |
| 6 | Видит ли reranker обязательное неподходящее требование? | Скорее нет: обучен на релевантности query–passage, не на противоречиях с профилем. Проверка без новой разметки описана в разделе 5 (вставка/удаление предложения-блокера и сдвиг ранга). |
| 7 | Минимальная ручная работа | 2–3 часа владельца профиля на 270–370 вакансий; 2–3 часа подтверждения 300–500 предложений для блокеров. Переиспользовать: словарь классов v2.x, уже размеченные предложения, протокол заморозки v1. Обязательно человеком: метки «откликнусь/нет» и подтверждение блокера как требования. Нельзя считать достоверными: метки агента, флаги regex, отсутствие флага. |
| 8 | Один эксперимент | Раздел 5. |
| 9 | Что запускать сейчас | Раздел 6: кандидатная лента с цитатами и блоком «проверьте в ТЗ», без процентов и слова «подходит». |
| 10 | Собственный путь и условия смены | Разделы 4 и 7. |
| Показатель | Значение | Источник |
|---|---|---|
| Строк baseline с tag_overlap = 1.0 | 200 / 200 | baseline.jsonl |
| Вакансий с одним тегом в baseline top-20 | 12 / 20 | baseline.jsonl |
| Диапазон дат, top-20 кандидатов / top-20 baseline | 07.09–01.10 / 03.10–05.10 | candidates.jsonl, baseline.jsonl |
| Упоминание WordPress: top-20 / top-100 / pool | 15 / 76 / 638 | candidates.jsonl |
| Флаги: top-20 / top-100 / pool; из них other_platform | 0 / 15 / 265; 237 | candidates.jsonl |
| Тексты pool длиннее 1 000 / 3 000 символов | 429 / 117 | candidates.jsonl |
| Обучающих вакансий v2.12 (все головы) | 13 | training.json |
| Положительных строк head input / optional / negated | 9 / 15 / 11 | training.json |
| Head input на dev: TP / FN (tfidf) | 0 / 5 | results.json |
| Инвентарь / полный пилот / структурный пилот / claims | 76 / 6 / 3 / 57 | status.json (context-extraction) |