Gemini in Java mit Vertex AI und LangChain4j

1. Einführung

In diesem Codelab liegt der Schwerpunkt auf dem Gemini Large Language Model (LLM), das in Vertex AI in Google Cloud gehostet wird. Vertex AI ist eine Plattform, die alle Machine-Learning-Produkte, ‑Dienste und ‑Modelle in Google Cloud umfasst.

Sie verwenden Java, um mit der Gemini API über das LangChain4j-Framework zu interagieren. Sie sehen sich konkrete Beispiele an, um das LLM für die Beantwortung von Fragen, die Ideenentwicklung, die Extraktion von Entitäten und strukturierten Inhalten, die Retrieval-Augmented Generation und den Funktionsaufruf zu nutzen.

Was ist generative KI?

Bei der generativen KI wird künstliche Intelligenz verwendet, um neue Inhalte wie Text, Bilder, Musik, Audio und Videos zu erstellen.

Die generative KI basiert auf Large Language Models (LLMs), die Multitaskingfähig sind und vordefinierte Aufgaben ausführen, darunter Zusammenfassungen, Fragen-/Antworten-Sessions, Klassifizierung und mehr. Mit nur minimalem Training können Fundamentmodelle an ausgewählte Anwendungsfälle mit sehr wenigen Beispieldaten angepasst werden.

Wie funktioniert Generative AI?

Generative KI nutzt ein ML-Modell, um die Muster und Beziehungen in einem Dataset aus von Menschen erstellten Inhalten zu ermitteln. Anschließend werden anhand der erlernten Muster neue Inhalte generiert.

Die gängigste Methode zum Trainieren eines generativen KI-Modells ist die Verwendung von überwachtem Lernen. Das Modell erhält eine Reihe von Inhalten, die von Menschen erstellt wurden, sowie zugehörige Labels. Anschließend lernt es, Inhalte zu erstellen, die den von Menschen erstellten Inhalten ähneln.

Welche Anwendungsfälle für generative KI sind üblich?

Mit generativer KI können Sie:

  • Kundeninteraktionen durch erweiterte Chat- und Suchfunktionen verbessern.
  • Über reaktive Oberflächen und Zusammenfassungen große Mengen unstrukturierter Daten untersuchen.
  • Unterstützung bei sich wiederholenden Aufgaben wie der Beantwortung von Angebotsanfragen, der Lokalisierung von Marketinginhalten in verschiedenen Sprachen und der Prüfung von Kundenverträgen auf Compliance.

Welche Angebote für generative KI hat Google Cloud?

Mit Vertex AI können Sie ohne oder mit nur wenig ML-Fachwissen mit Fundamentmodellen interagieren, sie anpassen und in Ihre Anwendungen einbetten. Sie können über Model Garden auf Fundamentmodelle zugreifen, sie mithilfe einer einfachen Benutzeroberfläche in Vertex AI Studio abstimmen oder sie direkt in einem Data-Science-Notebook verwenden.

Vertex AI Search and Conversation bietet Entwicklern die schnellste Möglichkeit, generative KI-gestützte Suchmaschinen und Chatbots zu erstellen.

Gemini for Google Cloud ist ein KI-gestütztes Tool, das in Google Cloud und IDEs genutzt werden kann, um in kürzerer Zeit mehr zu erledigen. Es basiert auf Gemini. Gemini Code Assist bietet Codevervollständigung, Codegenerierung, Codeerläuterungen und die Möglichkeit, technische Fragen zu stellen.

Was ist Gemini?

Gemini ist eine Familie generativer KI-Modelle, die von Google DeepMind entwickelt wurden und auf multimodale Anwendungsfälle ausgelegt sind. Multimodal bedeutet, dass es verschiedene Arten von Inhalten wie Text, Code, Bilder und Audio verarbeiten und generieren kann.

b9913d011999e7c7.png

Gemini ist in verschiedenen Varianten und Größen erhältlich:

  • Gemini Ultra: Die größte und leistungsfähigste Version für komplexe Aufgaben.
  • Gemini Flash: Am schnellsten und kostengünstigsten, optimiert für Aufgaben mit hohem Volumen.
  • Gemini Pro: Mittelgroßes Modell, optimiert für die Skalierung für verschiedene Aufgaben.
  • Gemini Nano: Das effizienteste Modell, entwickelt für On-Device-Aufgaben.

Wichtige Features:

  • Multimodalität: Die Fähigkeit von Gemini, mehrere Informationsformate zu verstehen und zu verarbeiten, ist ein wichtiger Schritt über herkömmliche Sprachmodelle, die nur Text verarbeiten, hinaus.
  • Leistung: Gemini Ultra übertrifft in vielen Benchmarks den aktuellen Stand der Technik und war das erste Modell, das menschliche Experten im anspruchsvollen MMLU-Benchmark (Massive Multitask Language Understanding) übertraf.
  • Flexibilität: Die verschiedenen Gemini-Größen machen es für verschiedene Anwendungsfälle geeignet, von groß angelegten Studien bis hin zur Bereitstellung auf Mobilgeräten.

Wie können Sie über Java mit Gemini in Vertex AI interagieren?

Es stehen zwei Optionen zur Verfügung:

  1. Die offizielle Vertex AI Java API für Gemini-Bibliothek.
  2. LangChain4j-Framework

In diesem Codelab verwenden Sie das LangChain4j-Framework.

Was ist das LangChain4j-Framework?

Das LangChain4j-Framework ist eine Open-Source-Bibliothek, mit der LLMs in Java-Anwendungen eingebunden werden können. Dazu werden verschiedene Komponenten orchestriert, z. B. das LLM selbst, aber auch andere Tools wie Vektordatenbanken (für semantische Suchanfragen), Dokumentenlader und -splitter (zum Analysieren und Lernen aus Dokumenten) und Ausgabeparser.

Das Projekt wurde vom Python-Projekt LangChain inspiriert, hat aber das Ziel, Java-Entwicklern zu dienen.

bb908ea1e6c96ac2.png

Aufgaben in diesem Lab

  • Java-Projekt für die Verwendung von Gemini und LangChain4j einrichten
  • Programmisch ersten Prompt an Gemini senden
  • Antworten von Gemini streamen
  • Unterhaltung zwischen einem Nutzer und Gemini erstellen
  • Gemini in einem multimodalen Kontext verwenden, indem sowohl Text als auch Bilder gesendet werden
  • Nützliche strukturierte Informationen aus unstrukturierten Inhalten extrahieren
  • Promptvorlagen bearbeiten
  • Textklassifizierung wie Sentimentanalyse
  • Mit eigenen Dokumenten chatten (Retrieval Augmented Generation)
  • Chatbots mit Funktionsaufrufen erweitern
  • Gemma lokal mit Ollama und TestContainers verwenden

Voraussetzungen

  • Kenntnisse der Programmiersprache Java
  • Ein Google Cloud-Projekt
  • Einen Browser wie Chrome oder Firefox

