Gemini w Javie z Vertex AI i LangChain4j

1. Wprowadzenie

Ten warsztat programistyczny koncentruje się na dużym modelu językowym (LLM) Gemini hostowanym w Vertex AI w Google Cloud. Vertex AI to platforma obejmująca wszystkie produkty, usługi i modele uczenia maszynowego w Google Cloud.

Do interakcji z Gemini API użyjesz języka Java w ramach frameworka LangChain4j. Poznasz konkretne przykłady wykorzystania LLM do odpowiadania na pytania, generowania pomysłów, wyodrębniania elementów i treści ustrukturyzowanych, generowania rozszerzonego przez wyszukiwanie w zapisanych informacjach oraz wywoływania funkcji.

Co to jest generatywna AI?

Generatywne AI to wykorzystanie sztucznej inteligencji do tworzenia nowych treści, takich jak tekst, obrazy, muzyka, dźwięki i filmy.

Generatywną AI obsługują duże modele językowe (LLM), które mogą wykonywać wiele zadań i wykonywać zadania typu „out-of-the-box”, takie jak podsumowanie, pytania i odpowiedzi czy klasyfikacja. Dzięki minimalnemu trenowaniu modele podstawowe można dostosować do konkretnych zastosowań przy użyciu bardzo małej ilości przykładowych danych.

Jak działa generatywna AI?

Generatywne AI korzysta z modelu uczenia maszynowego (ML), aby uczyć się wzorów i zależności w zbiorze danych z treściami stworzonymi przez ludzi. Następnie wykorzystuje wyuczone wzorce do generowania nowych treści.

Najczęstszym sposobem trenowania modelu generatywnej AI jest uczenie nadzorowane. Model otrzymuje zestaw treści stworzonych przez ludzi i odpowiednich etykiet. Następnie uczy się generować treści podobne do tych tworzonych przez ludzi.

Jakie są typowe zastosowania generatywnej AI?

Generatywnej AI można używać do:

  • Popraw interakcje z klientami dzięki ulepszonym funkcjom czatu i wyszukiwania.
  • Przeglądaj ogromne ilości nieuporządkowanych danych za pomocą interfejsów konwersacyjnych i podsumowań.
  • pomagać w wykonaniu powtarzających się zadań, takich jak odpowiadanie na prośby o oferty, lokalizacja treści marketingowych w różnych językach czy sprawdzanie zgodności umów z klientem z obowiązującymi przepisami.

Jakie rozwiązania generatywnej AI oferuje Google Cloud?

Dzięki Vertex AI możesz korzystać z modeli podstawowych, dostosowywać je i umieszczać w swoich aplikacjach bez konieczności posiadania specjalistycznej wiedzy z zakresu uczenia maszynowego. Możesz uzyskać dostęp do modeli podstawowych w bazie modeli, dostroić modele za pomocą prostego interfejsu w Vertex AI Studio lub używać modeli w notatniku do analizy danych.

Vertex AI Search and Conversation to najszybszy sposób na tworzenie wyszukiwarek i czatbotów opartych na generatywnej AI.

Gemini for Google Cloud to oparta na Gemini usługa wspomagająca oparta na AI, która jest dostępna w Google Cloud i IDE, aby pomóc Ci szybciej i wydajniej pracować. Gemini Code Assist umożliwia uzupełnianie i generowanie kodu, wyjaśnianie kodu oraz prowadzenie czatu, aby zadawać pytania techniczne.

Co to jest Gemini?

Gemini to rodzina generatywnych modeli AI opracowanych przez Google DeepMind, które są przeznaczone do zastosowań multimodalnych. Multimodalność oznacza, że może przetwarzać i generować różne rodzaje treści, takie jak tekst, kod, obrazy i dźwięk.

b9913d011999e7c7.png

Gemini jest dostępny w różnych wersjach i rozmiarach:

  • Gemini Ultra: największa i najbardziej zaawansowana wersja do złożonych zadań.
  • Gemini Flash: najszybszy i najbardziej opłacalny, zoptymalizowany pod kątem zadań o dużej objętości.
  • Gemini Pro: model średniej wielkości zoptymalizowany pod kątem skalowania na potrzeby różnych zadań.
  • Gemini Nano: najbardziej wydajny model stworzony z myślą o zadaniach wykonywanych na urządzeniu.

Najważniejsze funkcje:

  • Multimodalność: zdolność Gemini do rozumienia i obsługiwania wielu formatów informacji znacznie wykracza poza tradycyjne modele językowe oparte wyłącznie na tekście.
  • Wydajność: Gemini Ultra osiąga lepsze wyniki niż obecny stan wiedzy w wielu testach porównawczych i był pierwszym modelem, który wyprzedził ekspertów w trudnym teście MMLU (wielozadaniowe zrozumienie języka).
  • Elastyczność: różne rozmiary Gemini umożliwiają dostosowanie go do różnych zastosowań, od badań na dużą skalę po wdrażanie na urządzeniach mobilnych.

Jak można wchodzić w interakcje z Gemini w Vertex AI za pomocą języka Java?

Masz 2 możliwości:

  1. Oficjalna biblioteka Vertex AI Java API for Gemini.
  2. Framework LangChain4j.

W tym laboratorium kodu użyjesz platformy LangChain4j.

Co to jest framework LangChain4j?

Platforma LangChain4j to biblioteka open source do integracji dużych modeli językowych w aplikacjach Java. Umożliwia ona łączenie różnych komponentów, takich jak duże modele językowe, ale też inne narzędzia, np. bazy danych wektorów (do wyszukiwania semantycznego), narzędzia do wczytywania i dzielenia dokumentów (do analizowania dokumentów i uczenia się na ich podstawie), a także parsery danych wyjściowych.

Projekt został zainspirowany projektem Pythona LangChain, ale jego celem jest obsługa programistów Javy.

bb908ea1e6c96ac2.png

Czego się nauczysz

  • Jak skonfigurować projekt Java, aby używać Gemini i LangChain4j
  • Jak wysłać pierwszy prompt do Gemini za pomocą kodu
  • Jak przesyłać strumieniowo odpowiedzi z Gemini
  • Jak utworzyć rozmowę między użytkownikiem a Gemini
  • Jak używać Gemini w kontekście multimodalnym, wysyłając zarówno tekst, jak i obrazy
  • Jak wyodrębnić przydatne uporządkowane dane z treści nieuporządkowanych
  • Jak manipulować szablonami promptów
  • Jak klasyfikować teksty, np. za pomocą analizy nastawienia
  • Jak prowadzić rozmowy na czacie z własnymi dokumentami (metoda Retrieval Augmented Generation)
  • Jak rozszerzyć chatboty za pomocą wywoływania funkcji
  • Jak używać Gemma lokalnie z Ollama i TestContainers

