Skip to main content

HR-Bot van One16: hoe?

logos
Case

HR-Bot van One16: hoe?

30 April 2024 - reading time: 
8
 min.

Als je een beetje bijgebleven bent in de techwereld, heb je vast wel gehoord van LLM's. Deze indrukwekkende AI-systemen hebben enorme sprongen gemaakt in het begrijpen en genereren van menselijke taal. Klinkt leuk, maar hoe kunnen we deze krachtige LLM's nu eigenlijk gebruiken voor onze eigen doeleinden?

Wel, daar komt RAG om de hoek kijken. En wat is RAG dan juist?

Retrieval-augmented generation (RAG) is een techniek om de nauwkeurigheid en betrouwbaarheid van generatieve AI-modellen te verbeteren met feiten die worden opgehaald uit externe bronnen.

Goed getrainde LLM’s kunnen zeer snel reageren op algemene prompts, echter als je diepgaandere vragen stelt over een specifiek onderwerp, dan kom je dikwijls van een kale reis thuis.

Om dit te verhelpen dient het model gefinetuned te worden met zogenaamde “diepgaande kennis” over een bepaald onderwerp.

De auteurs van deze populaire paper hebben retrieval-augmented generation ontwikkeld om generatieve AI-services te koppelen aan externe bronnen, vooral bronnen die diepgaande details bevatten en zo toegevoegde waarde/kennis kunnen leveren.

De auteurs zien RAG dus als een techniek om de LLM te kunnen finetunen.

In ons geval van de HR-Bot zou een generatief AI Model pas nuttig zijn als het echte kennis heeft opgedaan van de policies, reglementen en handleidingen van One16.

Hoe gaat dit nu alles in zijn werk in onze use case?

Als software bedrijf zoeken we vooral flexibele oplossingen die passen bij onze expertise.  Een AI assistent, chatmodel of RAG component is voor ons een architectuurblok die we naadloos in een totaal oplossing moeten kunnen passen.

Aangezien python de meest populaire en relevante taal is voor alles wat maar met AI te maken heeft, zou dit een logische keuze zijn.  Python zit zeker in onze toolbox, maar gezien onze expertise kijken we ook altijd graag kritisch rond naar iets dichter bij huis.  Java heeft zeker ook enkele voordelen ten opzichte van python en dan kijken we vooral naar alles wat ruikt naar multithreading and concurrency.

Via deze weg, komen we algauw terecht bij een bruisende community rond langchain4j dat het mogelijk maakt om op een eenvoudige manier AI/LLM integraties in te bouwen in java applicaties.
 

langchain4j

Langchain4j heeft de nodige componenten om RAG technieken toe te passen en zo je favoriete LLM te voeden met relevante informatie.

Kijk zeker eens naar wat LangChain4j te bieden heeft, maar in het kort, bieden ze bouwblokken aan om documenten te laden, deze te parsen, transformeren en vervolgens te embedden gebruikmakende van zogenaamde embedding models.

Langchain4j biedt verschillende integraties aan met embeddingmodels, vector stores, LLM’s en builder patterns om je RAG flow op te bouwen.

Ingestion

De eerste stap is om onze documenten te laden, ze op te splitsen in segmenten, en vervolgens zogenaamde embeddings te genereren via een embeddingmodel. Embeddings zijn in feite niets meer dan een vectorrepresentatie van onze teksten. Deze kunnen dan worden opgeslagen in een embeddingopslag die kan worden bevraagd door een retriever.

Langchain4j biedt eigen embeddingmodellen en in memory embeddingstores aan zodat je laagdrempelig kan experimenteren.  Zodra je meer in een productiesetting komt, ga je beter kijken naar andere oplossingen.  Bij ons is dat Azure OpenAI geworden.

Ze bieden een hele catalog aan van modellen die eenvoudig gedeployed en gebruikt kunnen worden.  Langchain4j heeft een Azure OpenAI integratie klaar voor gebruik, waardoor het in gebruik nemen van een embeddingmodel een fluitje van een cent is.

Gebruikmakende van Spring Boot kunnen we deze modellen als Bean exposen naar onze applicatie toe. 

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j-azure-open-ai</artifactId>
   <version>${langchain4j.version}</version>
</dependency>

 
@Bean
public EmbeddingModel azureOpenAiEmbeddingModel() {
  EmbeddingModel embeddingModel =   AzureOpenAiEmbeddingModel.builder()
  .apiKey("...")
  .endpoint("...").
  .deploymentName("...")
  .build();
  return embeddingModel;
}
deployments

De documenten zelf kunnen bewaard worden op Azure Storage, waar LangChain4j wederom een documentloader component voorziet.  De documenten kunnen via deze weg eenvoudig opgehaald, geparsed en vervolgens opgesplitst worden in segmenten.  Ook hier zijn verschillende splitters voorzien.

Zodra we onze embeddings beet hebben kunnen we deze dan opslaan in een zogenaamde embeddings store, dit is een database die goed kan omgaan met vectors en snel gequeried kan worden.  Aangezien de data representatie hier resulteren in vectors, zijn vector databases de natuurlijk reflex.  Bij Azure is Cosmos DB (Mongo vCore) een logische keuze.  

Een weetje: vanaf je de onderliggende M40 clusters gebruikt, kan je opteren voor de nieuwere krachtere HNSW vector index, maar bij de free tier kan je ook gebruik maken van IVFFLAT.