2. Einrichtung und Anforderungen

Einrichtung der Umgebung im eigenen Tempo

  1. Melden Sie sich in der Google Cloud Console an und erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes. Wenn Sie noch kein Gmail- oder Google Workspace-Konto haben, müssen Sie ein Konto erstellen.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Der Projektname ist der Anzeigename für die Teilnehmer dieses Projekts. Es ist ein Zeichenstring, der von Google APIs nicht verwendet wird. Sie können ihn jederzeit aktualisieren.
  • Die Projekt-ID ist für alle Google Cloud-Projekte eindeutig und kann nach der Festlegung nicht mehr geändert werden. In der Cloud Console wird automatisch ein eindeutiger String generiert. In der Regel spielt es keine Rolle, wie er lautet. In den meisten Codelabs müssen Sie auf Ihre Projekt-ID verweisen (normalerweise als PROJECT_ID gekennzeichnet). Wenn Ihnen die generierte ID nicht gefällt, können Sie eine andere zufällige generieren. Alternativ können Sie Ihr eigenes Konto ausprobieren und prüfen, ob es verfügbar ist. Sie kann nach diesem Schritt nicht mehr geändert werden und bleibt für die Dauer des Projekts bestehen.
  • Zur Information: Es gibt einen dritten Wert, die Projektnummer, die von einigen APIs verwendet wird. Weitere Informationen zu diesen drei Werten finden Sie in der Dokumentation.
  1. Als Nächstes müssen Sie in der Cloud Console die Abrechnung aktivieren, um Cloud-Ressourcen/-APIs verwenden zu können. Die Durchführung dieses Codelabs ist kostenlos oder kostet nur sehr wenig. Wenn Sie die Ressourcen herunterfahren möchten, um Kosten nach Abschluss dieser Anleitung zu vermeiden, können Sie die von Ihnen erstellten Ressourcen oder das Projekt löschen. Neuen Google Cloud-Nutzern steht das kostenlose Testprogramm mit einem Guthaben von 300$ zur Verfügung.

Cloud Shell starten

Sie können Google Cloud zwar per Fernzugriff von Ihrem Laptop aus nutzen, in diesem Codelab verwenden Sie jedoch Cloud Shell, eine Befehlszeilenumgebung, die in der Cloud ausgeführt wird.

Cloud Shell aktivieren

  1. Klicken Sie in der Cloud Console auf Cloud Shell aktivieren 853e55310c205094.png.

3c1dabeca90e44e5.png

Wenn Sie Cloud Shell zum ersten Mal starten, wird ein Zwischenbildschirm mit einer Beschreibung angezeigt. Klicken Sie in diesem Fall auf Weiter.

9c92662c6a846a5c.png

Die Bereitstellung und Verbindung mit Cloud Shell sollte nur wenige Minuten dauern.

9f0e51b578fecce5.png

Auf dieser virtuellen Maschine sind alle erforderlichen Entwicklungstools installiert. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Die meisten, wenn nicht alle Aufgaben in diesem Codelab können mit einem Browser erledigt werden.

Sobald Sie mit Cloud Shell verbunden sind, sollten Sie sehen, dass Sie authentifiziert sind und das Projekt auf Ihre Projekt-ID festgelegt ist.

  1. Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:
gcloud auth list

Befehlsausgabe

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob der gcloud-Befehl Ihr Projekt kennt:
gcloud config list project

Befehlsausgabe

[core]
project = <PROJECT_ID>

Ist dies nicht der Fall, können Sie die Einstellung mit diesem Befehl vornehmen:

gcloud config set project <PROJECT_ID>

Befehlsausgabe

Updated property [core/project].

3. Entwicklungsumgebung vorbereiten

In diesem Codelab verwenden Sie das Cloud Shell-Terminal und den Cloud Shell-Editor, um Ihre Java-Programme zu entwickeln.

Vertex AI APIs aktivieren

Achten Sie darauf, dass der Projektname in der Google Cloud Console oben angezeigt wird. Falls nicht, klicken Sie auf Projekt auswählen, um die Projektauswahl zu öffnen, und wählen Sie das gewünschte Projekt aus.

Sie können Vertex AI APIs entweder im Abschnitt „Vertex AI“ der Google Cloud Console oder im Cloud Shell-Terminal aktivieren.

Wenn Sie die Funktion in der Google Cloud Console aktivieren möchten, rufen Sie zuerst den Bereich „Vertex AI“ im Menü der Google Cloud Console auf:

451976f1c8652341.png

Klicken Sie im Vertex AI-Dashboard auf Alle empfohlenen APIs aktivieren.

Dadurch werden mehrere APIs aktiviert, die wichtigste für das Codelab ist jedoch die aiplatform.googleapis.com.

Alternativ können Sie diese API auch über das Cloud Shell-Terminal mit dem folgenden Befehl aktivieren:

gcloud services enable aiplatform.googleapis.com

Klone das GitHub-Repository .

Klonen Sie im Cloud Shell-Terminal das Repository für dieses Codelab:

git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git

Um zu prüfen, ob das Projekt einsatzbereit ist, können Sie das „Hello World“-Programm ausführen.

Achten Sie darauf, dass Sie sich im Ordner der obersten Ebene befinden:

cd gemini-workshop-for-java-developers/ 

Erstellen Sie den Gradle-Wrapper:

gradle wrapper

Mit gradlew ausführen:

./gradlew run

Es sollte folgende Ausgabe angezeigt werden:

..
> Task :app:run
Hello World!

Cloud-Editor öffnen und einrichten

Öffnen Sie den Code mit dem Cloud Code-Editor in Cloud Shell:

42908e11b28f4383.png

Öffnen Sie im Cloud Code-Editor den Codelab-Quellcodeordner. Wählen Sie dazu File -> Open Folder aus und klicken Sie auf den Codelab-Quellcodeordner (z. B. /home/username/gemini-workshop-for-java-developers/) verwenden.

Umgebungsvariablen einrichten

Öffnen Sie ein neues Terminal im Cloud Code-Editor, indem Sie Terminal -> New Terminal auswählen. Richten Sie zwei Umgebungsvariablen ein, die zum Ausführen der Codebeispiele erforderlich sind:

  • PROJECT_ID: Ihre Google Cloud-Projekt-ID
  • LOCATION: Die Region, in der das Gemini-Modell bereitgestellt wird.

Exportieren Sie die Variablen so:

export PROJECT_ID=$(gcloud config get-value project)
export LOCATION=us-central1

4. Erster Aufruf des Gemini-Modells

Nachdem das Projekt richtig eingerichtet ist, können Sie die Gemini API aufrufen.

Sehen Sie sich QA.java im Verzeichnis app/src/main/java/gemini/workshop an:

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?"));
    }
}

In diesem ersten Beispiel müssen Sie die Klasse VertexAiGeminiChatModel importieren, die die Schnittstelle ChatModel implementiert.

