Більшість інженерних команд стикаються з однією і тією ж перешкодою при впровадженні генерації з доповненим пошуком (RAG). Вони діють за стандартним сценарієм із туторіалів: розбивають документи на фіксовані фрагменти (chunks) по 512 або 1024 токени, пропускають їх через одну модель ембедингів і звертаються до векторної бази даних за допомогою простого пошуку top-k. У презентації це виглядає переконливо. У реальній експлуатації (production) усе розвалюється.
Фіксовані фрагменти не зважають на зміст. Вони можуть легко розірвати юридичний контракт посеред речення, залишивши пункти про відповідальність розірваними між двома непов'язаними частинами тексту. Вони можуть запхати опис цілого API-ендпоінта в один роздутий фрагмент, настільки великий, що конкретний параметр, про який запитав користувач, просто загубиться в шумі. А коли пошук повільний, кожна мілісекунда затримки (latency) безпосередньо псує досвід користувача. Ми засвоїли це на власному гіркому досвіді. Потім ми повністю переробили наш рівень пошуку. Наш показник recall при k=10 зріс із 78% до 95%. Затримка не зросла. Вона різко зменшилася.
Проблема «копіпаст» RAG
Стандартний стек RAG став свого роду налаштуванням за замовчуванням. Маленькі фрагменти, одна модель ембедингів, векторний пошук — і готово. Такий підхід працює під час демо, тому що в демо використовують чіткі запитання та впорядковані документи. Реальні дані ніколи не бувають впорядкованими.
Юридичні документи мають ієрархічну структуру. Розділи містять підрозділи, а підрозділи — пункти. Якщо розрізати їх грубим лічильником токенів, ви знищите саме ті зв'язки, на основі яких модель має будувати міркування. Документація API також має структуру, але іншу. Сигнатура функції, її параметри, значення, що повертається, та приклад використання утворюють логічний юніт. Якщо запхнути це у фіксоване вікно токенів, ви або обріжете приклад, або переповните фрагмент непов'язаними функціями. Тикети підтримки — це хаотичні розмови з раптовими змінами тем. Вікі — це розгалужені структури з перехресними посиланнями. Одна стратегія фрагментації не може підійти для всього, проте команди регулярно впроваджують саме таку. Ми припинили вдавати, що це можливо.
Стратегічна фрагментація: підбір методу під матеріал
Ми перейшли до фрагментації з урахуванням змісту (content-aware chunking). Для юридичних документів ми використовуємо рекурсивну фрагментацію, яка враховує ієрархію документа. Вона зберігає пункти цілісними та підтримує зв'язки «батько-дитина» між розділами. Для документації API ми розробили фрагментацію з урахуванням функцій (function-aware chunking), де кожна функція або ендпоінт вважається межею. Якщо опис параметра занадто довгий, фрагмент розширюється навколо цієї функції, а не навколо ліміту токенів. Для тикетів підтримки ми використовуємо семантичну фрагментацію, яка визначає природні межі тем. Коли клієнт раптово переходить від скарги на оплату до технічного багу, розрив відбувається саме на цьому моменті. Для вікі та неструктурованих баз знань ми використовуємо агентну фрагментацію (agentic chunking), де легка LLM оцінює текст і вирішує, де має бути змістовна межа. Це складніше налаштувати, ніж просто розбиття за символами, але саме це відрізняє робочий пошук від пошуку, що базується на здогадках.
Гібридний пошук: чому одного векторного пошуку недостатньо
Векторний пошук розуміє значення, але може пропускати точні збіги. Якщо користувач вставляє код помилки, наприклад ERR_CONNECTION_RESET_0x5F3, семантична схожість може поставити його нижче за абзаци, які просто загалом обговорюють мережеві помилки. BM25, з іншого боку, знаходить точні рядки, але не бачить концептуального зв'язку. Вам потрібні обидва методи.
Ми запускаємо векторний пошук і BM25 паралельно. Потім ми поєднуємо результати за допомогою Reciprocal Rank Fusion (RRF), який нормалізує оцінки з двох різних просторів пошуку, не намагаючись привести їх до одного масштабу. Після об'єднання ми пропускаємо найкращих кандидатів через reranker на базі cross-encoder. Це додає невелику затримку, але суттєво підвищує точність. Reranker зчитує запит і кожного кандидата разом і призначає оцінку релевантності, яка набагато точніша за косинусну схожість початкового ембедингу. На практиці таке поєднання дозволяє знаходити точні коди помилок, які пропускає чистий векторний пошук, і водночас виводити концептуально пов'язані кроки усунення несправностей, які ігнорував би пошук за ключовими словами.
Розширення запитів: виправлення вводу користувача перед пошуком в індексі
Користувачі не пишуть ідеальних пошукових запитів. Вони ставлять складні запитання (multi-hop questions), наприклад: «чому мій останній деплой провалився і як мені зробити відкат?», що потребує знаходження двох окремих блоків знань і їхнього поєднання. Або вони ставлять розпливчасті запитання, які погано співпадають з індексом.
Ми трансформуємо запити перед пошуком. Багатоступеневе запитання розбивається на підзапитання. Нечіткий намір розширюється до кількох конкретних пошукових запитів. Ми виявили, що розширення одного запиту користувача до п'яти окремих пошукових запитів може підвищити повноту (recall) з сімдесяти восьми відсотків до дев'яноста шести відсотків. Йдеться не про те, щоб сильніше стимулювати LLM промптами. Йдеться про те, щоб дати системі пошуку більше спроб знайти правильний контекст. Кожен згенерований запит охоплює інший ракурс або термінологію, а об'єднані результати створюють цілісну картину.
Баєсівська оптимізація: припиніть гадати
Щойно ви матимете кілька стратегій чанкування, гібридний пошук та розширення запитів, ви зіткнетеся з новою проблемою. Параметрів стає занадто багато. Розмір чанку, відсоток перекриття, вага вектора порівняно з вагою BM25, пороги переранжування та значення top-k — усе це взаємодіє нелінійним чином. Ручне налаштування перетворюється на гру в здогадки.
Ми припинили гадати. Ми розглядаємо
