Gemini in Java mit Vertex AI und LangChain4j

1. Einführung

In diesem Codelab geht es um das Large Language Model (LLM) von Gemini, das in Vertex AI in Google Cloud gehostet wird. Vertex AI ist eine Plattform, die alle Produkte, Dienste und Modelle für maschinelles Lernen von Google Cloud umfasst.

Für die Interaktion mit der Gemini API über das LangChain4j-Framework verwenden Sie Java. Sie werden anhand konkreter Beispiele das LLM für Fragenantworten, Ideengenerierung, Extraktion von Entitäten und strukturierten Inhalten, Retrieval Augmented Generation und Funktionsaufrufe nutzen.

Was ist generative KI?

Generative KI bezieht sich auf den Einsatz von künstlicher Intelligenz, um neue Inhalte wie Texte, Bilder, Musik, Audio und Videos zu erstellen.

Generative AI basiert auf Large Language Models (LLMs), die Multitasking ermöglichen und sofort einsatzbereite Aufgaben wie Zusammenfassung, Fragen und Antworten, Klassifizierungen ausführen. Basismodelle können mit minimalem Training und mit sehr wenigen Beispieldaten für gezielte Anwendungsfälle angepasst werden.

Wie funktioniert generative KI?

Generative KI nutzt ein Modell für maschinelles Lernen (ML), um die Muster und Beziehungen in einem Dataset mit von Menschen erstellten Inhalten zu lernen. Anschließend werden anhand der erlernten Muster neue Inhalte generiert.

Die gängigste Methode zum Trainieren eines Generative-AI-Modells ist die Verwendung des überwachten Lernens. Das Modell erhält eine Reihe von manuell erstellten Inhalten und entsprechenden Labels. Dann lernt es, Inhalte zu generieren, die den von Menschen erstellten Inhalten ähneln.

Was sind gängige Generative AI-Anwendungen?

Mit generativer KI können Sie:

  • Verbessern Sie die Interaktion mit Kunden durch verbesserte Chat- und Suchfunktionen.
  • Über Konversationsschnittstellen und Zusammenfassungen können Sie riesige Mengen unstrukturierter Daten untersuchen.
  • Er unterstützt Sie bei sich wiederholenden Aufgaben wie dem Beantworten von Angebotsanfragen, der Lokalisierung von Marketinginhalten in verschiedenen Sprachen und der Überprüfung von Kundenverträgen auf Compliance.

Welche generative KI-Angebote bietet Google Cloud?

Mit Vertex AI können Sie mit wenig bis gar keinem ML-Fachwissen mit Foundation Models interagieren, sie anpassen und in Ihre Anwendungen einbetten. Sie können in Model Garden auf Foundation Models zugreifen, sie über eine einfache Benutzeroberfläche in Vertex AI Studio abstimmen oder sie in einem Data-Science-Notebook verwenden.

Vertex AI Search and Conversation bietet Entwicklern die schnellste Möglichkeit, auf generativer KI basierende Suchmaschinen und Chatbots zu erstellen.

Gemini für Google Cloud basiert auf Gemini und ist ein KI-gestütztes Tool, mit dem Sie in Google Cloud und IDEs schneller mehr erledigen können. Gemini Code Assist bietet Codevervollständigung, Codegenerierung und -erklärungen. Außerdem können Sie mit Gemini chatten, um 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 Audioinhalte verarbeiten und generieren kann.

b9913d011999e7c7.png

Gemini gibt es in verschiedenen Varianten und Größen:

  • Gemini Ultra: Die größte und leistungsstärkste Version für komplexe Aufgaben.
  • Gemini Flash: Schnellstes und kostengünstigstes Tool, optimiert für Aufgaben mit hohem Arbeitsvolumen.
  • Gemini Pro: Mittelgroß, optimiert für die Skalierung bei verschiedenen Aufgaben.
  • Gemini Nano: Das effizienteste Tool für On-Device-Aufgaben.

Wichtige Features:

  • Multimodalität: Die Fähigkeit von Gemini, mehrere Informationsformate zu verstehen und zu verarbeiten, ist ein deutlicher Fortschritt gegenüber herkömmlichen Nur-Text-Sprachmodellen.
  • Leistung: Gemini Ultra übertrifft in vielen Benchmarks den aktuellen Stand der Technik und war das erste Modell, das Experten beim anspruchsvollen MMLU-Benchmark (Massive Multitask Language Understanding) übertroffen hat.
  • Flexibilität: Dank der unterschiedlichen Größen von Gemini kann Gemini an verschiedene Anwendungsfälle angepasst werden – von groß angelegten Studien bis hin zur Bereitstellung auf Mobilgeräten.

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

Es stehen zwei Optionen zur Verfügung:

  1. Die offizielle Vertex AI Java API for 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 zur Integration von LLMs in Ihre Java-Anwendungen. Dabei werden verschiedene Komponenten wie das LLM selbst orchestriert, aber auch andere Tools wie Vektordatenbanken (für semantische Suchen), Dokumentladeprogramme und Aufteilungen (zum Analysieren und Lernen von Dokumenten), Ausgabeparser usw.

Das Projekt wurde vom Python-Projekt LangChain inspiriert, jedoch mit dem Ziel, Java-Entwickler zu unterstützen.

bb908ea1e6c96ac2.png

Aufgaben in diesem Lab

  • Java-Projekt für Gemini und LangChain4j einrichten
  • So senden Sie Ihren ersten Prompt programmatisch an Gemini
  • Antworten von Gemini streamen
  • So erstellst du eine Unterhaltung zwischen einem Nutzer und Gemini
  • Gemini in einem multimodalen Kontext verwenden, indem Sie Text und Bilder senden
  • Nützliche strukturierte Informationen aus unstrukturierten Inhalten extrahieren
  • Prompt-Vorlagen bearbeiten
  • So führen Sie eine Textklassifizierung wie z. B. eine Sentimentanalyse durch
  • Mit eigenen Dokumenten chatten (Retrieval Augmented Generation)
  • Chatbots mit Funktionsaufrufen erweitern
  • Gemma mit Ollama und TestContainers lokal verwenden

Voraussetzungen

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

2. Einrichtung und Anforderungen