In der main-Methode konfigurieren Sie das Chat-Sprachmodell mithilfe des Builders für die VertexAiGeminiChatModel und geben Folgendes an:

  • Projekt
  • Standort
  • Modellname (gemini-1.5-flash-002)

Nachdem das Sprachmodell bereit ist, können Sie die Methode generate() aufrufen und den Prompt, die Frage oder die Anweisungen an das LLM übergeben. Hier stellen Sie eine einfache Frage dazu, warum der Himmel blau ist.

Sie können diesen Prompt ändern, um andere Fragen oder Aufgaben auszuprobieren.

Führen Sie das Beispiel im Stammverzeichnis des Quellcodes aus:

./gradlew run -q -DjavaMainClass=gemini.workshop.QA

Die Ausgabe sollte in etwa so aussehen:

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.

Herzlichen Glückwunsch, Sie haben Ihren ersten Anruf an Gemini getätigt.

Streamingantwort

Haben Sie bemerkt, dass die Antwort nach wenigen Sekunden in einem Zug gegeben wurde? Dank der Streaming-Antwortvariante ist es auch möglich, die Antwort schrittweise zu erhalten. Bei der Streamingantwort gibt das Modell die Antwort nach und nach zurück, sobald sie verfügbar ist.

In diesem Codelab konzentrieren wir uns auf die Antwort ohne Streaming. Sehen wir uns aber trotzdem die Streamingantwort an, um zu sehen, wie das geht.

Unter StreamQA.java im Verzeichnis app/src/main/java/gemini/workshop sehen Sie die Streamingantwort in Aktion:

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));
    }
}

Dieses Mal importieren wir die Streaming-Klassenvarianten VertexAiGeminiStreamingChatModel, die die Schnittstelle StreamingChatLanguageModel implementieren. Außerdem musst du LambdaStreamingResponseHandler.onNext statisch importieren. Das ist eine praktische Methode, mit der StreamingResponseHandlers einen Streaming-Handler mit Java-Lambda-Ausdrücken erstellen können.

Dieses Mal ist die Signatur der generate()-Methode etwas anders. Anstatt einen String zurückzugeben, ist der Rückgabetyp „void“. Zusätzlich zum Prompt musst du einen Streaming-Response-Handler übergeben. Hier können wir dank des oben erwähnten statischen Imports einen Lambda-Ausdruck definieren, den wir an die onNext()-Methode übergeben. Der Lambda-Ausdruck wird jedes Mal aufgerufen, wenn ein neuer Teil der Antwort verfügbar ist, während letzterer nur aufgerufen wird, wenn ein Fehler auftritt.

Ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA

Sie erhalten eine ähnliche Antwort wie im vorherigen Kurs. Dieses Mal wird die Antwort jedoch nach und nach in der Shell angezeigt, anstatt dass Sie auf die vollständige Antwort warten müssen.

Zusätzliche Konfiguration

Für die Konfiguration haben wir nur das Projekt, den Speicherort und den Modellnamen definiert. Es gibt aber noch weitere Parameter, die Sie für das Modell angeben können:

  • temperature(Float temp) – damit Sie festlegen können, wie kreativ die Antwort sein soll (0 steht für wenig kreativ und oft sachlicher, während 2 für kreativere Ergebnisse steht)
  • topP(Float topP) – zum Auswählen der möglichen Wörter, deren Gesamtwahrscheinlichkeit zu dieser Gleitkommazahl (zwischen 0 und 1) addiert wird
  • topK(Integer topK): Wählt zufällig ein Wort aus einer maximalen Anzahl wahrscheinlicher Wörter für die Textvervollständigung aus (von 1 bis 40).
  • maxOutputTokens(Integer max) – gibt die maximale Länge der vom Modell gegebenen Antwort an (4 Tokens entsprechen in der Regel etwa 3 Wörtern)
  • maxRetries(Integer retries): Wenn Sie das Kontingent für Anfragen pro Zeiteinheit überschreiten oder auf der Plattform ein technisches Problem auftritt, kann das Modell den Aufruf dreimal wiederholen.

Bisher haben Sie Gemini nur eine Frage gestellt, aber Sie können auch eine Unterhaltung mit mehreren Antworten führen. Im nächsten Abschnitt erfahren Sie mehr dazu.

5. Mit Gemini chatten

Im vorherigen Schritt haben Sie eine einzelne Frage gestellt. Jetzt ist es an der Zeit, eine echte Unterhaltung zwischen einem Nutzer und dem LLM zu führen. Jede Frage und Antwort kann auf den vorherigen aufbauen, um eine echte Diskussion zu ermöglichen.

Sehen Sie sich Conversation.java im Ordner app/src/main/java/gemini/workshop an:

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));
        });
    }
}

Einige neue interessante Importe in dieser Klasse:

  • MessageWindowChatMemory – eine Klasse, die den mehrstufigen Aspekt der Unterhaltung unterstützt und die vorherigen Fragen und Antworten im lokalen Speicher speichert
  • AiServices – eine Abstraktionsklasse höherer Ebene, die das Chatmodell und den Chatspeicher verbindet

In der Hauptmethode richten Sie das Modell, den Chatspeicher und den KI-Dienst ein. Das Modell wird wie gewohnt mit den Informationen zum Projekt, Standort und Modellnamen konfiguriert.

Für den Chatspeicher verwenden wir den Builder von MessageWindowChatMemory, um einen Speicher zu erstellen, in dem die letzten 20 getauschten Nachrichten gespeichert werden. Es ist ein gleitendes Fenster über die Unterhaltung, dessen Kontext lokal in unserem Java-Klassenclient gespeichert wird.

Anschließend erstellen Sie die AI service, die das Chatmodell mit dem Chatspeicher verknüpft.

Beachten Sie, dass der KI-Dienst eine benutzerdefinierte ConversationService-Schnittstelle verwendet, die wir definiert haben, die von LangChain4j implementiert wird und eine String-Abfrage entgegennimmt und eine String-Antwort zurückgibt.

Jetzt ist es an der Zeit, mit Gemini zu sprechen. Zuerst wird eine einfache Begrüßung gesendet, dann eine erste Frage zum Eiffelturm, um herauszufinden, in welchem Land er sich befindet. Der letzte Satz bezieht sich auf die Antwort auf die erste Frage, da Sie wissen möchten, wie viele Einwohner das Land hat, in dem sich der Eiffelturm befindet, ohne das Land, das in der vorherigen Antwort genannt wurde, ausdrücklich zu erwähnen. Sie sehen, dass mit jedem Prompt frühere Fragen und Antworten gesendet werden.

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation

Sie sollten drei Antworten sehen, die in etwa so aussehen:

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.

Sie können Gemini einzelne Fragen stellen oder Unterhaltungen über mehrere Themen führen. Bisher war die Eingabe jedoch nur Text. Was ist mit Bildern? Im nächsten Schritt geht es um Bilder.

6. Multimodalität mit Gemini

