Ondra Bašista
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í:
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!
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.