Umgebung für das selbstbestimmte Lernen einrichten

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

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Der Projektname ist der Anzeigename für die Projektteilnehmer. Es handelt sich um eine Zeichenfolge, die von Google APIs nicht verwendet wird. Sie können sie jederzeit aktualisieren.
  • Die Projekt-ID ist für alle Google Cloud-Projekte eindeutig und unveränderlich. Sie kann nach dem Festlegen nicht mehr geändert werden. Die Cloud Console generiert automatisch einen eindeutigen String. ist Ihnen meist egal, was es ist. In den meisten Codelabs musst du auf deine Projekt-ID verweisen, die üblicherweise als PROJECT_ID bezeichnet wird. Wenn Ihnen die generierte ID nicht gefällt, können Sie eine weitere zufällige ID generieren. Alternativ können Sie einen eigenen verwenden und nachsehen, ob er verfügbar ist. Sie kann nach diesem Schritt nicht mehr geändert werden und bleibt für die Dauer des Projekts erhalten.
  • Zur Information gibt es noch einen dritten Wert, die Projektnummer, die von manchen APIs verwendet wird. Weitere Informationen zu allen 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. Dieses Codelab ist kostengünstig. Sie können die von Ihnen erstellten Ressourcen oder das Projekt löschen, um Ressourcen herunterzufahren, um zu vermeiden, dass über diese Anleitung hinaus Kosten anfallen. Neue Google Cloud-Nutzer haben Anspruch auf das kostenlose Testprogramm mit 300$Guthaben.

Cloud Shell starten

Sie können Google Cloud zwar von Ihrem Laptop aus der Ferne bedienen, 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.

5c1dabeca90e44e5.png

Wenn Sie Cloud Shell zum ersten Mal starten, wird ein Zwischenbildschirm mit einer Beschreibung der Funktion angezeigt. Wenn ein Zwischenbildschirm angezeigt wird, klicken Sie auf Weiter.

9c92662c6a846a5c.png

Die Bereitstellung und Verbindung mit Cloud Shell dauert nur einen Moment.

9f0e51b578fecce5.png

Diese virtuelle Maschine verfügt über alle erforderlichen Entwicklertools. Es bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und wird in Google Cloud ausgeführt. Dadurch werden die Netzwerkleistung und die Authentifizierung erheblich verbessert. Viele, wenn nicht sogar alle Arbeiten 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 eingestellt 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 entwickeln Sie Java-Programme mit dem Cloud Shell-Terminal und dem Cloud Shell-Editor.

Vertex AI APIs aktivieren

Prüfen Sie, ob der Projektname in der Google Cloud Console oben in der Google Cloud Console angezeigt wird. Ist dies nicht der Fall, klicken Sie auf Projekt auswählen, um die Projektauswahl zu öffnen und das gewünschte Projekt auszuwählen.

Sie können Vertex AI APIs entweder im Vertex AI-Bereich der Google Cloud Console oder über das Cloud Shell-Terminal aktivieren.

Rufen Sie zum Aktivieren über die Google Cloud Console zuerst den Abschnitt „Vertex AI“ im Menü der Google Cloud Console auf:

451976f1c8652341.png

Klicken Sie im Vertex AI-Dashboard auf Enable All Recommended APIs (Alle empfohlenen APIs aktivieren).

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

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

gcloud services enable aiplatform.googleapis.com

Klonen Sie 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

Wenn Sie prüfen möchten, ob das Projekt zur Ausführung bereit ist, versuchen Sie, „Hello World“ .

Achten Sie darauf, dass Sie sich auf der obersten Ordnerebene 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-Quellordner, indem Sie File auswählen -> Open Folder und verweisen Sie auf den Codelab-Quellordner (z. B. /home/username/gemini-workshop-for-java-developers/) verwenden.

Gradle für Java installieren

Damit der Cloud-Code-Editor ordnungsgemäß mit Gradle funktioniert, müssen Sie die Erweiterung Gradle für Java installieren.

Gehen Sie zunächst zum Abschnitt Java-Projekte und klicken Sie auf das Pluszeichen:

84d15639ac61c197.png

Gradle for Java auswählen:

34d6c4136a3cc9ff.png

Wählen Sie die Version Install Pre-Release aus:

3b044fb450cccb7.png

Nach der Installation sollten die Schaltflächen Disable und Uninstall angezeigt werden:

46410fe86d777f9c.png

Bereinigen Sie abschließend den Arbeitsbereich, damit die neuen Einstellungen angewendet werden:

31e27e9bb61d975d.png

Dadurch werden Sie aufgefordert, den Workshop neu zu laden und zu löschen. Wählen Sie Reload and delete aus:

d6303bc49e391dc.png

Wenn Sie eine der Dateien, z. B. App.java, öffnen, sollten Sie sehen, dass der Editor korrekt mit Syntaxhervorhebung funktioniert:

fed1b1b5de0dff58.png

Sie können jetzt einige Beispiele für Gemini ausführen.

Umgebungsvariablen einrichten

Öffnen Sie ein neues Terminal im Cloud Code-Editor, indem Sie Terminal -> auswählen. New Terminal. 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 nun korrekt 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-001")
            .build();

        System.out.println(model.generate("Why is the sky blue?"));
    }
}

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

In der Methode main konfigurieren Sie das Chat-Sprachmodell. Dazu verwenden Sie den Builder für VertexAiGeminiChatModel und geben Folgendes an:

  • Projekt
  • Standort
  • Modellname (gemini-1.5-flash-001).

Nachdem das Sprachmodell nun bereit ist, können Sie die generate()-Methode aufrufen und Ihren Prompt, Ihre Frage oder Ihre Anweisungen an das LLM senden. Hier stellen Sie eine einfache Frage dazu, wodurch der Himmel blau wird.

Du kannst diesen Prompt ändern, um andere Fragen oder Aufgaben auszuprobieren.

Führen Sie das Beispiel im Stammordner 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, du hast Gemini zum ersten Mal aufgerufen!

Streamingantwort

Haben Sie bemerkt, dass die Antwort nach ein paar Sekunden in einem Durchgang erfolgt ist? Dank der Streamingantwort-Variante ist es auch möglich, die Antwort schrittweise zu erhalten. Die Streaming-Antwort. Das Modell gibt die Antwort Stück für Stück zurück, sobald sie verfügbar ist.

