Praktilisi näiteid kohandatud RAG-lahendustest

Praktilised näited näitavad, kuidas kohandatud otsinguga täiendatud genereerimise süsteemid lahendavad ettevõtete keerukaid teadmusprobleeme.

Tänapäeval räägitakse RAG-ist vahel halvasti: ühed peavad seda täiesti lihtsaks (alustada ongi lihtne, skaleerida enam mitte), teised arvavad, et „agendipõhised süsteemid“ on selle välja vahetanud (kuigi pealispinna alla vaadates hakkavad need sageli kiiresti RAG-i meenutama…).

Selles blogipostituses toome paar praktilist näidet, et näidata, kuidas lahendame levinud probleeme, eelkõige järgmisi:

  • Teksti- ja arvandmete koos käsitlemine ning põhjus, miks lihtsakoeline RAG sellega hätta jääb: märksõnad kattuvad ja arvudel pole semantilist tähendust.

  • Miks tasub embedding’ud luua esmalt kokkuvõtetest: genereerige igale lõigule lühike kirjeldav kokkuvõte ning looge embedding ja tehke päring selle põhjal.

  • Kuidas genereerida kontekstipõhiseid kokkuvõtteid: lisage algdokumendi kontekst, et sarnase kujuga statistika oleks eristatav.

  • Millal tugineda koodile ja Pydanticu mudelitele: kui sisu tuleb säilitada sõna-sõnalt, ühendage töökindluse tagamiseks kohandatud kood ja/või Pydanticu mudel LLM-i päringutega.

Kohandatud RAG-lahenduste loomine

Põhitõed

RAG-süsteemidel põhinevad nii klienditoe robotid kui ka ettevõttesisesed teadmusabilised.

Taustal tehakse tavaliselt järgmist:

  1. Jagatakse lähtedokumendid lõikudeks

  2. Teisendatakse iga lõik embedding’u abil vektorruumi

  3. Otsitakse päringu ajal välja K kõige asjakohasemat lõiku

  4. Genereeritakse nende lõikude põhjal vastus

Populaarsed tööriistakomplektid – LangChain, LlamaIndex ja OpenAI Filestore – muudavad need sammud peaaegu triviaalseks. Päris töövoogudes kohtab aga ka muud kui tihedat teksti ning lihtne RAG võib sellega hätta jääda. Järgmistes osades tutvustame konkreetseid andmeprobleeme ja arendame lahendust järk-järgult sedamööda, kuidas keerukust lisandub.

Kui asi muutub keerulisemaks

  1. Kui andmed ei koosne üksnes tekstist (see pole sugugi haruldane)

Vaatleme järgmist mänguandmete lõiku:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

Embedding’ud toimivad tänu sõnade semantilisest tähendusest ja grammatikast õpitud seostele. Ülaltoodud andmetes on läbisegi tekst ja arvud, kuid väljaspool seda konkreetset konteksti pole arvudel sõnadega seost. Seega võiks öelda, et see andmelõik on sisuliselt kombinatsioon mõnevõrra kirjeldavatest sõnadest, millele järgnevad juhuslikud arvud.

Kui meil oleks ainult seda tüüpi andmeid, poleks see tegelikult probleem, sest saaksime endiselt otsida väheste saadaolevate kirjeldavate sõnade embedding’ute põhjal (või kasutada lihtsalt teksti SQL-iks teisendamist). Aga mis saab siis, kui see lõik on peidus paljude tekstitihedate lõikude vahel, kus esinevad samad sõnad? Näiteks:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

Kujutlegem nüüd, et soovime leida vastuse küsimusele „What is the attack range with Draconic Ascension?“ Tõenäoliselt ei suuda me vajalikku lõiku leida, sest see mattub samu märksõnu sisaldavate lõikude müra sisse.

Põhiprobleem seisneb selles, et me ei suuda neid andmelõike hästi eristada, kuigi need sisaldavad sama teema kohta eri tüüpi teavet. Kas saaksime neid kuidagi rikastada või täiustada? Muidugi saame:smile:

  1. Rikastage andmeid kokkuvõtetega – jah, lugesite õigesti

Selle asemel et luua embedding otse lõigust, võime esmalt genereerida andmete sisu kirjeldava kokkuvõtte ning luua embedding’u ja teha otsingu kokkuvõtte põhjal. Genereerimisel kasutaksime endiselt kokkuvõttega seotud algandmeid.

Ülaltoodud kahe näidislõigu jaoks genereeriksime näiteks järgmised kokkuvõtted:

  1. Rünnakuulatuse, -kiiruse ja -kahju statistika (vaikimisi ning võimega Draconic Ascension).

  2. Võime Draconic Ascension kirjeldus ja üksikasjad, sealhulgas aktiveerimistingimused, visuaalsed efektid ning taustalugu.

Seejärel täiendame ka päringut, et see kokkuvõttega „joondada“. Näiteks muudaksime küsimuse „What is the attack range with Draconic Ascension?“ kujule „What is the statistics of attack range with Draconic Ascension?“ See on eriti oluline siis, kui otsingupäringu esitavad tehnikavaldkonnast väljaspool olevad kasutajad ~~„vabas vormis“~~ tavalises inimkeeles. Lõppude lõpuks ei pea nad teadma ega muretsema selle pärast, kuidas RAG-i täpsuse ja saagise maksimeerimiseks rakendada.

Diagramm, mis illustreerib olukorra keerulisemaks muutumist.

  1. Ärge rebige asju kontekstist välja (see kehtib üldiselt ka elus)

Järgmise stsenaariumi puhul tuleb tegeleda suure hulga ühesuguste andmelõikudega, nagu allpool:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

