Останнім часом RAG іноді має погану репутацію: дехто вважає його геть тривіальним (почати справді легко, а от масштабувати — не дуже), а дехто думає, що його витіснили «агентні системи» (хоча в багатьох випадках варто лише придивитися — і вони швидко починають дуже нагадувати RAG…).
У цій статті ми наводимо кілька практичних прикладів і показуємо, як долаємо поширені труднощі, а саме:
Робота зі змішаними текстовими й числовими даними та причини, через які вони ламають примітивний RAG: ключові слова збігаються, а числа не мають семантичного значення.
Чому варто спочатку створювати резюме, а вже потім ембедінги: згенеруйте для кожного фрагмента короткий опис, а тоді створюйте ембедінг і виконуйте пошук за ним.
Як створювати контекстні резюме: додавайте контекст батьківського документа, щоб можна було розрізняти статистичні дані схожої структури.
Коли покладатися на код і моделі Pydantic: якщо важливо дослівно зберегти вміст, для надійності поєднуйте власний код та/або модель Pydantic із викликами LLM.
Основи
Системи RAG лежать в основі найрізноманітніших рішень — від ботів підтримки до внутрішніх помічників із пошуку знань.
Зазвичай усе працює так:
Розбийте вихідні документи на фрагменти
Перетворіть кожен фрагмент на ембедінг у векторному просторі
Отримайте K найрелевантніших фрагментів під час запиту
Згенеруйте відповідь на основі цих фрагментів
Популярні набори інструментів — LangChain, LlamaIndex і Filestore від OpenAI — роблять ці етапи майже тривіальними. Але в реальних конвеєрах трапляються дані, що складаються не лише із суцільного тексту, і базовий RAG може з ними не впоратися. У наступних розділах ми розглянемо конкретні приклади проблем із даними та поступово розбудуємо рішення в міру ускладнення завдання.
Коли ваші дані — не лише текст (а так буває часто)
Розгляньмо такий фрагмент даних у контексті гри:
JSON
Ембедінги працюють завдяки засвоєним зв’язкам між словами, що ґрунтуються на семантиці та граматиці. У наведених вище даних змішані текст і числа, причому поза цим конкретним контекстом числа ніяк не пов’язані зі словами. Тож можна сказати, що цей фрагмент даних — фактично поєднання кількох описових слів із набором випадкових чисел.
Це не було б проблемою, якби ми мали лише дані такого типу: потрібний фрагмент усе одно можна було б знайти за ембедінгами кількох наявних описових слів (або просто скористатися перетворенням тексту на SQL). Але що, як цей фрагмент загубився серед безлічі насичених текстом фрагментів, у яких також трапляються ці слова? Наприклад:
JSON
А тепер уявіть, що нам потрібно знайти відповідь на запит «What is the attack range with Draconic Ascension?» Найімовірніше, потрібний фрагмент знайти не вдасться, адже він загубиться в шумі інших фрагментів із тими самими ключовими словами.
Головна проблема в тому, що ми не можемо чітко розрізнити ці фрагменти даних, хоча вони містять різні типи інформації на ту саму тему. Чи можна якось збагатити або вдосконалити ці дані? Звісно, можна:smile:
Збагатіть дані, узагальнивши їх. Так, ви все правильно прочитали
Замість того щоб одразу створювати ембедінг самого фрагмента, можна спочатку згенерувати резюме з описом даних, а потім створити ембедінг і виконувати пошук за цим резюме. На етапі генерації ми й далі використовуватимемо оригінальні дані, пов’язані з резюме.
Отже, для двох наведених вище фрагментів можна згенерувати такі резюме:
Статистика дальності, швидкості та шкоди атаки (за замовчуванням і з Draconic Ascension).
Опис і подробиці Draconic Ascension, зокрема умови активації, візуальні ефекти та історія.
Потім ми також доповнюємо запит, щоб «узгодити» його з резюме. Наприклад, запит «What is the attack range with Draconic Ascension?» ми перетворимо на «What is the statistics of attack range with Draconic Ascension?» Це особливо важливо, коли пошуковий запит надходить від користувачів без технічної підготовки, які формулюють його ~~«у вільному стилі»~~ звичайною людською мовою. Зрештою, їм не потрібно знати, як працює RAG і як максимізувати точність та повноту пошуку.