Czego potrzebujesz

  • znajomość języka programowania Java;
  • projekt Google Cloud,
  • przeglądarki, takiej jak Chrome lub Firefox;

2. Konfiguracja i wymagania

Konfiguracja środowiska w samodzielnym tempie

  1. Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Nazwa projektu to wyświetlana nazwa uczestników tego projektu. Jest to ciąg znaków, którego nie używają interfejsy API Google. Zawsze możesz ją zaktualizować.
  • Identyfikator projektu jest niepowtarzalny w ramach wszystkich projektów Google Cloud i nie można go zmienić (po ustawieniu). Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie ma znaczenia, jaki to ciąg. W większości laboratoriów z kodem musisz podać identyfikator projektu (zwykle oznaczony jako PROJECT_ID). Jeśli nie podoba Ci się wygenerowany identyfikator, możesz wygenerować inny losowy. Możesz też spróbować użyć własnego adresu e-mail, aby sprawdzić, czy jest on dostępny. Nie można go zmienić po wykonaniu tego kroku. Pozostanie on do końca projektu.
  • Informacyjnie: istnieje jeszcze 3 wartość, numer projektu, której używają niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć rozliczenia w konsoli Cloud, aby korzystać z zasobów i interfejsów API Cloud. Przejście przez ten samouczek nie będzie kosztowne, a być może nawet bezpłatne. Aby wyłączyć zasoby i uniknąć obciążenia opłatami po zakończeniu samouczka, możesz usunąć utworzone zasoby lub usunąć projekt. Nowi użytkownicy Google Cloud mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.

Uruchamianie Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale w tym samouczku będziesz używać Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

Aktywowanie Cloud Shell

  1. W konsoli Google Cloud kliknij Aktywuj Cloud Shell 853e55310c205094.png.

3c1dabeca90e44e5.png

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni, na którym opisano, czym jest to środowisko. Jeśli taki ekran się wyświetlił, kliknij Dalej.

9c92662c6a846a5c.png

Uproszczenie i połączenie z Cloud Shell powinno zająć tylko kilka chwil.

9f0e51b578fecce5.png

Ta maszyna wirtualna zawiera wszystkie niezbędne narzędzia programistyczne. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie poprawia wydajność sieci i uwierzytelnianie. Większość, jeśli nie wszystkie, zadań w tym ćwiczeniu można wykonać w przeglądarce.

Po połączeniu z Cloud Shell powinieneś zobaczyć, że jesteś uwierzytelniony i że projekt jest ustawiony na identyfikator Twojego projektu.

  1. Aby potwierdzić uwierzytelnianie, uruchom w Cloud Shell to polecenie:
gcloud auth list

Wynik polecenia

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Aby sprawdzić, czy polecenie gcloud zna Twój projekt, uruchom w Cloud Shell to polecenie:
gcloud config list project

Wynik polecenia

[core]
project = <PROJECT_ID>

Jeśli nie, możesz ustawić je za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Wynik polecenia

Updated property [core/project].

3. Przygotowywanie środowiska programistycznego

W tym laboratorium kodu użyjesz terminala i edytora Cloud Shell do tworzenia programów w Javie.

Włączanie interfejsów API Vertex AI

Upewnij się, że w konsoli Google Cloud nazwa projektu jest wyświetlana u góry konsoli Google Cloud. Jeśli nie, kliknij Wybierz projekt, aby otworzyć selektor projektów, i wybierz odpowiedni projekt.

Interfejsy Vertex AI możesz włączyć w sekcji Vertex AI w konsoli Google Cloud lub w terminalu Cloud Shell.

Aby włączyć tę funkcję w konsoli Google Cloud, najpierw otwórz sekcję Vertex AI w menu konsoli Google Cloud:

451976f1c8652341.png

W panelu Vertex AI kliknij Włącz wszystkie zalecane interfejsy API.

Spowoduje to włączenie kilku interfejsów API, ale najważniejszym dla tego ćwiczenia jest aiplatform.googleapis.com.

Możesz też włączyć ten interfejs API w terminalu Cloud Shell za pomocą tego polecenia:

gcloud services enable aiplatform.googleapis.com

Sklonuj repozytorium GitHub

W terminalu Cloud Shell sklonuj repozytorium tego Codelab:

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

Aby sprawdzić, czy projekt jest gotowy do uruchomienia, możesz spróbować uruchomić program „Hello World”.

Upewnij się, że jesteś w folderze najwyższego poziomu:

cd gemini-workshop-for-java-developers/ 

Utwórz kod towarzyszący Gradle:

gradle wrapper

Uruchom, używając gradlew:

./gradlew run

Powinny się wyświetlić te dane wyjściowe:

..
> Task :app:run
Hello World!

Otwieranie i konfigurowanie Edytora Cloud

Otwórz kod za pomocą edytora kodu Cloud w Cloud Shell:

42908e11b28f4383.png

W edytorze kodu Cloud otwórz folder źródłowy Codelab, wybierając File -> Open Folder i wskaż folder źródłowy Codelab (np. /home/username/gemini-workshop-for-java-developers/).

Konfigurowanie zmiennych środowiskowych

Otwórz nowy terminal w edytorze Cloud Code, klikając Terminal -> New Terminal. Skonfiguruj 2 zmiennych środowiskowych wymaganych do uruchamiania przykładów kodu:

  • PROJECT_ID – identyfikator Twojego projektu Google Cloud.
  • LOKALIZACJA – region, w którym jest wdrożony model Gemini.

Wyeksportuj zmienne w ten sposób:

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

4. Pierwszy wywołanie modelu Gemini

Gdy projekt jest już prawidłowo skonfigurowany, możesz wywołać interfejs Gemini API.

W katalogu app/src/main/java/gemini/workshop sprawdź plik QA.java:

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

W pierwszym przykładzie musisz zaimportować klasę VertexAiGeminiChatModel, która implementuje interfejs ChatModel.

W metodzie main konfigurujesz model języka czatu, korzystając z twórcy VertexAiGeminiChatModel i określając:

  • Projekt
  • Lokalizacja
  • Nazwa modelu (gemini-1.5-flash-002).

Gdy model językowy jest gotowy, możesz wywołać metodę generate() i przekazać prompt, pytanie lub instrukcje do wysłania do modelu LLM. Tutaj zadajesz proste pytanie o to, dlaczego niebo jest niebieskie.

Możesz zmienić ten prompt, aby wypróbować inne pytania lub zadania.

Uruchom przykład w folderze głównym kodu źródłowego:

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

Powinny pojawić się dane wyjściowe podobne do tych:

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.