In diesem Codelab bleiben wir bei der Nicht-Streaming-Antwort, aber sehen wir uns nun die Streaming-Antwort an, um zu erfahren, wie das funktioniert.

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

package gemini.workshop;

import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import dev.langchain4j.model.StreamingResponseHandler;

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-001")
            .build();
        
        model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
            @Override
            public void onNext(String text) {
                System.out.println(text);
            }

            @Override
            public void onError(Throwable error) {
                error.printStackTrace();
            }
        });
    }
}

Dieses Mal importieren wir die Streaming-Klassenvarianten VertexAiGeminiStreamingChatModel, die die StreamingChatLanguageModel-Schnittstelle implementieren. Außerdem benötigst du eine StreamingResponseHandler.

Diesmal ist die Signatur der Methode generate() etwas anders. Anstatt einen String zurückzugeben, ist der Rückgabetyp „void“. Zusätzlich zum Prompt müssen Sie einen Streamingantwort-Handler übergeben. Hier implementieren Sie die Schnittstelle, indem Sie eine anonyme innere Klasse mit den beiden Methoden onNext(String text) und onError(Throwable error) erstellen. Ersteres wird jedes Mal aufgerufen, wenn ein neuer Teil der Antwort verfügbar ist, während Letzterer nur aufgerufen wird, wenn überhaupt ein Fehler auftritt.

Ausführen:

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

Sie erhalten eine ähnliche Antwort wie in der vorherigen Klasse. Dieses Mal werden Sie jedoch feststellen, dass die Antwort nach und nach in Ihrer Shell erscheint, anstatt auf die Anzeige der vollständigen Antwort zu warten.

Zusätzliche Konfiguration

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

  • temperature(Float temp), um zu definieren, wie kreativ die Antwort sein soll. 0 steht für wenig Creative und ist oft sachlicher, während 1 für mehr kreative Ergebnisse steht.
  • topP(Float topP) – zur Auswahl der möglichen Wörter, deren Gesamtwahrscheinlichkeit diese Gleitkommazahl (zwischen 0 und 1) ergibt
  • topK(Integer topK) – zur zufälligen Auswahl eines Wortes aus der maximalen Anzahl wahrscheinlicher Wörter für die Textvervollständigung (1 bis 40)
  • maxOutputTokens(Integer max) – zur Angabe der maximalen Länge der vom Modell gegebenen Antwort (im Allgemeinen stehen 4 Tokens für ungefähr 3 Wörter)
  • maxRetries(Integer retries) – Falls das Kontingent für die Anfrage pro Zeit überschritten wird oder bei der Plattform ein technisches Problem auftritt, kann das Modell den Aufruf dreimal wiederholen.

Bisher haben Sie Gemini nur eine Frage gestellt. Sie können aber auch eine Unterhaltung in mehreren Schritten führen. Damit beschäftigen wir uns im nächsten Abschnitt.

5. Mit Gemini chatten

Im vorherigen Schritt haben Sie eine einzelne Frage gestellt. Jetzt ist es an der Zeit, ein echtes Gespräch zwischen einer nutzenden Person und dem LLM zu führen. Jede Frage und Antwort kann auf den vorherigen aufbauen, um eine echte Diskussion zu bilden.

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-001")
            .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 diesem Kurs:

  • MessageWindowChatMemory – eine Klasse, die dabei hilft, den Multi-Turn-Aspekt der Unterhaltung zu bewältigen und die vorherigen Fragen und Antworten lokal im Gedächtnis zu halten
  • AiServices – eine Klasse, die das Chatmodell und die Chatspeicherung miteinander verbindet

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

Für die Chaterinnerung verwenden wir den Builder von MessageWindowChatMemory, um eine Erinnerung zu erstellen, in der die letzten 20 Nachrichten ausgetauscht werden. Es ist ein gleitendes Fenster über die Konversation, dessen Kontext lokal in unserem Java-Klassen-Client gespeichert wird.

Anschließend erstellen Sie das AI service, das das Chatmodell mit dem Chatspeicher verbindet.

Wie Sie sehen, verwendet der KI-Dienst eine von uns definierte, von LangChain4j implementierte benutzerdefinierte ConversationService-Schnittstelle, die über eine String-Abfrage eine String-Antwort zurückgibt.

Jetzt ist es an der Zeit, sich mit Gemini zu unterhalten. Zuerst wird eine einfache Begrüßung gesendet. Dann folgt eine erste Frage zum Eiffelturm, um herauszufinden, in welchem Land er sich befindet. Beachten Sie, dass sich der letzte Satz auf die Antwort auf die erste Frage bezieht, da Sie sich fragen, wie viele Einwohner sich in dem Land befinden, in dem sich der Eiffelturm befindet, ohne explizit das Land zu erwähnen, das in der vorherigen Antwort angegeben wurde. Es zeigt, dass frühere Fragen und Antworten mit jedem Prompt gesendet werden.

Führen Sie das Beispiel aus:

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

Es sollten drei Antworten angezeigt werden, die diesen ähneln:

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.

Mit Gemini können Sie Fragen in einer einzigen Antwort oder in mehreren Unterhaltungen mit Gemini führen, aber bisher bestand die Eingabe nur aus Text. Was ist mit Bildern? Sehen wir uns die Bilder im nächsten Schritt an.

6. Multimodalität mit Gemini

Gemini ist ein multimodales Modell. Es akzeptiert nicht nur Text, sondern auch Bilder oder sogar Videos. 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, aufgenommen 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-001")
            .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());
    }
}

Beachten Sie bei den Importen, dass wir zwischen verschiedenen Arten von Nachrichten und Inhalten unterscheiden. Ein UserMessage kann sowohl ein TextContent- als auch ein ImageContent-Objekt enthalten. Dabei geht es um Multimodalität, also die Kombination von Text und Bildern. Das Modell sendet ein Response zurück, das eine AiMessage enthält.

Anschließend rufen Sie über content() das AiMessage aus der Antwort ab und dann den Text der Nachricht mithilfe von text().

Führen Sie das Beispiel aus:

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

