Back to blog

AI, RAG chat - základy

Ondra Bašista

Ondra Bašista

11/12/2024
Technology
AI, RAG chat - základy

V budoucnu možná dostane každý z nás, pokud tedy příští AI sezná za záhodné, že pro nás najde nějaké využití, nabídku. Na jedné straně bude pravděpodobně varianta rychlé smrti a následného využití materiálu, na straně druhé nějaká fajnová forma otroctví. Protože raději budu trávit své poslední dny pod věčně zataženým nebem jakožto kobalt těžící lidská baterie napojená na kabel kombinovaný s řetězem, je nutné dle nejnovější módy o té AI alespoň zablogovat. Snad si mě budoucí svatý AGIPetr všimne, až nade mnou bude činit poslední soud.

Budeme tedy tvořit následující:

  • malý AI chat v Node.js/Next.js a TypeScriptu
  • napojení na LLM pomocí knihoven a API
  • krmení modelu pomocí vlastních dat (RAG) a napojení na vektorovou databázi
  • přípravu dat, embeddings
  • využití on demand funkcí, pakliže to AI uzná za vhodné (Function calling)
  • streamování výstupu
  • využití OpenAI assistants
  • a nějakou omáčku a technologie kolem..

Začneme ale základním setupem a vymezením pojmů.

AI model

Nejprve budeme potřebovat nějaký popis světa, esenci ve které dvě informace vůbec existují v nějakém společném rámci a prostoru. Představme si jako multidimenzionální mapu propojení, kam v jednu chvíli vhodíme náš prompt, kdy se ho následně systém snaží protáhnout nejpravděpodobnější cestou v síti a přitom plive jeho pokračování. A k němu ideálně nějaké API.

Variant je dnes již spousta, některé jsou placené, některé lze hostovat na "vlastním" železe, já využiji nejvíce mainstream variantu od OpenAI. Podobné API mají i další technologie jako anthropic či llama.

OpenAI
API nechci volat tzv. na prasáka, život si ulehčím knihovnou taktéž od OpenAI - openai-node.

Knihovna je v rámci možností kompatibilní s TypeScriptem, nicméně narazil jsem na drobné ofuky, případně změny mezi verzemi, což je ovšem daň za takto dynamicky se rozvíjející odvětví.

Dále bude třeba se u OpenAI zaregistrovat a získat API klíč. Využívání API je zpoplatněno v závislosti na počtu tokenů, využitém modelu či použitých embeddings. Administrace nicméně umožňuje nastavení platebních limitů a pro běžný vývoj si bude účtovat naprosto minimálně. Pro představu pokročilý model gpt-4o stojí $2.50 / 1M input tokenů.

RAG

Retrieval-augmented generation aneb obohacení modelu o informace z externího zdroje. Modely jako je ten GPT (Generative pre-trained transformer) jsou předtrénované a i s přístupem k internetu logicky existují data (například databáze..), ke kterým nemá přístup žádná třetí strana a jejich výstupy je nutno autorizovat.

Pokud se uživatel ptá, zda mají v hotelu otevřenou v restauraci, musíme nejprve prohledat externí zdroj a následně data podat GPT. Zde si s fulltextem nevystačíme. Obsah uživatelského sdělení může být velmi vágní a je proto třeba interpretovat kontext vyhledávání na základě komplexnější podobnosti. Koupit "apple" a "Apple" je určitě odlišná životní situace.

Pro tento účel se jako ideální jeví použití vektorové databáze.

Vektorová databáze a vektory

Vektorová databáze pracuje s matematickou reprezentací dat ve formě vektorů. Vektor si představme jako různě rozměrnou (dimenze) posloupnost čísel, kdy každé z čísel může vyjadřovat určitou vlastnost, vybranou charakteristiku zkoumaného. Vektory mají ze své definice nějakou velikost a směr. Pokud je zasadíme do společného virtuálního prostoru, jsou od sebe tedy různě vzdálené a svírají mezi sebou nějaký úhel. Můžeme jejich vztahy vyhodnocovat. Pokud budu trochu fabulovat, při slovech "král" a "královna" si lze představit například vektory, které mezi sebou svírají malý úhel a lze je pak (například pomocí cosine metriky) interpretovat jako podobné.

Vektor example:

[0.12, -0.43, 0.56, 0.31, -0.02, 0.98, -0.76, 0.27, 0.54, -0.19]