Gratulacje, pierwsza rozmowa z Gemini za Tobą.

Odpowiedź strumieniowa

Czy zauważyłeś/zauwadziłaś, że odpowiedź została podana od razu, po kilku sekundach? Dzięki opcji przesyłania strumieniowego odpowiedzi możesz też otrzymać odpowiedź stopniowo. W przypadku odpowiedzi strumieniowej model zwraca odpowiedź po kawałku, gdy staje się ona dostępna.

W tym samouczku skupimy się na odpowiedzi bez strumieniowania, ale najpierw przyjrzyjmy się odpowiedzi z odtwarzaniem strumieniowym, aby zobaczyć, jak to zrobić.

W katalogu StreamQA.java możesz zobaczyć działanie odpowiedzi strumieniowej:app/src/main/java/gemini/workshop

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

Tym razem importujemy warianty zajęć ze streamingiem VertexAiGeminiStreamingChatModel, które implementują interfejs StreamingChatLanguageModel. Musisz też zaimportować statycznie LambdaStreamingResponseHandler.onNext, która jest wygodną metodą zapewniającą StreamingResponseHandler do tworzenia modułu obsługi strumieniowego za pomocą wyrażeń lambda w Javie.

Tym razem podpis metody generate() jest nieco inny. Zamiast zwracać ciąg znaków, zwraca typ void. Oprócz prompta musisz przekazać też moduł obsługi odpowiedzi strumieniowej. Dzięki zaimportowaniu statycznych danych, o których wspomnieliśmy powyżej, możemy zdefiniować wyrażenie lambda, które przekazujesz metodzie onNext(). Wyrażenie lambda jest wywoływane za każdym razem, gdy jest dostępny nowy element odpowiedzi, a wyrażenie catch – tylko wtedy, gdy wystąpi błąd.

Uruchomienie:

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

Otrzymasz podobną odpowiedź jak w poprzedniej klasie, ale tym razem zauważysz, że odpowiedź będzie stopniowo pojawiać się w powłoce, zamiast czekać na wyświetlenie pełnej odpowiedzi.

Dodatkowa konfiguracja

W przypadku konfiguracji zdefiniowaliśmy tylko projekt, lokalizację i nazwę modelu, ale możesz też podać inne parametry modelu:

  • temperature(Float temp) – określenie, jak kreatywna ma być odpowiedź (0 oznacza niski poziom kreatywności i często bardziej rzeczowe odpowiedzi, a 2 – bardziej kreatywne wyniki)
  • topP(Float topP) – do wyboru możliwych słów, których łączne prawdopodobieństwo daje liczbę zmiennoprzecinkową (w przedziale od 0 do 1).
  • topK(Integer topK) – aby losowo wybrać słowo spośród maksymalnej liczby prawdopodobnych słów do uzupełnienia tekstu (od 1 do 40)
  • maxOutputTokens(Integer max) – aby określić maksymalną długość odpowiedzi podanej przez model (zazwyczaj 4 tokeny odpowiadają mniej więcej 3 słowom).
  • maxRetries(Integer retries) – jeśli przekroczysz limit żądań na czas lub platforma napotka problem techniczny, model może 3 razy ponownie wywołać funkcję

Do tej pory zadawaliśmy Gemini tylko jedno pytanie, ale możesz też prowadzić z nim dłuższą rozmowę. W następnej sekcji dowiesz się więcej o tym, jak to zrobić.

5. Czat z Gemini

W poprzednim kroku zadawaliśmy tylko jedno pytanie. Teraz nadszedł czas na prawdziwą rozmowę między użytkownikiem a LLM. Każde pytanie i odpowiedź może być powiązane z poprzednimi, tworząc prawdziwą dyskusję.

W folderze app/src/main/java/gemini/workshop sprawdź element Conversation.java:

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

W tej klasie pojawiło się kilka nowych interesujących importów:

  • MessageWindowChatMemory – klasa, która pomoże w obsługiwaniu wieloetapowych aspektów rozmowy i przechowywaniu w pamięci lokalnej poprzednich pytań i odpowiedzi.
  • AiServices – klasa abstrakcyjna wyższego poziomu, która łączy model czatu z pamięcią czatu.

W głównej metodzie skonfigurujesz model, pamięć czatu i usługę AI. Model jest konfigurowany w zwykły sposób z informacjami o nazwie projektu, lokalizacji i modelu.

W przypadku pamięci czatu używamy kreatora MessageWindowChatMemory, aby utworzyć pamięć, która przechowuje ostatnie 20 wymienionych wiadomości. Jest to okno przesuwające się w rozmowie, której kontekst jest przechowywany lokalnie w kliencie klasy Java.

Następnie utwórz AI service, który łączy model czatu z pamięcią czatu.

Zwróć uwagę, że usługa AI korzysta z zdefiniowanego przez nas niestandardowego interfejsu ConversationService, który implementuje LangChain4j i odpowiada na zapytanie String, zwracając odpowiedź String.

Czas na rozmowę z Gemini. Najpierw wysyłane jest proste powitanie, a potem pierwsze pytanie o wieżę Eiffla, aby dowiedzieć się, w jakim kraju się znajduje. Ostatnie zdanie jest powiązane z odpowiedzią na pierwsze pytanie, ponieważ zastanawiasz się, ile osób mieszka w kraju, w którym znajduje się Wieża Eiffla, bez wyraźnego wymienienia kraju, który został podany w poprzedniej odpowiedzi. Pokazuje, że poprzednie pytania i odpowiedzi są wysyłane z każdą prośbą.

Uruchom przykład:

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

Powinny pojawić się 3 odpowiednie odpowiedzi:

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.

Możesz zadawać pytania w jednym czy wielu zwrotach, ale do tej pory dane wejściowe były tylko tekstowe. A co z obrazami? W następnym kroku przyjrzymy się obrazom.

6. Multimodalność w Gemini

Gemini to model multimodalny. Jako dane wejściowe może on przyjmować nie tylko tekst, ale też obrazy, a nawet filmy. W tej sekcji znajdziesz przykład użycia łączenia tekstu i obrazów.

Czy Gemini rozpozna tego kota?

af00516493ec9ade.png

Zdjęcie kota na śniegu pochodzące z Wikipediihttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

Sprawdź plik Multimodal.java w katalogu app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;

public class Multimodal {

    static final String CAT_IMAGE_URL =
        "https://upload.wikimedia.org/wikipedia/" +
        "commons/b/b6/Felis_catus-cat_on_snow.jpg";


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

