Mikko Lehtimäki
Perustaja, Chief Data Scientist
icon

Tekoälyinsinöörin työkalut

Suuret kielimallit (LLM) muokkaavat tapaa, jolla tekoälysovelluksia kehitetään. Niille on löydetty käyttösovelluksia monissa eri tehtävissä, joista tällä hetkellä kaikkein kiinnostavimpia ovat autonomiset agentit!

Suurten kielimallien menestys perustuu niiden kykyyn "ymmärtää" ja generoida tekstiä. Kommunikoimme tekstin välityksellä kaiken aikaa, kaikkein tärkeimpänä ehkä koodi ja ohjeet, joita annamme tietokoneillemme tekstimuodossa. Kyky manipuloida tekstiä antaakin suurille kielimalleille laajan valikoiman työkaluja, joilla olla vuorovaikutuksessa meidän ja toisten kielimallien kanssa. Tämä sekä lisää tuottavuutta että auttaa ratkaisemaan haastavia ongelmia.

Tässä blogikirjoituksessa annamme käytännön ohjeet oman LLM-pohjaisen sovelluksen toteuttamiseen. Rakennamme tekoälyn, joka käyttää sinun dataasi tehtävien suorittamiseen. Olemme valinneet tekoälyn tehtäväksi poimia strukturoitua dataa tekstidokumenteista. Esimerkkiä on helppo soveltaa myös keskustelunomaisiin vuorovaikutuksiin tai generatiivisempiin tehtäviin.

Olemme aiemmin jakaneet joitain suosikkejamme NLP-työkaluista sekä kirjoittaneet osaamisesta, joita tarvitaan tekoälysovellusten kehittämisessä. Sovelletun tekoälyn insinööri (Applied AI Engineer) on se, joka rakentaa tekoälyn mahdollistamat ratkaisut perusmallien ja multimodaalisten mallien päälle sekä hienosäätää niitä teollisuuden tarpeisiin. Tässä kirjoituksessa palastelemme tekoälytuotteen rakentamisprosessiin ja kerromme työkaluista, jotka takaavat erinomaisen lopputuloksen.

Kun haluamme suurten kielimallien tekevän jotain yksityiseen dataamme perustuen, annamme useimmiten tämän datan kontekstina LLM-kyselyille. Mutta koska suuret kielimallit voivat käsitellä vain rajallisen määrän tekstiä yhdellä kerralla, tarvitsemme älykkäitä strategioita varmistaaksemme, että oleellinen tieto kontekstista välittyy kielimallille. Esittelemme pian tarvittavat työkalut.

Toteutamme sovelluksen, joka kerää, upottaa, tallentaa, hakee, analysoi ja todentaa dataa. Tarkemmin sanottuna

  1. keräämme sisältöä blogikirjoituksista

  2. laskemme vektoriupotukset kerätystä sisällöstä

  3. tallennamme tiedot vektoritietokantaan semanttista hakua varten

  4. teemme dataan kyselyitä vapaamuotoisella tekstillä

  5. käytämme dataa yhdessä suurten kielimallien kanssa tehtävän suorittamiseen

  6. varmistamme, että saamme vastauksena strukturoitua JSON-muotoista dataa!

Käytämme Qdrantia, Guardrailsia, LlamaIndexiä (GPT Indexiä), LangChainia ja OpenAI:ta. Löydät sovelluksen koodin kokonaisuudessaan Github-repositoriostamme osoitteesta https://github.com/Softlandia-Ltd/applied-ai-engineering.

Jos aihe kiinnostaa sinua, olemme järjestämässä siitä ilmaisen tapahtuman (livenä ja verkossa). Se on LLM-erikoistapahtuma data science infrastructure -sarjassamme. Ilmoittaudu täällä: https://www.eventbrite.fi/e/data-science-infrastructure-meetup-32023-tickets-600138017967 🤩

Nyt asiaan!

Rakennetaan tekoälyllä toimiva blogianalyysityökalu 🛠️ 

Aloitamme lataamme ympäristömuuttujat .env-tiedostosta, joka pitää arkaluontoiset API-avaimemme turvassa. Tiedosto myös tallentaa joitain LLM-asetuksia, mikä helpottaa mallien käyttöönottoa pilvessä, esimerkiksi:

dotenv.load_dotenv(override=True)
chunk_len = 256
chunk_overlap = 32
doc_urls = [
     "https://softlandia.fi/en/blog/the-rise-of-applied-ai-engineers-and-the-shift-in-ai-skillsets",
     "https://softlandia.fi/en/blog/real-time-data-processing-with-python-technology-evaluation",
     "https://softlandia.fi/en/blog/scheduling-data-science-workflows-in-azure-with-argo-and-metaflow",
]

# Setup OpenAI, we have these settings in a .env file as well
openai.api_key = os.environ["OPENAI_API_KEY"]
embedding_model = os.environ["EMBED_MODEL"] # text-embedding-ada-002 most likely
text_model = os.environ["TEXT_MODEL"# text-davinci-003 most likely

Koodi luo URL-osoitteiden listan blogikirjoituksista, jotka halutaan analysoida. chunk_len ja chunk_overlap ovat tarpeen LLM:n rajoitusten selättämiseksi: malli voi käsitellä vain rajoitetun määrän merkkejä (tokens) yhdellä kutsulla. Siksi jaamme blogikirjoitusten tekstit pieniksi lohkoiksi (chunk), jotka voidaan helposti syöttää LLM:lle.

Sitten määrittelemme kokoelman (collection) nimen Qdrant-vektoritietokantaa varten, asetamme Qdrantille isännän (host) ja portin sekä määrittelemme asiakkaan (client) tälle tietokannalle:

collection_name = "softlandia_blog_posts"
qdrant_host = os.environ["QDRANT_HOST"]
qdrant_port = 6333  # Qdrant default
qdrant_api_key = os.environ["QDRANT_API_KEY"]
qdrant_client = QdrantClient(
    url=qdrant_host,
    port=qdrant_port,  
    api_key=qdrant_api_key,
)

Qdrantin avulla tallennamme vektoreita ja niihin liittyviä hyötykuormia (payload) kokoelmaan. Qdrant-kokoelmat ovat kuin taulukoita SQL-tietokannoissa. Vektoritietokanta mahdollistaa vastaavien vektorien hakemisen tehokkaasti määrittelemäämme metriikkaa (yleensä kosini-) käyttäen. Samalla suodatamme hakutuloksia sattumanvaraisen metatiedon perusteella. Tarkka hakeminen on yksi perusmekanismeista, joiden avulla annamme LLM:ille yksilöllistä dataa kyselyhetkellä. Yllä asetamme osoitteen ja API-avaimen Qdrant-pilviklusterin käyttöä varten, vaikka heidän avoimen lähdekoodinsa ansiosta voimme helposti myös itse isännöidä omia klustereita tai jopa toteuttaa prototyypin in-memory-klustereilla.

Nyt asetamme upotusfunktiomme, joka käyttää muuttujaa embedding_model. Käytämme Adaa, se on tarkka ja edullinen! Upotusten avulla laskemme tekstille numeerisen esityksen, jotta voimme arvioida tekstien (tai kuvien) samankaltaisuutta ohjelmallisesti. Kun käyttäjä esittää kysymyksen, upotamme myös sen ja noudamme sitten semanttisesti samankaltaisia tekstilohkoja vektoritietokannasta antaaksemme LLM:lle tietoa kontekstista.

embed_model = LangchainEmbedding(
    OpenAIEmbeddings(
        query_model_name=embedding_model
    )
)
llm = OpenAI(model_name=text_model, max_tokens=2000)
llm_predictor = LLMPredictor(llm=llm)

Tässä OpenAIEmbeddings on LangChainin kääre (wrapper) OpenAI:n upotusrajapinnoille, ja OpenAI on LangChainin kääre OpenAI:n tekstintäydennysrajapinnoille. LangChain on avoimen lähdekoodin projekti, joka mahdollistaa vuorovaikutuksen LLM:ien kanssa ja tehtävälähtöisten sovellusten rakentamisen LLM-kutsujen avulla. Me pidämme LangChainin käyttöliittymästä, koska se tarjoaa yhteisen sisääntulopisteen eri malleille. Jos vaihdamme upotusmallia, käytämme vain eri LangChain-wrapperia, eikä koodia tarvitse muuten muuttaa. Tietysti LangChainilla voi tehdä paljon enemmänkin, kuten määritellä tekstien tiivistämistehtäviä tai autonomisia agenteja, jotka suorittavat toimenpiteitä tavoitteiden saavuttamiseksi. Kaikki tämä helpottuu, kun LLM-kutsuilla on yhteinen API.

Käärimme LangChain-objektit edelleen LlamaIndexin LangchainEmbedding- ja LLMPredictor-luokkiin. LlamaIndex on kokonaisvaltainen ratkaisu, joka mahdollistaa vuorovaikutuksen tietoaineistosi kanssa käyttäen LLM:iä. Se on huikea projekti, joka auttaa tekstin ja datan noutamisessa LLM-kyselyjä varten. Tämän se tekee antamalla meidän määrittää erilaisia indeksejä tietojemme päälle. Jokaisella indeksillä on oma logiikkansa kontekstitiedon tarjoamiseksi LLM-kehotteillemme (prompts). Tässä Llama-index loistaa.

Käytämme LlamaIndexiä noutamaan käyttäjän kyselyn perusteella parhaiten vastaavan tekstidatan Qdrant-tietokannastamme sekä generoimaan vastauksen kyselyyn noudetun datan perusteella. Muokataanpa vielä tarkemmin, miten LlamaIndex tekee tämän:

splitter = TokenTextSplitter(chunk_size=chunk_len, chunk_overlap=chunk_overlap)
node_parser = SimpleNodeParser(
     text_splitter=splitter, include_extra_info=True, include_prev_next_rel=False
)
prompt_helper = PromptHelper.from_llm_predictor(
     llm_predictor=llm_predictor,
)
service_context = ServiceContext.from_defaults(
     llm_predictor=llm_predictor,
     prompt_helper=prompt_helper,
     embed_model=embed_model,
     node_parser=node_parser,
)

NodeParser määrittää, miten teksti otetaan käyttöön vektoritietokantaamme. Parametrisoimme LangChainista tekstin jakajan , joka käyttää aiemmin määrittelemäämme lohkon pituutta ja päällekkäisyyden parametreja (muista, että LLM:t voivat käsitellä rajoitetun määrän tekstiä yhdellä kutsulla). PromptHelper kertoo LlamaIndexille, minkä LLM:n olemme määritelleet käytettäväksi. Välitämme mukautetun kehotteen ja indeksin luonnin ServiceContextin avulla. Määritämme siinä myös upotusmallin.

Nyt olemme määrittäneet tietokantamme upotettua tekstidataa varten ja määritelleet mallit upotusten laskemiseksi ja LLM-kyselyjen tekemiseksi. Molemmat on yhdistetty LllamaIndex-objekteiksi. On aika hankkia dataa!

reader = download_loader("BeautifulSoupWebReader")
loader = reader(website_extractor={"softlandia.fi": slreader})
documents = loader.load_data(
    urls=doc_urls, custom_hostname="softlandia.fi"
)

Voimme käyttää LlamaIndex-lataimia (loader) hakemaan dataa monista lähteistä, kuten verkkosivuilta, Github-repositorioista ja YouTubesta. Käytämme BeatifulSoupia lukemaan Softlandia.fi-blogikirjoituksia. Mukautamme tuloksia hieman website_extractorin avulla. Tämän tuloksena documents on lista LlamaIndex-dokumentteja, jotka ovat valmiita toimimaan yhdessä vektorivarastojen ja LLM:ien kanssa! Tallennamme datan Qdrantiin määrittelemällä LlamaIndex-vektorin indeksin:

index = GPTQdrantIndex.from_documents(
     documents,
     client=qdrant_client,
     collection_name=collection_name,
     service_context=service_context,
)

Tässä vaiheessa välitämme tekstidatamme (documents), tietokanta-asiakkaamme (database client) sekä LLM:n ja upotusmallin asetukset (service_context). Pellin alla LlamaIndex jakaa datan pieniksi lohkoiksi, kutsuu upotusmalliamme saadakseen vektoriedustukset, liittää vektoreihin metatietoja ja tallentaa kaiken Qdrantiin.

On aika kysellä LLM:ltä kysymyksiä!
task = "List technologies that are mentioned in the blog posts, and their date of mention."
result = index.query(
     task,
     similarity_top_k=3  # Increase this to get more results
)

Määrittelemme tehtävän: etsitään blogiteksteissä mainitut avoimen lähdekoodin teknologiat ja blogikirjoitusten julkaisuajat. similarity_top_k määrittelee, montaako blogikirjoitusten tekstilohkoa käytetään kontekstina. Mitä suurempi luku, sitä enemmän aikaa ja rahaa kyselysi vie. 😇 Kun tämä kysely suoritetaan, LlamaIndex upottaa kyselyn Qdrantiin ja tekee sinne vektorihaun, jossa vektorien samankaltaisuudet lasketaan tehokkaasti suoraan tietokantapalvelimella. Sitten LlamaIndex lisää noudetun tekstin LLM-kutsuihin ja ketjuttaa kutsut siten, että saamme yhden yhtenäisen LLM-vastauksen. Erittäin siistiä!

Kun kysely on suoritettu, result.response sisältää LLM:n tekstitulosteen, joka on nähnyt personoidun datamme. Tutkimallaresult.source_nodesia voit nähdä tarkalleen mitä tietoja käytettiin vastauksen syntetisointiin.

LLM:istä pitää ottaa huomioon, että ne tuottavat oletusarvoisesti jäsentämätöntä tekstiä, ja vastauksen muoto vaihtelee paljon mallista toiseen. Esimerkiksi ChatGPT-3.5-turbo on paljon keskustelevampi kuin Davinci-sarja. Tarvitsemme jonkinlaisen menetelmän varmistaaksemme, että mallin tuottama tulos on ennustettavissa, ja mieluiten konekielisessä muodossa. Saamamme vastaus näyttää jotakuinkin tällaiselta: 

Cloud Native Solutions (April 5, 2023)
Sensor Fusion & IoT (April 5, 2023)
Software Consulting (April 5, 2023)
Kubernetes (February 13, 2023)
Python APIs (February 13, 2023)
Bytewax (February 14, 2023)
Vuorossa Guardrails.AI!

Guardrailsin avulla voimme määrittää tarkasti LLM:n tulosteen sisällön ja rakenteen. Lisäksi voimme validoida tulosteen ja tehdä tarvittaessa korjaavia toimenpiteitä. Tämä on ehdottoman tärkeää, kun haluamme hyödyntää LLM:n tulosteita ohjelmallisesti. Validointi tapahtuu XML-pohjaisen RAIL-määrittelyn avulla seuraavasti:

TECHNOLOGIES_SPEC = """
<rail version="0.1">
  <output>

    <list name="technologies"  format="length: 1" on-fail-length="reask">
      <object>
        <string name="item" description="name of the technology"/>
        <date name="date" date-format="%Y-%m-%d"/>
      </object>
    </list>

  </output>
  <prompt>
  
    @xml_prefix_prompt

    {output_schema}

    {{task}}

    {{text}}

    @json_suffix_prompt_v2_wo_none

  </prompt>
</rail>
"""

Yllä oleva määrittely <output>-tagien sisällä kertoo LLM:lle, että sen tulisi tuottaa JSON-lista objekteista, joilla on kentät "item" ja "date". Päivämäärän tulee olla muodossa vuosi-kuukausi-päivä. Käytettävissä on iso liuta validoijia, kuten listan pituuden tarkistaminen tai sen varmistaminen, että tuloste on kelvollista Python-koodia. Todella kätevää! <prompt>-osiossa käytämme joitain ennalta määriteltyjä lyhenteitä (@xml_prefix_prompt, @json_suffix_prompt_v2_wo_none) ohjeistamaan LLM:ää, missä muodossa haluamme tulosteen. {output_schema} jäsennetään <output>-tageista ja annetaan LLM:lle promptissa, jotta se tietää, millaista tulostusmuotoa käyttää.

Näin toteutamme koodissamme validoinnin. Ensin määrittelemme Guard-objektin annetusta RAIL-määrittelystä (jonka tuomme toisesta tiedostosta nimeltä blog_guard.py): 

guard = gd.Guard.from_rail_string(blog_guard.TECHNOLOGIES_SPEC)
guard_task = "Format the technologies and their date from the text below. Only list one technology per item."
raw_llm_output, validated_output = guard(
     llm,  # We can pass any callable
     # Task and text keys are defined in our RAIL spec
     prompt_params={"task": guard_task, "text": result.response},
     num_reasks=1,
)

guard_taskilla annamme lisäohjeita kertoaksemme LLM:lle tavoitteemme. Tämä tehtävä lisätään RAIL XML:ssä nähtävään {{task}} paikanpitäjään (placeholder). guard()-kutsun ensimmäinen argumentti on LLM-funktio, ja voimme kätevästi välittää sille LangChain-callablen tai minkä tahansa muun, kuten openai.Completions.createn. Lisäksi käskemme Guardrailsia pyytämään, että LLM korjaa tulostetta kerran, jos se ei läpäise listan pituuden validointia vähintään kerran.

Lisäämme edelliseen LLM-kutsun kyselyyn saamamme vastauksen {{text}} paikanpitäjään (placeholder). Oikeastaan aiempi kyselymme teknologioiden ja päivämäärien saamiseksi voi palauttaa vastauksen hyvin eri muodoissa. Näin pyydämme LLM:ää muotoilemaan vastauksen asianmukaisesti. Tämä on yksinkertaisin tapa validoida LLM:n tulosteet (outputs). Guardrails tarjoaa kuitenkin myös syvemmän integraation sekä LlamaIndexille (output parsereilla ) että LangChainille (Guardrails-integraatio). Kannattaa tutustua niihin!

validated_outputin pitäisi olla sanakirja (dictionary), koska Guardrails käsittelee validoinnin ja muunnoksen. Voimme pretty printata sen. Tuloksen pitäisi näyttää suunnilleen tällaiselta:

{
    "technologies": [
        {
            "item": "NLP Solutions: Strategies and Tools",
            "date": "2023-04-05"
        },
        {
            "item": "Cloud Native Solutions",
            "date": "2023-04-05"
        },
        {
            "item": "Sensor Fusion & IoT",
            "date": "2023-04-05"
        },
        {
            "item": "Software Consulting",
            "date": "2023-04-05"
        },
        {
            "item": "Kubernetes",
            "date": "2023-02-13"
        },
        {
            "item": "Python APIs",
            "date": "2023-02-13"
        },
        {
            "item": "Bytewax",
            "date": "2023-02-14"
        }
    ]
}

Ja rakenne on täydellinen :) 