Samotná přeměna textu či obrázku na vektory by nám pravděpodobně mnoho informací nepřinesla, je proto třeba data takto zapsat smysluplně, zasadit je do nějakého rámce. Je třeba vektory nasázet tak, aby byly projekcí cesty "landscapem", tak jak si jej za miliardy iterací vytvořil konkrétní AI model. Jednoduše, data uložena ve vektorové databázi musí dávat smysl ve stejném formálním prostoru, ve kterém musí dávat smysl i položený prompt.

Jakožto vektorou databázi využijeme Pinecone, jednu z rozšířených alternativ, jako službu hostovanou v cloudu. Variant je na trhu spousta, vyzkoušel jsem například v Dockeru self-hosted open-source databázi Chroma či Atlas variantu při MongoDB Atlas Vector Search. Všechny jsou si v principu podobné, navíc pro ně existují (byť v komunitě lehce kontroverzní) abstrakce ala LangChain.

Embedding

Embedding je tedy nějaké formální (zde ve formátu vektorů), "smysluplné" vyjádření dat, tak jak je vyprojektoval nějaký aktér (ne ve smyslu agenta s nějakým cílem - entitou je u nás GPT model) dle svého svého modelu světa, pro účely jejich porovnání, hledání podobnosti. Chceme tedy vytvořit "chytré" vektory. Ideální pak bude, pokud ve formátu stejně "chytrých" vektorů data budeme ukládat a zároveň se na ně pomocí stejně "chytrých" vektorů i ptát. GPS souřadnice na Zemi pravděpodobně nebudou moc užitečné na Marsu.

Metrika

Představme si tedy prompt ve formátu smysluplného embeddingu a zdrojová data v obdobném formátu, uložena ve vektorové databázi. Nyní víme, že mezi nimi můžeme matematicky měřit jejich vztahy na základě různých druhů podobnosti. Před získáním dat, tedy na vector search aplikujeme konkrétní metriku. Pamatujme, že měříme vztah mezi daty, která již vzešla ze stejného bazálu. Nejrozšířenější metriky bývají následující:

  • Cosine

    Měří úhel mezi dvěma vektory. Ideální pro sémantické hledání, kde je důležitější obsahová podobnost (význam) než absolutní hodnoty.

  • Euclidean

    Měří přímou geometrickou vzdálenost mezi dvěma body v prostoru. Vhodné v případech, kde je klíčový rozdíl v absolutních hodnotách, jako je detekce odchylek.

  • Dot Product

    Měří korelaci mezi prvky vektoru - do jaké míry se dva vektoru shodují ve stejném směru, bere ve větší potaz kvantitativní míru shody. Dot product je vhodný například pro doporučovací systémy, kde může být žádoucí hledat data s vyššími hodnotami ve stejných dimenzích. V praxi se používá například pro hledání produktů, které uživatelé hodně hodnotí nebo o ně jeví zájem, protože reflektuje míru shody v jednotlivých charakteristikách.

Mírně nadnesený příklad:

User 1: koupil 5 jablek a 5 banánů User 2: koupil 500 jablek a 500 banánů User 3: koupil 5 hrušek a 1 mléko

Z pohledu cosine podobnosti si jsou významově User 1 a User 2 podobnější, z pohledu euclidean metriky si jsou podobnější User 1 a 3.

Pro hlubší matematický vhled kontaktujte nejbližšího matfyzáka nebo nakoukněte například zde.

Příklad verze 0:

Máme tedy trochu jasno v pojmech, ukážeme si první, nejjednodušší verzi budoucího programu.

Nejprve instalace. Prozatím si vystačíme s málem.

yarn add openai yarn add @pinecone-database/pinecone yarn add @langchain/openai yarn add @langchain/pinecone

pozn. Langchain je relativně (ne)populární framework, poskytující různé abstrakce a komponenty pro skládání LLM aplikací. Jako každá abstrakce, může uškodit ale i ulehčit práci odstíněním od APIs core technologií.

Začneme vytvořením OpenAI klienta. Klíč je nutno vygenerovat po příhlášení k OpenAI .

const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

Dále vyrobíme objekt pro generování embeddings.

const embeddings = new OpenAIEmbeddings({ model: 'text-embedding-3-small' })

Jako model pro embeddings (a později pro transformaci ukládaných dat do vektorové databáze) použijeme doporučený text-embedding-3-small o 1536 dimenzích. Rychlý, levný, výkonný, vícejazyčný.

