1. Introduction
Cet atelier de programmation se concentre sur le grand modèle de langage (LLM) Gemini, hébergé sur Vertex AI sur Google Cloud. Vertex AI est une plate-forme qui englobe tous les produits, services et modèles de machine learning sur Google Cloud.
Vous utiliserez Java pour interagir avec l'API Gemini à l'aide du framework LangChain4j. Vous allez découvrir des exemples concrets pour exploiter le LLM pour répondre aux questions, générer des idées, extraire des entités et du contenu structuré, générer des réponses augmentées par récupération et appeler des fonctions.
Qu'est-ce que l'IA générative ?
L'IA générative fait référence à l'utilisation de l'intelligence artificielle pour créer de nouveaux contenus, comme du texte, des images, de la musique, de l'audio et des vidéos.
L'IA générative s'appuie sur des grands modèles de langage (LLM) qui peuvent effectuer plusieurs opérations en même temps et réaliser des tâches prêtes à l'emploi, telles que la synthèse, les questions/réponses, la classification, etc. Avec un entraînement minimal, les modèles de base peuvent être adaptés à des cas d'utilisation ciblés avec très peu d'exemples de données.
Comment fonctionne l'IA générative ?
L'IA générative s'appuie sur un modèle de machine learning (ML) pour apprendre les schémas et les relations dans un ensemble de données de contenus créés manuellement. Elle utilise ensuite les schémas appris pour générer de nouveaux contenus.
La méthode la plus courante pour entraîner un modèle d'IA générative consiste à utiliser l'apprentissage supervisé. Le modèle se voit attribuer un ensemble de contenus créés manuellement et des étiquettes correspondantes. Il apprend ensuite à générer du contenu semblable au contenu créé par un humain.
Quelles sont les applications courantes de l'IA générative ?
Voici des exemple d'utilisations de l'IA générative :
- Améliorez les interactions client grâce à des fonctionnalités optimisées de chat et de recherche.
- Explorer de grandes quantités de données non structurées via des interfaces de conversation et des résumés
- Faciliter les tâches répétitives en répondant aux appels d'offres, en localisant le contenu marketing dans différentes langues et en vérifiant la conformité des contrats client, etc.
Quelles sont les offres d'IA générative disponibles dans Google Cloud ?
Vertex AI vous permet d'interagir avec des modèles de fondation, de les personnaliser et de les intégrer à vos applications avec peu ou pas de connaissances en ML. Vous pouvez accéder aux modèles de fondation dans Model Garden, les ajuster via une UI simple dans Vertex AI Studio ou les utiliser dans un notebook de data science.
La fonctionnalité Search and Conversation de Vertex AI offre aux développeurs le moyen le plus rapide de créer des moteurs de recherche et des chatbots optimisés par l'IA générative.
Gemini pour Google Cloud, basé sur Gemini, est un outil collaboratif optimisé par l'IA, disponible sur Google Cloud et les IDE, pour vous aider à gagner en efficacité. Gemini Code Assist permet de compléter, de générer et d'expliquer le code, et de discuter avec lui pour poser des questions techniques.
Qu'est-ce que Gemini ?
Gemini est une famille de modèles d'IA générative développés par Google DeepMind et conçus pour les cas d'utilisation multimodaux. Il est multimodal, c'est-à-dire qu'il peut traiter et générer différents types de contenus, comme du texte, du code, des images et de l'audio.
Gemini est disponible en différentes variantes et tailles:
- Gemini Ultra: la version la plus grande et la plus performante pour les tâches complexes.
- Gemini Flash: le plus rapide et le plus économique, optimisé pour les tâches à fort volume.
- Gemini Pro: modèle de taille moyenne, optimisé pour le scaling sur différentes tâches.
- Gemini Nano: le plus efficace, conçu pour les tâches sur l'appareil.
Principales caractéristiques :
- Multimodalité: la capacité de Gemini à comprendre et à gérer plusieurs formats d'informations représente une avancée significative par rapport aux modèles de langage traditionnels basés uniquement sur le texte.
- Performances: Gemini Ultra surpasse les modèles de pointe actuels sur de nombreux benchmarks et a été le premier modèle à surpasser les experts humains au test MMLU (Massive Multitask Language Understanding) difficile.
- Flexibilité: les différentes tailles de Gemini permettent de l'adapter à différents cas d'utilisation, de la recherche à grande échelle au déploiement sur des appareils mobiles.
Comment pouvez-vous interagir avec Gemini sur Vertex AI à partir de Java ?
Vous avez deux possibilités :
- Bibliothèque officielle de l'API Java Vertex AI pour Gemini.
- Framework LangChain4j.
Dans cet atelier de programmation, vous allez utiliser le framework LangChain4j.
Qu'est-ce que le framework LangChain4j ?
Le framework LangChain4j est une bibliothèque Open Source permettant d'intégrer des LLM dans vos applications Java, en orchestrant divers composants, tels que le LLM lui-même, mais aussi d'autres outils tels que des bases de données vectorielles (pour les recherches sémantiques), des chargeurs et des séparateurs de documents (pour analyser les documents et en tirer des enseignements), des analyseurs de sortie, etc.
Ce projet a été inspiré par le projet Python LangChain, mais son objectif est de servir les développeurs Java.
Points abordés
- Configurer un projet Java pour utiliser Gemini et LangChain4j
- Envoyer votre première requête à Gemini de façon programmatique
- Diffuser des réponses depuis Gemini
- Créer une conversation entre un utilisateur et Gemini
- Utiliser Gemini dans un contexte multimodal en envoyant du texte et des images
- Extraire des informations structurées utiles à partir de contenus non structurés
- Manipuler des modèles de requêtes
- Comment effectuer une classification de texte, comme l'analyse des sentiments
- Discuter avec vos propres documents (génération augmentée de récupération)
- Étendre vos chatbots avec les appels de fonction
- Utiliser Gemma en local avec Ollama et TestContainers
Prérequis
- Connaissances du langage de programmation Java
- Un projet Google Cloud
- Un navigateur, comme Chrome ou Firefox
2. Préparation
Configuration de l'environnement au rythme de chacun
- Connectez-vous à la console Google Cloud, puis créez un projet ou réutilisez un projet existant. Si vous n'avez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.
- Le nom du projet est le nom à afficher pour les participants au projet. Il s'agit d'une chaîne de caractères non utilisée par les API Google. Vous pourrez toujours le modifier.
- L'ID du projet est unique parmi tous les projets Google Cloud et non modifiable une fois défini. La console Cloud génère automatiquement une chaîne unique (en général, vous n'y accordez d'importance particulière). Dans la plupart des ateliers de programmation, vous devrez indiquer l'ID de votre projet (généralement identifié par
PROJECT_ID
). Si l'ID généré ne vous convient pas, vous pouvez en générer un autre de manière aléatoire. Vous pouvez également en spécifier un et voir s'il est disponible. Après cette étape, l'ID n'est plus modifiable et restera donc le même pour toute la durée du projet. - Pour information, il existe une troisième valeur (le numéro de projet) que certaines API utilisent. Pour en savoir plus sur ces trois valeurs, consultez la documentation.
- Vous devez ensuite activer la facturation dans la console Cloud pour utiliser les ressources/API Cloud. L'exécution de cet atelier de programmation est très peu coûteuse, voire sans frais. Pour désactiver les ressources et éviter ainsi que des frais ne vous soient facturés après ce tutoriel, vous pouvez supprimer le projet ou les ressources que vous avez créées. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai sans frais pour bénéficier d'un crédit de 300 $.
Démarrer Cloud Shell
Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, vous allez utiliser Cloud Shell, un environnement de ligne de commande exécuté dans le cloud, lors de cet atelier de programmation.
Activer Cloud Shell
- Dans Cloud Console, cliquez sur Activer Cloud Shell
.
Si vous démarrez Cloud Shell pour la première fois, un écran intermédiaire s'affiche pour décrire de quoi il s'agit. Si tel est le cas, cliquez sur Continuer.
Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.
Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier de programmation dans un navigateur.
Une fois connecté à Cloud Shell, vous êtes en principe authentifié, et le projet est défini avec votre ID de projet.
- Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list
Résultat de la commande
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- Exécutez la commande suivante dans Cloud Shell pour vérifier que la commande gcloud connaît votre projet:
gcloud config list project
Résultat de la commande
[core] project = <PROJECT_ID>
Si vous obtenez un résultat différent, exécutez cette commande :
gcloud config set project <PROJECT_ID>
Résultat de la commande
Updated property [core/project].
3. Préparer votre environnement de développement
Dans cet atelier de programmation, vous allez utiliser le terminal et l'éditeur Cloud Shell pour développer vos programmes Java.
Activer les API Vertex AI
Dans la console Google Cloud, assurez-vous que le nom de votre projet s'affiche en haut de la console Google Cloud. Si ce n'est pas le cas, cliquez sur Sélectionner un projet pour ouvrir le sélecteur de projet, puis sélectionnez le projet souhaité.
Vous pouvez activer les API Vertex AI depuis la section Vertex AI de la console Google Cloud ou depuis le terminal Cloud Shell.
Pour l'activer depuis la console Google Cloud, accédez d'abord à la section Vertex AI du menu de la console Google Cloud:
Cliquez sur Activer toutes les API recommandées dans le tableau de bord Vertex AI.
Plusieurs API seront activées, mais la plus importante pour l'atelier de programmation est aiplatform.googleapis.com
.
Vous pouvez également activer cette API à partir du terminal Cloud Shell avec la commande suivante:
gcloud services enable aiplatform.googleapis.com
Cloner le dépôt GitHub
Dans le terminal Cloud Shell, clonez le dépôt de cet atelier de programmation:
git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git
Pour vérifier que le projet est prêt à s'exécuter, vous pouvez essayer d'exécuter le programme "Hello World".
Assurez-vous d'être dans le dossier racine:
cd gemini-workshop-for-java-developers/
Créez le wrapper Gradle:
gradle wrapper
Exécutez avec gradlew
:
./gradlew run
Vous devriez obtenir le résultat suivant :
.. > Task :app:run Hello World!
Ouvrir et configurer Cloud Editor
Ouvrez le code avec l'éditeur de code Cloud depuis Cloud Shell:
Dans l'éditeur Cloud Code, ouvrez le dossier source de l'atelier de programmation en sélectionnant File
-> Open Folder
, puis pointez sur le dossier source de l'atelier de programmation (par exemple, /home/username/gemini-workshop-for-java-developers/
).
Configurer des variables d'environnement
Ouvrez un nouveau terminal dans l'éditeur Cloud Code en sélectionnant Terminal
-> New Terminal
. Configurez deux variables d'environnement requises pour exécuter les exemples de code:
- PROJECT_ID : ID de votre projet Google Cloud
- LOCATION : région dans laquelle le modèle Gemini est déployé
Exportez les variables comme suit:
export PROJECT_ID=$(gcloud config get-value project) export LOCATION=us-central1
4. Premier appel au modèle Gemini
Maintenant que le projet est correctement configuré, il est temps d'appeler l'API Gemini.
Examinez QA.java
dans le répertoire app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;
public class QA {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
Dans ce premier exemple, vous devez importer la classe VertexAiGeminiChatModel
, qui implémente l'interface ChatModel
.
Dans la méthode main
, vous configurez le modèle de langage de chat à l'aide du générateur pour VertexAiGeminiChatModel
et spécifiez:
- Projet
- Lieu
- Nom du modèle (
gemini-1.5-flash-002
)
Maintenant que le modèle de langage est prêt, vous pouvez appeler la méthode generate()
et transmettre votre requête, votre question ou vos instructions à envoyer au LLM. Vous posez ici une question simple sur la raison pour laquelle le ciel est bleu.
N'hésitez pas à modifier cette requête pour essayer différentes questions ou tâches.
Exécutez l'exemple dans le dossier racine du code source:
./gradlew run -q -DjavaMainClass=gemini.workshop.QA
Un résultat semblable à celui-ci s'affiche:
The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight enters the atmosphere, it is made up of a mixture of different wavelengths of light, each with a different color. The different wavelengths of light interact with the molecules and particles in the atmosphere in different ways. The shorter wavelengths of light, such as those corresponding to blue and violet light, are more likely to be scattered in all directions by these particles than the longer wavelengths of light, such as those corresponding to red and orange light. This is because the shorter wavelengths of light have a smaller wavelength and are able to bend around the particles more easily. As a result of Rayleigh scattering, the blue light from the sun is scattered in all directions, and it is this scattered blue light that we see when we look up at the sky. The blue light from the sun is not actually scattered in a single direction, so the color of the sky can vary depending on the position of the sun in the sky and the amount of dust and water droplets in the atmosphere.
Félicitations, vous avez effectué votre premier appel à Gemini !
Réponse en streaming
Avez-vous remarqué que la réponse a été donnée en une seule fois, après quelques secondes ? Il est également possible d'obtenir la réponse progressivement, grâce à la variante de réponse en streaming. Dans la réponse en streaming, le modèle renvoie la réponse par morceaux, à mesure qu'elle devient disponible.
Dans cet atelier de programmation, nous allons nous concentrer sur la réponse non en streaming, mais examinons la réponse en streaming pour voir comment procéder.
Dans StreamQA.java
, dans le répertoire app/src/main/java/gemini/workshop
, vous pouvez voir la réponse en streaming en action:
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(4000)
.build();
model.generate("Why is the sky blue?", onNext(System.out::println));
}
}
Cette fois, nous importons les variantes de classe de streaming VertexAiGeminiStreamingChatModel
qui implémentent l'interface StreamingChatLanguageModel
. Vous devez également importer de manière statique LambdaStreamingResponseHandler.onNext
, une méthode pratique qui fournit des StreamingResponseHandler
pour créer un gestionnaire de streaming avec des expressions lambda Java.
Cette fois, la signature de la méthode generate()
est légèrement différente. Au lieu de renvoyer une chaîne, le type de retour est "void". En plus de l'invite, vous devez transmettre un gestionnaire de réponse de streaming. Ici, grâce à l'importation statique que nous avons mentionnée ci-dessus, nous pouvons définir une expression lambda que vous transmettez à la méthode onNext()
. L'expression lambda est appelée chaque fois qu'une nouvelle partie de la réponse est disponible, tandis que la seconde n'est appelée que si une erreur se produit.
Exécutez la commande suivante :
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
Vous obtiendrez une réponse semblable à celle de la classe précédente, mais cette fois, vous remarquerez que la réponse apparaît progressivement dans votre shell, au lieu d'attendre l'affichage de la réponse complète.
Configuration supplémentaire
Pour la configuration, nous n'avons défini que le projet, l'emplacement et le nom du modèle, mais vous pouvez spécifier d'autres paramètres pour le modèle:
temperature(Float temp)
: pour définir le niveau de créativité de la réponse (0 correspond à une faible créativité et est souvent plus factuel, tandis que 2 correspond à des résultats plus créatifs)topP(Float topP)
: pour sélectionner les mots possibles dont la probabilité totale correspond à ce nombre à virgule flottante (compris entre 0 et 1)topK(Integer topK)
: permet de sélectionner un mot au hasard parmi un nombre maximal de mots probables pour la saisie de texte (de 1 à 40)maxOutputTokens(Integer max)
: pour spécifier la longueur maximale de la réponse donnée par le modèle (généralement, quatre jetons représentent environ trois mots)maxRetries(Integer retries)
: si vous dépassez le quota de requêtes par heure ou si la plate-forme rencontre un problème technique, vous pouvez demander au modèle de réessayer l'appel trois fois.
Jusqu'à présent, vous n'avez posé qu'une seule question à Gemini, mais vous pouvez également échanger avec lui sur plusieurs tours de conversation. C'est ce que vous allez découvrir dans la section suivante.
5. Discuter avec Gemini
À l'étape précédente, vous avez posé une seule question. Il est maintenant temps d'avoir une vraie conversation entre un utilisateur et le LLM. Chaque question et réponse peut s'appuyer sur les précédentes pour former une véritable discussion.
Examinez Conversation.java
dans le dossier app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
Quelques nouvelles importations intéressantes dans cette classe:
MessageWindowChatMemory
: classe qui permet de gérer l'aspect multitours de la conversation et de conserver en mémoire locale les questions et réponses précédentesAiServices
: classe d'abstraction de niveau supérieur qui relie le modèle de chat et la mémoire de chat
Dans la méthode principale, vous allez configurer le modèle, la mémoire de chat et le service d'IA. Le modèle est configuré comme d'habitude avec les informations sur le projet, l'emplacement et le nom du modèle.
Pour la mémoire de chat, nous utilisons le générateur de MessageWindowChatMemory
pour créer une mémoire qui conserve les 20 derniers messages échangés. Il s'agit d'une fenêtre glissante sur la conversation dont le contexte est conservé localement dans notre client de classe Java.
Vous créez ensuite le AI service
qui lie le modèle de chat à la mémoire de chat.
Notez que le service d'IA utilise une interface ConversationService
personnalisée que nous avons définie, que LangChain4j implémente, qui reçoit une requête String
et renvoie une réponse String
.
Il est maintenant temps de discuter avec Gemini. Tout d'abord, un simple message d'accueil est envoyé, puis une première question sur la tour Eiffel pour savoir dans quel pays elle se trouve. Notez que la dernière phrase est liée à la réponse de la première question, car vous vous demandez combien d'habitants compte le pays où se trouve la tour Eiffel, sans mentionner explicitement le pays indiqué dans la réponse précédente. Il montre que les questions et réponses précédentes sont envoyées avec chaque requête.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
Trois réponses semblables à celles-ci doivent s'afficher:
User: Hello! Gemini: Hi there! How can I assist you today? User: What is the country where the Eiffel tower is situated? Gemini: France User: How many inhabitants are there in that country? Gemini: As of 2023, the population of France is estimated to be around 67.8 million.
Vous pouvez poser des questions en une seule étape ou avoir des conversations multitours avec Gemini, mais jusqu'à présent, les entrées n'étaient que textuelles. Qu'en est-il des images ? À l'étape suivante, nous allons explorer les images.
6. La multimodalité avec Gemini
Gemini est un modèle multimodal. Il accepte non seulement du texte, mais aussi des images, voire des vidéos. Dans cette section, vous allez découvrir un cas d'utilisation de la combinaison de texte et d'images.
Pensez-vous que Gemini reconnaîtra ce chat ?
Image d'un chat dans la neige extraite de Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg
Examinez Multimodal.java
dans le répertoire app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
public class Multimodal {
static final String CAT_IMAGE_URL =
"https://upload.wikimedia.org/wikipedia/" +
"commons/b/b6/Felis_catus-cat_on_snow.jpg";
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
Dans les importations, notez que nous distinguons différents types de messages et de contenus. Un UserMessage
peut contenir à la fois un objet TextContent
et un objet ImageContent
. C'est la multimodalité à l'œuvre: mélanger du texte et des images. Nous n'envoyons pas simplement une invite de chaîne simple, mais un objet plus structuré qui représente un message utilisateur, composé d'un élément de contenu image et d'un élément de contenu texte. Le modèle renvoie un Response
contenant un AiMessage
.
Vous récupérez ensuite le AiMessage
de la réponse via content()
, puis le texte du message grâce à text()
.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
Le nom de l'image vous a certainement donné une idée de son contenu, mais la sortie de Gemini ressemble à ceci:
A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.
Combiner des images et des requêtes de texte ouvre des cas d'utilisation intéressants. Vous pouvez créer des applications qui peuvent:
- Reconnaître le texte dans les images
- Vérifier si une image peut être affichée
- Créez des légendes pour les images.
- Rechercher dans une base de données d'images avec des descriptions en texte brut
En plus d'extraire des informations à partir d'images, vous pouvez également extraire des informations à partir de texte non structuré. C'est ce que vous allez découvrir dans la section suivante.
7. Extraire des informations structurées à partir de texte non structuré
Dans de nombreuses situations, des informations importantes sont fournies de manière non structurée dans des rapports, des e-mails ou d'autres textes longs. Idéalement, vous souhaitez pouvoir extraire les informations clés contenues dans le texte non structuré, sous la forme d'objets structurés. Voyons comment procéder.
Supposons que vous souhaitiez extraire le nom et l'âge d'une personne à partir d'une biographie, d'un CV ou d'une description de cette personne. Vous pouvez demander au LLM d'extraire du texte JSON à partir d'un texte non structuré à l'aide d'une invite astucieusement modifiée (c'est ce que l'on appelle communément l'ingénierie d'invite).
Toutefois, dans l'exemple ci-dessous, plutôt que de créer une invite décrivant la sortie JSON, nous allons utiliser une fonctionnalité puissante de Gemini appelée sortie structurée, ou parfois décodage contraint, qui oblige le modèle à ne générer que du contenu JSON valide, conformément à un schéma JSON spécifié.
Examinez ExtractData.java
dans app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;
public class ExtractData {
record Person(String name, int age) { }
interface PersonExtractor {
@SystemMessage("""
Your role is to extract the name and age
of the person described in the biography.
""")
Person extractPerson(String biography);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.responseMimeType("application/json")
.responseSchema(fromClass(Person.class))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String bio = """
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
""";
Person person = extractor.extractPerson(bio);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
Examinons les différentes étapes de ce fichier:
- Un enregistrement
Person
est défini pour représenter les informations décrivant une personne (nom et âge). - L'interface
PersonExtractor
est définie avec une méthode qui, étant donné une chaîne de texte non structurée, renvoie une instancePerson
. extractPerson()
est annoté avec une annotation@SystemMessage
qui lui associe une invite d'instruction. C'est l'invite que le modèle utilisera pour guider l'extraction des informations et renvoyer les détails sous la forme d'un document JSON, qui sera analysé pour vous et désérialisé dans une instancePerson
.
Examinons maintenant le contenu de la méthode main()
:
- Le modèle de chat est configuré et instancié. Nous utilisons deux nouvelles méthodes de la classe de l'outil de création de modèles:
responseMimeType()
etresponseSchema()
. Le premier indique à Gemini de générer un code JSON valide en sortie. La deuxième méthode définit le schéma de l'objet JSON à renvoyer. De plus, ce dernier délègue à une méthode pratique capable de convertir une classe ou un enregistrement Java en un schéma JSON approprié. - Un objet
PersonExtractor
est créé grâce à la classeAiServices
de LangChain4j. - Vous pouvez ensuite simplement appeler
Person person = extractor.extractPerson(...)
pour extraire les informations sur la personne à partir du texte non structuré, et obtenir une instancePerson
avec le nom et l'âge.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
Vous devriez obtenir le résultat suivant :
Anna 23
Oui, c\'est Anna. Elle a 23 ans.
Avec cette approche AiServices
, vous travaillez avec des objets fortement typés. Vous n'interagissez pas directement avec le LLM. Vous travaillez plutôt avec des classes concrètes, comme l'enregistrement Person
pour représenter les informations personnelles extraites, et vous disposez d'un objet PersonExtractor
avec une méthode extractPerson()
qui renvoie une instance Person
. La notion de LLM est abstraite. En tant que développeur Java, vous ne manipulez que des classes et des objets normaux lorsque vous utilisez cette interface PersonExtractor
.
8. Structurer les requêtes à l'aide de modèles de requêtes
Lorsque vous interagissez avec un LLM à l'aide d'un ensemble d'instructions ou de questions commun, une partie de cette requête ne change jamais, tandis que d'autres parties contiennent les données. Par exemple, si vous souhaitez créer des recettes, vous pouvez utiliser une invite comme "Vous êtes un chef talentueux. Veuillez créer une recette avec les ingrédients suivants: ...", puis ajouter les ingrédients à la fin de ce texte. C'est à cela que servent les modèles d'invites, qui sont semblables aux chaînes interpolées dans les langages de programmation. Un modèle de requête contient des espaces réservés que vous pouvez remplacer par les données appropriées pour un appel particulier au LLM.
Plus concrètement, examinons TemplatePrompt.java
dans le répertoire app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.HashMap;
import java.util.Map;
public class TemplatePrompt {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(500)
.temperature(1.0f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
Comme d'habitude, vous configurez le modèle VertexAiGeminiChatModel
avec un niveau de créativité élevé, une température élevée, ainsi que des valeurs topP et topK élevées. Vous créez ensuite un PromptTemplate
avec sa méthode statique from()
en transmettant la chaîne de notre invite, puis utilisez les variables d'espace réservé à double crochets: {{dish}}
et {{ingredients}}
.
Vous créez la requête finale en appelant apply()
, qui utilise un mappage de paires clé/valeur représentant le nom de l'espace réservé et la valeur de chaîne à le remplacer.
Enfin, vous appelez la méthode generate()
du modèle Gemini en créant un message utilisateur à partir de cette requête, avec l'instruction prompt.toUserMessage()
.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt
Un résultat semblable à celui-ci doit s'afficher:
**Strawberry Shortcake** Ingredients: * 1 pint strawberries, hulled and sliced * 1/2 cup sugar * 1/4 cup cornstarch * 1/4 cup water * 1 tablespoon lemon juice * 1/2 cup heavy cream, whipped * 1/4 cup confectioners' sugar * 1/4 teaspoon vanilla extract * 6 graham cracker squares, crushed Instructions: 1. In a medium saucepan, combine the strawberries, sugar, cornstarch, water, and lemon juice. Bring to a boil over medium heat, stirring constantly. Reduce heat and simmer for 5 minutes, or until the sauce has thickened. 2. Remove from heat and let cool slightly. 3. In a large bowl, combine the whipped cream, confectioners' sugar, and vanilla extract. Beat until soft peaks form. 4. To assemble the shortcakes, place a graham cracker square on each of 6 dessert plates. Top with a scoop of whipped cream, then a spoonful of strawberry sauce. Repeat layers, ending with a graham cracker square. 5. Serve immediately. **Tips:** * For a more elegant presentation, you can use fresh strawberries instead of sliced strawberries. * If you don't have time to make your own whipped cream, you can use store-bought whipped cream.
N'hésitez pas à modifier les valeurs de dish
et ingredients
dans la carte, et à ajuster la température, topK
et tokP
, puis à exécuter à nouveau le code. Vous pourrez ainsi observer l'impact de la modification de ces paramètres sur le LLM.
Les modèles de requêtes sont un bon moyen de disposer d'instructions réutilisables et paramétrables pour les appels de LLM. Vous pouvez transmettre des données et personnaliser les requêtes pour différentes valeurs fournies par vos utilisateurs.
9. Classification de texte avec requêtes few-shot
Les LLM sont assez efficaces pour classer le texte dans différentes catégories. Vous pouvez aider un LLM dans cette tâche en lui fournissant des exemples de textes et les catégories qui leur sont associées. Cette approche est souvent appelée requêtes few-shot.
Ouvrons TextClassification.java
dans le répertoire app/src/main/java/gemini/workshop
pour effectuer un type particulier de classification de texte: l'analyse du sentiment.
package gemini.workshop;
import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import java.util.List;
public class TextClassification {
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(10)
.maxRetries(3)
.responseSchema(Schema.newBuilder()
.setType(Type.STRING)
.addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
.build())
.build();
interface SentimentAnalysis {
@SystemMessage("""
Analyze the sentiment of the text below.
Respond only with one word to describe the sentiment.
""")
Sentiment analyze(String text);
}
MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
memory.add(UserMessage.from("This is fantastic news!"));
memory.add(AiMessage.from(Sentiment.POSITIVE.name()));
memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));
memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));
SentimentAnalysis sentimentAnalysis =
AiServices.builder(SentimentAnalysis.class)
.chatLanguageModel(model)
.chatMemory(memory)
.build();
System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
}
}
Une énumération Sentiment
liste les différentes valeurs d'un sentiment: négatif, neutre ou positif.
Dans la méthode main()
, vous créez le modèle de chat Gemini comme d'habitude, mais avec un nombre de jetons de sortie maximal faible, car vous ne souhaitez qu'une réponse courte: le texte est POSITIVE
, NEGATIVE
ou NEUTRAL
. Pour que le modèle ne renvoie que ces valeurs, vous pouvez utiliser la compatibilité avec les sorties structurées que vous avez découverte dans la section "Extraction de données". C'est pourquoi la méthode responseSchema()
est utilisée. Cette fois, vous n'utiliserez pas la méthode pratique de SchemaHelper
pour inférer la définition du schéma, mais vous utiliserez plutôt le générateur Schema
pour comprendre à quoi ressemble la définition du schéma.
Une fois le modèle configuré, vous créez une interface SentimentAnalysis
que AiServices
de LangChain4j implémentera pour vous à l'aide du LLM. Cette interface contient une méthode: analyze()
. Elle prend le texte à analyser en entrée et renvoie une valeur d'énumération Sentiment
. Vous ne manipulez donc qu'un objet fortement typé qui représente la classe de sentiment reconnue.
Ensuite, pour fournir les "quelques exemples" qui inciteront le modèle à effectuer sa classification, vous créez une mémoire de chat pour transmettre des paires de messages utilisateur et de réponses d'IA représentant le texte et le sentiment qui lui est associé.
Liaisons tout ensemble avec la méthode AiServices.builder()
, en transmettant notre interface SentimentAnalysis
, le modèle à utiliser et la mémoire de chat avec les exemples à quelques exemples. Enfin, appelez la méthode analyze()
avec le texte à analyser.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
Un seul mot devrait s'afficher:
POSITIVE
Il semble que les fraises soient appréciées.
10. Génération augmentée par récupération
Les LLM sont entraînés sur une grande quantité de texte. Cependant, ses connaissances ne couvrent que les informations qu'il a vues au cours de son entraînement. Si de nouvelles informations sont publiées après la date limite d'entraînement du modèle, elles ne seront pas disponibles pour le modèle. Par conséquent, le modèle ne pourra pas répondre aux questions portant sur des informations qu'il n'a pas vues.
C'est pourquoi des approches telles que la génération augmentée par récupération (RAG), qui seront abordées dans cette section, permettent de fournir les informations supplémentaires dont un LLM peut avoir besoin pour répondre aux requêtes de ses utilisateurs, en fournissant des informations plus récentes ou des informations privées qui ne sont pas accessibles au moment de l'entraînement.
Revenons aux conversations. Cette fois, vous pourrez poser des questions sur vos documents. Vous allez créer un chatbot capable de récupérer des informations pertinentes à partir d'une base de données contenant vos documents divisés en petits morceaux (ou "chunks"). Ces informations seront utilisées par le modèle pour étayer ses réponses, au lieu de s'appuyer uniquement sur les connaissances contenues dans son entraînement.
La RAG comporte deux phases:
- Phase d'ingestion : les documents sont chargés en mémoire, divisés en petits fragments, et des représentations vectorielles continues (une représentation vectorielle multidimensionnelle de haute qualité des fragments) sont calculées et stockées dans une base de données vectorielle capable d'effectuer des recherches sémantiques. Cette phase d'ingestion est généralement effectuée une seule fois, lorsque de nouveaux documents doivent être ajoutés au corpus de documents.
- Phase de requête : les utilisateurs peuvent désormais poser des questions sur les documents. La question sera également transformée en vecteur et comparée à tous les autres vecteurs de la base de données. Les vecteurs les plus similaires sont généralement liés sémantiquement et sont renvoyés par la base de données vectorielle. Le LLM reçoit ensuite le contexte de la conversation, les fragments de texte correspondant aux vecteurs renvoyés par la base de données, et il est invité à fonder sa réponse en examinant ces fragments.
Préparer vos documents
Dans cet exemple, vous allez poser des questions sur un modèle de voiture fictif d'un constructeur automobile fictif: la Cymbal Starlight. L'idée est qu'un document sur une voiture fictive ne doit pas faire partie des connaissances du modèle. Si Gemini est en mesure de répondre correctement aux questions sur cette voiture, cela signifie que l'approche RAG fonctionne: il est capable de rechercher dans votre document.
Implémenter le chatbot
Voyons comment créer l'approche en deux phases: d'abord avec l'ingestion de documents, puis au moment de la requête (également appelée "phase de récupération") lorsque les utilisateurs posent des questions sur le document.
Dans cet exemple, les deux phases sont implémentées dans la même classe. Normalement, une application s'occupe de l'ingestion et une autre offre l'interface du chatbot à vos utilisateurs.
De plus, dans cet exemple, nous utiliserons une base de données vectorielle en mémoire. Dans un scénario de production réel, les phases d'ingestion et de requête sont séparées dans deux applications distinctes, et les vecteurs sont conservés dans une base de données autonome.
Ingestion de documents
La toute première étape de la phase d'ingestion de documents consiste à localiser le fichier PDF sur notre voiture fictive et à préparer un PdfParser
pour le lire:
URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());
Au lieu de créer d'abord le modèle de langage de chat habituel, vous créez une instance d'un modèle d'encapsulation. Il s'agit d'un modèle particulier dont le rôle est de créer des représentations vectorielles de morceaux de texte (mots, phrases ou même paragraphes). Elle renvoie des vecteurs de nombres à virgule flottante, plutôt que des réponses textuelles.
VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
.endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.publisher("google")
.modelName("text-embedding-005")
.maxRetries(3)
.build();
Vous aurez ensuite besoin de quelques classes pour collaborer:
- Chargez et divisez le document PDF en fragments.
- Créez des représentations vectorielles continues pour tous ces segments.
InMemoryEmbeddingStore<TextSegment> embeddingStore =
new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 100))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
storeIngestor.ingest(document);
Une instance de InMemoryEmbeddingStore
, une base de données vectorielle en mémoire, est créée pour stocker les embeddings vectoriels.
Le document est divisé en fragments grâce à la classe DocumentSplitters
. Il va diviser le texte du fichier PDF en extraits de 500 caractères, avec un chevauchement de 100 caractères (avec le segment suivant, pour éviter de couper des mots ou des phrases en morceaux).
L'ingesteur de magasin associe le séparateur de documents, le modèle d'embedding pour calculer les vecteurs et la base de données vectorielle en mémoire. La méthode ingest()
s'occupera ensuite de l'ingestion.
La première phase est maintenant terminée. Le document a été transformé en segments de texte avec leurs embeddings vectoriels associés et stocké dans la base de données vectorielle.
Poser des questions
Il est temps de vous préparer à poser des questions. Créez un modèle de chat pour démarrer la conversation:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(1000)
.build();
Vous avez également besoin d'une classe de récupération pour associer la base de données vectorielle (dans la variable embeddingStore
) au modèle d'encapsulation. Son rôle est d'interroger la base de données de vecteurs en calculant une représentation vectorielle continue pour la requête de l'utilisateur, afin de trouver des vecteurs similaires dans la base de données:
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
Créez une interface qui représente un assistant expert en voitures. Il s'agit d'une interface que la classe AiServices
implémentera pour vous permettre d'interagir avec le modèle:
interface CarExpert {
Result<String> ask(String question);
}
L'interface CarExpert
renvoie une réponse sous forme de chaîne encapsulée dans la classe Result
de LangChain4j. Pourquoi utiliser ce wrapper ? En effet, cela vous permettra non seulement d'obtenir la réponse, mais aussi d'examiner les segments de la base de données qui ont été renvoyés par le récupérateur de contenu. Vous pouvez ainsi afficher les sources du ou des documents utilisés pour étayer la réponse finale à l'utilisateur.
À ce stade, vous pouvez configurer un nouveau service d'IA:
CarExpert expert = AiServices.builder(CarExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
Ce service lie les éléments suivants:
- Modèle de langue de chat que vous avez configuré précédemment.
- Une mémoire de chat pour suivre la conversation.
- Le retriever compare une requête d'embedding vectoriel aux vecteurs de la base de données.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in car automotive, and you answer concisely.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
Vous êtes enfin prêt à poser vos questions.
List.of(
"What is the cargo capacity of Cymbal Starlight?",
"What's the emergency roadside assistance phone number?",
"Are there some special kits available on that car?"
).forEach(query -> {
Result<String> response = expert.ask(query);
System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});
Le code source complet se trouve dans RAG.java
, dans le répertoire app/src/main/java/gemini/workshop
.
Exécutez l'exemple:
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
Dans la sortie, vous devriez obtenir les réponses à vos questions:
=== What is the cargo capacity of Cymbal Starlight? === The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. SOURCE: Cargo The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of the vehicle. To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell. When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this could affect the vehicle's handling and stability. Luggage === What's the emergency roadside assistance phone number? === The emergency roadside assistance phone number is 1-800-555-1212. SOURCE: Chapter 18: Emergencies Roadside Assistance If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside assistance for help. Roadside assistance is available 24 hours a day, 7 days a week. To call roadside assistance, dial the following number: 1-800-555-1212 When you call roadside assistance, be prepared to provide the following information: Your name and contact information Your vehicle's make, model, and year Your vehicle's location === Are there some special kits available on that car? === Yes, the Cymbal Starlight comes with a tire repair kit. SOURCE: Lane keeping assist: This feature helps to keep you in your lane by gently steering the vehicle back into the lane if you start to drift. Adaptive cruise control: This feature automatically adjusts your speed to maintain a safe following distance from the vehicle in front of you. Forward collision warning: This feature warns you if you are approaching another vehicle too quickly. Automatic emergency braking: This feature can automatically apply the brakes to avoid a collision.
11. Appel de fonction
Dans certains cas, vous souhaitez qu'un LLM ait accès à des systèmes externes, comme une API Web distante qui récupère des informations ou effectue une action, ou des services qui effectuent un type de calcul. Exemple :
API Web distantes:
- Suivre et mettre à jour les commandes des clients
- Recherchez ou créez une demande dans un outil de suivi des problèmes.
- Récupérer des données en temps réel, comme les cours boursiers ou les mesures des capteurs IoT
- envoyer un e-mail ;
Outils de calcul:
- Une calculatrice pour les problèmes mathématiques plus avancés.
- Interprétation du code pour l'exécution du code lorsque les LLM ont besoin d'une logique de raisonnement.
- Convertir les requêtes en langage naturel en requêtes SQL afin qu'un LLM puisse interroger une base de données
L'appel de fonction (parfois appelé "outils" ou "utilisation d'outils") permet au modèle de demander qu'un ou plusieurs appels de fonction soient effectués en son nom afin qu'il puisse répondre correctement à la requête d'un utilisateur avec des données plus récentes.
Compte tenu d'une requête particulière d'un utilisateur et de la connaissance des fonctions existantes pouvant être pertinentes pour ce contexte, un LLM peut répondre par une requête d'appel de fonction. L'application intégrant le LLM peut ensuite appeler la fonction en son nom, puis répondre au LLM avec une réponse, et le LLM interprète ensuite en répondant avec une réponse textuelle.
Quatre étapes de l'appel de fonction
Voyons un exemple d'appel de fonction: obtenir des informations sur les prévisions météo.
Si vous demandez à Gemini ou à tout autre LLM la météo à Paris, il vous répondra qu'il ne dispose d'aucune information sur les prévisions météorologiques actuelles. Si vous souhaitez que le LLM ait accès aux données météorologiques en temps réel, vous devez définir certaines fonctions qu'il peut demander à utiliser.
Examinez le diagramme suivant:
1️⃣ Tout d'abord, un utilisateur demande quel temps fait-il à Paris. L'application de chatbot (à l'aide de LangChain4j) sait qu'une ou plusieurs fonctions sont à sa disposition pour aider le LLM à répondre à la requête. Le chatbot envoie à la fois la requête initiale et la liste des fonctions pouvant être appelées. Ici, une fonction appelée getWeather()
qui utilise un paramètre de chaîne pour l'emplacement.
Comme le LLM ne connaît pas les prévisions météo, au lieu de répondre par texte, il renvoie une requête d'exécution de fonction. Le chatbot doit appeler la fonction getWeather()
avec "Paris"
comme paramètre d'emplacement.
2️⃣ Le chatbot appelle cette fonction au nom du LLM et récupère la réponse de la fonction. Ici, nous imaginons que la réponse est {"forecast": "sunny"}
.
3️⃣ L'application de chatbot renvoie la réponse JSON au LLM.
4️⃣ Le LLM examine la réponse JSON, interprète ces informations et renvoie finalement le message indiquant qu'il fait beau à Paris.
Chaque étape sous forme de code
Vous commencerez par configurer le modèle Gemini comme d'habitude:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(100)
.build();
Vous définissez une spécification d'outil qui décrit la fonction pouvant être appelée:
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeather")
.description("Get the weather forecast for a given location or city")
.parameters(JsonObjectSchema.builder()
.addStringProperty(
"location",
"the location or city to get the weather forecast for")
.build())
.build();
Le nom de la fonction est défini, ainsi que le nom et le type du paramètre. Notez que la fonction et les paramètres sont tous deux associés à une description. Les descriptions sont très importantes et aident le LLM à comprendre vraiment ce qu'une fonction peut faire et donc à déterminer si cette fonction doit être appelée dans le contexte de la conversation.
Commençons par l'étape 1, en envoyant la première question sur la météo à Paris:
List<ChatMessage> allMessages = new ArrayList<>();
// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);
À l'étape 2, nous transmettons l'outil que nous souhaitons que le modèle utilise, et le modèle répond avec une requête d'exécution de l'outil:
// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());
Étape 3. À ce stade, nous savons quelle fonction le LLM souhaite que nous appelions. Dans le code, nous n'effectuons pas d'appel réel à une API externe. Nous renvoyons simplement directement une prévision météo hypothétique:
// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
"{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);
À l'étape 4, le LLM apprend le résultat de l'exécution de la fonction et peut ensuite synthétiser une réponse textuelle:
// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());
Voici le résultat :
Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer: The weather in Paris is sunny with a temperature of 20 degrees Celsius.
Dans la sortie ci-dessus, vous pouvez voir la requête d'exécution de l'outil, ainsi que la réponse.
Le code source complet se trouve dans FunctionCalling.java
, dans le répertoire app/src/main/java/gemini/workshop
:
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling
Un résultat semblable aux lignes suivantes doit s'afficher :
Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer: The weather in Paris is sunny with a temperature of 20 degrees Celsius.
12. LangChain4j gère l'appel de fonction
Dans l'étape précédente, vous avez vu comment les interactions question/réponse et requête/réponse de fonction textuelles normales sont entrelacées. Entre-temps, vous avez fourni directement la réponse de fonction demandée, sans appeler de fonction réelle.
Cependant, LangChain4j propose également une abstraction de niveau supérieur qui peut gérer les appels de fonction de manière transparente pour vous, tout en gérant la conversation comme d'habitude.
Appel de fonction unique
Examinons FunctionCallingAssistant.java
, élément par élément.
Commencez par créer un enregistrement qui représentera la structure de données de réponse de la fonction:
record WeatherForecast(String location, String forecast, int temperature) {}
La réponse contient des informations sur l'emplacement, les prévisions et la température.
Vous créez ensuite une classe contenant la fonction réelle que vous souhaitez mettre à la disposition du modèle:
static class WeatherForecastService {
@Tool("Get the weather forecast for a location")
WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
if (location.equals("Paris")) {
return new WeatherForecast("Paris", "Sunny", 20);
} else if (location.equals("London")) {
return new WeatherForecast("London", "Rainy", 15);
} else {
return new WeatherForecast("Unknown", "Unknown", 0);
}
}
}
Notez que cette classe ne contient qu'une seule fonction, mais elle est annotée avec l'annotation @Tool
, qui correspond à la description de la fonction que le modèle peut demander à appeler.
Les paramètres de la fonction (un seul ici) sont également annotés, mais avec cette courte annotation @P
, qui fournit également une description du paramètre. Vous pouvez ajouter autant de fonctions que vous le souhaitez pour les mettre à la disposition du modèle, pour des scénarios plus complexes.
Dans cette classe, vous renvoyez des réponses prédéfinies, mais si vous souhaitez appeler un véritable service de prévisions météorologiques externe, vous devez le faire dans le corps de cette méthode.
Comme nous l'avons vu lorsque vous avez créé un ToolSpecification
dans l'approche précédente, il est important de documenter ce qu'une fonction fait et de décrire à quoi les paramètres correspondent. Cela aide le modèle à comprendre comment et quand cette fonction peut être utilisée.
Ensuite, LangChain4j vous permet de fournir une interface qui correspond au contrat que vous souhaitez utiliser pour interagir avec le modèle. Il s'agit ici d'une interface simple qui reçoit une chaîne représentant le message de l'utilisateur et renvoie une chaîne correspondant à la réponse du modèle:
interface WeatherAssistant {
String chat(String userMessage);
}
Il est également possible d'utiliser des signatures plus complexes impliquant UserMessage
(pour un message utilisateur) ou AiMessage
(pour une réponse de modèle) de LangChain4j, ou même TokenStream
, si vous souhaitez gérer des situations plus avancées, car ces objets plus complexes contiennent également des informations supplémentaires telles que le nombre de jetons consommés, etc. Mais par souci de simplicité, nous allons simplement prendre une chaîne en entrée et une chaîne en sortie.
Terminons avec la méthode main()
qui relie tous les éléments:
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-pro-002")
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
}
Comme d'habitude, vous configurez le modèle de chat Gemini. Vous instanciez ensuite votre service de prévisions météorologiques qui contient la "fonction" que le modèle nous demandera d'appeler.
Vous allez maintenant utiliser à nouveau la classe AiServices
pour lier le modèle de chat, la mémoire de chat et l'outil (c'est-à-dire le service de prévisions météo avec sa fonction). AiServices
renvoie un objet qui implémente l'interface WeatherAssistant
que vous avez définie. Il ne reste plus qu'à appeler la méthode chat()
de cet assistant. Lorsque vous l'appelez, vous ne voyez que les réponses textuelles, mais les requêtes d'appel de fonction et les réponses d'appel de fonction ne sont pas visibles par le développeur. Ces requêtes sont gérées automatiquement et de manière transparente. Si Gemini pense qu'une fonction doit être appelée, il répondra avec la requête d'appel de fonction, et LangChain4j se chargera d'appeler la fonction locale en votre nom.
Exécutez l'exemple:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
Un résultat semblable aux lignes suivantes doit s'afficher :
OK. The weather in Paris is sunny with a temperature of 20 degrees.
Il s'agissait d'un exemple de fonction unique.
Appels de fonction multiples
Vous pouvez également avoir plusieurs fonctions et laisser LangChain4j gérer plusieurs appels de fonction en votre nom. Consultez MultiFunctionCallingAssistant.java
pour obtenir un exemple de fonction multiple.
Il dispose d'une fonction permettant de convertir des devises:
@Tool("Convert amounts between two currencies")
double convertCurrency(
@P("Currency to convert from") String fromCurrency,
@P("Currency to convert to") String toCurrency,
@P("Amount to convert") double amount) {
double result = amount;
if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
result = amount * 0.93;
} else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
result = amount * 0.79;
}
System.out.println(
"convertCurrency(fromCurrency = " + fromCurrency +
", toCurrency = " + toCurrency +
", amount = " + amount + ") == " + result);
return result;
}
Autre fonction permettant d'obtenir la valeur d'une action:
@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
double result = 170.0 + 10 * new Random().nextDouble();
System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);
return result;
}
Autre fonction permettant d'appliquer un pourcentage à un montant donné:
@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
double result = amount * (percentage / 100);
System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);
return result;
}
Vous pouvez ensuite combiner toutes ces fonctions et une classe MultiTools pour poser des questions telles que "Quel est le prix de 10% de l'action AAPL converti de l'USD en EUR ?".
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
Exécutez-la comme suit:
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
Vous devriez voir plusieurs fonctions appelées:
getStockPrice(symbol = AAPL) == 172.8022224055534 convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468 applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647 10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.
Vers les agents
L'appel de fonction est un excellent mécanisme d'extension pour les grands modèles de langage tels que Gemini. Cela nous permet de créer des systèmes plus complexes, souvent appelés "agents" ou "assistants d'IA". Ces agents peuvent interagir avec le monde extérieur via des API externes et avec des services pouvant avoir des effets secondaires sur l'environnement externe (par exemple, envoyer des e-mails, créer des tickets, etc.).
Lorsque vous créez des agents aussi puissants, vous devez le faire de manière responsable. Vous devez envisager d'impliquer un humain dans le processus avant d'effectuer des actions automatiques. Il est important de garder à l'esprit la sécurité lorsque vous concevez des agents LLM qui interagissent avec le monde extérieur.
13. Exécuter Gemma avec Ollama et TestContainers
Jusqu'à présent, nous avons utilisé Gemini, mais il existe également Gemma, son petit frère.
Gemma est une famille de modèles ouverts, légers et à la pointe de la technologie, basés sur la recherche et la technologie utilisées pour créer les modèles Gemini. Gemma est disponible en deux variantes, Gemma1 et Gemma2, chacune avec différentes tailles. Gemma1 est disponible en deux tailles: 2B et 7B. Gemma 2 est disponible en deux tailles: 9B et 27B. Leurs poids sont librement disponibles, et leur petite taille vous permet de les exécuter vous-même, même sur votre ordinateur portable ou dans Cloud Shell.
Comment exécutez-vous Gemma ?
Il existe de nombreuses façons d'exécuter Gemma: dans le cloud, via Vertex AI en un clic, ou GKE avec certains GPU, mais vous pouvez également l'exécuter en local.
Une bonne option pour exécuter Gemma en local est Ollama, un outil qui vous permet d'exécuter de petits modèles, comme Llama 2, Mistral et bien d'autres, sur votre machine locale. Il est semblable à Docker, mais pour les LLM.
Installez Ollama en suivant les instructions pour votre système d'exploitation.
Si vous utilisez un environnement Linux, vous devez d'abord activer Ollama après l'avoir installé.
ollama serve > /dev/null 2>&1 &
Une fois installé en local, vous pouvez exécuter des commandes pour extraire un modèle:
ollama pull gemma:2b
Attendez que le modèle soit extrait. Cette opération peut prendre un certain temps.
Exécutez le modèle:
ollama run gemma:2b
Vous pouvez maintenant interagir avec le modèle:
>>> Hello! Hello! It's nice to hear from you. What can I do for you today?
Pour quitter l'invite, appuyez sur Ctrl+D.
Exécuter Gemma dans Ollama sur TestContainers
Au lieu d'avoir à installer et à exécuter Ollama en local, vous pouvez l'utiliser dans un conteneur géré par TestContainers.
TestContainers n'est pas seulement utile pour les tests. Vous pouvez également l'utiliser pour exécuter des conteneurs. Vous pouvez même profiter d'un OllamaContainer
spécifique.
Voici le tableau complet:
Mise en œuvre
Examinons GemmaWithOllamaContainer.java
, élément par élément.
Vous devez d'abord créer un conteneur Ollama dérivé qui extrait le modèle Gemma. Cette image existe déjà à partir d'une exécution précédente ou sera créée. Si l'image existe déjà, il vous suffit d'indiquer à TestContainers que vous souhaitez remplacer l'image Ollama par défaut par votre variante Gemma:
private static final String TC_OLLAMA_GEMMA_2_B = "tc-ollama-gemma-2b";
// Creating an Ollama container with Gemma 2B if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {
// Check if the custom Gemma Ollama image exists already
List<Image> listImagesCmd = DockerClientFactory.lazyClient()
.listImagesCmd()
.withImageNameFilter(TC_OLLAMA_GEMMA_2_B)
.exec();
if (listImagesCmd.isEmpty()) {
System.out.println("Creating a new Ollama container with Gemma 2B image...");
OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26");
ollama.start();
ollama.execInContainer("ollama", "pull", "gemma:2b");
ollama.commitToImage(TC_OLLAMA_GEMMA_2_B);
return ollama;
} else {
System.out.println("Using existing Ollama container with Gemma 2B image...");
// Substitute the default Ollama image with our Gemma variant
return new OllamaContainer(
DockerImageName.parse(TC_OLLAMA_GEMMA_2_B)
.asCompatibleSubstituteFor("ollama/ollama"));
}
}
Vous créez ensuite et démarrez un conteneur de test Ollama, puis un modèle de chat Ollama en pointant vers l'adresse et le port du conteneur avec le modèle que vous souhaitez utiliser. Enfin, il vous suffit d'appeler model.generate(yourPrompt)
comme d'habitude:
public static void main(String[] args) throws IOException, InterruptedException {
OllamaContainer ollama = createGemmaOllamaContainer();
ollama.start();
ChatLanguageModel model = OllamaChatModel.builder()
.baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
.modelName("gemma:2b")
.build();
String response = model.generate("Why is the sky blue?");
System.out.println(response);
}
Exécutez-la comme suit:
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
La première exécution prendra un certain temps pour créer et exécuter le conteneur, mais une fois terminée, Gemma devrait répondre:
INFO: Container ollama/ollama:0.1.26 started in PT2.827064047S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.
* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.
This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.
In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.
Gemma s'exécute dans Cloud Shell.
14. Félicitations
Félicitations, vous avez créé votre première application de chat avec IA générative en Java à l'aide de LangChain4j et de l'API Gemini. Vous avez découvert au fil du temps que les grands modèles de langage multimodaux sont très puissants et capables de gérer diverses tâches, comme les questions/réponses, même sur votre propre documentation, l'extraction de données, l'interaction avec des API externes, etc.
Et ensuite ?
C'est à vous de mettre en valeur vos applications grâce à de puissantes intégrations de LLM.
Complément d'informations
- Cas d'utilisation courants de l'IA générative
- Ressources de formation sur l'IA générative
- Interagir avec Gemini via Generative AI Studio
- IA responsable