        UserMessage userMessage = UserMessage.from(
            ImageContent.from(CAT_IMAGE_URL),
            TextContent.from("Describe the picture")
        );

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

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

Podczas importowania rozróżniamy różne rodzaje wiadomości i treści. Obiekt UserMessage może zawierać obiekt TextContentImageContent. To jest multimodalność w działaniu: mieszanie tekstu i obrazów. Nie wysyłamy tylko prostego ciągu znaków, lecz bardziej ustrukturyzowany obiekt, który reprezentuje wiadomość użytkownika i składa się z elementu treści obrazkowych oraz elementu treści tekstowych. Model zwraca Response, który zawiera AiMessage.

Następnie pobierasz AiMessage z odpowiedzi za pomocą content(), a potem tekst wiadomości dzięki text().

Uruchom przykład:

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

Nazwa zdjęcia z pewnością daje wskazówkę, co się na nim znajduje, ale wyniki Gemini są podobne do tych:

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.

Połączenie obrazów i promptów tekstowych otwiera ciekawe możliwości zastosowania. Możesz tworzyć aplikacje, które:

  • rozpoznawać tekst na zdjęciach;
  • Sprawdź, czy obraz jest bezpieczny do wyświetlenia.
  • tworzyć podpisy do zdjęć.
  • wyszukiwać w bazie danych obrazów za pomocą zwykłych tekstowych opisów;

Oprócz informacji z obrazów możesz też wydobywać informacje z tekstu nieustrukturyzowanego. W następnej sekcji dowiesz się, jak to zrobić.

7. Wyodrębnianie uporządkowanych informacji z tekstu nieuporządkowanego

W wielu sytuacjach ważne informacje są podawane w dokumentach raportów, e-mailach lub innych długich tekstach w nieustrukturyzowany sposób. Najlepiej byłoby, gdybyś mógł wyodrębnić kluczowe szczegóły zawarte w nieuporządkowanym tekście w formie obiektów uporządkowanych. Zobaczmy, jak to zrobić.

Załóżmy, że chcesz wyodrębnić imię i nazwisko oraz wiek osoby na podstawie jej biografii, CV lub opisu. Możesz polecić LLM wyodrębnianie danych JSON z tekstu nieustrukturyzowanego za pomocą sprytnie dostosowanego promptu (nazywamy to „inżynierią promptów”).

W przykładzie poniżej zamiast tworzyć prompt opisujący dane wyjściowe w formacie JSON użyjemy zaawansowanej funkcji Gemini o nazwie dane wyjściowe w formacie uporządkowanym, czyli czasami ograniczonego dekodowania, która zmusza model do zwracania tylko prawidłowego kodu JSON zgodnie ze wskazanym schematem JSON.

Sprawdź ExtractData.javaapp/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;

public class ExtractData {

    record Person(String name, int age) { }

    interface PersonExtractor {
        @SystemMessage("""
            Your role is to extract the name and age 
            of the person described in the biography.
            """)
        Person extractPerson(String biography);
    }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .responseMimeType("application/json")
            .responseSchema(fromClass(Person.class))
            .build();

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

        String bio = """
            Anna is a 23 year old artist based in Brooklyn, New York. She was born and 
            raised in the suburbs of Chicago, where she developed a love for art at a 
            young age. She attended the School of the Art Institute of Chicago, where 
            she studied painting and drawing. After graduating, she moved to New York 
            City to pursue her art career. Anna's work is inspired by her personal 
            experiences and observations of the world around her. She often uses bright 
            colors and bold lines to create vibrant and energetic paintings. Her work 
            has been exhibited in galleries and museums in New York City and Chicago.    
            """;
        Person person = extractor.extractPerson(bio);

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

Przyjrzyjmy się różnym czynnościom w tym pliku:

  • Rekord Person służy do reprezentowania szczegółów opisujących osobę (nazwę i wiek).
  • Interfejs PersonExtractor jest definiowany za pomocą metody, która zwraca instancję Person na podstawie nieustrukturyzowanego ciągu tekstowego.
  • Element extractPerson() jest opatrzony adnotacją @SystemMessage, która łączy go z prośbą o instrukcje. Jest to prompt, którego model używa do kierowania wyodrębniania informacji. W zwrotnej informacji otrzymasz dokument JSON, który zostanie przeanalizowany i przekształcony w instancję Person.

Teraz przyjrzyjmy się zawartości metody main():

