Главная / Статьи / Почему плохой чанкинг ломает векторный поиск даже с хорошими эмбеддингами

Почему плохой чанкинг ломает векторный поиск даже с хорошими эмбеддингами

Сплиттер с фиксированными 800 символами разрезал ключевую фразу из регламента пополам и испортил поиск. Разбираем, как помогают перекрытие и рекурсивный чанкинг и почему важен каждый этап пайплайна.

809 слов

Когда векторный поиск выдаёт чепуху, первыми под подозрение попадают база данных и модель эмбеддингов. Обычно обе ни при чём. В пайплайне загрузки на TypeScript, построенном на разборе PDF, эмбеддингах Gemini и ChromaDB, виновником оказалась одна строчка логики подсчёта символов в чанкере.

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

Что делал первый чанкер

Исходная модель в голове была до неприличия простой:

  1. Извлечь текст из PDF.
  2. Резать его каждые 800 символов.
  3. Получить эмбеддинг для каждого куска.
  4. Загрузить векторы в ChromaDB.

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

  • Чанк 0 заканчивался так: "...the right to decline or d"
  • Чанк 1 начинался так: "e-register students who have not completed fee payments."

"de-register" превратилось в одинокую "d" в одном чанке и "e-register" в следующем. Разорванное слово выглядело некрасиво, но потеря смысла была хуже. В чанке 0 больше не было самого действия. В чанке 1 не осталось ни субъекта, ни условия, которое объясняет, почему это действие происходит.

То есть векторная база вовсе не теряла нужный фрагмент. Смысл был уничтожен ещё до того, как его увидела модель эмбеддингов.

Перекрытие даёт второй шанс

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

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

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

Поиск сразу стал лучше, но сама конструкция оставалась неверной в корне, потому что мыслила сырыми символами, а не языком. Она всё ещё могла разрубить токен или фрагмент кода пополам и оставить вам что-то вроде function calculateT.

Значит, нужно было поменять вопрос, который задаёт себе чанкер:

  • Старый вопрос: где заканчиваются следующие 800 символов?
  • Новый вопрос: где у этого текста самая естественная смысловая граница для разреза?

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

// The Golden Rule of Chunking:
// Preserve meaning at scale; cut aggressively only when forced to.

Каждый этап наследует предыдущий

Рекурсивное разбиение лишь деталь реализации. Главный урок в том, как представление данных каскадом проходит через всю ИИ-систему:

Doc ➔ Extraction ➔ Chunking ➔ Embedding ➔ Storage ➔ Retrieval ➔ LLM ➔ Answer

Каждый узел ниже по потоку ограничен тем, что ему передал узел выше. Если на этапе загрузки контекст разрушен, никакой промпт-инжиниринг и никакая настройка индекса его не вернут. Мусор на входе даёт смысловой мусор на выходе.

Исправил одно, вылезли ещё два

Как только чанкер заработал как надо, сразу за ним показались две новые проблемы.

  • Структура: с абзацами всё просто, а что делать с финансовой таблицей на три страницы или с многоколоночной вёрсткой? При наивном разбиении заголовки столбцов отрываются от значений под ними, и данные теряют всякий смысл.
  • Цена перекрытия: контекст не бесплатен. Больше перекрытия значит больше чанков, а значит больше вызовов API эмбеддингов, больше хранилища и более медленная индексация. На 10 000 векторов этого не заметишь. На 1 000 000 это уже настоящее инженерное и денежное узкое место.

Как изменилось понимание

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

Написать код было просто. Сложно было найти скрытое допущение, которое всё сломало, и именно эту привычку стоит сохранить: построй, сломай, разберись почему и выйди с более точной моделью системы.