Bullshit-bekjemperen: En introduksjon til RAG med LLM

Mikkel Nylend
Systek
Published in
7 min readJan 24, 2024

Vi lever i en verden oversvømmet av informasjon. I dette informasjonslandskapet har sann, pålitelig og fersk informasjon blitt en verdifull valuta, noe som har gitt opphav til teknologigiganter som Google, og nå i det siste, OpenAI. “Large Language Models (LLM) som OpenAIs ChatGPT er den nye diamanten alle snakker om. Men LLMer har enn så lenge en stor svakhet, de snakker ikke alltid sant. Noen kaller dem “bullshit-generatorer”, da de har en tendens til å produsere innhold som er overbevisende, men fullstendig feil. Spør du ChatGPT om komplekse vitenskapelige teorier, historiske hendelser eller helt ferske nyhetsartikler, vil du svært ofte få hallusinasjoner og feil svar tilbake i en overbevisende tone. Men hva om det finnes en måte å forbedre faktasikkerheten og kunnskapen til LLMene betraktelig?

Møt Retrieval Augmented Generation (RAG).

Introduksjon

Teknikken stammer fra en artikkel i 2020: Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks [1]. RAG integrerer forhåndstrente språkmodeller med kontekstuell data som fører til mer relevante og korrekte svar. Enkelt sagt kombineres relevant data med forespørselen fra brukeren og sendes inn til LLMen. En RAG består av noen ulike steg og komponenter. Hvert steg kan håndteres på ulike måter og kan variere i kompleksitet. La oss se på hovedingrediensene.

RAG arkitekturen
  1. Data: En RAG starter med at vi har data vi ønsker at LLMen skal kunne bruke som et slags oppslagsverk. Dataen kan for eksempel være din private dokument-mappe på PCen din. For at delene som sendes inn til LLMen ikke skal være for store så splittes dataen ofte i mindre biter, også kalt chunks.
  2. Indeksering: Dokumentbitene blir så konvertert til vektorformat, dvs. en form for embedding. Dette gjøres for at det skal være mulig for RAGen å effektivt hente de mest relevante bitene. Opprettelsen av vektorene gjøres typisk med en embedding-modell som tar inn tekst og spytter ut en vektor. Vektorene kan deretter indekseres i en form for database.
  3. Forespørsel (prompt): Brukeren gir en forespørsel eller et spørsmål som LLMen skal fullføre. Dette brukes både direkte inn mot LLMen, men også for å hente ut relevant informasjon fra vektordatabasen. Den relevante informasjonen sammen med brukerens forespørsel blir så slått sammen i en forespørselmal (prompt template).
  4. Innhenting (retrieval): Dette steget omfatter prosessen der en søker gjennom vektordatabasen for å finne de mest relevante delene av dokumentene basert på brukerens prompt. Dette gjøres ofte med et likhetsøk med en vektor fra brukerens forespørsel. Vektoren blir opprettet på samme måte med embeddings-modellen som ble brukt under indekseringssteget. De mest relevante databitene blir lagt inn i forespørselmalen og sendt videre til LLMen.
  5. LLM (Large Language Model): Dette er hovedkomponenten i hver RAG. Det er denne delen av RAGen som gir deg resultatet til slutt, akkurat som om du skulle spurt en vanlig LLM. Teksten som går inn til LLMen er en sammenslått tekst av brukeren sin forespørsel og relevant data funnet under innhenting-steget. De relevante dataene gir LLMen kontekst og oppdatert informasjon som gjør at svarene blir mye mer presise.

Lag en RAG på 1, 2, 3…

Med Python-rammeverket LangChain og en API-nøkkel mot OpenAIs API så kan en lage en RAG med svært lite kode. La oss ta en titt.

Vårt eget datasett

For å få testet RAG skikkelig trenger vi et datasett som OpenAIs LLM aldri har sett før. Treningsdataen til modellen gpt-3.5-turbo har en cut-off fra september 2021. Det betyr at alt som har skjedd etter dette tidspunktet er ikke noe modellen har sett fra før. Siden vi jobber med et ganske så nytt rammeverk, LangChain, så vil det være en perfekt mulighet å bruke dokumentasjonen for dette rammeverket som et lite datasett. Dokumentasjonen er enkelt å hente fra GitHub-repositoryet. Vi kan så konvertere dokumentene til tekstdokumenter slik at de blir enklere å håndtere med LangChain.

LangChain lar oss enkelt opprette en datalaster for en gitt mappe. I dette eksempelet er datalasteren er satt opp til å scanne alle tekstdokumenter under mappen “lang_chain_docs_2024. I mappen finnes det deler av den nyeste dokumentasjonen til rammeverket LangChain (v0.1.0). Videre setter vi opp en splitter som deler større tekster opp i biter (chunks). Dette er for å kunne enklere plukke ut relevant informasjon fra større dokumenter.

loader = DirectoryLoader("./lang_chain_docs_2024", glob="**/*.txt", 
show_progress=True, recursive=True,
silent_errors=True, loader_cls=TextLoader)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(documents)

Indeksering til vektordatabasen