  • Model czatu jest skonfigurowany i utworzony. Używamy 2 nowych metod klasy ModelBuilder: responseMimeType()responseSchema(). Pierwsza z nich instruuje Gemini, aby wygenerował prawidłowy ciąg tekstowy JSON. Druga metoda definiuje schemat obiektu JSON, który ma zostać zwrócony. Ponadto ta druga metoda deleguje do metody ułatwiającej, która może przekonwertować klasę lub rekord Java na odpowiedni schemat JSON.
  • Obiekt PersonExtractor jest tworzony dzięki klasie AiServices w LangChain4j.
  • Następnie możesz po prostu wywołać funkcję Person person = extractor.extractPerson(...), aby wyodrębnić szczegóły dotyczące osoby z tekstu nieustrukturyzowanego, a w zwrotnym wywołaniu funkcji Person uzyskać instancję z nazwą i wiekiem.

Uruchom przykład:

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

Powinny się wyświetlić te dane wyjściowe:

Anna
23

Tak, to Anna. Ma 23 lata.

W przypadku tego AiServices podejścia pracujesz z obiektowymi typami danych. Nie wchodzisz w bezpośrednią interakcję z LLM. Zamiast tego pracujesz z konkretnymi klasami, takimi jak rekord Person, który reprezentuje wyodrębnione dane osobowe, i masz obiekt PersonExtractor z metodą extractPerson(), która zwraca instancję Person. Pojęcie LLM jest abstrakcyjne, a programista Java, gdy korzysta z interfejsu PersonExtractor, po prostu manipuluje zwykłymi klasami i obiektmi.

8. Struktura promptów za pomocą szablonów

Gdy wchodzisz w interakcję z LLM za pomocą wspólnego zestawu instrukcji lub pytań, część promptu nigdy się nie zmienia, a inne części zawierają dane. Jeśli np. chcesz tworzyć przepisy, możesz użyć promptu takiego jak „Jesteś utalentowanym kucharzem. Utwórz przepis z tych składników:…”, a potem dodać składniki na końcu tego promptu. Do tego służą szablony promptów – są one podobne do interpolowanych ciągów znaków w językach programowania. Szablon prompta zawiera pola zastępcze, które możesz zastąpić odpowiednimi danymi w przypadku konkretnego wywołania LLM.

Dokładniej rzecz biorąc, w katalogu app/src/main/java/gemini/workshop przyjrzyjmy się zmiennej TemplatePrompt.java:

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

Jak zwykle, konfigurujesz model VertexAiGeminiChatModel z wysokim poziomem kreatywności, wysoką temperaturą i wysokimi wartościami topP i topK. Następnie tworzysz obiekt PromptTemplate za pomocą statycznej metody from(), przekazując ciąg znaków promptu i używając zmiennych zastępczych w podwójnych nawiasach klamrowych: {{dish}}{{ingredients}}.

Ostatni prompt tworzysz, wywołując funkcję apply(), która przyjmuje mapę par klucz-wartość reprezentujących nazwę elementu zastępczego i wartość ciągu, która ma go zastąpić.

Na koniec wywołujesz metodę generate() modelu Gemini, tworząc wiadomość dla użytkownika na podstawie tego prompta z instrukcją prompt.toUserMessage().

Uruchom przykład:

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

Powinny zostać wygenerowane dane wyjściowe podobne do tych:

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

Możesz zmienić wartości dishingredients na mapie oraz dostosować temperaturę topKtokP, a następnie ponownie uruchom kod. Dzięki temu możesz obserwować wpływ zmiany tych parametrów na LLM.

Szablony promptów to dobry sposób na tworzenie powtarzalnych i parametryzowanych instrukcji do wywołań LLM. Możesz przekazywać dane i dostosowywać prompty do różnych wartości podanych przez użytkowników.

9. Klasyfikacja tekstu z promptami „few-shot”

Sieci LLM dobrze radzą sobie z klasyfikowaniem tekstu do różnych kategorii. Możesz pomóc LLM w tym zadaniu, podając przykłady tekstów i powiązanych z nimi kategorii. Takie podejście nazywa się często promptowaniem za pomocą kilku zdjęć.

Otwórz zadanie TextClassification.java w katalogu app/src/main/java/gemini/workshop, aby przeprowadzić określony typ klasyfikacji tekstu: analizę nastawienia.

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

Wyliczenie Sentiment zawiera różne wartości nastroju: negatywny, neutralny i pozytywny.

W metodie main() tworzysz model Gemini Chat w zwykły sposób, ale z małą maksymalną liczbą tokenów wyjściowych, ponieważ chcesz uzyskać tylko krótką odpowiedź: tekst POSITIVE, NEGATIVE lub NEUTRAL. Aby ograniczyć model do zwracania tylko tych wartości, możesz skorzystać z obsługi uporządkowanych danych, o której mowa w sekcji dotyczącej wyodrębniania danych. Dlatego używamy metody responseSchema(). Tym razem nie używasz wygodnej metody z SchemaHelper, aby wywnioskować definicję schematu, ale zamiast tego użyjesz kreatora Schema, aby dowiedzieć się, jak wygląda definicja schematu.

Po skonfigurowaniu modelu utwórz interfejs SentimentAnalysis, który LangChain4j AiServices zaimplementuje za Ciebie za pomocą LLM. Ten interfejs zawiera jedną metodę: analyze(). Funkcja ta przyjmuje tekst do analizy jako dane wejściowe i zwraca wartość typu Sentiment. Możesz więc manipulować tylko obiektem o ściśle określonym typie, który reprezentuje rozpoznaną klasę nastroju.

Następnie, aby dostarczyć modelowi „kilka przykładów”, które pomogą mu w klasyfikacji, tworzysz pamięć czatu, aby przekazać pary wiadomości od użytkowników i odpowiedzi AI, które reprezentują tekst i powiązane z nim nastroje.

Połączmy wszystko za pomocą metody AiServices.builder(), przekazując interfejs SentimentAnalysis, model do użycia oraz pamięć czatu z przykładami typu „few-shot”. Na koniec wywołaj metodę analyze() z tekstem do analizy.

Uruchom przykład:

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

Powinien pojawić się pojedynczy wyraz:

POSITIVE

Wygląda na to, że kochanie truskawek to pozytywne uczucie.

10. Retrieval-Augmented Generation

Duże modele językowe są trenowane z wykorzystaniem dużej ilości tekstu. Jednak ich wiedza obejmuje tylko informacje, które zostały zaobserwowane podczas trenowania. Jeśli po dacie granicznej trenowania modelu zostaną opublikowane nowe informacje, nie będą one dostępne dla modelu. Dlatego model nie będzie w stanie odpowiadać na pytania dotyczące informacji, których nie widział.

Dlatego metody takie jak generowanie rozszerzone o wyszukiwanie (RAG), które zostaną omówione w tej sekcji, pomagają dostarczyć dodatkowe informacje, których LLM może potrzebować, aby spełnić prośby użytkowników, odpowiedzieć z użyciem informacji, które mogą być bardziej aktualne, lub informacji prywatnych, które nie są dostępne w czasie szkolenia.

Wróćmy do rozmów. Tym razem możesz zadawać pytania na temat swoich dokumentów. Utworzysz chatbota, który będzie mógł pobierać odpowiednie informacje z bazy danych zawierającej Twoje dokumenty podzielone na mniejsze części („fragmenty”). Te informacje będą wykorzystywane przez model do udzielania odpowiedzi zamiast polegania wyłącznie na wiedzy uzyskanej podczas trenowania.

Proces RAG składa się z 2 etapów:

  1. Faza przetwarzania – dokumenty są wczytywane do pamięci, dzielone na mniejsze fragmenty, a potem obliczane są wektory dystrybucyjne (wielowymiarowe wektory reprezentujące fragmenty) i przechowywane w bazie danych wektorów, która umożliwia przeprowadzanie wyszukiwań semantycznych. Ta faza przetwarzania jest zwykle wykonywana raz, gdy do zbioru dokumentów trzeba dodać nowe dokumenty.

cd07d33d20ffa1c8.png