Gemini ist ein multimodales Modell. Als Eingaben sind nicht nur Text, sondern auch Bilder oder sogar Videos zulässig. In diesem Abschnitt sehen Sie einen Anwendungsfall für die Kombination von Text und Bildern.

Glaubst du, dass Gemini diese Katze erkennt?

af00516493ec9ade.png

Bild einer Katze im Schnee von Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

Sehen Sie sich Multimodal.java im Verzeichnis app/src/main/java/gemini/workshop an:

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());
    }
}

Bei den Importen wird zwischen verschiedenen Arten von Nachrichten und Inhalten unterschieden. Ein UserMessage kann sowohl ein TextContent- als auch ein ImageContent-Objekt enthalten. Das ist Multimodalität: Text und Bilder werden kombiniert. Wir senden nicht nur einen einfachen String-Prompt, sondern ein strukturierteres Objekt, das eine Nutzernachricht darstellt und aus einem Bild- und einem Textelement besteht. Das Modell sendet ein Response zurück, das ein AiMessage enthält.

Sie rufen dann den AiMessage über content() aus der Antwort ab und dann den Text der Nachricht über text().

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal

Der Name des Bildes hat Ihnen sicherlich einen Hinweis darauf gegeben, was es enthält. Die Gemini-Ausgabe sieht jedoch in etwa so aus:

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.

Die Kombination von Bildern und Textprompts eröffnet interessante Anwendungsfälle. Sie können Anwendungen erstellen, die Folgendes können:

  • Text in Bildern erkennen
  • Prüfen, ob ein Bild sicher angezeigt werden kann
  • Bildunterschriften erstellen
  • In einer Datenbank mit Bildern mit Beschreibungen in Form von einfachen Texten suchen

Sie können nicht nur Informationen aus Bildern, sondern auch aus unstrukturiertem Text extrahieren. Das erfahren Sie im nächsten Abschnitt.

7. Strukturierte Informationen aus unstrukturiertem Text extrahieren

Es gibt viele Situationen, in denen wichtige Informationen in Berichtsdokumenten, E-Mails oder anderen Texten im Langformat unstrukturiert angegeben werden. Idealerweise sollten Sie die wichtigsten Details im unstrukturierten Text in Form von strukturierten Objekten extrahieren können. Sehen wir uns an, wie das geht.

Angenommen, Sie möchten den Namen und das Alter einer Person anhand einer Biografie, eines Lebenslaufs oder einer Beschreibung dieser Person extrahieren. Sie können das LLM anweisen, JSON mit einem intelligent optimierten Prompt aus unstrukturiertem Text zu extrahieren. Dieser Vorgang wird auch als Prompt-Engineering bezeichnet.

Im folgenden Beispiel verwenden wir jedoch keine Prompts, die die JSON-Ausgabe beschreiben, sondern eine leistungsstarke Funktion von Gemini namens strukturierte Ausgabe, manchmal auch eingeschränkte Dekodierung, die das Modell dazu zwingt, nur gültige JSON-Inhalte gemäß einem bestimmten JSON-Schema auszugeben.

Sehen Sie sich ExtractData.java in app/src/main/java/gemini/workshop an:

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

Sehen wir uns die verschiedenen Schritte in dieser Datei an:

  • Ein Person-Datensatz ist definiert, um die Details einer Person (Name und Alter) darzustellen.
  • Die PersonExtractor-Schnittstelle ist mit einer Methode definiert, die für einen unstrukturierten Textstring eine Person-Instanz zurückgibt.
  • Die extractPerson() ist mit einer @SystemMessage-Anmerkung versehen, die ihr einen Anweisungsprompt zuordnet. Anhand dieses Prompts extrahiert das Modell die Informationen und gibt die Details in Form eines JSON-Dokuments zurück, das für Sie geparst und in eine Person-Instanz umgewandelt wird.

Sehen wir uns nun den Inhalt der Methode main() an:

  • Das Chatmodell wird konfiguriert und instanziiert. Wir verwenden zwei neue Methoden der Klasse „ModelBuilder“: responseMimeType() und responseSchema(). Mit der ersten wird Gemini angewiesen, gültiges JSON in der Ausgabe zu generieren. Die zweite Methode definiert das Schema des JSON-Objekts, das zurückgegeben werden soll. Außerdem wird an eine praktische Methode delegiert, die einen Java-Klassen- oder -Datensatz in ein gültiges JSON-Schema konvertieren kann.
  • Ein PersonExtractor-Objekt wird mit der AiServices-Klasse von LangChain4j erstellt.
  • Anschließend können Sie einfach Person person = extractor.extractPerson(...) aufrufen, um die Details der Person aus dem unstrukturierten Text zu extrahieren, und eine Person-Instanz mit dem Namen und dem Alter zurückerhalten.

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData

Es sollte folgende Ausgabe angezeigt werden:

Anna
23

Ja, das ist Anna und sie ist 23 Jahre alt.

Bei diesem AiServices-Ansatz arbeiten Sie mit stark typisierten Objekten. Sie interagieren nicht direkt mit dem LLM. Stattdessen arbeiten Sie mit konkreten Klassen, z. B. dem Person-Eintrag, um die extrahierten personenbezogenen Daten darzustellen, und Sie haben ein PersonExtractor-Objekt mit einer extractPerson()-Methode, die eine Person-Instanz zurückgibt. Der Begriff LLM wird abstrahiert und als Java-Entwickler manipulieren Sie nur normale Klassen und Objekte, wenn Sie diese PersonExtractor-Schnittstelle verwenden.

8. Prompts mit Promptvorlagen strukturieren

Wenn Sie mit einem LLM über eine Reihe von Anweisungen oder Fragen interagieren, gibt es einen Teil dieser Aufforderung, der sich nie ändert, während andere Teile die Daten enthalten. Wenn Sie beispielsweise Rezepte erstellen möchten, können Sie einen Prompt wie „Sie sind ein talentierter Koch. Bitte erstellen Sie ein Rezept mit den folgenden Zutaten: …“ verwenden und dann die Zutaten an das Ende dieses Textes anhängen. Dazu dienen Promptvorlagen, ähnlich wie interpolierte Strings in Programmiersprachen. Eine Promptvorlage enthält Platzhalter, die Sie für einen bestimmten Aufruf der LLM durch die richtigen Daten ersetzen können.

Sehen wir uns TemplatePrompt.java im Verzeichnis app/src/main/java/gemini/workshop genauer an:

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());
    }
}

Wie gewohnt konfigurieren Sie das VertexAiGeminiChatModel-Modell mit einer hohen Kreativität, einer hohen Temperatur sowie hohen Top-P- und Top-K-Werten. Anschließend erstellen Sie einen PromptTemplate mit der statischen Methode from(), indem Sie den String unseres Prompts übergeben und die Platzhaltervariablen {{dish}} und {{ingredients}} in doppelten geschweiften Klammern verwenden.