Der Name des Bildes verrät Ihnen sicher, was das Bild enthielt, aber die Gemini-Ausgabe sieht 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.

Durch die Kombination von Prompts mit Bildern und Text ergeben sich interessante Anwendungsfälle. Sie können Anwendungen erstellen, die Folgendes können:

  • Text in Bildern erkennen.
  • Prüfen Sie, ob ein Bild sicher angezeigt werden kann.
  • Bildunterschriften erstellen.
  • Durchsuchen Sie eine Datenbank mit Bildern mit Nur-Text-Beschreibungen.

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

7. Extrahiert strukturierte Informationen aus unstrukturiertem Text

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

Angenommen, Sie möchten den Namen und das Alter einer Person anhand einer Biografie oder Beschreibung dieser Person extrahieren. Sie können das LLM anweisen, JSON mit einem geschickt abgestimmten Prompt aus unstrukturiertem Text zu extrahieren (dies wird allgemein als „Prompt Engineering“ bezeichnet).

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.UserMessage;

public class ExtractData {

    static record Person(String name, int age) {}

    interface PersonExtractor {
        @UserMessage("""
            Extract the name and age of the person described below.
            Return a JSON document with a "name" and an "age" property, \
            following this structure: {"name": "John Doe", "age": 34}
            Return only JSON, without any markdown markup surrounding it.
            Here is the document describing the person:
            ---
            {{it}}
            ---
            JSON:
            """)
        Person extractPerson(String text);
    }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-001")
            .temperature(0f)
            .topK(1)
            .build();

        PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);

        Person person = extractor.extractPerson("""
            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.    
            """
        );

        System.out.println(person.name());  // Anna
        System.out.println(person.age());   // 23
    }
}

Sehen wir uns die verschiedenen Schritte in dieser Datei an:

  • Ein Person-Eintrag wird definiert, um die Details einer Person darzustellen ( Name und Alter).
  • Die PersonExtractor-Schnittstelle wird mit einer Methode definiert, die einen unstrukturierten Textstring angibt und eine Person-Instanz zurückgibt.
  • extractPerson() wird mit einer @UserMessage-Anmerkung versehen, die eine Aufforderung mit ihr verknüpft. Das ist der Prompt, mit dem das Modell die Informationen extrahiert und die Details in Form eines JSON-Dokuments zurückgibt. Dieses wird für Sie geparst und in eine Person-Instanz demarshalliert.

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

  • Das Chatmodell wird instanziiert. Beachten Sie, dass wir einen sehr niedrigen temperature von null und einen topK von nur eins verwenden, um eine sehr deterministische Antwort sicherzustellen. Dies hilft dem Modell auch, die Anweisungen besser zu befolgen. Insbesondere möchten wir nicht, dass Gemini die JSON-Antwort mit zusätzlichem Markdown-Markup umschließt.
  • Mithilfe der Klasse AiServices von LangChain4j wird ein PersonExtractor-Objekt 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 Alter zurückzugeben.

Führen Sie das Beispiel aus:

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

Es sollte folgende Ausgabe angezeigt werden:

Anna
23

Ja, das ist Anna und sie sind 23!

Bei diesem AiServices-Ansatz arbeiten Sie mit stark typisierten Objekten. Sie interagieren nicht direkt mit dem LLM. Stattdessen arbeiten Sie mit konkreten Klassen wie dem Datensatz Person zur Darstellung der extrahierten personenbezogenen Daten und Sie haben ein PersonExtractor-Objekt mit einer extractPerson()-Methode, die eine Person-Instanz zurückgibt. Das Konzept von LLM ist abstrahiert, und als Java-Entwickler bearbeiten Sie nur normale Klassen und Objekte.

8. Prompts mit Prompt-Vorlagen strukturieren

Wenn Sie mit einem LLM mit einem gemeinsamen Satz von Anweisungen oder Fragen interagieren, gibt es einen Teil dieses Prompts, der sich nie ändert, während andere Teile die Daten enthalten. Wenn Sie beispielsweise Rezepte erstellen möchten, könnten Sie einen Prompt wie „Du bist ein talentierter Koch, erstelle bitte ein Rezept mit den folgenden Zutaten: ...“ verwenden und hängst dann die Zutaten am Ende dieses Textes an. Dafür sind Prompt-Vorlagen da – ähnlich wie interpolierte Strings in Programmiersprachen. Eine Prompt-Vorlage enthält Platzhalter, die Sie durch die richtigen Daten für einen bestimmten Aufruf des LLM ersetzen können.

Konkret betrachten wir TemplatePrompt.java im Verzeichnis 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-001")
            .maxOutputTokens(500)
            .temperature(0.8f)
            .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 Modell VertexAiGeminiChatModel mit einem hohen Maß an Kreativität, einer hohen Temperatur und hohen Werten von „TopP“ und „TopK“. Anschließend erstellen Sie mit der statischen Methode from() ein PromptTemplate-Objekt, indem Sie den String des Prompts übergeben und die Platzhaltervariablen {{dish}} und {{ingredients}} verwenden.

Zum Erstellen der letzten Aufforderung rufen Sie apply() auf. Daraufhin wird eine Zuordnung von Schlüssel/Wert-Paaren erstellt, die den Namen des Platzhalters und den Stringwert darstellen, durch den er ersetzt werden soll.

Zum Abschluss rufen Sie die Methode generate() des Gemini-Modells auf, indem Sie über diesen Prompt eine Nutzernachricht mit der Anweisung prompt.toUserMessage() erstellen.

Führen Sie das Beispiel aus:

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

Die generierte 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 sowie topK und tokP anpassen und den Code noch einmal ausführen. So können Sie die Auswirkungen von Änderungen dieser Parameter auf das LLM beobachten.

Prompt-Vorlagen sind eine gute Möglichkeit, wiederverwendbare und parametrierbare Anweisungen für LLM-Aufrufe zu erhalten. Sie können Daten übergeben und Prompts für verschiedene Werte anpassen, die von Ihren Nutzern bereitgestellt werden.

9. Textklassifizierung mit Wenig-Shot-Prompts

