Алексей Маркин рабочий журнал

ищет по заголовкам и тексту записей и заметок

запись 3 сентября 2026 г. · 6 мин
Все статьи

Метрики поиска без магии: почему recall@k врёт на уровне документов

Метрика растёт, ответы не улучшаются. Разбираю, почему считать надо по кускам, и показываю на измерении собственного корпуса, что делает с текстом фиксированная нарезка.

Знакомая картина: recall@5 подняли с 0.71 до 0.86, отчёт красивый, а пользователи по-прежнему жалуются, что система «не находит очевидного».

Обычно в этот момент начинают менять модель эмбеддингов. Почти всегда зря: метрика измеряет не то, от чего зависит ответ.

Что такое recall@k на самом деле

Определение простое. Берём набор вопросов, для каждого заранее помечено, что является правильным источником. Смотрим первые k результатов выдачи и считаем долю вопросов, для которых правильный источник туда попал.

Вся сложность прячется в словах «правильный источник». Обычно им объявляют документ — файл, страницу, статью. Так размечать быстрее: разметчику достаточно сказать «ответ в регламенте по возвратам».

И вот здесь метрика отрывается от реальности.

Найден документ — не значит найден ответ

Модель не читает документ. Она читает кусок, который ей принёс ретривер. Между «нужный документ попал в выдачу» и «в контекст приехал абзац с ответом» лежит целый шаг, и метрика на уровне документов его не видит.

Разница проявляется в двух направлениях сразу.

Ложный успех. Регламент найден, но в контекст уехал кусок из раздела про оформление, а не про отмену. Recall@5 засчитан, ответа нет. Чем длиннее документы, тем чаще: в стостраничном регламенте попадание «в документ» почти ничего не гарантирует.

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

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

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

Измерение: что фиксированная нарезка делает с текстом

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

Материал: тексты этого сайта. Двадцать девять материалов, 103 695 знаков, 99 смысловых разделов, границы которых расставил автор, а не алгоритм.

Сначала о том, какого размера вообще бывает смысл:

ПоказательЗначение
Разделов всего99
Медианная длина раздела641 знак
Средняя длина756 знаков
Самый короткий205 знаков
Самый длинный4389 знаков

Медиана — 641 знак. Это и есть естественная единица смысла в таком тексте: столько занимает одна законченная мысль с примером.

Теперь режем фиксированными кусками по 500 знаков, как советуют в большинстве руководств, и смотрим, что получилось:

Что измерялиРезультат
Кусков получилось220
Не содержат ни одного заголовка119 (54%)
Начинаются не с начала предложения186 (85%)
Смысловых разделов разорвано между кусками94 из 99 (95%)
ПО СТРУКТУРЕ## Как отменить заказОтмена возможна до передачи……средства возвращаются за 3 дня.## Что делать при бракеБрак фиксируется актом……замена в течение недели.кусок = мысль целикомтема названа в самом кускеФИКСИРОВАННО, 500 ЗНАКОВ## Как отменить заказОтмена возможна до передачи…граница по счётчику знаков…средства возвращаются за 3 дня.## Что делать при бракеответ оторван от вопроса54% кусков вообще без заголовка
рис. 1 Одна и та же страница в двух нарезках. Слева граница куска проходит там, где её поставил автор. Справа — там, где закончились 500 знаков.

Цифра 95% — главная. Практически каждый раздел, написанный как единая мысль, оказывается разрезанным: начало в одном куске, конец в другом. Ретривер приносит половину ответа, модель достраивает вторую половину сама, и получается тот самый правдоподобный ответ, который невозможно отличить от верного.

Число 54% не менее важно, хотя выглядит скромнее. Больше половины кусков не содержат ни одного заголовка — то есть в них нет ни слова о том, к какой теме они относятся. Для векторного поиска это значит, что кусок представлен только собственной лексикой, без контекста; для модели, которая потом читает найденное, — что она видит абзац, не зная, откуда он.

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

Что делать

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

Приклеивать путь заголовков к куску. К тексту раздела добавляется строка вида «Регламент возвратов → Отмена заказа → После оплаты». Десять строк кода, и 54% кусков без темы превращаются в ноль.

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

Считать recall по кускам. Иначе всё вышесказанное невозможно проверить: метрика на документах покажет одинаковые числа до и после.

Условие несогласия

Если ваши документы сами по себе короткие — карточки товаров, ответы на частые вопросы, записи справочника, — вся эта работа не нужна. Документ и есть кусок, разметка на уровне документа корректна, и recall@k измеряет ровно то, что надо.

Граница проходит там, где документ перестаёт быть об одной вещи. Признак простой: если к документу можно задать два вопроса с разными ответами, его надо резать и размечать по кускам.

Чего я делать не стал бы

Не стал бы подбирать размер куска экспериментально, пока нарезка идёт по счётчику знаков. Вы будете искать оптимум между «рвёт мысли» и «тащит лишнее», хотя оба недостатка происходят от одного и того же решения резать не там.

Не стал бы гнаться за recall@20. Большое k почти всегда даёт красивую метрику и худший ответ: в контекст приезжает двадцать кусков, из которых девятнадцать не по делу, а модель, как известно, теряет середину.

Не стал бы менять модель эмбеддингов, не посмотрев глазами на десяток кусков, которые она вернула. В половине случаев видно сразу: модель нашла ровно то, о чём просили, — просто в найденном куске нет ответа.

Открытый вопрос

Нарезка по структуре опирается на то, что структура есть. В аккуратной документации она есть; в переписке, в расшифровках созвонов, в выгрузке из старой системы — нет, и разметить её автоматически я пока не умею. Модель, которую просят расставить границы разделов, справляется прилично, но это ещё один вызов на каждый документ при загрузке — и я не знаю, окупается ли он, потому что честно сравнить не с чем: эталонной нарезки для такого текста тоже никто не размечал.