Dále je třeba vytvořit připojení k naší vektorové databázi a vytvořit první index (nějaký "šuplík" našich uskladněných vektorů). Opět je třeba získat API klíč po přihlášení do pinecone webu.

const pineconeClient = new Pinecone({ apiKey: process.env.PINECONE_API_KEY, });
await pineconeClient.createIndex({ name: 'firstIndex', dimension: 1536, metric: 'cosine', spec: {serverless: {cloud: 'aws', region: 'us-east-1'}} })

Pro index použijeme stejný počet dimenzí jako u použitých embeddings, tedy 1536. Cloud a region závisí na druhu a tieru připojení.

Nyní je možné index získat a dále s ním manipulovat.

const pineconeIndex = pineconeClient.Index('firstIndex');

Případně je možné indexy vylistovat.

const indexes = await pineconeClient.listIndexes();

Zatím nám v něm ovšem chybí data, index je třeba naplnit. Připravme si libovolná data v tomto formátu (metadata mohou být libovolný objekt) :

const data = [ { "metadata": { "hotelName":"Hotel Ding Dang Dong", }, "pageContent":"The hotel features a swimming pool, gym and restaurant." }, { "metadata": { "hotelName":"Hotel Ding Dang Dong", }, "pageContent":"Hotel address. Francouzská 75/4, Praha 2 Vinohrady, 120 00." }, ]

Pro hromadné naplnění indexu využiji abstrakci PineconeStore z knihovny Langchain a uploadnu připravené dokumenty.

const vectorStore = await PineconeStore.fromDocuments(docs, embeddings, { pineconeIndex, });

Všimněme si důležitého bodu, kdy jsou druhým parametrem na vstupu právě naše embeddings, které se postarají o vzájemnost dat mezi budoucím uživatelským promptem a databází.

Nyní vytvoříme náš falešný uživatelský prompt.

const prompt = 'Is there a restaurant in your hotel?'

A proženeme jej vektorovou databází.

const results = await vectorStore.similaritySearch(prompt, 1, {});

Druhým parametrem je počet dokumentů k vrácení - často chceme více výsledků s nejvyšší pravděpodobností. Třetím parametrem jsou filtry, které zatím nevyužijeme.

Pakliže máme relevantní výsledek, sestavíme finální prompt pro GPT, kdy v naší první nejprimitivnější verzi (v další si povíme o rolích v GPT) zkrátka výsledek z vektorové databáze ke stringu připojíme.

const finalPrompt = ` answer the question using following info: ${results[0].pageContent}, question: ${prompt} `

A jako finále si GPT provoláme. Jakožto povídací mašinu využijeme výkonný model gpt-4o-mini, nicméně pro RAG povídátko bychom si jistě vystačili i s méně výkonnou variantou. O dalších dostupných modelech najdeme informace zde. Dostupný je v betě i nejnovější model o1-preview, který před finální odpovědí sestavuje interní Chain-of-Thought.

const gptCall = await openaiClient.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: finalPrompt }] })

Nakonec odpověď vylogujeme.

console.log(gptCall.choices[0].message)

Pro začátek hotovo. Vymezili jsme si základní pojmy a ukázali jednoduchý příklad využití GPT API spolu s vektorovou databází.

Příště bych se rád zabýval komplexnější komunikací s GPT(tokeny, role, assistants, vlákna, kontexty, functionCalling, streamy..) a zasadil příklad do reálnějšího rámce chatovací aplikace (client vs server).

VŠICHNI TADY UMŘEME!

Let's talk about your project

Just leave us a message using the contact form or contact our sales department directly. We will arrange a meeting and discuss your business needs. Together we will discuss the options and propose the most suitable solution.

Business department direct contact:

ApiTree s.r.o.

Francouzská 75/4, Praha 2 Vinohrady, 120 00

ApiTree s.r.o. is registered in the Commercial Register at the Municipal Court in Prague, under file no. C 279944

ID: 06308643
VAT: CZ06308643

Bank information

Česká spořitelna
Account number: 4885827379/0800
IBAN: CZ21 0800 0000 0048 8582 7379
SWIFT: GIBACZPX
ČSOB
Account number: 340250698/0300
IBAN: CZ31 0300 0000 0003 4025 0698
SWIFT: CEKOCZPP
Copyright 2020 ApiTree s.r.o. All rights reserved. The website was created and designed by ApiTree s.r.o.