LLMs sind ziemlich gut darin, Text in verschiedene Kategorien einzuteilen. Sie können ein LLM bei dieser Aufgabe unterstützen, indem Sie einige Beispiele für Texte und die zugehörigen Kategorien angeben. Dieser Ansatz wird oft als wenige Shot-Prompting bezeichnet.

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

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;

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.Map;

public class TextClassification {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-001")
            .maxOutputTokens(10)
            .maxRetries(3)
            .build();

        PromptTemplate promptTemplate = PromptTemplate.from("""
            Analyze the sentiment of the text below. Respond only with one word to describe the sentiment.

            INPUT: This is fantastic news!
            OUTPUT: POSITIVE

            INPUT: Pi is roughly equal to 3.14
            OUTPUT: NEUTRAL

            INPUT: I really disliked the pizza. Who would use pineapples as a pizza topping?
            OUTPUT: NEGATIVE

            INPUT: {{text}}
            OUTPUT: 
            """);

        Prompt prompt = promptTemplate.apply(
            Map.of("text", "I love strawberries!"));

        Response<AiMessage> response = model.generate(prompt.toUserMessage());

        System.out.println(response.content().text());
    }
}

In der main()-Methode erstellen Sie das Gemini-Chatmodell wie gewohnt, aber mit einer kleinen maximalen Ausgabetokennummer, da Sie nur eine kurze Antwort wünschen: Der Text ist POSITIVE, NEGATIVE oder NEUTRAL.

Anschließend erstellen Sie eine wiederverwendbare Prompt-Vorlage mit der Prompt-Technik „wen-shot“ und weisen das Modell mit einigen Beispielen für Ein- und Ausgaben an. So kann das Modell auch der tatsächlichen Ausgabe folgen. Gemini antwortet nicht mit einem ganzen Satz, sondern mit nur einem Wort.

Sie wenden die Variablen mit der Methode apply() an, um den Platzhalter {{text}} durch den echten Parameter ("I love strawberries") zu ersetzen und diese Vorlage mit toUserMessage() in eine Nutzernachricht umzuwandeln.

Führen Sie das Beispiel aus:

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

Sie sollten ein einzelnes Wort sehen:

POSITIVE

Erdbeeren zu lieben ist anscheinend ein positives Gefühl!

10. Retrieval-Augmented Generation

LLMs werden mit einer großen Textmenge trainiert. Ihr Wissen deckt jedoch nur Informationen ab, die sie 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 gefunden hat.

Aus diesem Grund tragen Ansätze wie Retrieval Augmented Generation (RAG) dazu bei, die zusätzlichen Informationen bereitzustellen, die ein LLM möglicherweise benötigt, um die Anfragen seiner Nutzer zu erfüllen und mit aktuellen Informationen zu antworten oder private Informationen zu liefern, die zum Zeitpunkt des Trainings nicht zugänglich sind.

Zurück zu den Gesprächen. Dieses Mal können Sie Fragen zu Ihren Dokumenten stellen. Sie erstellen einen Chatbot, der in der Lage ist, relevante Informationen aus einer Datenbank abzurufen, die Ihre in kleinere Teile ("Blöcke") aufgeteilten Dokumente enthält. Diese Informationen werden vom Modell verwendet, um seine Antworten zu begründen, anstatt sich ausschließlich auf das Wissen des Trainings zu verlassen.

In RAG gibt es zwei Phasen:

  1. Aufnahmephase: Dokumente werden im Arbeitsspeicher geladen, in kleinere Blöcke aufgeteilt und Vektoreinbettungen (eine hochgradig multidimensionale Vektordarstellung der Blöcke) werden berechnet und in einer Vektordatenbank gespeichert, die semantische Suchen ausführen kann. Diese Aufnahmephase wird normalerweise einmal ausgeführt, wenn neue Dokumente zum Dokumentkorpus 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. Dann erhält das LLM den Kontext der Unterhaltung, die Textblöcke, die den von der Datenbank zurückgegebenen Vektoren entsprechen, und wird aufgefordert, seine Antwort zu begründen, indem es sich diese Blöcke ansieht.

a1d2e2deb83c6d27.png

Dokumente vorbereiten

In dieser neuen Demo stellen Sie sich Fragen zum Thema "Aufmerksamkeit ist alles, was Sie brauchen" Forschungsbericht. Sie beschreibt die von Google entwickelte neuronale Transformer-Netzwerkarchitektur, auf der heutzutage alle modernen Large Language Models implementiert werden.

Die Publikation wurde bereits in die Datei attention-is-all-you-need.pdf im Repository heruntergeladen.

Chatbot implementieren

Sehen wir uns an, wie der zweiphasige Ansatz erstellt wird: zuerst mit der Dokumentaufnahme und dann mit der Abfragezeit, wenn Nutzer Fragen zum Dokument stellen.

In diesem Beispiel werden beide Phasen in derselben Klasse implementiert. Normalerweise gibt es eine Anwendung, die die Datenaufnahme übernimmt, und eine andere, die den Nutzern die Chatbot-Schnittstelle zur Verfügung stellt.

Außerdem verwenden wir in diesem Beispiel eine speicherinterne Vektordatenbank. In einem realen Produktionsszenario würden die Aufnahme- und die Abfragephase in zwei verschiedene Anwendungen getrennt werden und die Vektoren werden in einer eigenständigen Datenbank gespeichert.

Dokumentaufnahme

Der erste Schritt in der Phase der Dokumentaufnahme besteht darin, die bereits heruntergeladene PDF-Datei zu suchen und ein PdfParser zum Lesen vorzubereiten:

URL url = new URI("https://github.com/glaforge/gemini-workshop-for-java-developers/raw/main/attention-is-all-you-need.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());

Anstelle des üblichen Chat-Sprachmodells erstellen Sie eine Instanz eines Einbettungsmodells. Dies ist ein bestimmtes Modell, dessen Aufgabe es ist, Vektordarstellungen von Textstücken (Wörter, Sätze oder sogar Absätze) zu erstellen. Sie gibt Vektoren von Gleitkommazahlen statt Textantworten zurück.

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .publisher("google")
    .modelName("textembedding-gecko@003")
    .maxRetries(3)
    .build();

Als Nächstes benötigen Sie mehrere Kurse, die gemeinsam für Folgendes gelten:

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

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

Zum Speichern der Vektoreinbettungen wird eine Instanz der speicherinternen Vektordatenbank InMemoryEmbeddingStore erstellt.

Das Dokument wird dank der Klasse DocumentSplitters in mehrere Abschnitte unterteilt. Der Text der PDF-Datei wird in Snippets mit je 500 Zeichen aufgeteilt, die sich jeweils 100 Zeichen überschneiden. Der folgende Block wird verwendet, um zu vermeiden, dass Wörter oder Sätze stückweise geschnitten werden.

Der Speicheraufnahmeprozessor verknüpft den Dokumentaufteilunger, das Einbettungsmodell zur Berechnung der Vektoren und die speicherinterne Vektordatenbank. Die Aufnahme erfolgt dann über die Methode ingest().

Jetzt ist die erste Phase vorbei. Das Dokument wurde in Textblöcke mit den zugehörigen Vektoreinbettungen umgewandelt und in der Vektordatenbank gespeichert.

Fragen stellen

Jetzt ist es an der Zeit, Fragen zu stellen. Erstellen Sie ein Chatmodell, um die Unterhaltung zu beginnen:

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

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

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Erstellen Sie abgesehen von der primären Methode eine Schnittstelle, die einen Assistenten von LLM-Experten darstellt. Das ist eine Schnittstelle, die von der AiServices-Klasse implementiert wird, damit Sie mit dem Modell interagieren können:

interface LlmExpert {
    String ask(String question);
}

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

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

Dieser Dienst verbindet sich:

  • Das Chat-Sprachmodell, das Sie zuvor konfiguriert haben
  • Eine Chat-Erinnerung, um die Unterhaltung im Auge zu behalten.
  • Der Retriever vergleicht eine Abfrage zur Vektoreinbettung mit den Vektoren in der Datenbank.
  • In einer Prompt-Vorlage heißt es explizit, dass das Chatmodell antworten soll, indem die Antwort auf den bereitgestellten Informationen basiert, d.h. auf relevanten Auszügen der Dokumentation, deren Vektoreinbettung dem Vektor der Frage des Nutzers ähnelt.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in large language models,\s
            you excel at explaining simply and clearly questions about LLMs.

            Here is the question: {{userMessage}}

            Answer using the following information:
            {{contents}}
            """))
        .build())
    .contentRetriever(retriever)
    .build())

Du kannst endlich Fragen stellen!

List.of(
    "What neural network architecture can be used for language models?",
    "What are the different components of a transformer neural network?",
    "What is attention in large language models?",
    "What is the name of the process that transforms text into vectors?"
).forEach(query ->
    System.out.printf("%n=== %s === %n%n %s %n%n", query, expert.ask(query)));
);

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

Führen Sie das Beispiel aus:

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

Die Ausgabe sollte Antworten auf Ihre Fragen enthalten:

=== What neural network architecture can be used for language models? === 

 Transformer architecture 


=== What are the different components of a transformer neural network? === 

 The different components of a transformer neural network are:

1. Encoder: The encoder takes the input sequence and converts it into a 
sequence of hidden states. Each hidden state represents the context of 
the corresponding input token.
2. Decoder: The decoder takes the hidden states from the encoder and 
uses them to generate the output sequence. Each output token is 
generated by attending to the hidden states and then using a 
feed-forward network to predict the token's probability distribution.
3. Attention mechanism: The attention mechanism allows the decoder to 
attend to the hidden states from the encoder when generating each output 
token. This allows the decoder to take into account the context of the 
input sequence when generating the output sequence.
4. Positional encoding: Positional encoding is a technique used to 
inject positional information into the input sequence. This is important 
because the transformer neural network does not have any inherent sense 
of the order of the tokens in the input sequence.
5. Feed-forward network: The feed-forward network is a type of neural 
network that is used to predict the probability distribution of each 
output token. The feed-forward network takes the hidden state from the 
decoder as input and outputs a vector of probabilities. 


=== What is attention in large language models? === 

Attention in large language models is a mechanism that allows the model 
to focus on specific parts of the input sequence when generating the 
output sequence. This is important because it allows the model to take 
into account the context of the input sequence when generating each output token.

Attention is implemented using a function that takes two sequences as 
input: a query sequence and a key-value sequence. The query sequence is 
typically the hidden state from the previous decoder layer, and the 
key-value sequence is typically the sequence of hidden states from the 
encoder. The attention function computes a weighted sum of the values in 
the key-value sequence, where the weights are determined by the 
similarity between the query and the keys.

The output of the attention function is a vector of context vectors, 
which are then used as input to the feed-forward network in the decoder. 
The feed-forward network then predicts the probability distribution of 
the next output token.

Attention is a powerful mechanism that allows large language models to 
generate text that is both coherent and informative. It is one of the 
key factors that has contributed to the recent success of large language 
models in a wide range of natural language processing tasks. 


=== What is the name of the process that transforms text into vectors? === 

The process of transforming text into vectors is called **word embedding**.

Word embedding is a technique used in natural language processing (NLP) 
to represent words as vectors of real numbers. Each word is assigned a 
unique vector, which captures its meaning and semantic relationships 
with other words. Word embeddings are used in a variety of NLP tasks, 
such as machine translation, text classification, and question 
answering.

There are a number of different word embedding techniques, but one of 
the most common is the **skip-gram** model. The skip-gram model is a 
neural network that is trained to predict the surrounding words of a 
given word. By learning to predict the surrounding words, the skip-gram 
model learns to capture the meaning and semantic relationships of words.

Once a word embedding model has been trained, it can be used to 
transform text into vectors. To do this, each word in the text is 
converted to its corresponding vector. The vectors for all of the words 
in the text are then concatenated to form a single vector, which 
represents the entire text.

Text vectors can be used in a variety of NLP tasks. For example, text 
vectors can be used to train machine translation models, text 
classification models, and question answering models. Text vectors can 
also be used to perform tasks such as text summarization and text 
clustering. 

11. Funktionsaufrufe

Es gibt auch Situationen, in denen ein LLM Zugriff auf externe Systeme haben soll, z. B. eine Remote-Web-API, die Informationen abruft oder eine Aktion ausführt, oder Dienste, die eine Art von Berechnung durchführen. Beispiel:

Remote-Web-APIs:

  • Kundenbestellungen verfolgen und aktualisieren
  • Suchen oder erstellen Sie ein Ticket in einem Issue Tracker.
  • Rufen Sie Echtzeitdaten wie Börsenkurse oder IoT-Sensormessungen ab.
  • E-Mail senden

Computing-Tools:

  • Ein Rechner für komplexere mathematische Aufgaben.
  • Codeinterpretation zum Ausführen von Code, wenn LLMs eine Schlussfolgerungslogik benötigen.
  • Anfragen in natürlicher Sprache in SQL-Abfragen umwandeln, damit ein LLM eine Datenbank abfragen kann

Funktionsaufrufe ermöglichen es dem Modell, einen oder mehrere Funktionsaufrufe anzufordern, die in seinem Namen ausgeführt werden. Auf diese Weise kann es den Prompt eines Nutzers ordnungsgemäß mit neuen Daten beantworten.

Mit einem bestimmten Prompt eines Nutzers und dem Wissen über vorhandene Funktionen, die für diesen Kontext relevant sein können, kann ein LLM mit einer Anfrage für einen Funktionsaufruf antworten. Die Anwendung, die das LLM einbindet, kann dann die Funktion aufrufen und dann dem LLM mit einer Antwort antworten. Das LLM antwortet dann mit einer Textantwort.

Vier Schritte für Funktionsaufrufe

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

Wenn Sie Gemini oder ein anderes LLM nach dem Wetter in Paris fragen, erhalten Sie die Antwort, dass es keine Informationen zur Wettervorhersage habe. Wenn das LLM in Echtzeit auf die Wetterdaten zugreifen soll, müssen Sie einige Funktionen definieren, die es verwenden kann.

Sehen Sie sich das folgende Diagramm an:

31e0c2aba5e6f21c.png

1️⃣ Zuerst fragt ein Nutzer nach dem Wetter in Paris. Die Chatbot-App weiß, dass es eine oder mehrere Funktionen gibt, die dem LLM zur Ausführung der Abfrage zur Verfügung stehen. Der Chatbot sendet sowohl die erste Aufforderung als auch die Liste der Funktionen, die aufgerufen werden können. Hier ist eine Funktion namens getWeather(), die einen Stringparameter für den Standort verwendet.

8863be53a73c4a70.png

Da das LLM keine Wettervorhersagen hat, antwortet es nicht per Text, sondern eine Anfrage zur Funktionsausführung. 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. Hier stellen wir uns als Antwort {"forecast": "sunny"} vor.

73a5f2ed19f47d8.png

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

20832cb1ee6fbfeb.png

4️⃣ Das LLM sieht sich die JSON-Antwort an, interpretiert diese Informationen und antwortet schließlich mit dem Text, dass das Wetter in Paris sonnig ist.

Jeder Schritt als Code

Zuerst konfigurieren Sie das Gemini-Modell wie gewohnt:

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

Sie geben eine Toolspezifikation an, die die aufgerufene Funktion beschreibt:

ToolSpecification weatherToolSpec = ToolSpecification.builder()
    .name("getWeatherForecast")
    .description("Get the weather forecast for a location")
    .addParameter("location", JsonSchemaProperty.STRING,
        JsonSchemaProperty.description("the location to get the weather forecast for"))
    .build();

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

Beginnen wir mit Schritt 1, indem wir die erste Frage zum Wetter in Paris senden:

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, und das Modell antwortet mit einer Anfrage zur weiteren Ausführung:

// 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: Jetzt wissen wir, welche Funktion wir für das LLM aufrufen sollen. Im Code wird kein wirklicher Aufruf an eine externe API ausgeführt. Stattdessen 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);

Und in Schritt 4 erfährt das LLM das Ergebnis der Funktionsausführung und kann dann eine Antwort in Textform 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.

Sie können die Ausgabe über der Anfrage zur Toolausführung sowie die Antwort sehen.

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

Führen Sie das Beispiel aus:

./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 übernimmt Funktionsaufrufe

Im vorherigen Schritt haben Sie gesehen, wie die normalen Interaktionen zwischen Textfragen/Antworten und Funktionsanfragen/Antworten verschränkt sind. Dazwischen haben Sie die angeforderte Funktionsantwort direkt angegeben, ohne eine echte Funktion aufzurufen.

LangChain4j bietet jedoch auch eine übergeordnete Abstraktionsebene, die die Funktionsaufrufe transparent für Sie verarbeiten kann, während die Konversation wie gewohnt gehandhabt wird.

Einzelner Funktionsaufruf

Sehen wir uns FunctionCallingAssistant.java Stück für Stück 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 Wettervorhersage 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);
        }
    }
}

Beachten Sie, dass diese Klasse eine einzelne Funktion enthält, die jedoch mit der Anmerkung @Tool annotiert ist, die der Beschreibung der Funktion entspricht, die das Modell aufrufen kann.

Die Parameter der Funktion (hier nur einer) sind ebenfalls annotiert, jedoch mit dieser kurzen @P-Anmerkung, die auch eine Beschreibung des Parameters liefert. Sie können beliebig viele Funktionen hinzufügen, um sie dem Modell für komplexere Szenarien zur Verfügung zu stellen.

In dieser Klasse geben Sie einige Antwortvorlagen zurück. Wenn Sie jedoch einen echten externen Wettervorhersagedienst aufrufen möchten, befindet sich dies im Text der Methode, die Sie für den Aufruf an diesen Dienst verwenden würden.

Wie Sie bei der Erstellung einer ToolSpecification im vorherigen Ansatz gesehen haben, ist es wichtig, die Funktionsweise einer Funktion zu dokumentieren und zu beschreiben, welchen Parametern sie entsprechen. So kann das Modell besser verstehen, wie und wann diese Funktion verwendet werden kann.

Als Nächstes können Sie in LangChain4j eine Oberfläche bereitstellen, die dem Vertrag entspricht, den Sie für die Interaktion mit dem Modell verwenden möchten. Hier handelt es sich um eine einfache Schnittstelle, die einen String verwendet, der die Nutzernachricht darstellt, 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 UserMessage (für eine Nutzernachricht) oder AiMessage (für eine Modellantwort) von LangChain4j (für eine Modellantwort) oder sogar ein TokenStream beinhalten, wenn Sie komplexere Situationen behandeln möchten, da diese komplizierteren Objekte auch zusätzliche Informationen wie die Anzahl der verbrauchten Tokens enthalten. Der Einfachheit halber nehmen wir jedoch nur den String in der Eingabe und den String in der Ausgabe.

Lassen Sie uns mit der Methode main() abschließen, mit der alle Teile verbunden werden:

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-1.5-flash-001")
        .maxOutputTokens(100)
        .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?"));
}

Wie gewohnt konfigurieren Sie das Gemini-Chatmodell. Dann instanziieren Sie den Wettervorhersagedienst, der die "Funktion" das Modell uns auffordert.

Jetzt verwenden Sie wieder die Klasse AiServices, um das Chatmodell, den Chatspeicher und das Tool (den Wettervorhersagedienst mit seiner Funktion) zu binden. AiServices gibt ein Objekt zurück, das die von Ihnen definierte WeatherAssistant-Schnittstelle implementiert. Jetzt musst du nur noch die chat()-Methode dieses Assistenten aufrufen. Beim Aufrufen sehen Sie nur die Textantworten, aber die Funktionsaufrufanfragen und die Funktionsaufrufantworten sind für den Entwickler nicht sichtbar. Diese Anfragen werden automatisch und transparent verarbeitet. Wenn Gemini der Meinung ist, dass eine Funktion aufgerufen werden sollte, antwortet es mit der Anfrage zum Funktionsaufruf. LangChain4j übernimmt dann den Aufruf der lokalen Funktion in Ihrem Namen.

Führen Sie das Beispiel aus:

./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 über mehrere Funktionen verfügen und LangChain4j mehrere Funktionsaufrufe für Sie verarbeiten lassen. Sehen Sie sich MultiFunctionCallingAssistant.java an, um ein Beispiel für mehrere Funktionen zu sehen.

Sie verfügt über 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 zum Anwenden eines Prozentsatzes auf einen bestimmten Betrag:

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

Anschließend können Sie all diese Funktionen mit einer MultiTools-Klasse kombinieren und Fragen stellen wie „Wie lauten 10% des Aktienkurses der AAPL von USD in 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-001")
        .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 es so aus:

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

Nun sollten die verschiedenen Funktionen angezeigt werden:

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.

An Kundenservicemitarbeiter

Funktionsaufrufe sind ein hervorragender Erweiterungsmechanismus für Large Language Models wie Gemini. Es ermöglicht uns, komplexere Systeme zu entwickeln, die oft als „Agents“ bezeichnet werden. oder „KI-Assistenten“. Diese Agents können über externe APIs mit der externen Welt interagieren und mit Diensten, die Nebenwirkungen auf die externe Umgebung haben können (z. B. E-Mails senden, Tickets erstellen usw.)

Wenn Sie so leistungsstarke Agents erstellen, sollten Sie verantwortungsvoll vorgehen. Bevor Sie automatische Aktionen ausführen, sollten Sie einen Human in the Loop berücksichtigen. Beim Entwerfen von LLM-gestützten Agents, die mit der Außenwelt interagieren, ist die Sicherheit wichtig.

13. Gemma mit Ollama und TestContainers ausführen

Bisher haben wir Gemini verwendet, aber es gibt auch Gemma, ein kleines Schwestermodell.

Gemma ist eine Familie leichtgewichtiger, hochmoderner offener Modelle, die auf derselben Forschung und Technologie basieren, die auch zur Erstellung der Gemini-Modelle verwendet wurden. Gemma ist in den beiden Varianten Gemma1 und Gemma2 in verschiedenen Größen verfügbar. Gemma1 ist in zwei Größen verfügbar: 2B und 7B. Gemma2 ist in zwei Größen verfügbar: 9B und 27B. Die Gewichte sind frei verfügbar und aufgrund der geringen Größe können Sie es selbst auf Ihrem Laptop oder in Cloud Shell ausführen.

Wie führt Gemma aus?

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

Eine gute Option für die lokale Ausführung von Gemma 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, aber für LLMs.

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.

Führen Sie das Modell aus:

ollama run gemma:2b

Jetzt können Sie mit dem Modell interagieren:

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

Zum Schließen der Aufforderung Strg + D drücken

Gemma in Ollama in TestContainers ausführen

Anstatt Ollama lokal zu installieren und auszuführen, können Sie Ollama in einem Container verwenden, der von TestContainers verarbeitet wird.

TestContainers ist nicht nur zum Testen nützlich, sondern kann auch zum Ausführen von Containern verwendet werden. Es gibt sogar eine spezielle OllamaContainer, die du nutzen kannst.

Hier ist das Gesamtbild:

2382c05a48708dfd.png

Implementierung

Sehen wir uns GemmaWithOllamaContainer.java Stück für Stück an.

Zuerst müssen Sie einen abgeleiteten Ollama-Container erstellen, der das Gemma-Modell abruft. Dieses Image ist entweder bereits aus einer früheren Ausführung vorhanden oder wird erstellt. Wenn das Image bereits vorhanden ist, teilen Sie TestContainers einfach mit, dass Sie das Ollama-Standard-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. Verweisen Sie dazu auf die Adresse und den Port des Containers mit dem Modell, das Sie verwenden möchten. Zum Schluss rufen Sie einfach wie gewohnt model.generate(yourPrompt) 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 es so aus:

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

Bei der ersten Ausführung 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.

Gemma wird gerade in Cloud Shell ausgeführt.

14. Glückwunsch

Herzlichen Glückwunsch, Sie haben mit LangChain4j und der Gemini API Ihre erste Chatanwendung mit generativer KI in Java erstellt. Dabei haben Sie herausgefunden, dass multimodale Large Language Models ziemlich leistungsstark sind und in der Lage sind, verschiedene Aufgaben wie Fragen und Antworten zu bearbeiten, selbst bei der eigenen Dokumentation, der Datenextraktion und der Interaktion mit externen APIs.

Was liegt als Nächstes an?

Jetzt sind Sie an der Reihe, Ihre Anwendungen mit leistungsstarken LLM-Integrationen zu optimieren.

Weitere Informationen

Referenzdokumente