Sie erstellen den endgültigen Prompt durch Aufrufen von apply(), wobei eine Zuordnung von Schlüssel/Wert-Paaren verwendet wird, die den Namen des Platzhalters und den Stringwert darstellen, durch den er ersetzt werden soll.

Rufen Sie abschließend die Methode generate() des Gemini-Modells auf, indem Sie mit der Anweisung prompt.toUserMessage() eine Nutzernachricht aus diesem Prompt erstellen.

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt

Die Ausgabe sollte in etwa so aussehen:

**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.

Sie können die Werte von dish und ingredients in der Karte ändern, die Temperatur (topK und tokP) anpassen und den Code noch einmal ausführen. So können Sie die Auswirkungen der Änderung dieser Parameter auf die LLM beobachten.

Promptvorlagen sind eine gute Möglichkeit, wiederverwendbare und parametrisierbare Anweisungen für LLM-Aufrufe zu haben. Sie können Daten übergeben und Prompts für verschiedene Werte anpassen, die von Ihren Nutzern angegeben werden.

9. Textklassifizierung mit Few-Shot-Prompts

LLMs sind ziemlich gut darin, Text in verschiedene Kategorien zu klassifizieren. Sie können einem LLM bei dieser Aufgabe helfen, indem Sie einige Textbeispiele und die zugehörigen Kategorien angeben. Dieser Ansatz wird oft als Few-Shot-Prompting bezeichnet.

Öffnen Sie TextClassification.java im Verzeichnis app/src/main/java/gemini/workshop, um eine bestimmte Art der Textklassifizierung durchzuführen: die Sentimentanalyse.

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!"));
    }
}

Eine Sentiment-Enumeration enthält die verschiedenen Werte für ein Sentiment: negativ, neutral oder positiv.

Bei der Methode main() erstellen Sie das Gemini-Chatmodell wie gewohnt, aber mit einer kleinen maximalen Anzahl von Ausgabetokens, da Sie nur eine kurze Antwort wünschen: Der Text ist POSITIVE, NEGATIVE oder NEUTRAL. Wenn Sie das Modell so einschränken möchten, dass es nur diese Werte zurückgibt, können Sie die Unterstützung für strukturierte Ausgabe nutzen, die Sie im Abschnitt zur Datenextraktion kennengelernt haben. Deshalb wird die Methode responseSchema() verwendet. Dieses Mal verwenden Sie nicht die praktische Methode von SchemaHelper, um die Schemadefinition abzuleiten, sondern den Schema-Builder, um zu sehen, wie eine Schemadefinition aussieht.

Nachdem das Modell konfiguriert ist, erstellen Sie eine SentimentAnalysis-Schnittstelle, die die AiServices von LangChain4j mithilfe des LLM für Sie implementiert. Diese Schnittstelle enthält eine Methode: analyze(). Er nimmt den zu analysierenden Text als Eingabe entgegen und gibt einen Sentiment-Enum-Wert zurück. Sie manipulieren also nur ein stark typisiertes Objekt, das die erkannte Stimmungsklasse darstellt.

Um dem Modell dann mit wenigen Beispielen zu helfen, die Klassifizierung durchzuführen, erstellen Sie ein Chat-Gedächtnis, in dem Nutzernachrichten und KI-Antworten übergeben werden, die den Text und das zugehörige Sentiment repräsentieren.

Binden wir alles mit der AiServices.builder()-Methode zusammen, indem wir unsere SentimentAnalysis-Benutzeroberfläche, das zu verwendende Modell und den Chatspeicher mit den Few-Shot-Beispielen übergeben. Rufen Sie abschließend die Methode analyze() mit dem zu analysierenden Text auf.

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification

Es sollte ein einzelnes Wort angezeigt werden:

POSITIVE

Anscheinend ist es positiv, Erdbeeren zu mögen.

10. Retrieval-Augmented Generation

LLMs werden mit einer großen Menge an Text trainiert. Das Wissen des Modells umfasst jedoch nur Informationen, die es während des Trainings gesehen hat. Wenn nach dem Stichtag für das Modelltraining neue Informationen veröffentlicht werden, sind diese Details für das Modell nicht verfügbar. Daher kann das Modell keine Fragen zu Informationen beantworten, die es nicht gesehen hat.

Deshalb können Ansätze wie die Retrieval-Augmented Generation (RAG), die in diesem Abschnitt behandelt werden, dazu beitragen, die zusätzlichen Informationen bereitzustellen, die ein LLM möglicherweise benötigt, um die Anfragen seiner Nutzer zu erfüllen, mit aktuelleren Informationen zu antworten oder auf private Informationen zurückzugreifen, auf die während des Trainings nicht zugegriffen werden kann.

Kommen wir zurück zu Unterhaltungen. Dieses Mal können Sie Fragen zu Ihren Dokumenten stellen. Sie erstellen einen Chatbot, der relevante Informationen aus einer Datenbank abrufen kann, die Ihre Dokumente in kleinere Teile („Chunks“) aufteilt. Diese Informationen werden vom Modell verwendet, um seine Antworten zu begründen, anstatt sich ausschließlich auf das Wissen zu verlassen, das im Training enthalten ist.

Bei der RAG-Methode gibt es zwei Phasen:

  1. Aufnahmephase: Dokumente werden in den Arbeitsspeicher geladen, in kleinere Blöcke aufgeteilt und Vektoreinbettungen (eine hochdimensionale Vektordarstellung der Blöcke) berechnet und in einer Vektordatenbank gespeichert, die semantische Suchanfragen ermöglicht. Diese Aufnahmephase wird normalerweise einmal durchgeführt, wenn dem Dokumentkorpus neue Dokumente hinzugefügt werden müssen.

cd07d33d20ffa1c8.png

  1. Abfragephase: Nutzer können jetzt Fragen zu den Dokumenten stellen. Die Frage wird ebenfalls in einen Vektor umgewandelt und mit allen anderen Vektoren in der Datenbank verglichen. Die ähnlichsten Vektoren sind in der Regel semantisch verwandt und werden von der Vektordatenbank zurückgegeben. Anschließend wird dem LLM der Kontext der Unterhaltung und die Textblöcke übergeben, die den von der Datenbank zurückgegebenen Vektoren entsprechen. Es wird aufgefordert, seine Antwort anhand dieser Blöcke zu begründen.

a1d2e2deb83c6d27.png

Dokumente vorbereiten

In diesem neuen Beispiel stellen Sie Fragen zu einem fiktiven Automodell eines ebenfalls fiktiven Autoherstellers: dem Cymbal Starlight. Ein Dokument zu einem fiktiven Auto sollte nicht Teil des Wissens zum Modell sein. Wenn Gemini also Fragen zu diesem Auto richtig beantworten kann, funktioniert der RAG-Ansatz: Das System kann Ihr Dokument durchsuchen.

Chatbot implementieren

Sehen wir uns an, wie der zweiphasige Ansatz funktioniert: zuerst die Dokumentaufnahme und dann die Abfragezeit (auch als „Abrufphase“ bezeichnet), wenn Nutzer Fragen zum Dokument stellen.

