LLM & Quiz avec Ollama, Quarkus & LangChain4j
Bien que je ne sois pas vraiment convaincu par les usages actuels de lâIA gĂ©nĂ©rative, ma curiositĂ© mâa poussĂ© Ă tester quelques modĂšles et Ă regarder comment ce nouvel outil pouvait ĂȘtre intĂ©grĂ© Ă une application.
Dans cet article, nous allons dĂ©crire la maniĂšre dont nous pouvons exĂ©cuter un LLM (Large Language Model) en local et lâutiliser au sein dâune application Quarkus. Cette application aura pour objectif de fournir des questions & rĂ©ponses de quiz. Ces donnĂ©es seront fournies par le LLM.
Installation du LLM local avec Ollama #
Pour exĂ©cuter un LLM en local, nous allons nous appuyer sur Ollama. Lâinstallation est trĂšs simple et je vous invite Ă vous rendre sur le site officiel pour le faire.
A lâheure oĂč jâĂ©cris ces lignes, Ollama est dispo sur Linux, MacOS et en preview sur Windows. Mes tests ont Ă©tĂ© rĂ©alisĂ©s sur un Macbook Pro M1 14â de 2021 avec 16Go de RAM. Ceci a Ă©tĂ© suffisant pour exĂ©cuter sans difficultĂ© des modĂšles tels que llama2 ou mistral.
Une fois lâinstallation rĂ©alisĂ©e, il suffit de lancer la commande suivante dans un terminal:
ollama run llama2
Lâoutil va ainsi lancer le tĂ©lĂ©chargement du modĂšle choisi (ici llama2). Une fois le tĂ©lĂ©chargement terminĂ©, vous pourrez saisir votre premier prompt. Par exemple:
>>> Can you tell me what Quarkus is in one sentence?
Quarkus is an open-source, reactive application platform for Java that provides a lightweight and efficient way to build microservices
and serverless applications with a focus on performance, scalability, and ease of development.
Si vous souhaitez consulter la liste des modĂšles disponibles dans Ollama, le site officiel dispose dâune page dĂ©diĂ©e.
Pour quitter la session Ollama en cours, saisissez le prompt â/byeâ.
Initialisation du projet Quarkus #
Maintenant que nous avons un LLM local Ă disposition, nous allons initialiser notre projet Quarkus. Pour ce faire, nous allons utiliser les extensions suivantes:
- quarkus-rest-jackson pour créer un endpoint retournant une question et ses réponses
- quarkus-langchain4j-ollama pour interagir avec le LLM:
- LangChain4j est un projet qui vise à simplifier les interactions avec les LLM en proposant une API unifiée et des outils couvrant les besoins les plus courants. Ainsi, il est possible de basculer entre les services LLM sans pour autant réapprendre à chaque fois leur API spécifique.
- Lâextension Quarkus LangChain4j permet lâintĂ©gration simple et rapide de LangChain4j dans un projet Quarkus.
quarkus create app dlemontagner.jquiz:jquiz-ai-poc --extensions=quarkus-rest-jackson,quarkus-langchain4j-ollama --no-code
cd jquiz-ai-poc
quarkus dev
Nous allons ensuite définir les clés suivantes dans application.properties:
# Activation des logs
quarkus.log.console.enable = true
quarkus.log.file.enable = false
quarkus.langchain4j.ollama.log-responses=true
quarkus.langchain4j.ollama.log-requests=true
# Configuration du modĂšle ollama
quarkus.langchain4j.ollama.chat-model.model-id=llama2:latest
quarkus.langchain4j.ollama.chat-model.num-predict=-1
Nous aurons ainsi la possibilitĂ© de voir les requĂȘtes en entrĂ©e et sortie de Ollama. Pour cet exemple, nous allons utiliser le modĂšle llama2 dans sa derniĂšre version. La clĂ© ânum-predictâ permet de ne pas se limiter dans la taille des rĂ©ponses renvoyĂ©es par le LLM.
Dans cet article, nous allons exclusivement utiliser Ollama. Mais, comme je lâai indiquĂ©, LangChain4j vise Ă basculer facilement vers dâautres services. Pour utiliser un autre LLM, il suffit donc de remplacer les clĂ©s Ollama par les clĂ©s spĂ©cifiques au service que vous souhaitez utiliser (par exemple Open AI). Le code en tant que tel restera le mĂȘme.
Génération des questions/réponses par le LLM #
Lâobjectif de cet exemple est de gĂ©nĂ©rer des questions et rĂ©ponses pour un quizz. Nous allons partir sur le format JSON suivant pour dĂ©crire une question et ses rĂ©ponses:
{
"question": "Which planet is known as the Red Planet?",
"answers": [
{
"text": "Jupiter",
"correct": false
},
{
"text": "Mars",
"correct": true
},
{
"text": "Saturn",
"correct": false
},
{
"text": "Venus",
"correct": false
}
]
}
Ceci se traduit par les records Java suivants:
package dlemontagner.jquiz;
import java.util.List;
public record QuizAIQuestion(String question, List<QuizAIAnswer> answers) {
}
package dlemontagner.jquiz;
public record QuizAIAnswer(String text, boolean correct) {
}
Il faut dorĂ©navant que nous soyons en mesure de dĂ©crire notre besoin au LLM. Ainsi, il faut lui expliquer lâobjectif de notre demande câest Ă dire le rĂŽle du LLM mais Ă©galement le format dans lequel nous souhaitons obtenir sa rĂ©ponse.
Voici le prompt qui mâa permis dâobtenir le rĂ©sultat attendu avec, pour exemple, la fourniture dâune question sur le thĂšme âelectric vehiclesâ:
You are a quiz questions provider.
Provide a question about "electric vehicles".
You will provide one question and four answers about this theme.
The theme should not be the answer as participants will know about the theme.
Only one of these answers is correct.
If you can't understand the theme or if you don't have any questions about it use the theme "general knowledge" instead of the given theme.
Never return the same question twice.
You must respond in a valid JSON format.
You must not wrap JSON response in backticks, markdown, or in any other way, but return it as plain text.
You must answer strictly in the following JSON format: {
"question": (type: string),
"answers": (type: array of dlemontagner.jquiz.AIAnswer: {
"text": (type: string),
"correct": (type: boolean),
}),
}
Retranscrire ceci au sein de notre application Quarkus est trÚs simple et se résume aux 5 étapes suivantes:
- CrĂ©er une interface (QuizAIProvider) annotĂ©e avec @RegisterAiService. Cette interface sera notre point dâentrĂ©e vers le LLM.
- Créer une méthode (newQuestion) correspondant à notre prompt. Cette méthode a un paramÚtre String theme qui nous permettra de préciser le thÚme sur lequel nous souhaitons que la question porte.
- Annoter la méthode avec @SystemMessage de façon à indiquer au LLM quel sera son rÎle
- Annoter la méthode avec @UserMessage de façon à passer le prompt. A noter que:
- Le prompt peut contenir la valeur des paramĂštres comme ici avec ${theme}
- Le format JSON souhaitĂ© nâa pas Ă ĂȘtre prĂ©cisĂ©. Ce tour de magie est opĂ©rĂ© par Quarkus & LangChain4j qui enrichissent automatiquement le prompt en identifiant que ma mĂ©thode retourne un record QuizAIQuestion.
- Ajouter Ă la mĂ©thode le paramĂštre int id annotĂ© par @MemoryId. En effet, le LLM est stateless et ceci permet de garder en mĂ©moire lâhistorique des Ă©changes de la session (identifiĂ©e grace Ă lâid). Ainsi, le LLM saura rĂ©pondre Ă lâexigence âNever return the same question twice.â
package dlemontagner.jquiz;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService
public interface QuizAIProvider {
@SystemMessage("You are a quiz questions provider")
@UserMessage(
"""
Provide a question about ${theme}.
You will provide one question and four answers about this theme.
The theme should not be the answer as participants will know about the theme.
Only one of these answers is correct.
If you can't understand the theme or if you don't have any questions about it use the theme "general knowledge" instead of the given theme.
Never return the same question twice.
You must respond in a valid JSON format.
You must not wrap JSON response in backticks, markdown, or in any other way, but return it as plain text.
""")
QuizAIQuestion newQuestion(@MemoryId int id, String theme);
}
CrĂ©ation du endpoint de lâAPI #
Maintenant que nous avons prĂ©parĂ© notre passerelle vers le LLM, il ne nous reste plus quâĂ crĂ©er le endpoint de lâAPI de façon Ă ce que lâutilisateur puisse soumettre un thĂšme puis rĂ©cupĂ©rer une question et ses rĂ©ponses.
Pour ce faire, il suffit dâinjecter notre QuizAIProvider puis dây faire appel dans la mĂ©thode correspondante Ă notre endpoint:
package dlemontagner.jquiz;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/quiz")
public class QuizResource {
@Inject
QuizAIProvider quizAIProvider;
@POST
@Produces(MediaType.APPLICATION_JSON)
public QuizAIQuestion newQuestion(String theme) {
return quizAIProvider.newQuestion(1, theme);
}
}
Enfin, nous pouvons tester le tout en rĂ©alisant une requĂȘte de ce type avec httpie:
http POST :8080/quiz --raw "electric vehicles"
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 253
{
"answers": [
{
"correct": true,
"text": "Batteries"
},
{
"correct": false,
"text": "Solar panels"
},
{
"correct": false,
"text": "Hydrogen fuel cells"
},
{
"correct": false,
"text": "Gasoline engines"
}
],
"question": "What is the primary source of energy for electric vehicles?"
}
Code source #
Retrouvez l'intégralité du code source dans ce repository.
Conclusion #
Comme vous avez pu le constater, il est trÚs simple de créer une premiÚre application basique utilisant un LLM.
Ceci nâest quâune brĂšve introduction aux LLM et Ă lâutilisation de LangChain4j via Quarkus. Il existe dâautres concepts plus avancĂ©s qui sont dĂ©jĂ disponibles au sein de ces outils. Pour en avoir un aperçu, vous pouvez jeter un oeil Ă dâautres exemples sur le repository de lâextension LangChain4j de Quarkus.
A noter que Spring propose également son intégration de LLM avec Spring AI. La documentation comporte notamment une section Ollama.
Pour conclure, nous nâavons pas fait de considĂ©rations architecturales pour cet exemple mais nous pouvons considĂ©rer le LLM comme un fournisseur de services comme les autres. Ainsi, dans une architecture hexagonale, le LLM se prĂȘte parfaitement Ă une implĂ©mentation sous la forme dâun adapteur SPI. Ainsi, vous pourrez facilement remplacer le LLM par une BDD peuplĂ©e de questions de meilleur qualitĂ©, Ă©crites avec amour par un humain đ