Näin saat luotua omasta datastasi kokonaisen sovelluksen, joka tuottaa strukturoituja tulosteita! Tekoälymaailman avoimen lähdekoodin työkalut ovat kehittyneet viime kuukausina aika uskomattomalla tavalla. Ne ovat muuttaneet tapaa, jolla kehittäjät rakentavat ja ottavat käyttöön huippuluokan sovelluksia sekä mahdollistaneet kaikille pääsyn tehokkaisiin tekoälyratkaisuihin. Näiden työkalujen nopean lisääntymisen myötä tekoälyinsinöörit voivat tehdä arvokkaita oivalluksia, automatisoida monimutkaisia tehtäviä ja luoda innovatiivisia tuotteita. Avoimen lähdekoodin elinvoimaisuus edistää yhteistyötä, luovuutta ja jatkuvaa kehittämistä sekä ajaa tekoälyvallankumousta eteenpäin hämmästyttävällä vauhdilla!

Softlandian tiimi julkaisi juuri oman keskustelevan tekoälyratkaisunsa yrityskäyttöön. YOKOT.AI on rakennettu hyödyntäen tämän hetken parasta LLM-teknologiaa, ja se tuo tekoälyvallankumouksen päivittäiseen liiketoimintaan.

Olethan meihin yhteydessä, jos haluat oppia lisää LLM-pohjaisten sovellusten rakentamisesta!