In diesem Beispiel sind beide Phasen in derselben Klasse implementiert. Normalerweise haben Sie eine Anwendung, die die Datenaufnahme übernimmt, und eine andere Anwendung, die Ihren Nutzern die Chatbot-Benutzeroberfläche bietet.

Außerdem verwenden wir in diesem Beispiel eine In-Memory-Vektordatenbank. In einem echten Produktionsszenario werden die Phasen der Datenaufnahme und der Abfrage in zwei separaten Anwendungen getrennt und die Vektoren in einer eigenständigen Datenbank gespeichert.

Dokumentaufnahme

Der erste Schritt in der Phase der Dokumentaufnahme besteht darin, die PDF-Datei zu unserem fiktiven Auto zu finden und einen PdfParser zum Lesen vorzubereiten:

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());

Anstatt zuerst das übliche Chat-Sprachmodell zu erstellen, erstellen Sie eine Instanz eines Embedding-Modells. Dieses Modell dient dazu, Vektordarstellungen von Textstücken (Wörtern, Sätzen oder sogar Absätzen) zu erstellen. Es gibt keine Textantworten zurück, sondern Vektoren von Gleitkommazahlen.

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();

Als Nächstes benötigen Sie einige Kurse, in denen Sie zusammenarbeiten können, um:

  • Laden Sie das PDF-Dokument und teilen Sie es in Blöcke auf.
  • Erstellen Sie Vektoreinbettungen für alle diese Segmente.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
    .documentSplitter(DocumentSplitters.recursive(500, 100))
    .embeddingModel(embeddingModel)
    .embeddingStore(embeddingStore)
    .build();
storeIngestor.ingest(document);

Eine Instanz von InMemoryEmbeddingStore, einer In-Memory-Vektordatenbank, wird zum Speichern der Vektoreinbettungen erstellt.

Das Dokument wird dank der Klasse DocumentSplitters in Blöcke unterteilt. Der Text der PDF-Datei wird in Snippets mit 500 Zeichen mit einer Überschneidung von 100 Zeichen (mit dem nächsten Teil, um zu vermeiden, dass Wörter oder Sätze in Stücke geschnitten werden) aufgeteilt.

Der Datenaufnahmemechanismus für den Vektorspeicher verknüpft den Dokumentensplitter, das Einbettungsmodell zum Berechnen der Vektoren und die In-Memory-Vektordatenbank. Die Datenaufnahme wird dann von der Methode ingest() durchgeführt.

Die erste Phase ist jetzt vorbei. Das Dokument wurde in Textchunks mit den zugehörigen Vektor-Embeddings umgewandelt und in der Vektordatenbank gespeichert.

Fragen stellen

Jetzt ist es an der Zeit, Fragen zu stellen. Erstellen Sie ein Chat-Modell, um die Unterhaltung zu starten:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-1.5-flash-002")
        .maxOutputTokens(1000)
        .build();

Außerdem benötigen Sie eine Retriever-Klasse, um die Vektordatenbank (in der Variablen embeddingStore) mit dem Einbettungsmodell zu verknüpfen. Seine Aufgabe besteht darin, die Vektordatenbank abzufragen, indem eine Vektoreinbettung für die Abfrage des Nutzers berechnet wird, um ähnliche Vektoren in der Datenbank zu finden:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Erstellen Sie eine Benutzeroberfläche, die einen Autoexperten darstellt. Diese Benutzeroberfläche wird von der Klasse AiServices implementiert, damit Sie mit dem Modell interagieren können:

interface CarExpert {
    Result<String> ask(String question);
}

Die CarExpert-Schnittstelle gibt eine Stringantwort zurück, die in der Result-Klasse von LangChain4j verpackt ist. Vorteile dieses Wrappers Denn damit erhalten Sie nicht nur die Antwort, sondern können auch die Chunks aus der Datenbank prüfen, die vom Content Retriever zurückgegeben wurden. So können Sie die Quellen der Dokumente anzeigen, die für die endgültige Antwort an den Nutzer verwendet werden.

Jetzt können Sie einen neuen KI-Dienst konfigurieren:

CarExpert expert = AiServices.builder(CarExpert.class)
    .chatLanguageModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .contentRetriever(retriever)
    .build();

Dieser Dienst verbindet Folgendes:

  • Das Modell für die Chatsprache, das Sie zuvor konfiguriert haben.
  • Eine Chat-Erinnerung, um den Überblick über die Unterhaltung zu behalten.
  • Der retriever vergleicht eine Vektor-Embedding-Abfrage mit den Vektoren in der Datenbank.
.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())

Sie können jetzt endlich Ihre Fragen stellen.

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());
});

Der vollständige Quellcode befindet sich im Verzeichnis app/src/main/java/gemini/workshop unter RAG.java.

Beispiel ausführen:

./gradlew -q run -DjavaMainClass=gemini.workshop.RAG

In der Ausgabe sollten Antworten auf Ihre Fragen angezeigt werden:

=== 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. Funktionsaufrufe

Es gibt Situationen, in denen Sie möchten, dass ein LLM Zugriff auf externe Systeme hat, z. B. auf eine Remote-Web-API, die Informationen abrufen oder eine Aktion ausführen soll, oder auf Dienste, die eine Art von Berechnung ausführen. Beispiel:

Remote-Web-APIs:

  • Kundenbestellungen verfolgen und aktualisieren
  • Suchen Sie in einem Issue-Tracker nach einem Ticket oder erstellen Sie ein neues.
  • Echtzeitdaten wie Aktienkurse oder IoT-Sensormessungen abrufen
  • E-Mail senden

Berechnungstools:

  • Ein Rechner für komplexere mathematische Probleme.
  • Codeinterpretation für den Codeausführung, wenn LLMs eine Argumentationslogik benötigen.
  • Anfragen in natürlicher Sprache in SQL-Abfragen umwandeln, damit ein LLM eine Datenbank abfragen kann.

Der Funktionsaufruf (manchmal auch als Tools oder Toolnutzung bezeichnet) ist die Möglichkeit für das Modell, einen oder mehrere Funktionsaufrufe in seinem Namen anzufordern, damit es die Aufforderung eines Nutzers mit aktuelleren Daten richtig beantworten kann.

Wenn ein Nutzer einen bestimmten Prompt gibt und das LLM über vorhandene Funktionen verfügt, die für diesen Kontext relevant sein können, kann es mit einer Funktionsaufrufanfrage antworten. Die Anwendung, in die das LLM eingebunden ist, kann die Funktion dann in seinem Namen aufrufen und dann mit einer Antwort an das LLM antworten. Das LLM interpretiert die Antwort dann und antwortet mit einer Textantwort.

Vier Schritte des Funktionsaufrufs

Sehen wir uns ein Beispiel für einen Funktionsaufruf an: das Abrufen von Informationen zur Wettervorhersage.