  1. Faza zadawania pytań – użytkownicy mogą teraz zadawać pytania na temat dokumentów. Pytanie zostanie również przekształcone w wektora i porównane ze wszystkimi innymi wektorami w bazie danych. Najbardziej podobne wektory są zwykle powiązane semantycznie i zwracane przez bazę danych wektorową. Następnie model LLM otrzymuje kontekst rozmowy, czyli fragmenty tekstu odpowiadające wektorom zwróconym przez bazę danych, i prosi się go o opracowanie odpowiedzi na podstawie tych fragmentów.

a1d2e2deb83c6d27.png

Przygotuj dokumenty

W tym nowym przykładzie zadasz pytania dotyczące fikcyjnego modelu samochodu wyprodukowanego przez fikcyjnego producenta: samochód Cymbal Starlight. Dokument dotyczący fikcyjnego samochodu nie powinien być częścią wiedzy o danym modelu. Jeśli Gemini jest w stanie udzielić prawidłowych odpowiedzi na pytania dotyczące tego samochodu, oznacza to, że podejście RAG działa: Gemini może przeszukać Twój dokument.

Implementowanie czatbota

Zobaczmy, jak stworzyć podejście dwufazowe: najpierw przetwarzanie dokumentu, a potem czas zapytania (zwany też „fazą pobierania”), gdy użytkownicy zadadzą pytania dotyczące dokumentu.

W tym przykładzie obie fazy są implementowane w tej samej klasie. Zazwyczaj masz jedną aplikację, która zajmuje się przetwarzaniem, i drugą aplikację, która udostępnia interfejs chatbota użytkownikom.

W tym przykładzie użyjemy bazy danych wektorów w pamięci. W rzeczywistych warunkach produkcyjnych fazy przetwarzania i wysyłania zapytań byłyby rozdzielone w 2 różnych aplikacjach, a wektory byłyby przechowywane w samodzielnej bazie danych.

Przetwarzanie dokumentów

Pierwszym krokiem w etapie przetwarzania dokumentu jest znalezienie pliku PDF o naszym fikcyjnym samochodzie i przygotowanie PdfParser do jego odczytania:

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

Zamiast tworzenia zwykłego modelu języka do czatu, tworzysz instancję modelu embeddingu. Jest to model, którego zadaniem jest tworzenie wektorowych reprezentacji fragmentów tekstu (słów, zdań, a nawet akapitów). Zwraca wektory liczb zmiennoprzecinkowych, a nie odpowiedzi tekstowe.

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

Następnie musisz mieć kilka zajęć, na których będziecie mogli współpracować:

  • Załaduj i dziel dokument PDF na części.
  • Utwórz wektory dystrybucyjne dla wszystkich tych fragmentów.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

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

Tworzona jest instancja InMemoryEmbeddingStore, czyli bazy danych wektorów w pamięci, która służy do przechowywania wektorów dystrybucyjnych.

Dokument jest dzielony na fragmenty dzięki klasie DocumentSplitters. Podzieli tekst pliku PDF na fragmenty o długości 500 znaków, z nachodzącymi na siebie 100 znakami (z kolejną częścią, aby uniknąć dzielenia słów lub zdań na kawałki).

Przetwarzanie danych w sklepie łączy rozdzielacz dokumentów, model osadzania do obliczania wektorów i bazowe wektorów w pamięci. Następnie metoda ingest() zajmie się przetwarzaniem.

Pierwsza faza została zakończona, dokument został przekształcony w fragmenty tekstu z powiązanymi wektorami zastępczymi i przechowywany w bazie danych wektorów.

Zadawanie pytań

Czas na zadawanie pytań. Aby rozpocząć rozmowę, utwórz model czatu:

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

Potrzebujesz też klasy retrievera, aby połączyć bazę danych wektorów (w zmiennej embeddingStore) z modelem embeddingu. Jego zadaniem jest wysłanie zapytania do bazy danych wektorów przez obliczenie wektora dystrybucyjnego dla zapytania użytkownika, aby znaleźć podobne wektory w bazie danych:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Utwórz interfejs, który będzie reprezentować asystenta eksperta samochodowego. Jest to interfejs, który klasa AiServices zaimplementuje, aby umożliwić Ci interakcję z modelem:

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

Interfejs CarExpert zwraca ciąg znaków zapakowany w klasę Result w LangChain4j. Dlaczego warto używać tego opakowania? Ponieważ nie tylko otrzymasz odpowiedź, ale także będziesz mieć możliwość zbadania fragmentów z bazy danych, które zostały zwrócone przez moduł pobierania treści. Dzięki temu możesz wyświetlić źródła dokumentów, które posłużyły do wygenerowania ostatecznej odpowiedzi dla użytkownika.

Teraz możesz skonfigurować nową usługę AI:

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

Ta usługa łączy:

  • Skonfigurowany wcześniej model języka czatu.
  • pamięć czatu, która pozwala śledzić rozmowę.
  • retriever porównuje zapytanie dotyczące wektora dystrybucyjnego z wektorami w bazie danych.
.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())

Możesz już zadawać pytania.

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

Pełny kod źródłowy znajduje się w katalogu RAG.java w katalogu app/src/main/java/gemini/workshop.

Uruchom przykład:

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

W danych wyjściowych powinny pojawić się odpowiedzi na pytania:

=== 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. Wywoływanie funkcji

Czasami może się zdarzyć, że LLM będzie potrzebować dostępu do systemów zewnętrznych, takich jak zdalne interfejsy API do obsługi stron internetowych, które pobierają informacje lub wykonują jakieś działanie, albo usługi, które wykonują jakieś obliczenia. Na przykład:

Zdalne interfejsy API:

  • śledzić i aktualizować zamówienia klientów;
  • Znajdź lub utwórz zgłoszenie w systemie śledzenia problemów.
  • Pobieranie danych w czasie rzeczywistym, takich jak kursy giełdowe czy pomiary czujników IoT.
  • Wyślij e-maila.

Narzędzia do obliczeń:

  • Kalkulator do rozwiązywania bardziej zaawansowanych zadań matematycznych.
  • Interpretacja kodu podczas wykonywania kodu, gdy LLM wymagają logiki rozumowania.
  • Konwertowanie żądań w języku naturalnym na zapytania SQL, aby model LLM mógł wysyłać zapytania do bazy danych.

Funkcja wywoływania (czasami nazywana narzędziami lub użyciem narzędzi) to możliwość żądania przez model wykonania co najmniej jednego wywołania funkcji w jego imieniu, aby mógł on prawidłowo odpowiedzieć na prompt użytkownika, korzystając z aktualniejszych danych.

Na podstawie konkretnego prompta od użytkownika i wiedzy o dostępnych funkcjach, które mogą być odpowiednie w danym kontekście, model LLM może odpowiedzieć prośbą o wywołanie funkcji. Aplikacja integrująca LLM może wywołać funkcję w jej imieniu, a potem odpowiedzieć LLM-owi, a LLM zinterpretuje odpowiedź i zwróci odpowiedź tekstową.

4 etapów wywoływania funkcji

Zobaczmy przykład wywołania funkcji: uzyskiwanie informacji o prognozie pogody.

Jeśli zapytasz Gemini lub inny model LLM o pogodę w Paryżu, otrzymasz odpowiedź, że nie ma on informacji o aktualnej prognozie pogody. Jeśli chcesz, aby LLM miał dostęp do danych o pogodzie w czasie rzeczywistym, musisz zdefiniować funkcje, których może używać.

Zapoznaj się z tym diagramem:

31e0c2aba5e6f21c.png

1️⃣ Najpierw użytkownik pyta o pogodę w Paryżu. Aplikacja chatbota (korzystająca z LangChain4j) wie, że ma do dyspozycji co najmniej 1 funkcję, która pomoże LLM w wypełnieniu zapytania. Bot wysyła zarówno początkowe prompty, jak i listę funkcji, które można wywołać. Tutaj funkcja o nazwie getWeather(), która przyjmuje parametr ciągu znaków dla lokalizacji.

8863be53a73c4a70.png

Ponieważ model LLM nie zna prognozy pogody, zamiast odpowiedzi tekstowej wysyła prośbę o wykonanie funkcji. Czatbot musi wywołać funkcję getWeather() z parametrem lokalizacji "Paris".

d1367cc69c07b14d.png

2️⃣ Chatbot wywołuje tę funkcję w imieniu LLM i pobiera odpowiedź funkcji. Tutaj zakładamy, że odpowiedź to {"forecast": "sunny"}.

73a5f2ed19f47d8.png

3️⃣ Aplikacja chatbota wysyła odpowiedź JSON z powrotem do LLM.

20832cb1ee6fbfeb.png

4️⃣ LLM analizuje odpowiedź w formacie JSON, interpretuje te informacje i ostatecznie odpowiada, że w Paryżu jest słonecznie.

Każdy krok jako kod

Najpierw skonfiguruj model Gemini w zwykły sposób:

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

Definiujesz specyfikację narzędzia, która opisuje funkcję, którą można wywołać:

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

Zdefiniowana jest nazwa funkcji, a także nazwa i typ parametru, ale zwróć uwagę, że zarówno funkcja, jak i parametry mają swoje opisy. Opisy są bardzo ważne i pomagają LLM lepiej zrozumieć, co może robić dana funkcja, a tym samym ocenić, czy należy ją wywołać w kontekście rozmowy.

Zacznijmy od kroku 1. Wyślij pierwsze pytanie dotyczące pogody w Paryżu:

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

W kroku 2 przekazujemy narzędzie, którego model ma użyć, a model odpowiada prośbą o wykonanie kodu:

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

Krok 3. W tym momencie wiemy, jaką funkcję ma wywołać LLM. W kodzie nie wywołujemy zewnętrznego interfejsu API, tylko zwracamy bezpośrednio hipotetyczną prognozę pogody:

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

Na etapie 4 model LLM dowiaduje się o wyniku wykonania funkcji i może zsyntetyzować odpowiedź tekstową:

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

Dane wyjściowe:

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.

W wyniku powyżej żądania wykonania narzędzia możesz zobaczyć odpowiedź.

Pełny kod źródłowy znajduje się w FunctionCalling.java w katalogu app/src/main/java/gemini/workshop:

Uruchom przykład:

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

Powinny się wyświetlić dane wyjściowe podobne do tych:

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 obsługuje wywoływanie funkcji

W poprzednim kroku zobaczysz, jak normalne interakcje dotyczące pytania/odpowiedzi i żądania/odpowiedzi funkcji są przeplatane, a pomiędzy nimi bezpośrednio podasz odpowiedź funkcji bez wywoływania prawdziwej funkcji.

LangChain4j oferuje też abstrakcję na wyższym poziomie, która może przejrzyście obsługiwać wywołania funkcji, a jednocześnie zarządzać rozmową w zwykły sposób.

Pojedyncze wywołanie funkcji

Przyjrzyjmy się poszczególnym elementom FunctionCallingAssistant.java.

Najpierw utwórz rekord, który będzie reprezentować strukturę danych odpowiedzi funkcji:

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

Odpowiedź zawiera informacje o lokalizacji, prognozie i temperaturze.

Następnie tworzysz klasę zawierającą funkcję, którą chcesz udostępnić modelowi:

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

Pamiętaj, że ta klasa zawiera jedną funkcję, ale jest opatrzona adnotacją @Tool, która odpowiada opisowi funkcji, którą model może wywołać.

Parametry funkcji (w tym przypadku jeden) są również opatrzone adnotacjami, ale za pomocą krótkiej adnotacji @P, która zawiera też opis parametru. Możesz dodać dowolną liczbę funkcji, aby udostępnić je modelowi w przypadku bardziej złożonych scenariuszy.

W tej klasie zwracasz gotowe odpowiedzi, ale jeśli chcesz wywołać prawdziwą zewnętrzną usługę prognozy pogody, to właśnie w ciele tej metody wywołasz tę usługę.

Jak widzieliśmy w poprzednim podejściu, gdy tworzysz funkcję ToolSpecification, musisz opisać, co ona robi, i do czego odnoszą się parametry. Pomaga to modelowi zrozumieć, jak i kiedy można używać tej funkcji.

Następnie LangChain4j pozwala na udostępnienie interfejsu, który odpowiada umowie, której chcesz użyć do interakcji z modelem. Jest to prosty interfejs, który przyjmuje ciąg znaków reprezentujący wiadomość użytkownika i zwraca ciąg znaków odpowiadający odpowiedzi modelu:

interface WeatherAssistant {
    String chat(String userMessage);
}

Można też użyć bardziej złożonych sygnatur, które obejmują obiekt UserMessage (dla wiadomości od użytkownika) lub AiMessage (dla odpowiedzi modelu) albo nawet TokenStream, jeśli chcesz obsłużyć bardziej zaawansowane sytuacje, ponieważ te bardziej złożone obiekty zawierają też dodatkowe informacje, takie jak liczba zużytych tokenów itp. Jednak ze względu na prostotę przyjmimy, że na wejściu jest string, a na wyjściu string.

Na koniec podam metodę main(), która łączy wszystkie elementy:

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

Jak zwykle konfigurujesz model czatu Gemini. Następnie tworzysz instancję usługi prognozy pogody, która zawiera „funkcję”, którą model poprosi Cię o wywołanie.

Teraz ponownie używasz klasy AiServices, aby powiązać model czatu, pamięć czatu i narzędzie (np. usługę prognozy pogody z jej funkcją). AiServices zwraca obiekt, który implementuje zdefiniowany przez Ciebie interfejs WeatherAssistant. Teraz wystarczy wywołać metodę chat() tego asystenta. Gdy go wywołasz, widzisz tylko odpowiedzi tekstowe, ale żądania wywołania funkcji i ich odpowiedzi nie są widoczne dla dewelopera. Są one obsługiwane automatycznie i przezroczystie. Jeśli Gemini uzna, że należy wywołać funkcję, odpowie prośbą o wywołanie funkcji, a LangChain4j zajmie się wywołaniem funkcji lokalnej w Twoim imieniu.

Uruchom przykład:

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

Powinny się wyświetlić dane wyjściowe podobne do tych:

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

To był przykład pojedynczej funkcji.

Wielokrotne wywołania funkcji

Możesz też użyć wielu funkcji i pozwolić LangChain4j na obsługę wielu wywołań funkcji w Twoim imieniu. Przykład użycia wielu funkcji znajdziesz w formule MultiFunctionCallingAssistant.java.

Ma funkcję przeliczania walut:

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

Inna funkcja służąca do uzyskiwania wartości akcji:

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

Inna funkcja do zastosowania procentu do danej kwoty:

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

Następnie możesz połączyć wszystkie te funkcje z klasą MultiTools i zadawać pytania w takim stylu: „Ile wynosi 10% ceny akcji AAPL w USD przeliczone na EUR?”.

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