Не виривайте нічого з контексту (це загалом корисна життєва порада)
А тепер розгляньмо ситуацію, коли потрібно опрацювати безліч однакових на вигляд фрагментів даних, як-от наведені нижче:
Plain Text
Якщо дотримуватися того самого підходу, уявімо запит «what is character X’s attack range?» Із щойно створеними резюме нам довелося б грати у вгадування, покладаючись на удачу, адже вони також були б дуже схожими. Як же їх розрізнити?
Відповідь проста: додати контекст. Можна просто додати до фрагмента даних посилання на його батьківський документ — наприклад, у цьому випадку {”character”: “X”}. Тепер ми зможемо точно знайти потрібні дані для персонажа X, навіть якщо маємо такі самі дані для персонажів Y і Z.
Однак кращим і універсальнішим підходом буде створення контекстного резюме фрагмента. Тобто замість резюме лише самого фрагмента даних можна передати і батьківський документ, і фрагмент, щоб створити загальне контекстне резюме. У ньому слід пояснити, яке місце цей фрагмент посідає в батьківському документі, наприклад:
Цей фрагмент містить докладну статистику … для персонажа X. Цей фрагмент доповнює повний документ, демонструючи перевагу X у швидкості атаки…
Цей фрагмент містить докладну статистику … для персонажа Y. Цей фрагмент доповнює повний документ, демонструючи покращені характеристики Y під дією її особливої здібності…
Цей фрагмент містить докладну статистику … для персонажа Z. Цей фрагмент доповнює повний документ, демонструючи характеристики Z, завдяки яким персонаж добре підходить на роль танка в командних матчах…
Цей метод (частково натхнений підходом Anthropic) може здатися надмірним для наведеного вище прикладу. Однак він дуже ефективний для фрагментів, які можна неправильно витлумачити «поза контекстом», а також забезпечує єдиний підхід для всіх фрагментів і дає змогу зберегти охайну структуру інженерного конвеєра.


Коли потрібно бути ~~схибленим на контролі~~ методичним
Зазвичай ми отримуємо цілісні масиви даних і розбиваємо їх на фрагменти для системи RAG. У цьому прикладі ситуація дещо інша: дані вже розбито на фрагменти, але невдало. Це випадкові частини логічного фрагмента, які насправді потрібно знову об’єднати. Логічний фрагмент — це частина вмісту, елементи якої природно мають бути разом, наприклад підрозділ документа або зв’язний абзац.


Спочатку ми вирішили передати всі ці дані в одному виклику LLM, попросити згрупувати їх на власний розсуд і повернути згрупований вміст. LLM має чудово впоратися із цим, правда ж? І так, і ні.
У цьому та кількох інших випадках ми виявили, що LLM схильні лінуватися й працюють ненадійно, коли потрібно повернути повний і точний вміст, особливо за довгого контексту. І це цілком логічно. Але для цього конкретного сценарію такий недолік був критичним, адже нам потрібен точний вміст слово в слово — без резюме й без пропусків будь-яких частин оригіналу. Не можна втратити жодної деталі.
З іншого боку, LLM чудово розуміла семантику та структуру розрізнених фрагментів. Аби тільки вона не відмовлялася дослівно повертати точний вміст. От халепа:/
Як же скористатися сильними сторонами LLM, уникнувши того, у чому вона ненадійна? Ми звернулися до нашого доброго старого друга — коду (тобто спеціально написаної функції Python). І до «простішої не буває» моделі Pydantic. Ось рішення:
Послідовно перебирати розділи, зберігаючи поточний логічний фрагмент
Для кожного розділу запитувати LLM: чи належить цей розділ до поточного логічного фрагмента? Відповідь має бути «так» або «ні» згідно з моделлю Pydantic.
Якщо так — додати розділ до фрагмента. Якщо ні — вивести завершений поточний логічний фрагмент, а потім почати з цього розділу новий.


Звісно, тут ми витрачаємо трохи більше токенів, ніж за один прохід усім вмістом. Але в цьому сценарії, де найважливіше — точно зберегти вміст, незначні додаткові витрати цілком виправдані.
Це дуже просте рішення, але воно спирається на важливий принцип: коли потрібна точність, не варто покладатися лише на LLM, адже за своєю природою вони ймовірнісні.
Власний код і функції, а також моделі Pydantic дають змогу отримувати передбачуваний і надійний результат, водночас повною мірою використовуючи можливості LLM.
Створення рішення на основі генеративного ШІ — це однаковою мірою і інженерне завдання, і завдання у сфері ШІ. Сподіваємося, ці приклади надихнули вас узятися за власні унікальні завдання. Щоб дізнатися більше про інженерний підхід до рішень на основі генеративного ШІ, прочитайте нашу статтю про проєктування агентних систем на основі маршрутизаторів.