Wenn Sie Gemini oder ein anderes LLM nach dem Wetter in Paris fragen, antwortet es, dass es keine Informationen zur aktuellen Wettervorhersage hat. Wenn der LLM Echtzeitzugriff auf die Wetterdaten haben soll, müssen Sie einige Funktionen definieren, die er anfordern kann.

Sehen Sie sich das folgende Diagramm an:

31e0c2aba5e6f21c.png

1️⃣ Zuerst fragt ein Nutzer nach dem Wetter in Paris. Die Chatbot-App (mit LangChain4j) weiß, dass eine oder mehrere Funktionen zur Verfügung stehen, um dem LLM bei der Ausführung der Abfrage zu helfen. Der Chatbot sendet sowohl den ursprünglichen Prompt als auch die Liste der aufrufbaren Funktionen. Hier wird eine Funktion namens getWeather() verwendet, die einen Stringparameter für den Standort annimmt.

8863be53a73c4a70.png

Da das LLM keine Wettervorhersagen kennt, sendet es anstelle einer Antwort per SMS eine Funktionsausführungsanfrage zurück. Der Chatbot muss die Funktion getWeather() mit "Paris" als Standortparameter aufrufen.

d1367cc69c07b14d.png

2️⃣ Der Chatbot ruft diese Funktion im Namen des LLM auf und ruft die Funktionsantwort ab. Angenommen, die Antwort lautet {"forecast": "sunny"}.

73a5f2ed19f47d8.png

3️⃣ Die Chatbot-App sendet die JSON-Antwort an den LLM zurück.

20832cb1ee6fbfeb.png

4️⃣ Der LLM prüft die JSON-Antwort, interpretiert diese Informationen und antwortet schließlich mit dem Text, dass das Wetter in Paris sonnig ist.

Jeder Schritt als Code

Konfigurieren Sie zuerst das Gemini-Modell wie gewohnt:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .modelName("gemini-1.5-flash-002")
    .maxOutputTokens(100)
    .build();

Sie definieren eine Tool-Spezifikation, die die aufrufbare Funktion beschreibt:

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();

Der Name der Funktion sowie der Name und Typ des Parameters werden definiert. Beachten Sie jedoch, dass sowohl die Funktion als auch die Parameter Beschreibungen haben. Beschreibungen sind sehr wichtig und helfen dem LLM, wirklich zu verstehen, was eine Funktion tun kann, und so zu beurteilen, ob diese Funktion im Kontext der Unterhaltung aufgerufen werden muss.

Beginnen wir mit Schritt 1 und senden Sie die erste Frage zum Wetter in 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);

In Schritt 2 übergeben wir das Tool, das das Modell verwenden soll. Das Modell antwortet mit einer Toolausführungsanfrage:

// 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());

Schritt 3: An dieser Stelle wissen wir, welche Funktion der LLM von uns aufgerufen haben möchte. Im Code wird keine externe API aufgerufen, sondern es wird direkt eine hypothetische Wettervorhersage zurückgegeben:

// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
    "{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);

In Schritt 4 lernt das LLM das Ergebnis der Funktionsausführung kennen und kann dann eine textbasierte Antwort synthetisieren:

// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());

Die Ausgabe sieht so aus:

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.

In der Ausgabe oben sehen Sie die Anfrage zur Toolausführung sowie die Antwort.

Der vollständige Quellcode befindet sich im Verzeichnis app/src/main/java/gemini/workshop unter FunctionCalling.java:

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling

Die Ausgabe sollte in etwa so aussehen:

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 verarbeitet Funktionsaufrufe

Im vorherigen Schritt haben Sie gesehen, wie die normalen Textfragen/-antworten und Funktionsanfragen/-antworten überlagert werden. Dazwischen haben Sie die angeforderte Funktionsantwort direkt bereitgestellt, ohne eine echte Funktion aufzurufen.

LangChain4j bietet jedoch auch eine Abstraktion auf höherer Ebene, die die Funktionsaufrufe transparent für Sie verarbeiten kann, während die Unterhaltung wie gewohnt abläuft.

Einzelner Funktionsaufruf

Sehen wir uns FunctionCallingAssistant.java im Detail an.

Zuerst erstellen Sie einen Datensatz, der die Antwortdatenstruktur der Funktion darstellt:

record WeatherForecast(String location, String forecast, int temperature) {}

Die Antwort enthält Informationen zum Standort, zur Vorhersage und zur Temperatur.

Anschließend erstellen Sie eine Klasse, die die eigentliche Funktion enthält, die Sie dem Modell zur Verfügung stellen möchten:

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);
        }
    }
}

Diese Klasse enthält nur eine Funktion, die aber mit der Anmerkung @Tool versehen ist. Diese entspricht der Beschreibung der Funktion, die das Modell aufrufen kann.

Die Parameter der Funktion (hier nur einer) sind ebenfalls mit einer kurzen @P-Anmerkung versehen, die auch eine Beschreibung des Parameters enthält. Sie können beliebig viele Funktionen hinzufügen, um sie für komplexere Szenarien für das Modell verfügbar zu machen.

In dieser Klasse geben Sie einige vordefinierte Antworten zurück. Wenn Sie jedoch einen echten externen Wettervorhersagedienst aufrufen möchten, würden Sie dies im Body dieser Methode tun.

Wie wir beim Erstellen einer ToolSpecification im vorherigen Ansatz gesehen haben, ist es wichtig, zu dokumentieren, was eine Funktion tut, und zu beschreiben, wofür die Parameter stehen. So kann das Modell besser nachvollziehen, wie und wann diese Funktion verwendet werden kann.

Als Nächstes können Sie mit LangChain4j eine Benutzeroberfläche bereitstellen, die dem Vertrag entspricht, den Sie für die Interaktion mit dem Modell verwenden möchten. Hier ist es eine einfache Benutzeroberfläche, die einen String für die Nutzernachricht entgegennimmt und einen String zurückgibt, der der Antwort des Modells entspricht:

interface WeatherAssistant {
    String chat(String userMessage);
}

Es ist auch möglich, komplexere Signaturen zu verwenden, die die UserMessage (für eine Nutzernachricht) oder AiMessage (für eine Modellantwort) von LangChain4j enthalten, oder sogar eine TokenStream, wenn Sie komplexere Situationen verarbeiten möchten, da diese komplizierteren Objekte auch zusätzliche Informationen wie die Anzahl der verwendeten Tokens enthalten. Der Einfachheit halber nehmen wir jedoch nur String als Eingabe und String als Ausgabe.

Zum Schluss sehen wir uns die main()-Methode an, die alle Teile zusammenführt:

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?"));
}

Sie konfigurieren das Gemini-Chatmodell wie gewohnt. Anschließend instanziieren Sie Ihren Wettervorhersagedienst, der die „Funktion“ enthält, die das Modell aufrufen soll.

