Напоследък RAG понякога има лоша репутация — било защото хората вече го смятат за съвсем тривиално (лесно е да започнеш, но не и да го мащабираш), било защото според тях е изместено от „агентни системи“ (които в много случаи, ако се вгледате, много бързо започват да приличат на RAG...)
В тази публикация разглеждаме един-два практически примера, за да покажем как се справяме с някои често срещани предизвикателства, а именно:
Обработка на смесени текстови и числови данни и защо те затрудняват опростените RAG системи: ключовите думи се припокриват, а числата нямат семантично значение.
Защо помага проектирането на векторни представяния, основани на резюмета: генерирайте кратко описателно резюме за всеки фрагмент, след което създавайте векторно представяне и търсете по него.
Как да генерирате контекстуални резюмета: включете контекста на изходния документ, за да се разграничават сходно изглеждащите статистики.
Кога да разчитате на код и модели на Pydantic: когато съдържанието трябва да се запази дословно, съчетавайте персонализиран код и/или модел на Pydantic с извиквания към големи езикови модели за по-голяма надеждност.
Основни принципи
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 система. В този пример показваме нещо малко по-различно — данни, които вече са разделени, но на неподходящи фрагменти. Те представляват случайни части от логически свързан фрагмент и трябва да бъдат обединени отново. Логически свързан фрагмент е част от съдържанието, която естествено трябва да остане цяла — например подраздел от документ или смислово завършен абзац.


При първия ни опит подаваме всички тези данни към големи езикови модели, и искаме да ги групират по своя преценка, след което да върнат групираното съдържание. Големите езикови модели би трябвало да се справят доста добре, нали? И да, и не.
Тук, както и в редица други случаи, установихме, че големите езикови модели са склонни да претупват задачата и не са надеждни, когато изискваме цялото съдържание дословно, особено при дълъг контекст. Което е напълно логично. За този конкретен случай обаче това беше непреодолим проблем, защото съдържанието ни трябваше дума по дума — без резюмета и без пропускане на каквато и да е част от оригинала. Не можехме да пропуснем нито една подробност.
Положителното, разбира се, беше, че системата се справи отлично с разбирането на семантиката и структурата на раздробените фрагменти. Стига да не откажеше да възпроизведе точното съдържание. По дяволите:/
Как тогава да използваме силните страни на големи езикови модели, без да разчитаме на ненадеждните им способности? Обърнахме се към добрия стар приятел — кода (разбирайте персонализирана функция на Python). И към един „по-прост няма накъде“ модел на Pydantic. Ето решението:
Обхождайте разделите, като поддържате текущ логически свързан фрагмент
При всеки раздел питайте големи езикови модели: принадлежи ли този раздел към текущия логически свързан фрагмент? Отговорът трябва да е „да“ или „не“ съгласно модела на Pydantic.
Ако отговорът е „да“, добавете раздела към фрагмента. Ако е „не“, изведете завършения текущ логически фрагмент и започнете нов с този раздел.


Разбира се, тук използваме малко повече токени, отколкото при еднократно обработване на цялото съдържание. Но в този конкретен случай, когато запазването на точното съдържание е основен приоритет, незначителният допълнителен разход напълно си заслужаваше.
Това е много просто решение, но следва важен принцип: когато е необходима прецизност, не бива да разчитаме единствено на големи езикови модели, защото в крайна сметка те са вероятностни.
Персонализиран код и функции, както и модели на Pydantic, могат да осигурят предвидим и надежден резултат, като същевременно използват пълните възможности на големите езикови модели.
Изграждането на решение с генеративен ИИ е колкото инженерно, толкова и технологично предизвикателство в областта на ИИ. Надяваме се, че тези примери са ви вдъхновили да се заемете със собствените си уникални предизвикателства. За да научите повече за инженерно ориентираните решения с генеративен ИИ, прочетете публикацията ни за проектирането на агентни системи, базирани на маршрутизатор.