De HNSW is vooral veel sneller, maar complexer om op te bouwen en vraagt ook veel meer geheugen, wat vermoedelijk de reden is dat deze pas aangeboden kan worden vanaf een bepaalde tier.


@Bean
public EmbeddingStore<TextSegment> mongoEmbeddingStore() {
 MongoClient client = ...;

 AzureCosmosDbMongoVCoreEmbeddingStore embeddingStore = AzureCosmosDbMongoVCoreEmbeddingStore
  .builder()
  .mongoClient(client)
  ...  
  .createIndex(true)
  .kind("vector-ivf") //available on all tiers
  ...
  .dimensions("<dimensions in ada 2>")
  ...
  .build();
 return embeddingStore;
}
Retrieval

Retrieval van de data gebeurt op dezelfde manier, de query zal namelijk via ons embedding model passeren.  Deze "query embedding" haalt de relevante segmenten op uit onze embedding store en reikt die dan samen met de relevante segmenten aan onze LLM aan zodat die ons een antwoord kan geven. 
Dit totaalpakket is dan onze "User Message".

Soms is het nodig om verschillende segmenten te combineren en te aggregeren alvorens deze te te verpakken.  

De contentaggregator kan gebruikt worden als we bijvoorbeeld een scoringmodel gaan gebruiken om content te herevalueren indien we merken dat een eenvoudige vector semantische match onvoldoende is.  

Herinner u dat het ontvangen van onze documentsegmenten niet meer is dan een semantische query op een vector database (onze embeddingstore).

Tijdens onze PoC merkten we dat we hier geen nood aan hadden.  Vermoedelijk omdat onze policies heel wederzijds exclusief zijn opgesteld, wat eigen is aan ons probleemdomein.

Onze RAG component

Nu we alle puzzelstukken samen hebben kunnen we onze RAG component samenstellen.

RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor
.builder()
.contentRetriever(contentRetriever)
//.contentAggregator(reranker)
.contentInjector(contentInjector)
.build();

De retriever is onze embeddingstore met bijhorend embeddingmodel.  Een contentinjector kan dan weer gebruikt worden om een prompttemplate te voeden aan de LLM of zoals in ons geval om metadata aan te leveren aan het model zoals de filename.

Chatmodel

Als chatmodel hebben we OpenAI's GPT 3.5 Turbo gebruikt, één van de voordelen om dit via Azure af te nemen is vooral de disclaimer die erbij komt.  Je eigen data wordt niet gedeeld met een 3de partij, zal niet gebruikt worden als trainingsdata en niet gedeeld worden met OpenAI.

Data privacy Microsoft OpenAI Services

Als chatmodel gebruiken we streaming chatmodel zodat de responses als een stream binnenkomen we hierop kunnen ingrijpen.

@Bean
public StreamingChatLanguageModel azureOpenAiStreamingChatModel() {
StreamingChatLanguageModel chatModel =
AzureOpenAiStreamingChatModel
  .builder()
  .endpoint("...")
  .apiKey("...")
  .temperature("...")
  .topP("...")
  .maxTokens("...")
  .frequencyPenalty("...")
  .presencePenalty("...")
  .timeout("...") .deploymentName("...")
  .build();
return chatModel;
}
Tools

We kunnen aan onze AI service ook tools aanreiken.  Hierdoor kan onze LLM in zijn response ook een function call embedden.  Bij het ontleden van de response zal dan effectief een call gebeuren.  Waarbij het resultaat van deze tool dan terug in een nieuwe call naar de LLM vloeit, deze keer kan de LLM dan wel een goed antwoord vormen op basis van deze additionele informatie.

Ideaal voor dynamische data, die ergens te vinden is in een database of een externe API.  Denk bijvoorbeeld in onze case aan onze verlofplanning.

Eenvoudig is dit niet, de LLM moet heel bewust gemaakt worden wat de intentie van de tool is en hoe hij deze kan gebruiken.

Gelukkig heeft ons model Azure OpenAI model tool support, wat niet noodzakelijk bij alle modellen het geval is.

Hierbij een voorbeeld van hoe we contact details kunnen ophalen op basis van een naam.  Onze contactservice is een query service die specifiek hiermee kan omgaan.

@Tool(""" Get the contact details of a specific person of One16 using the name """)
public String getContactDetailsForName(@P("The name of the person for which the contact details should be returned") String name) {
  ...    
  return contactService.getContactDetails(name);
}
Onze AI Service

Dit alles tesamen zorgt dat we eenvoudig onze HR AI Service kunnen bouwen.

AiServices.builder(HrChatAgent.class).
streamingChatLanguageModel(...)
.retrievalAugmentor(...)
.tools(hrTools)
.chatMemory(...)
.build();

De HRChatAgent zal dan als Spring Bean exposed worden en kunnen we gebruiken als elke andere architectuur component.

Cool stuff!
 

Sustainable AI

Het is bekend dat AI de motor zal worden van heel wat meer CO2 uitstoot de komende jaren.  Microsoft heeft echter gecommit om hier iets aan te doen.  
Ook wijzelf als gebruikers kunnen hier iets aan doen door bijvoorbeeld te zorgen dat we niet zomaar the biggest and baddest model gebruiken.  Kleinere modellen hebben immers minder computing power nodig, wat minder co2 uitstoot met zich meebrengt.  Ook bijvoorbeeld caching technieken kunnen zeker helpen, om niet telkens dezelfde vragen te moeten beantwoorden via het model.
Gelukkig zijn er verschillende initiatieven en onderzoeken die ons hierin kunnen ondersteunen!

Referenties