Jetzt verwenden Sie die Klasse AiServices noch einmal, um das Chatmodell, das Chatgedächtnis und das Tool (d. h. den Wettervorhersagedienst mit seiner Funktion) zu verknüpfen. AiServices gibt ein Objekt zurück, das die von Ihnen definierte WeatherAssistant-Schnittstelle implementiert. Jetzt müssen Sie nur noch die chat()-Methode dieses Assistenten aufrufen. Wenn Sie die Funktion aufrufen, sehen Sie nur die Textantworten. Die Funktionsaufrufanfragen und die Funktionsaufrufantworten sind für den Entwickler nicht sichtbar und werden automatisch und transparent verarbeitet. Wenn Gemini der Meinung ist, dass eine Funktion aufgerufen werden sollte, antwortet es mit der Funktionsaufrufanfrage. LangChain4j ruft dann die lokale Funktion in Ihrem Namen auf.

Beispiel ausführen:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant

Die Ausgabe sollte in etwa so aussehen:

OK. The weather in Paris is sunny with a temperature of 20 degrees.

Dies war ein Beispiel für eine einzelne Funktion.

Mehrere Funktionsaufrufe

Sie können auch mehrere Funktionen haben und LangChain4j mehrere Funktionsaufrufe in Ihrem Namen verarbeiten lassen. Ein Beispiel für mehrere Funktionen findest du unter MultiFunctionCallingAssistant.java.

Es gibt eine Funktion zum Umrechnen von Währungen:

@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;
}

Eine weitere Funktion zum Abrufen des Werts einer Aktie:

@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;
}

Eine weitere Funktion, mit der Sie einen Prozentsatz auf einen bestimmten Betrag anwenden können:

@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;
}

Sie können dann alle diese Funktionen und eine MultiTools-Klasse kombinieren und Fragen wie „Was sind 10% des Aktienkurses von AAPL in Euro, umgerechnet von USD?“ stellen.

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?"));
}

Führen Sie ihn so aus:

./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant

Außerdem sollten Sie die folgenden Funktionen sehen:

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.

Zu Agents

Funktionsaufrufe sind ein hervorragender Erweiterungsmechanismus für Large Language Models wie Gemini. So können wir komplexere Systeme entwickeln, die oft als „Agenten“ oder „KI-Assistenten“ bezeichnet werden. Diese Bots können über externe APIs und Dienste mit der Außenwelt interagieren, die Nebenwirkungen auf die externe Umgebung haben können (z. B. das Senden von E-Mails oder das Erstellen von Tickets).

Wenn Sie solche leistungsstarken Bots erstellen, sollten Sie dies verantwortungsvoll tun. Sie sollten vor dem Ausführen automatischer Aktionen einen Mitarbeiter hinzuziehen. Bei der Entwicklung von LLM-basierten Agenten, die mit der Außenwelt interagieren, ist es wichtig, die Sicherheit im Auge zu behalten.

13. Gemma mit Ollama und TestContainers ausführen

Bisher haben wir Gemini verwendet, aber es gibt auch Gemma, das kleine Schwestermodell.

Gemma ist eine Familie leichter, hochmoderner offener Modelle, die auf derselben Forschung und Technologie basieren, die auch für die Erstellung der Gemini-Modelle verwendet werden. Gemma ist in zwei Varianten erhältlich: Gemma1 und Gemma2, jeweils in verschiedenen Größen. Gemma1 ist in zwei Größen erhältlich: 2B und 7B. Gemma2 ist in zwei Größen erhältlich: 9B und 27B. Die Gewichte sind frei verfügbar und da sie klein sind, können Sie sie auch auf Ihrem Laptop oder in Cloud Shell ausführen.

Wie führe ich Gemma aus?

Es gibt viele Möglichkeiten, Gemma auszuführen: in der Cloud, über Vertex AI mit nur einem Klick oder in GKE mit einigen GPUs. Sie können es aber auch lokal ausführen.

Eine gute Option, Gemma lokal auszuführen, ist Ollama. Mit diesem Tool können Sie kleine Modelle wie Llama 2, Mistral und viele andere auf Ihrem lokalen Computer ausführen. Es ähnelt Docker, ist aber für LLMs gedacht.

Installieren Sie Ollama gemäß der Anleitung für Ihr Betriebssystem.

Wenn Sie eine Linux-Umgebung verwenden, müssen Sie Ollama nach der Installation zuerst aktivieren.

ollama serve > /dev/null 2>&1 & 

Nach der lokalen Installation können Sie Befehle ausführen, um ein Modell abzurufen:

ollama pull gemma:2b

Warten Sie, bis das Modell abgerufen wurde. Das kann eine Weile dauern.

Modell ausführen:

ollama run gemma:2b

Sie können jetzt mit dem Modell interagieren:

>>> Hello!
Hello! It's nice to hear from you. What can I do for you today?

Drücken Sie Strg + D, um die Eingabeaufforderung zu schließen.

Gemma in Ollama auf TestContainers ausführen

Anstatt Ollama lokal installieren und ausführen zu müssen, können Sie Ollama in einem Container verwenden, der von TestContainers verwaltet wird.

TestContainers ist nicht nur für Tests nützlich, sondern kann auch zum Ausführen von Containern verwendet werden. Es gibt sogar einen speziellen OllamaContainer, den du nutzen kannst.

Hier ist das Gesamtbild:

2382c05a48708dfd.png

Implementierung

Sehen wir uns GemmaWithOllamaContainer.java im Detail an.

Zuerst müssen Sie einen abgeleiteten Ollama-Container erstellen, der das Gemma-Modell abruft. Dieses Image ist entweder bereits aus einem vorherigen Lauf vorhanden oder wird erstellt. Wenn das Image bereits vorhanden ist, teilen Sie TestContainers einfach mit, dass Sie das Standard-Ollama-Image durch Ihre Gemma-basierte Variante ersetzen möchten:

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"));
    }
}

Als Nächstes erstellen und starten Sie einen Ollama-Testcontainer und erstellen dann ein Ollama-Chatmodell, indem Sie die Adresse und den Port des Containers mit dem gewünschten Modell angeben. Rufen Sie abschließend model.generate(yourPrompt) wie gewohnt auf:

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);
}

Führen Sie ihn so aus:

./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer

Beim ersten Durchlauf dauert es eine Weile, bis der Container erstellt und ausgeführt wird. Danach sollte Gemma antworten:

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.

Sie haben Gemma in Cloud Shell ausgeführt.

14. Glückwunsch

Herzlichen Glückwunsch! Sie haben Ihre erste Chatanwendung mit generativer KI in Java mit LangChain4j und der Gemini API erstellt. Sie haben festgestellt, dass multimodale Large Language Models ziemlich leistungsstark sind und verschiedene Aufgaben wie Fragen/Antworten, auch in Ihrer eigenen Dokumentation, Datenextraktion und Interaktion mit externen APIs bewältigen können.

Was liegt als Nächstes an?

Jetzt können Sie Ihre Anwendungen mit leistungsstarken LLM-Integrationen optimieren.

Weitere Informationen

Referenzdokumente