Sama lähenemisviisi jätkates kujutlegem küsimust „what is character X’s attack range?“ Äsja genereeritud kokkuvõtetega muutuks see õnnemänguks, sest ka need näeksid väga sarnased välja. Kuidas neid siis eristada?

Lihtne vastus: lisage kontekst. Võiksime lihtsalt lisada andmelõiku viite selle algdokumendile, näiteks antud juhul {”character”: “X”}. Nii saaksime tegelase X õiged andmed täpselt üles leida ka siis, kui meil on samad andmed tegelaste Y ja Z kohta.

Parem ja üldisem lähenemisviis oleks aga genereerida lõigust kontekstipõhine kokkuvõte. See tähendab, et üksnes andmelõigu enda kokkuvõtte genereerimise asemel võiksime sisestada nii selle algdokumendi kui ka lõigu, et genereerida üldine kontekstipõhine kokkuvõte. Kirjeldaksime selles, kuidas lõik algdokumenti sobitub, näiteks:

  1. See lõik sisaldab tegelase X … üksikasjalikku statistikat. Lõik sobitub tervikdokumenti, näidates tegelase X tugevust rünnakukiiruses…

  2. See lõik sisaldab tegelase Y … üksikasjalikku statistikat. Lõik sobitub tervikdokumenti, näidates tegelase Y erivõimega võimendatud näitajaid…

  3. See lõik sisaldab tegelase Z … üksikasjalikku statistikat. Lõik sobitub tervikdokumenti, näidates tegelase Z näitajaid, mis sobivad hästi meeskonnamängudes tanki rolli…

See meetod (mis on osaliselt inspireeritud Anthropicu lahendusest) võib ülaltoodud näite puhul tunduda liialdusena. See on aga väga tõhus lõikude korral, mida võiks „kontekstiväliselt“ valesti tõlgendada, ning pakub kõigile lõikudele ühtset lähenemisviisi, hoides tehnilise töövoo korras.

Diagramm, mis illustreerib olukorra keerulisemaks muutumist.

  1. Kui peate olema ~~kontrollifriik~~ täpne

Tavaliselt saame andmed tervikuna ja jagame need RAG-süsteemi jaoks lõikudeks. Selles näites käsitleme midagi pisut teistsugust: andmed on küll lõikudeks jagatud, kuid halvasti. Need on ühe loogilise lõigu juhuslikud osad, mis tuleb uuesti kokku rühmitada. Loogiline lõik on sisutervik, mis peaks loomulikult kokku kuuluma, näiteks dokumendi alajaotis või sidus tekstilõik.

Diagramm, mis illustreerib olukorra keerulisemaks muutumist.

Esimesel katsel sisestasime kõik andmed LLM-ile, palusime need sobival viisil rühmitada ja seejärel rühmitatud sisu tagastada. LLM peaks selles ju päris hea olema? Nii ja naa.

Oleme ka mitmel muul korral märganud, et LLM-id kipuvad olema laisad ega ole kogu täpse sisu nõudmisel usaldusväärsed, eriti pika konteksti korral. See on täiesti mõistetav. Selle kasutusjuhtumi puhul oli see aga välistav puudus, sest vajame täpselt sõna-sõnalist sisu – ei mingeid kokkuvõtteid ega algsisu osade vahelejätmist. Ükski detail ei tohi kaduma minna.

Positiivne oli muidugi see, et LLM mõistis katkiste lõikude semantikat ja struktuuri suurepäraselt. Kui ta vaid ei keelduks täpset sisu taasesitamast. Pagana pihta:/

Kuidas saaksime siis kasutada LLM-i tugevaid külgi, kuid vältida valdkondi, kus see pole usaldusväärne? Pöördusime oma vana hea sõbra ehk koodi poole (loe: kohandatud Pythoni funktsioon). Lisaks kasutasime „lihtsamat-enam-ei-saa“ Pydanticu mudelit. Lahendus on järgmine:

  • Käige jaotised ükshaaval läbi, säilitades jooksva loogilise lõigu

  • Küsige iga jaotise puhul LLM-ilt: „Kas see jaotis kuulub jooksvasse loogilisse lõiku? Vasta jah või ei (järgides Pydanticu mudelit).“

  • Kui jah, lisage jaotis lõigule. Kui ei, väljastage lõpetatud loogiline lõik ja alustage selle jaotisega uut.

Diagramm, mis illustreerib olukorra keerulisemaks muutumist.

Loomulikult kulub siin veidi rohkem tokeneid kui kogu sisu ühe läbimise korral, kuid selles kasutusjuhtumis oli täpse sisu säilitamine esmatähtis ja (väike) lisakulu tasus end igati ära.

See on väga lihtne lahendus, kuid järgib olulist põhimõtet: kui vaja on ranget täpsust, ei tohiks tugineda ainult LLM-idele, sest need on lõppude lõpuks tõenäosuslikud.

Kohandatud koodi või funktsioonide ja Pydanticu mudelite abil saab saavutada prognoositava ning usaldusväärse tulemuse, kasutades samal ajal täiel määral LLM-ide võimekust.

Kokkuvõte

Generatiivse tehisaru lahenduse loomine on ühtaegu nii insenertehniline kui ka tehisaru valdkonna ülesanne. Loodame, et need näited innustasid teid oma ainulaadseid probleeme lahendama. Kui soovite lugeda rohkem insenertehnilisest vaatenurgast lähtuvate generatiivse tehisaru lahenduste kohta, vaadake meie blogipostitust ruuteripõhiste agendisüsteemide kavandamisest.

Autor

Cynthia Yu