Skip to main content
David Le Montagner Home Page David Le Montagner

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 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:

  1. CrĂ©er une interface (QuizAIProvider) annotĂ©e avec @RegisterAiService. Cette interface sera notre point d’entrĂ©e vers le LLM.
  2. 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.
  3. Annoter la méthode avec @SystemMessage de façon à indiquer au LLM quel sera son rÎle
  4. 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.
  5. 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 😉