Pour lancer ce projet vous avez besoin:
- Java 17
Une fois le projet ouvert, vous pouvez installer les dépendances via la commande:
./mvnw clean package
Le suite du codelab aura lieu dans la classe ChatBot
Assurez-vous également que Ollama
est démarrer, accessible et que le modèle OpenHermes est bien disponible.
Pour cela vous pouvez accéder à l'URL suivante: http://localhost:11434/api/tags et valider que OpenHermes est bien présent.
Maintenant que tout est installé, nous allons pouvoir démarrer notre première application.
Afin d'appeler notre modèle, nous allons utiliser LangChain.
LangChain Community contient les intégrations pour les applications tierce comme Ollama.
Dans la class ChatBot
, vous pouvez ajouter l'imports suivant :
import dev.langchain4j.model.ollama.OllamaChatModel;
Notre modèle est actuellement accessible via l'URL http://localhost:11434
Nous allons créer l'objet permettant d'intéragir avec Ollama via le code suivant:
OllamaChatModel llm = new OllamaChatModel.OllamaChatModelBuilder()
.baseUrl("http://localhost:11434")
.modelName("openhermes")
.build();
Une fois cet objet créé nous allons pouvoir intéragir avec le modèle openhermes. Pour cela on déclare un prompt:
var prompt = UserMessage.from("Who are you ?");
Puis on invoque le modèle :
var response = llm.generate(List.of(prompt));
System.out.println(response.content().text());
Executer la méthode main de la classe ChatBot
pour lancer la première inférence du model.
Et voila! Nous avons effectué notre premier appel.
LangChain fournit un ensemble de fonction et d'utilitaire permettant de configurer plus finement notre application.
Dans un premier temps, rendons notre application un peu plus vivante. Plutôt que de générer une réponse d'un coup, LangChain nous permet de streamer le flux de la réponse.
Pour cela, il faut modifier le type de model utiliser pour un model qui implémente l'interface StreamingChatLanguageModel
.
On modifie la déclaration de notre model par le code suivant ainsi que l'appel au model:
OllamaStreamingChatModel llm = new OllamaStreamingChatModel.OllamaStreamingChatModelBuilder()
.baseUrl("http://localhost:11434")
.modelName("openhermes")
.build();
var prompt = UserMessage.from("Who are you ?");
llm.generate(List.of(prompt), new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String s) {
System.out.print(s);
}
@Override
public void onError(Throwable throwable) {
}
});
Réexecuter le fichier pour voir la différence
Afin de limiter les hallucinations des modèles, il est possible de faire varier le paramètre temperature
.
Ce paramètre (compris entre 0 et 1) permet de définir la "créativité" du modèle.
Plus la valeur est proche de 1, plus le modèle va pouvoir halluciner.
Plus la valeur est proche de 0, plus le modèle va être déterministe.
Règles de bases
- Pour des taches de transformation (correction de fautes, extraction de données, conversion de format) on vise une temperature entre 0 et 0.3
- Pour des taches d'écriture simple, de résumé, on vise une température proche de 0.5
- Pour des taches nécessitant de la créativité (marketing, pub), on vise une température entre 0.7 et 1
Pour configurer la temperature, modifier la déclaration du modèle:
OllamaChatModel llm = new OllamaChatModel.OllamaChatModelBuilder()
.baseUrl("http://localhost:11434")
.modelName("openhermes")
.temperature(0.7)
.build();
Faite varier la temperature pour voir les différentes réponses possibles.
Afin d'éviter la répétition, LangChain nous donne la possibilité de variabiliser notre prompt.
Déclarer votre template:
import dev.langchain4j.model.input.PromptTemplate;
var template = PromptTemplate.from("explain the purpose of this regular expression {{regexp}}");
var prompt = template
.apply(
Map.of("regexp", "^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
.toUserMessage();
L'invocation du modèle est identique:
Response<AiMessage> response = llm.generate(List.of(prompt));
System.out.println(response.content().text());
Il est également possible de passer par AiService
. Pour cela, on commence par créer une interface:
interface RegExpAssistant {
String explain(String regexp);
}
On annote la méthode de cette interface afin de lui injecter un template de prompt:
@UserMessage("explain the purpose of this regular expression {{regexp}}")
String explain(@V("regexp") String regexp);
On peut maintenant instancier notre service et le tester:
RegExpAssistant assistant = AiServices.create(RegExpAssistant.class, llm);
System.out.println(assistant.explain("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"));
Il existe différentes techniques permettant de contextualiser les réponses. Une première technique consiste à passer des exemples de questions / réponses dans le contexte.
Dans un premier temps, on commence par définir une simple interface avec laquelle nous allons pouvoir intéragir:
interface Assistant {
String predictAnimalSound(String animal);
}
On annote notre interface pour lui fournir un context contenant les exemples nécéssaires:
@SystemMessage("You are an animal sound expert, able to give the sound an animal does based on the name of the animal")
@UserMessage("cow: moo, cat: meow, dog: woof, {{it}}: ")
String predictSound(String animal);
On peut maintenant instancier notre service et le tester:
Assistant assistant = AiServices.create(Assistant.class, llm);
System.out.println(assistant.predictSound("lion"));
Langchain4j ne possède pas autant d'intégration que la version Python mais il est possible de facilement résumé un texte, pour cela, nous avons besoin de plusieurs objets
EmbeddingModel embeddingModel = new OllamaEmbeddingModel("http://localhost:11434", "openhermes", Duration.of(60, ChronoUnit.SECONDS), 2);
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(300, 0))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
Tout d'abord, nous avons déclarer un model permettant de calculer des embeddings. Nous avons ensuite créer un objet permettant de stocker ces embeddings en mémoire.
Enfin, nous avons créer un objet permettant d'assembler: notre modèle de calcul d'embeddings, notre store ainsi qu'un objet permettant de découper le texte en "chunk".
Pour découper le document, nous avons dans un premier temps besoin de le parser, pour cela, LangChain4J fournit une classe utilitaire:
Document document = loadDocument(Path.of("java_introduction.txt"), new TextDocumentParser());
Une fois ce document parsé, on peut l'indexer:
ingestor.ingest(document);
Une fois cela fait, nous pouvons alors définir une nouvelle chain
qui se basera sur notre index d'embeddings en mémoire:
ConversationalRetrievalChain c = ConversationalRetrievalChain.builder()
.chatLanguageModel(llm)
.retriever(EmbeddingStoreRetriever.from(embeddingStore, embeddingModel))
.build();
Nous pouvons alors demander au modèle de nous faire un résumé:
System.out.println(c.execute("Write a concise summary of the following of the java_introduction.txt document"));
Dans le cas ou nous avons de très gros document ou un ensemble de document, il est préférable d'utiliser une base de données vectorielle pour stocker nos embeddings.
Par défaut, chaque invocation au modèle se comportera comme si c'était la première. Afin de simuler une conversation, il est possible de configurer une mémoire à notre modèle.
Pour ce faire, on peut utiliser un template de prompt qui va assembler un historique de nos message à chaque nouvelle inférence.
Langchain nous propose un objet permettant de gérer un historique de message:
var store = new InMemoryChatMemoryStore();
var memory = new MessageWindowChatMemory.Builder()
.chatMemoryStore(store)
.build();
On peut ensuite initialiser notre chain
:
import dev.langchain4j.chain.ConversationalChain;
ConversationalChain chain = ConversationalChain.builder()
.chatLanguageModel(llm)
.chatMemory(memory)
.maxMessages(10)
.build();
Nous pouvons ensuite inférer notre modèle plusieurs fois et vérifier qu'il a bien de la mémoire:
var prompt1 = "Can you translate I love programming in French ?";
System.out.println("[Human]: " + prompt1);
System.out.println("[AI] : " + chain.execute(prompt1));
var prompt2 = "What did I just ask you ?";
System.out.println("[Human]: " + prompt2);
System.out.println("[AI] : " + chain.execute(prompt2));