Головна / Статті / Чому поганий чанкінг ламає векторний пошук навіть із хорошими ембедингами

Чому поганий чанкінг ламає векторний пошук навіть із хорошими ембедингами

Сплітер із фіксованими 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 це вже справжнє інженерне й фінансове вузьке місце.

Як змінилося розуміння

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

Написати код було легко. Складно було знайти приховане припущення, яке все зламало, і саме цю звичку варто зберегти: збудуй, зламай, з'ясуй чому і вийди з точнішою моделлю системи.