Chat med dokumentasjonen din - Nøkkelkonsepter

Dette er del 2

Dette er del 2 av en artikkelserie hvor vi bygger vår egen chat bot som lar oss snakke med vår egen dokumentasjon. Del 1 er tilgjengelig her: Chat med dokumentasjonen din

Før vi starter å gå ned i detaljene for hvordan vi implementerer chatboten vår, kan det være nyttig å få en oversikt over de ulike byggeklossene vi skal ta i bruk, og hvordan de fungerer.

Det aller første konseptet vi må bli kjent med er "Context Injection".

LLMer som GPT-4 kan ikke tilegne seg ny informasjon fortløpende. De har bare kunnskap fra den teksten de originalt ble trent på. Eksempelvis vet ikke ChatGPT noe om hendelser etter September 2021. Om man spør hvem statsministeren i Norge er, vil svaret være Erna Solberg.
Vi må altså gi modellen vår en "jukselapp" med det den trenger å vite for å svare på spørsmålet vårt. Eksempelvise kan jeg si "I oktober 2021 ble Jonas Gahr Støre statsminister. Hvem er statsminister i Norge?". Vi vil da få det korrekte svaret. Denne jukselappen med relevant kunnskap, som modellen selv ikke har, kalles "Context Injection".

Spørsmålet da er hvordan vi skal generere denne jukselappen?
Vi trenger på en eller annen måte å finne riktig del av dokumentasjonen, som ikke overskrider den tillatte størrelsen for input i modellen. Og som gjerne er så konsis som mulig, da disse modellene som regel tar betalt for mengden tekst man sender inn i spørringene.

Vi trenger et langtidsminne. Et sted hvor all dokumentasjonen vår er oppbevart. Dokumentasjonen er jo allerede lagret et sted, så hvorfor ikke bare bruke datakilden hvor vi allerede har det lagret? Problemet her er at vi ikke har noen enkel måte å hente ut de delene som er relevant til spørsmålet vårt. Vi trenger å lagre dokumentasjonen et sted som lar oss finne tekst som er relevant, basert på hva spørsmålet vårt er.

Løsningen på dette er å kombinere en vektor-database sammen med det som kalles Embeddings.

La oss se nærmere på hva disse tingene er og hvordan de henger sammen.

Vektorer

For å kunne forklare hva en Embedding er, må vi først ha en felles forståelse for hva en vektor er: En vektor er en liste med tall. Verken mer eller mindre. Antall dimensjoner i en vektor, er det samme som antall elementer i vektoren. En 4-dimensjonell vektor kan se slik ut: [1,6,2,4]. En 1-dimensjonell vektor kan se slik ut: [8]. Det er ingen begrensninger på hvor mange dimensjoner en vektor kan ha.

Min, og jeg antar mange andres, mentale modell for vektorer stammer fra fysikk. Der er vektorer noe som blir illustrert med en pil i et 2D plan, eller i et 3D-rom. Vektorene har da enten 2 eller 3 "koordinater". En vektor i 2D planet kan eksempelvis ha disse verdiene: [2,4] . Den vil bli illustrert slik:

Om vi dropper pilen, og bare behandler vektoren som et koordinat, kan vi tegne den slik:

Det å se på vektorer som punkter i koordinatsystem kan hjelpe oss å tenke klart rundt et viktig faktum: vektorer har en avstand mellom hverandre, og denne avstanden kan måles.
Det er viktig å understreke at en vektor ikke er begrenset til 2 eller 3 dimensjoner. Disse dimensjonene gir intuitivt mening. Men intuisjonen bryter sammen når vi starter med 4-dimensjonale vektorer. Heldigvis bryr ikke matten som brukes for vektorer seg med hvor mange dimensjoner vi har.

Vi kan derfor regne på ting som avstanden mellom to vektorer uavhengig av hvor mange dimensjoner de har. Men hvordan hjelper dette oss? Her kommer Embeddings inn.

Embeddings

En Embedding er en vektor som representerer meningen til data. Kort forklart: ved å gi en tekst til en Large Language Model, f.eks. via et API kall, vil den kunne spytte tilbake en vektor som inneholder informasjon om hva denne teksten faktisk betyr. La oss ta et eksempel:

La oss si jeg ønsker å hente ut Embeddings for disse fire ordene:
Piano, Gitar, Stol, Sol

Dersom det ble brukt 2-dimensjonale vektorer, kunne det sett noe sånn ut:

Både piano og gitar er instrumenter, og ligger derfor nært hverandre. Ordene stol og sol ligner på hverande, men ikke i mening. De er to vidt forskjellige konsepter, og havner derfor langt fra hverandre.

Om jeg nå introduserer et femte ord, "banjo", kan jeg hente ut dens Embedding-vektor, for så å finne den vektoren i grafen over som er nærmest. Mest sannsynlig vil det være ordet "Gitar".

(For de som vil få en grundigere forståelse av dette anbefaler jeg denne videon fra Computerphile om embeddings)

Dette betyr at vi kan dele opp dokumentasjonen vår i biter, f.eks. hver paragraf, for så å generere Embedding vektorer for hver av dem. Deretter lagrer vi disse vektorene i en vektordatabase, sammen med dne originale teksten. De vektorene som da ligner på hverandre i mening, vil klynges sammen. Paragrafer som bærer lik mening vil være nær hverandre.

Med dette på plass kan vi bruke vektordatabasen vår med Embeddinger til å finne relevante paragrafer i dokumentasjonen. Når noen stiller et spørsmåle omgjør vi det til en Embeddingvektor. Deretter spør vi om hvilke vektorer i databasen som er nærest denne og som dermed har relevant informasjon for spørsmålet. Når vi har disse paragrafene kan vi gi denne informasjonen til GPT-4, før vi stiller den spørsmålet vi lurer på.

Sette alt sammen

Da har vi det vi trenger for å kunne sette alt sammen.

Steg 1: Populér vektor-databasen

Første del er å splitte opp dokumentasjonen i flere biter. Hver av disse bitene vil bli omgjort til en Embedding, og skal til syven og sist bli brukt som jukselapp til GPT. Man kan eksempelvis dele det opp slik at hver overskrift og tilhørende avsnitt blir sin egen bit. Eller at hver side blir sin eget bit. Eksperimenter med hva som fungerer best for din dokumentasjon.

Etter man har generert en liste med tekster, skal disse konverteres til en Embedding. Det finnes flere tjenester som kan gjøre dette. Vi skal ta en titt på konkrete måter å gjøre dette på senere, når vi skal implementere en Wikipedia chatboten i del 3.

Etter å ha hentet ut Embeddings for tekstene våre har vi nå en liste med vektorer, hvor hver av disse vektorene representerer "meningen" til teksten vi sendte inn. Vi vil da lagre alle disse i vektor-databasen vår, sammen med den original teksten og eventuell annen relevant informasjon.

Steg 2: Chat med dokumentasjonen din

Se for deg at dokumentasjonen din har en seksjon som omhandler hvordan man skal sette opp autentisering i nye løsninger, og at noen har bruk for denne informasjonen.

Hendelsesforløpet kan da se slik ut:

Bruker spør: "Hvordan setter jeg opp autentisering i et nytt prosjekt?"
Vi mottar dette spørsmålet, og konverterer det til en Embedding. Med Embeddingen vår, som altså representerer meningen til spørsmålet, kan vi spørre vektor-databasen om topp fem Embeddingvektorer som er nærest dette spørsmålet i mening. Seksjonene om autentisering vil da dukke opp. Vi henter ut disse, for å kunne gi dem som "jukselapp" til GPT-4.

Vi må nå bygge opp en spørring til GPT som gir resultatene vi er ute etter. Øvelsen å sette sammen en god spørring kalles ofte "Prompt Engineering". En god oppskrift på å skrive en spørring, eller "prompt", er å bruke følgende oppsett:

  1. Gi LLMen en identitet: "Du er en behjelpelig senior-utvikler som elsker å hjelpe folk med å finne svar på det de lurer på."
  2. Hva er oppgaven: "Gitt følgende avsnitt fra dokumentasjonen, svar på spørsmålet ved å kun bruke informasjonen som er tilgjengelig der"
  3. Eventuelle betingelse: "Om du er usikker og svaret ikke er tilgjengelig å finne i dokumentasjonen si 'Beklager, men jeg fant ikke noe som kan hjelpe deg med det du lurer på.'".
  4. Jukselapp/Context Injection: "Avsnitt fra dokumentasjonen: {Tekst-avsnittene vi fant i databasen vår.}"
  5. Spørsmål "Spørsmål: {spørsmål fra bruker}".
  6. Eventuelle ekstra hint: "Inkluder relaterte kodesnutter dersom de er tilgjengelige"

Totalt vil flyten se ca. slik ut:

Nå som vi har det overordnede konseptet på plass kan vi implementere et script som lar oss chatte med vilkårlige Wikipedia artikler.

Chat med dokumentasjonen din - Implementasjon