RAG je zadnje čase včasih na slabem glasu: nekateri menijo, da je povsem preprost (za začetek že, pri razširjanju pa manj), drugi pa, da so ga prehiteli »agentski sistemi« (ki so pogosto, če pogledamo pod površje, zelo hitro videti precej podobni RAG-u …).
V tem prispevku bomo s praktičnima primeroma pokazali, kako rešujemo nekatere pogoste izzive:
Obravnava kombinacije besedilnih in številskih podatkov ter razlogi, zakaj ti preprost RAG ni kos: ključne besede se prekrivajo, številke pa nimajo semantičnega pomena.
Zakaj je koristno vdelave zasnovati na povzetkih: za vsak del ustvarimo kratek opisni povzetek, nato pa vdelamo in poizvedujemo po njem.
Kako ustvariti kontekstualne povzetke: vključimo kontekst nadrejenega dokumenta, da lahko razlikujemo med podobno oblikovanimi statističnimi podatki.
Kdaj se zanesti na kodo in modele Pydantic: kadar je pomembna dobesedna vsebina, za zanesljivost združimo kodo po meri in/ali model Pydantic s klici velikemu jezikovnemu modelu.
Osnove
Sistemi RAG poganjajo vse od botov za podporo do internih pomočnikov za znanje.
V ozadju običajno izvedemo naslednje korake:
Izvorne dokumente razdelimo na dele
Vsak del vdelamo v vektorski prostor
Ob poizvedbi pridobimo najboljših K delov
Na podlagi teh delov ustvarimo odgovor
Priljubljena orodja, kot so LangChain, LlamaIndex in OpenAI Filestore, te korake naredijo skoraj povsem preproste. Toda v dejanskih cevovodih naletimo tudi na podatke, ki niso le zgoščeno besedilo, in osnovni RAG jim je lahko težko kos. V naslednjih razdelkih bomo prikazali konkretne primere podatkovnih izzivov in z naraščanjem kompleksnosti postopoma razvijali rešitev.
Ko vaši podatki niso zgolj besedilo (kar pravzaprav ni redko)
Oglejmo si naslednji del podatkov iz konteksta igre:
JSON
Vdelave delujejo zaradi naučenih razmerij med besedami, ki izhajajo iz semantičnega pomena in slovnice. V zgornjih podatkih so pomešani besedilo in številke, pri čemer številke zunaj tega natančnega konteksta niso povezane z besedami. Lahko bi torej rekli, da je ta del podatkov predvsem kombinacija nekoliko opisnih besed, ki jim sledi nekaj naključnih številk.
To pravzaprav ne bi bilo težavno, če bi imeli samo takšne podatke, saj bi jih še vedno lahko pridobili z vdelavami nekaj razpoložljivih opisnih besed (ali pa preprosto uporabili pretvorbo besedila v SQL). Kaj pa, če je ta del zakopan med številne dele z veliko besedila, v katerih se pojavljajo iste besede? Na primer:
JSON
Zdaj si predstavljajte, da želimo pridobiti odgovor na vprašanje »What is the attack range with Draconic Ascension?«Zelo verjetno ne bomo pridobili želenega dela, saj se bo izgubil v šumu drugih delov z istimi ključnimi besedami.
Bistvena težava je, da teh delov podatkov ne moremo dobro razlikovati, čeprav vsebujejo različne vrste informacij o isti temi. Bi jih lahko kako obogatili oziroma izboljšali? Seveda jih lahko :smile:
Podatke obogatite s povzemanjem – da, prav ste prebrali
Namesto neposredne vdelave samega dela lahko najprej ustvarimo povzetek, ki opisuje podatke, nato pa vdelamo in pridobivamo na podlagi tega povzetka. Pri ustvarjanju odgovora bi še vedno uporabili izvirne podatke, povezane s povzetkom.
Za zgornja primera delov bi torej ustvarili približno takšna povzetka:
Statistični podatki o dosegu, hitrosti in škodi napada (privzeto in z Draconic Ascension).
Opis in podrobnosti sposobnosti Draconic Ascension, vključno s pogoji aktivacije, vizualnimi učinki in izročilom.
Nato tudi poizvedbo razširimo, da jo »uskladimo« s povzetkom. Poizvedbo »What is the attack range with Draconic Ascension?« bi na primer spremeniliv »What is the statistics of attack range with Draconic Ascension?«To je še posebej pomembno, ko iskalno poizvedbo vnesejo uporabniki zunaj tehničnih področij in sprašujejo v ~~»svobodnem slogu«~~ običajnem človeškem jeziku. Navsezadnje ni njihova naloga vedeti, kako RAG deluje, da bi dosegli čim večjo natančnost in priklic.