Når dokumentene er delt opp i passe små biter kan vi konvertere de til vektorer og lagre de i en vektordatabase. I dette tilfellet brukes OpenAIs embedding-modell, text-embedding-ada-002, som går via APIet deres. I tillegg brukes ChromaDB, som er en lokalt kjørende vektordatabase pent integrert i LangChain. Når vektordatabasen er konfigurert og dataen indeksert, kan vi lage innhenteren (retriever) som har ansvaret for å hente relevante biter fra databasen basert på prompten vår og et likhetssøk.

vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

Oppkobling mot OpenAIs API

For å gjøre det enkelt har LangChain laget en del maler for prompts. Dette er kjekt for å komme raskt i gang.

prompt = hub.pull("rlm/rag-prompt")

Forespørselmalen som lages automagisk for oss ser slik ut:

You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don’t know the answer, just say that you don’t know. Use three sentences maximum and keep the answer concise.\nQuestion: {question} \nContext: {context} \nAnswer:

Her blir {question} fyllt ut med brukerens forespørsel og {context} fyllt ut med relevante databiter fra datasettet vårt. Når malen er klar kan vi begynne på hovedretten, nemlig LLMen. For det enkleste oppsettet og de beste resultatene kan vi bruke OpenAI sitt API. APIet gjør at vi kan kalle på deres LLMer, som også ligger bak applikasjonen ChatGPT. Merk at for å kalle APIet så må det settes opp en API-nøkkel. Dette gjøres enkelt fra siden deres: https://platform.openai.com/api-keys. I dette eksempelet brukes modellen gpt-3.5-turbo, men vi kunne også ha valgt gpt-4 som ville gitt bedre svar til en litt stivere pris for hver forespørsel.

llm = ChatOpenAI(model_name="gpt-3.5-turbo")

Oppsett av RAGen

Til slutt kan vi sette opp prosesskjeden som utgjør RAGen. Dette gjøres på en elegant måte via LangChain Expression Language (LECL). Her settes stegene opp som en pipeline på tilsvarende måte som i Unix (piping). Til slutt kan vi kalle på RAGen med en hjemmesnekret forespørsel. I dette eksempelet spør vi om hvordan en kan skrive ut en graf over prosesskjeden vår i LangChain på ASCII-format.

rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
result = rag_chain.invoke("In LangChain, how can I get the graph"
" of a chain and print it as ascii?")
print(result)

20 sekunder senere og snaue 50 øre fattigere så får vi svaret:

To get the graph of a chain in LangChain, you can use the command “chain.get_graph()”. To print the graph as ASCII, you can use the command “chain.get_graph().print_ascii()”.

Fantastisk! Med cirka 10 kodelinjer har vi forbedret ChatGPT til å kunne svare mer korrekt og på ny data som den aldri har sett før. Svaret er helt korrekt. Av de 0,5 kronene brukt på denne forespørselen er cirka 98% engangsjobben som gjøres for å lage vektorene til datasettet. Det interessante her er at uten tilknytning til de nyeste dataene fra LangChain sin dokumentasjon så klarer ikke ChatGPT å svare dette spørsmålet. Her er svaret fra modellen uten RAG:

I’m sorry, but as of my last knowledge update in January 2022, there is no widely known or established programming or scripting language called “LangChain” that I am aware of. It’s possible that “LangChain” may be a new or obscure programming language or framework that has been developed after my last update, or it could be a term specific to a certain context or organization.

To help you further, please provide more information or context about “LangChain” or specify what you are trying to achieve, and I will do my best to assist you based on the information available up to my last update.

Siden vi nå har indeksert datasettet kan vi billig og raskt spørre modellen videre om hvordan en gjør ting i LangChain. For eksempel kan vi prøve en forespørsel til: “Can you describe the LangChain Expression Language in 3 sentences?”. Det tar kun 4 sekunder før vi får svaret:

The LangChain Expression Language (LCEL) is a declarative way to compose chains together, allowing for easy prototyping and production deployment without code changes. LCEL supports streaming, providing incremental chunks of output at the same rate as the raw tokens are generated. It also seamlessly integrates with LangServe for deployment.

Konklusjon

Rammeverk som LangChain og ChromaDB, kombinert OpenAIs API mot deres Large Language Models (LLM), gjør det svært enkelt å utføre Retrieval Augmented Generation (RAG) med egne data. RAG kan være et kraftig verktøy da det er med på å løse to av hovedutfordringene til LLMer. Rammeverket adresserer spesielt utfordringene med tendensen til å generere unøyaktig informasjon og mangelen på evne til å integrere kunnskap oppdatert etter deres siste treningsdata. Etter å ha testet RAG med OpenAIs LLM gpt-3.5-turbo, observerer vi at RAG-arkitekturen forbedrer svarene og gir en mulighet til å tilføre LLMer spesifisert og oppdatert data i et gitt domene.

For fullstendige kodeeksempler, sjekk ut tilhørende GitHub-repo:
https://github.com/mrminy/LangChainRAGExamples

Har du spørsmål eller kommentarer, ta gjerne kontakt på systek@systek.no.

Sjekk også ut våre tjenester og ledige stillinger på https://www.systek.no/

--

--