    MultiTools multiTools = new MultiTools();

    MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(withMaxMessages(10))
        .tools(multiTools)
        .build();

    System.out.println(assistant.chat(
        "What is 10% of the AAPL stock price converted from USD to EUR?"));
}

Wykonaj je w ten sposób:

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

Powinny się też wyświetlić wywołania wielu funkcji:

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.

Dla pracowników obsługi klienta

Wywoływanie funkcji to świetny mechanizm rozszerzeń dla dużych modeli językowych, takich jak Gemini. Umożliwia nam to tworzenie bardziej złożonych systemów, które często nazywamy „agentami” lub „asystentami AI”. Mogą one wchodzić w interakcje ze światem zewnętrznym za pomocą zewnętrznych interfejsów API oraz usług, które mogą mieć wpływ na środowisko zewnętrzne (np. wysyłanie e-maili, tworzenie zgłoszeń itp.).

Podczas tworzenia takich zaawansowanych agentów należy to robić odpowiedzialnie. Przed podjęciem działań automatycznych należy rozważyć przeprowadzenie procesu z udziałem człowieka. Podczas projektowania agentów opartych na LLM, które będą wchodzić w interakcje ze światem zewnętrznym, należy pamiętać o bezpieczeństwie.

13. Uruchamianie Gemma za pomocą Ollama i TestContainers

Do tej pory używaliśmy Gemini, ale jest też Gemma, czyli młodsza siostra tego modelu.

Gemma to rodzina lekkich, najnowocześniejszych otwartych modeli opartych na tych samych badaniach i technologiach, które posłużyły do utworzenia modeli Gemini. Gemma jest dostępna w 2 wersjach: Gemma1 i Gemma2, każda w różnych rozmiarach. Gemma1 jest dostępna w 2 rozmiarach: 2B i 7B. Gemma2 jest dostępna w 2 rozmiarach: 9B i 27B. Ich wagi są łatwo dostępne, a ich małe rozmiary oznaczają, że możesz je uruchamiać samodzielnie, nawet na laptopie lub w Cloud Shell.

Jak uruchamiać Gemma?

Gemma może działać na wiele sposobów: w chmurze, w Vertex AI po kliknięciu przycisku lub w GKE z niektórymi GPU, ale możesz też uruchomić ją lokalnie.

Jednym z dobrych sposobów na uruchomienie Gemma lokalnie jest użycie Ollama, narzędzia, które pozwala uruchamiać małe modele, takie jak Llama 2, Mistral i wiele innych, na komputerze lokalnym. Jest to coś podobnego do Dockera, ale w przypadku dużych modeli językowych.

Zainstaluj Ollama, postępując zgodnie z instrukcjami dotyczącymi Twojego systemu operacyjnego.

Jeśli używasz środowiska Linux, musisz najpierw włączyć Ollama po zainstalowaniu.

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

Po zainstalowaniu lokalnie możesz uruchomić polecenia, aby pobrać model:

ollama pull gemma:2b

Poczekaj, aż model zostanie pobrany. Może to chwilę potrwać.

Uruchom model:

ollama run gemma:2b

Teraz możesz wchodzić w interakcję z modelem:

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

Aby zamknąć okno, naciśnij Ctrl+D.

Uruchamianie Gemma w Ollama na TestContainers

Zamiast instalować i uruchamiać Ollama lokalnie, możesz użyć Ollama w kontenerze obsługiwanym przez TestContainers.

TestContainers jest przydatny nie tylko do testowania, ale też do uruchamiania kontenerów. Możesz nawet skorzystać z specjalnego OllamaContainer.

Oto pełny obraz:

2382c05a48708dfd.png

Implementacja

Przyjrzyjmy się poszczególnym elementom GemmaWithOllamaContainer.java.

Najpierw musisz utworzyć pochodny kontener Ollama, który pobiera model Gemma. Ten obraz albo już istnieje z poprzedniego uruchomienia, albo zostanie utworzony. Jeśli obraz już istnieje, po prostu poinformujesz TestContainers, że chcesz zastąpić domyślny obraz Ollama wariantem Gemma:

private static final String TC_OLLAMA_GEMMA_2_B = "tc-ollama-gemma-2b";

// Creating an Ollama container with Gemma 2B if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {

    // Check if the custom Gemma Ollama image exists already
    List<Image> listImagesCmd = DockerClientFactory.lazyClient()
        .listImagesCmd()
        .withImageNameFilter(TC_OLLAMA_GEMMA_2_B)
        .exec();

    if (listImagesCmd.isEmpty()) {
        System.out.println("Creating a new Ollama container with Gemma 2B image...");
        OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26");
        ollama.start();
        ollama.execInContainer("ollama", "pull", "gemma:2b");
        ollama.commitToImage(TC_OLLAMA_GEMMA_2_B);
        return ollama;
    } else {
        System.out.println("Using existing Ollama container with Gemma 2B image...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA_2_B)
                .asCompatibleSubstituteFor("ollama/ollama"));
    }
}

Następnie utwórz i uruchom kontener testowy Ollama, a następnie utwórz model czatu Ollama, wskazując adres i port kontenera z modelem, którego chcesz użyć. Na koniec wywołaj model.generate(yourPrompt) jak zwykle:

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

Wykonaj je w ten sposób:

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

Pierwsze uruchomienie zajmie trochę czasu, ponieważ trzeba utworzyć i uruchomić kontener. Gdy to zrobisz, Gemma powinna odpowiedzieć:

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 działa w Cloud Shell.

14. Gratulacje

Gratulacje! Udało Ci się utworzyć pierwszą aplikację do czatu z generatywną AI w języku Java przy użyciu biblioteki LangChain4j i interfejsu Gemini API. W trakcie pracy odkryjesz, że multimodalne duże modele językowe są bardzo wydajne i mogą wykonywać różne zadania, takie jak odpowiadanie na pytania (nawet w Twojej dokumentacji), wydobywanie danych, interakcje z zewnętrznych interfejsów API itp.

Co dalej?

Teraz możesz ulepszać swoje aplikacje dzięki potężnym integracjom z LLM.

Więcej informacji

Dokumenty referencyjne