Ne jemljite stvari iz konteksta (to na splošno velja tudi v življenju)
Naslednji možni scenarij je obravnava množice delov podatkov, ki so videti enako, kot v nadaljevanju:
Plain Text
Če ohranimo isti pristop, si predstavljajte vprašanje »what is character X’s attack range?«Pri pravkar ustvarjenih povzetkih bi se morali zanašati na srečo in ugibanje, saj bi bili tudi ti zelo podobni. Kako bi jih torej lahko razlikovali?
Odgovor je preprost: dodamo kontekst. V del podatkov bi lahko preprosto vključili sklic na njegov nadrejeni dokument, v tem primeru na primer {”character”: “X”}. Tako bi lahko natančno pridobili prave podatke za lik X, tudi če imamo enake podatke za lika Y in Z.
Boljši in splošneje uporaben pristop pa je ustvarjanje kontekstualnega povzetka dela. Namesto da bi povzeli samo posamezni del podatkov, bi lahko modelu posredovali tako nadrejeni dokument kot ta del in ustvarili splošen kontekstualni povzetek. V njem bi pojasnili, kako se del umešča v nadrejeni dokument, na primer:
Ta del vsebuje podrobne statistične podatke o … za lik X. V celoten dokument se umešča tako, da prikazuje prednosti lika X pri hitrosti napada …
Ta del vsebuje podrobne statistične podatke o … za lik Y. V celoten dokument se umešča tako, da prikazuje izboljšane statistične vrednosti lika Y ob uporabi njene posebne sposobnosti …
Ta del vsebuje podrobne statistične podatke o … za lik Z. V celoten dokument se umešča tako, da prikazuje statistične vrednosti lika Z, zaradi katerih je zelo primeren za vlogo tanka v ekipnih tekmah …
Ta metoda (ki jo je delno navdihnil Anthropic) se morda zdi pretirana za zgornji primer, vendar je zelo učinkovita pri delih, ki bi jih lahko »zunaj konteksta« napačno razumeli. Poleg tega zagotavlja enoten pristop za vse dele in ohranja urejen inženirski cevovod.


Ko morate biti ~~obsedeni z nadzorom~~ dosledni
Podatke običajno dobimo kot celote in jih za sistem RAG razdelimo na dele. V tem primeru prikazujemo nekaj nekoliko drugačnega: podatke, ki so že razdeljeni, vendar slabo. Gre za naključne odseke logične celote, ki jih moramo znova združiti. Logični del je vsebinska celota, ki naravno sodi skupaj, na primer podrazdelek dokumenta ali smiselno povezan odstavek.


Najprej smo poskusili vse podatke poslati velikemu jezikovnemu modelu ter mu naročiti, naj jih smiselno razvrsti in vrne združeno vsebino. Veliki jezikovni model bi moral biti pri tem precej dober, kajne? No, da in ne.
Ob tej in več drugih priložnostih smo ugotovili, da veliki jezikovni modeli radi uberejo bližnjico in niso zanesljivi, ko potrebujemo popolno in natančno vsebino, zlasti pri dolgem kontekstu. Kar je povsem razumljivo. Toda pri tem konkretnem primeru uporabe je bila to nepremostljiva ovira, saj smo potrebovali dobesedno natančno vsebino – brez povzetkov in brez izpuščanja katerega koli dela izvirnika. Izpustiti ne smemo nobene podrobnosti.
Pozitivna plat pa je bila seveda ta, da je odlično razumel pomen in strukturo razdrobljenih delov. Vsaj kadar ni zavrnil natančnega navajanja vsebine. Prekleto :/
Kako bi lahko izkoristili prednosti velikega jezikovnega modela in se hkrati izognili področjem, na katerih ni zanesljiv? Obrnili smo se na starega dobrega prijatelja – kodo (beri: funkcijo Python po meri). In na model Pydantic, ki »ne bi mogel biti preprostejši«. Rešitev je naslednja:
Pomikamo se skozi odseke in pri tem vzdržujemo trenutni logični del.
Pri vsakem odseku vprašamo veliki jezikovni model: »Ali ta odsek sodi v trenutni logični del?« Odgovoriti mora z da ali ne (skladno z modelom Pydantic).
Če je odgovor pritrdilen, odsek dodamo delu. Če je nikalen, dokončani trenutni logični del vrnemo in z odsekom začnemo novega.


Seveda porabimo nekoliko več žetonov kot pri enem samem prehodu skozi celotno vsebino, vendar je bil pri tem primeru uporabe, kjer je ohranitev natančne vsebine najpomembnejša, manjši dodatni strošek povsem upravičen.
Rešitev je zelo preprosta, vendar sledi pomembnemu načelu: kadar je potrebna doslednost, se ne smemo zanašati izključno na velike jezikovne modele, saj so navsezadnje verjetnostni.
S kodo oziroma funkcijami po meri in modeli Pydantic lahko dosežemo predvidljive in zanesljive rezultate, hkrati pa še vedno izkoristimo zmogljivosti velikih jezikovnih modelov.
Izdelava rešitve z generativno umetno inteligenco je tako inženirski izziv kot izziv umetne inteligence. Upamo, da so vas ti primeri spodbudili k reševanju lastnih edinstvenih izzivov. Več o inženirsko usmerjenih rešitvah z generativno umetno inteligenco preberite v našem prispevku o zasnovi agentskih sistemov z usmerjevalniki.