Gemini w Javie z Vertex AI i LangChain4j

1. Wprowadzenie

Ten przewodnik skupia się na dużym modelu językowym 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 interfejsem Gemini API będziesz używać języka Java w ramach LangChain4j. Poznasz konkretne przykłady, które pomogą Ci wykorzystać LLM do odpowiadania na pytania, generowania pomysłów, wyodrębniania jednostek i treści strukturalnych, generowania rozszerzonego przez wyszukiwanie w zapisanych informacjach oraz wywoływania funkcji.

Czym jest generatywna AI?

Generatywna AI odnosi się do wykorzystywania sztucznej inteligencji do tworzenia nowych treści, takich jak tekst, obrazy, muzyka, dźwięki i filmy.

Generatywna AI jest oparta na dużych modelach językowych (LLM), które mogą wykonywać wiele zadań jednocześnie, w tym podsumowywać, odpowiadać na pytania i klasyfikować. Modele podstawowe można dostosować do konkretnych przypadków użycia przy użyciu bardzo małej ilości danych przykładowych i minimalnym nakładzie pracy związanym z trenowaniem.

Jak działa generatywna AI?

Generatywna AI działa na podstawie modelu uczenia maszynowego, który rozpoznaje wzorce i zależności w zbiorze danych zawierającym treści utworzone 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 utworzonych przez ludzi i odpowiadających im etykiet. Następnie uczy się generować treści podobne do tych utworzonych przez człowieka.

Jakie są typowe zastosowania generatywnej AI?

Generatywna AI może służyć do:

  • Usprawnianie interakcji z klientami dzięki ulepszonym funkcjom czatu i wyszukiwania.
  • Przeglądaj ogromne ilości nieuporządkowanych danych za pomocą interfejsów konwersacyjnych i podsumowań.
  • Pomagają w powtarzalnych zadaniach, takich jak odpowiadanie na zapytania ofertowe, lokalizowanie treści marketingowych w różnych językach czy sprawdzanie umów z klientami pod kątem zgodności z przepisami.

Jakie oferty generatywnej AI są dostępne w Google Cloud?

Dzięki Vertex AI możesz wchodzić w interakcje z modelami podstawowymi, dostosowywać je i osadzać w aplikacjach, nie mając przy tym 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żyć modeli w notatniku do obsługi danych.

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

Gemini w Google Cloud to oparta na AI usługa wspomagająca dostępna w Google Cloud i IDE, która pomaga szybciej wykonywać więcej zadań. Gemini Code Assist uzupełnia i generuje kod, wyjaśnia go oraz umożliwia czatowanie w celu zadawania pytań technicznych.

Co to jest Gemini?

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

b9913d011999e7c7.png

Gemini występuje w różnych wersjach i rozmiarach:

  • Gemini 2.0 Flash: nasze najnowsze funkcje nowej generacji i ulepszone możliwości.
  • Gemini 2.0 Flash-Lite: model Gemini 2.0 Flash zoptymalizowany pod kątem niskich kosztów i krótkiego czasu oczekiwania.
  • Gemini 2.5 Pro: nasz najbardziej zaawansowany model rozumowania.
  • Gemini 2.5 Flash: model myślący o wszechstronnych możliwościach. Została zaprojektowana tak, aby zapewnić równowagę między ceną a wydajnością.

Najważniejsze funkcje:

  • Wielomodalność: zdolność Gemini do rozumienia i obsługi wielu formatów informacji to znaczący krok naprzód w porównaniu z tradycyjnymi modelami językowymi, które obsługują tylko tekst.
  • Wydajność: Gemini 2.5 Pro przewyższa obecne najnowocześniejsze modele w wielu testach porównawczych i jako pierwszy model pokonał ekspertów w trudnym teście MMLU (Massive Multitask Language Understanding).
  • Elastyczność: różne rozmiary Gemini sprawiają, że model ten można dostosować do różnych przypadków użycia, od badań na dużą skalę po wdrożenia na urządzeniach mobilnych.

Jak można wchodzić w interakcje z Gemini w Vertex AI z poziomu Javy?

Dostępne są dwie opcje:

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

W tym ćwiczeniu użyjesz platformy LangChain4j.

Czym jest platforma LangChain4j?

Platforma LangChain4j to biblioteka open source do integrowania dużych modeli językowych z aplikacjami w języku Java. Umożliwia ona koordynowanie różnych komponentów, takich jak sam LLM, ale też inne narzędzia, np. bazy danych wektorowych (do wyszukiwania semantycznego), moduły do wczytywania i dzielenia dokumentów (do analizowania dokumentów i uczenia się na ich podstawie), parsery danych wyjściowych i inne.

Projekt został zainspirowany projektem LangChain w języku Python, ale jego celem jest obsługa programistów Java.

bb908ea1e6c96ac2.png

Czego się nauczysz

  • Jak skonfigurować projekt w Javie, aby korzystać z Gemini i LangChain4j
  • Jak programowo wysłać pierwszego prompta do Gemini
  • Jak przesyłać strumieniowo odpowiedzi z Gemini
  • Jak utworzyć rozmowę między użytkownikiem a Gemini
  • Jak korzystać z Gemini w kontekście multimodalnym, wysyłając zarówno tekst, jak i obrazy
  • Jak wyodrębniać przydatne uporządkowane informacje z nieuporządkowanych treści
  • Jak modyfikować szablony promptów
  • Jak przeprowadzić klasyfikację tekstu, np. analizę nastawienia
  • Jak czatować przy użyciu własnych dokumentów (generowanie z wyszukiwaniem)
  • Jak rozszerzyć możliwości chatbotów za pomocą wywoływania funkcji
  • Jak używać modelu Gemma lokalnie z Ollamą i TestContainers

Czego potrzebujesz

  • Znajomość języka programowania Java
  • projekt Google Cloud,
  • przeglądarka, np. Chrome lub Firefox;

2. Konfiguracja i wymagania

Samodzielne konfigurowanie środowiska

  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óry nie jest używany przez interfejsy API Google. Zawsze możesz ją zaktualizować.
  • Identyfikator projektu jest unikalny we wszystkich projektach Google Cloud i nie można go zmienić po ustawieniu. Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie musisz się tym przejmować. W większości ćwiczeń z programowania musisz odwoływać się do identyfikatora projektu (zwykle oznaczanego jako PROJECT_ID). Jeśli wygenerowany identyfikator Ci się nie podoba, możesz wygenerować inny losowy identyfikator. Możesz też spróbować własnej nazwy i sprawdzić, czy jest dostępna. Po tym kroku nie można go zmienić i pozostaje on taki przez cały czas trwania projektu.
  • Warto wiedzieć, że istnieje też trzecia wartość, numer projektu, której używają niektóre interfejsy API. Więcej informacji o tych 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby korzystać z zasobów i interfejsów API Google Cloud. Wykonanie tego laboratorium nie będzie kosztować dużo, a może nawet nic. Aby wyłączyć zasoby i uniknąć naliczania opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub 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

Z Google Cloud można korzystać zdalnie na laptopie, ale w tym module użyjemy Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

Aktywowanie Cloud Shell

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

3c1dabeca90e44e5.png

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tego środowiska. Jeśli pojawił się ekran pośredni, kliknij Dalej.

9c92662c6a846a5c.png

Uzyskanie dostępu do środowiska Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.

9f0e51b578fecce5.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Większość zadań w tym module, a być może wszystkie, możesz wykonać w przeglądarce.

Po połączeniu z Cloud Shell zobaczysz, że uwierzytelnianie zostało już przeprowadzone, a projekt jest już ustawiony na Twój identyfikator projektu.

  1. Aby potwierdzić, że uwierzytelnianie zostało przeprowadzone, 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 potwierdzić, że 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 go ustawić za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Wynik polecenia

Updated property [core/project].

3. Przygotowywanie środowiska programistycznego

W tym ćwiczeniu programistycznym do tworzenia programów w Javie użyjesz terminala i edytora Cloud Shell.

Włączanie interfejsów API Vertex AI

W konsoli Google Cloud upewnij się, że 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 API 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żniejszy w tym ćwiczeniu jest interfejs 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 ćwiczenia:

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 wrapper Gradle:

gradle wrapper

Bieg z gradlew:

./gradlew run

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

..
> Task :app:run
Hello World!

Otwieranie i konfigurowanie edytora Cloud

Otwórz kod w edytorze Cloud Code w Cloud Shell:

42908e11b28f4383.png

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

Konfigurowanie zmiennych środowiskowych

Otwórz nowy terminal w edytorze Cloud Code, wybierając Terminal -> New Terminal. Skonfiguruj 2 zmienne środowiskowe wymagane do uruchomienia przykładowych kodów:

  • PROJECT_ID – identyfikator Twojego projektu Google Cloud.
  • LOCATION – 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. Pierwsze wywołanie modelu Gemini

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

Przyjrzyj się plikowi QA.java w katalogu app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

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

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

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

W metodzie main konfigurujesz model językowy czatu za pomocą narzędzia do tworzenia VertexAiGeminiChatModel i określasz:

  • Projekt
  • Lokalizacja
  • Nazwa modelu (gemini-2.0-flash).

Model językowy jest już gotowy. Możesz wywołać metodę generate() i przekazać prompt, pytanie lub instrukcje do wysłania do LLM. 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 głównym folderze 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.

Gratulujemy, udało Ci się wykonać pierwsze wywołanie Gemini.

Odpowiadaj na bieżąco

Czy zauważyliście, że odpowiedź została udzielona od razu, po kilku sekundach? Dzięki wariantowi odpowiedzi strumieniowej można też otrzymywać odpowiedź stopniowo. Odpowiedź strumieniowa – model zwraca odpowiedź po kawałku, gdy tylko jest dostępna.

W tych ćwiczeniach skupimy się na odpowiedziach bez przesyłania strumieniowego, ale przyjrzyjmy się odpowiedziom z przesyłaniem strumieniowym, aby zobaczyć, jak to działa.

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

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-2.0-flash")
            .maxOutputTokens(4000)
            .build();

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

Tym razem importujemy warianty klasy przesyłania strumieniowego VertexAiGeminiStreamingChatModel, które implementują interfejs StreamingChatLanguageModel. Musisz też statycznie zaimportować LambdaStreamingResponseHandler.onNext, czyli wygodną metodę, która udostępnia StreamingResponseHandler, aby utworzyć obsługę strumieniowania za pomocą wyrażeń lambda w Javie.

Tym razem sygnatura metody generate() jest nieco inna. Zamiast zwracać ciąg znaków, zwraca typ void. Oprócz promptu musisz przekazać moduł obsługi odpowiedzi strumieniowej. Dzięki wspomnianemu powyżej importowi statycznemu możemy zdefiniować wyrażenie lambda, które przekazujemy do metody onNext(). Wyrażenie lambda jest wywoływane za każdym razem, gdy dostępna jest nowa część odpowiedzi, a funkcja jest wywoływana tylko wtedy, gdy wystąpi błąd.

Uruchomienie:

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

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

Dodatkowa konfiguracja

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

  • temperature(Float temp) – określa, jak kreatywna ma być odpowiedź (0 – mało kreatywna i często bardziej oparta na faktach, 2 – bardziej kreatywna).
  • topP(Float topP) – aby wybrać możliwe słowa, których łączne prawdopodobieństwo wynosi tę liczbę zmiennoprzecinkową (od 0 do 1).
  • topK(Integer topK) – losowe wybranie słowa z maksymalnej liczby prawdopodobnych słów do dokończenia tekstu (od 1 do 40).
  • maxOutputTokens(Integer max) – określa maksymalną długość odpowiedzi modelu (zwykle 4 tokeny to około 3 słowa).
  • maxRetries(Integer retries) – jeśli przekroczysz limit żądań na określony czas lub platforma napotka problem techniczny, model może ponowić wywołanie 3 razy.

Do tej pory zadano Gemini tylko jedno pytanie, ale możesz też prowadzić wieloetapową rozmowę. Więcej informacji znajdziesz w następnej sekcji.

5. Czat z Gemini

W poprzednim kroku zadano jedno pytanie. Teraz nadszedł czas na prawdziwą rozmowę między użytkownikiem a LLM. Każde pytanie i każda odpowiedź mogą być powiązane z poprzednimi, tworząc prawdziwą dyskusję.

Zapoznaj się z plikiem Conversation.java w folderze app/src/main/java/gemini/workshop:

package gemini.workshop;

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

import java.util.List;

public class Conversation {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .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łudze wieloetapowego charakteru rozmowy i będzie przechowywać w pamięci lokalnej poprzednie pytania i odpowiedzi.
  • AiServices – klasa abstrakcji wyższego poziomu, która łączy model czatu i pamięć czatu.

W metodzie głównej skonfigurujesz model, pamięć czatu i usługę AI. Model jest konfigurowany jak zwykle, z informacjami o projekcie, lokalizacji i nazwie modelu.

W przypadku pamięci czatu używamy kreatora MessageWindowChatMemory, aby utworzyć pamięć, która przechowuje 20 ostatnich wymienionych wiadomości. Jest to przesuwne okno obejmujące rozmowę, którego kontekst jest przechowywany lokalnie w naszym kliencie klasy Java.

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

Zwróć uwagę, że usługa AI korzysta z niestandardowego interfejsu ConversationService, który zdefiniowaliśmy i który jest zaimplementowany w LangChain4j. Przyjmuje on String zapytanie i zwraca String odpowiedź.

Teraz możesz porozmawiać z Gemini. Najpierw wysyłane jest proste powitanie, a potem pierwsze pytanie o Wieżę Eiffla, aby dowiedzieć się, w którym kraju się znajduje. Zwróć uwagę, że ostatnie zdanie jest powiązane z odpowiedzią na pierwsze pytanie, ponieważ zastanawiasz się, ilu mieszkańców ma kraj, w którym znajduje się wieża Eiffla, nie wspominając wprost o kraju podanym w poprzedniej odpowiedzi. Widać na nim, że poprzednie pytania i odpowiedzi są wysyłane z każdym promptem.

Uruchom przykład:

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

Powinny pojawić się 3 odpowiedzi podobne do tych:

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ć Gemini pytania jednorazowe lub prowadzić wielokrotne rozmowy, ale do tej pory dane wejściowe były tylko tekstowe. A co z obrazami? W następnym kroku przyjrzymy się obrazom.

6. Multimodalność z Gemini

Gemini to model multimodalny. Może przyjmować nie tylko tekst, ale też obrazy, a nawet filmy. W tej sekcji znajdziesz przykład zastosowania mieszania tekstu i obrazów.

Czy Gemini rozpozna tego kota?

af00516493ec9ade.png

Zdjęcie kota na śniegu pochodzące z Wikipedii

Sprawdź 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-2.0-flash")
            .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());
    }
}

Zwróć uwagę, że w importach rozróżniamy różne rodzaje wiadomości i treści. Obiekt UserMessage może zawierać zarówno obiekt TextContent, jak i ImageContent. To przykład multimodalności: mieszanie tekstu i obrazów. Nie wysyłamy prostego prompta w postaci ciągu znaków, ale bardziej złożony obiekt, który reprezentuje wiadomość użytkownika i składa się z elementu treści obrazu i elementu treści tekstowej. Model odsyła 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 obrazu z pewnością dała Ci wskazówkę, co zawiera obraz, ale dane wyjściowe 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.

Łączenie promptów z obrazami i tekstem otwiera ciekawe możliwości zastosowania. Możesz tworzyć aplikacje, które mogą:

  • rozpoznawać tekst na zdjęciach;
  • Sprawdzanie, czy obraz jest bezpieczny do wyświetlenia.
  • tworzyć opisy obrazów,
  • Przeszukuj bazę danych obrazów za pomocą opisów w formie zwykłego tekstu.

Oprócz wyodrębniania informacji z obrazów możesz też wyodrębniać informacje z nieustrukturyzowanego tekstu. Tego dowiesz się z następnej sekcji.

7. Wyodrębnianie uporządkowanych informacji z nieustrukturyzowanego tekstu

W wielu sytuacjach ważne informacje są podawane w dokumentach raportów, e-mailach lub innych długich tekstach w sposób nieustrukturyzowany. Najlepiej byłoby wyodrębnić kluczowe informacje zawarte w nieustrukturyzowanym tekście w postaci obiektów strukturalnych. 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 poprosić LLM o wyodrębnienie kodu JSON z nieustrukturyzowanego tekstu za pomocą sprytnie zmodyfikowanego promptu (jest to powszechnie nazywane „inżynierią promptów”).

W poniższym przykładzie zamiast tworzyć prompta opisującego dane wyjściowe w formacie JSON użyjemy zaawansowanej funkcji Gemini o nazwie dane wyjściowe w formacie strukturalnym (czasami nazywanej generowaniem z ograniczeniami), która wymusza na modelu generowanie tylko prawidłowych treści w formacie JSON zgodnie z określonym schematem JSON.

Sprawdź ExtractData.java w app/src/main/java/gemini/workshop:

package gemini.workshop;

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

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

public class ExtractData {

    record Person(String name, int age) { }

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

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-2.0-flash")
            .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ę poszczególnym etapom w tym pliku:

  • Rekord Person jest zdefiniowany tak, aby reprezentować szczegóły opisujące osobę (imię i nazwisko oraz wiek).
  • Interfejs PersonExtractor jest zdefiniowany za pomocą metody, która na podstawie nieustrukturyzowanego ciągu tekstowego zwraca instancję Person.
  • Znak extractPerson() jest opatrzony adnotacją @SystemMessage, która łączy z nim prompt z instrukcjami. Jest to prompt, którego model użyje do wyodrębnienia informacji i zwrócenia szczegółów w postaci dokumentu JSON, który zostanie przeanalizowany i zdeserializowany do instancji Person.

Przyjrzyjmy się teraz zawartości metody main():

  • Model czatu jest skonfigurowany i utworzony. Używamy 2 nowych metod klasy konstruktora modelu: responseMimeType()responseSchema(). Pierwszy z nich nakazuje Gemini generowanie prawidłowego kodu JSON w danych wyjściowych. Druga metoda definiuje schemat obiektu JSON, który powinien zostać zwrócony. Ta druga deleguje do wygodnej metody, która może przekonwertować klasę lub rekord Java na odpowiedni schemat JSON.
  • Obiekt PersonExtractor jest tworzony dzięki klasie AiServices LangChain4j.
  • Następnie możesz po prostu zadzwonić pod numer Person person = extractor.extractPerson(...), aby wyodrębnić szczegóły osoby z nieustrukturyzowanego tekstu i otrzymać instancję Person z imieniem i nazwiskiem oraz wiekiem.

Uruchom przykład:

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

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

Anna
23

Tak, to Anna i ma 23 lata.

Dzięki temu AiServicespodejściu pracujesz z obiektami o ściśle określonym typie. Nie wchodzisz w bezpośrednią interakcję z LLM. Zamiast tego pracujesz z konkretnymi klasami, np. Person, aby reprezentować wyodrębnione dane osobowe, i masz obiekt PersonExtractor z metodą extractPerson(), która zwraca instancję Person. Pojęcie LLM jest tu abstrakcyjne, a jako programista Java podczas korzystania z tego interfejsu PersonExtractor manipulujesz tylko zwykłymi klasami i obiektami.

8. Strukturyzowanie promptów za pomocą szablonów promptó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 na przykład chcesz tworzyć przepisy kulinarne, możesz użyć promptu „Jesteś utalentowanym szefem kuchni. Utwórz przepis z tych składników: …”, a następnie dodać składniki na końcu tego tekstu. Do tego służą szablony promptów – podobnie jak ciągi znaków interpolowane w językach programowania. Szablon prompta zawiera symbole zastępcze, które możesz zastąpić odpowiednimi danymi w przypadku konkretnego wywołania LLM.

Przyjrzyjmy się konkretnemu przykładowi: TemplatePrompt.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.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-2.0-flash")
            .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 skonfiguruj model VertexAiGeminiChatModel, zachowując wysoki poziom kreatywności dzięki wysokiej temperaturze oraz wysokim wartościom topP i topK. Następnie utwórz PromptTemplate za pomocą jego from() metody statycznej, przekazując ciąg tekstowy prompta, i użyj zmiennych zastępczych w podwójnych nawiasach klamrowych: {{dish}}{{ingredients}}.

Ostateczny prompt tworzysz, wywołując funkcję apply(), która przyjmuje mapę par klucz/wartość reprezentujących nazwę obiektu zastępczego i wartość ciągu, którą należy go zastąpić.

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

Uruchom przykład:

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

Powinny pojawić się 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 potem ponownie uruchomić kod. Dzięki temu możesz obserwować wpływ zmiany tych parametrów na LLM.

Szablony promptów to dobry sposób na tworzenie instrukcji do wywołań LLM, które można wykorzystywać ponownie i parametryzować. Możesz przekazywać dane i dostosowywać prompty do różnych wartości podawanych przez użytkowników.

9. Klasyfikacja tekstu z użyciem tworzenia promptów few-shot

Modele LLM dobrze radzą sobie z klasyfikowaniem tekstu w różnych kategoriach. Możesz pomóc LLM w tym zadaniu, podając przykłady tekstów i powiązanych z nimi kategorii. To podejście jest często nazywane promptowaniem z użyciem kilku przykładów.

Otwórzmy plik 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-2.0-flash")
            .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!"));
    }
}

Typ wyliczeniowy Sentiment zawiera listę różnych wartości nastawienia: negatywnego, neutralnego lub pozytywnego.

W metodzie main() tworzysz model rozmowy z Gemini w zwykły sposób, ale z małą maksymalną liczbą tokenów wyjściowych, ponieważ chcesz uzyskać tylko krótką odpowiedź: tekst to POSITIVE, NEGATIVE lub NEUTRAL. Aby ograniczyć model do zwracania tylko tych wartości, możesz skorzystać z obsługi danych wyjściowych w formacie strukturalnym, którą opisaliśmy w sekcji dotyczącej wyodrębniania danych. Dlatego używana jest metoda responseSchema(). Tym razem nie będziesz używać wygodnej metody z SchemaHelper do wywnioskowania definicji schematu, ale zamiast tego użyjesz narzędzia do tworzenia Schema, aby zrozumieć, jak wygląda definicja schematu.

Po skonfigurowaniu modelu utwórz interfejs SentimentAnalysis, który AiServices LangChain4j zaimplementuje za Ciebie za pomocą LLM. Ten interfejs zawiera 1 metodę: analyze(). Przyjmuje tekst do analizy i zwraca wartość wyliczeniową Sentiment. Dlatego manipulujesz tylko obiektem o silnym typie, który reprezentuje rozpoznaną klasę sentymentu.

Następnie, aby podać „przykłady krótkich serii” i zachęcić model do wykonania pracy klasyfikacyjnej, tworzysz pamięć czatu, aby przekazywać pary wiadomości użytkownika i odpowiedzi AI, które reprezentują tekst i powiązane z nim nastawienie.

Połączmy wszystko za pomocą metody AiServices.builder(), przekazując interfejs SentimentAnalysis, model do użycia i 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

Powinno pojawić się jedno słowo:

POSITIVE

Wygląda na to, że uwielbienie truskawek to pozytywne nastawienie.

10. Retrieval-Augmented Generation

Duże modele językowe są trenowane z wykorzystaniem dużych ilości tekstu. Ich wiedza obejmuje jednak tylko informacje, które zostały wykorzystane do trenowania. Jeśli po dacie odcięcia trenowania modelu pojawią się 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 podejścia takie jak generowanie wspomagane wyszukiwaniem (RAG), które omówimy w tej sekcji, pomagają dostarczać dodatkowe informacje, które LLM może potrzebować, aby spełniać prośby użytkowników i odpowiadać informacjami, które mogą być bardziej aktualne lub dotyczyć informacji prywatnych niedostępnych w czasie trenowania.

Wróćmy do rozmów. Tym razem możesz zadawać pytania dotyczące dokumentów. Utworzysz chatbota, który będzie w stanie pobierać odpowiednie informacje z bazy danych zawierającej Twoje dokumenty podzielone na mniejsze części („fragmenty”). Informacje te będą używane przez model do uzasadniania odpowiedzi zamiast polegać wyłącznie na wiedzy zawartej w jego szkoleniu.

W przypadku RAG są 2 fazy:

  1. Faza wczytywania – dokumenty są wczytywane do pamięci, dzielone na mniejsze części, a wektory dystrybucyjne (wielowymiarowa reprezentacja wektorowa części) są obliczane i przechowywane w bazie danych wektorów, która umożliwia wyszukiwanie semantyczne. Ta faza przetwarzania jest zwykle wykonywana raz, gdy do korpusu dokumentów trzeba dodać nowe dokumenty.

cd07d33d20ffa1c8.png

  1. Faza zapytania – użytkownicy mogą teraz zadawać pytania dotyczące dokumentów. Pytanie zostanie również przekształcone w wektor i porównane ze wszystkimi innymi wektorami w bazie danych. Najbardziej podobne wektory są zwykle powiązane semantycznie i zwracane przez wektorową bazę danych. Następnie model LLM otrzymuje kontekst rozmowy, fragmenty tekstu odpowiadające wektorom zwróconym przez bazę danych i jest proszony o uzasadnienie swojej odpowiedzi na podstawie tych fragmentów.

a1d2e2deb83c6d27.png

Przygotuj dokumenty

W tym nowym przykładzie zadamy pytania dotyczące fikcyjnego modelu samochodu fikcyjnego producenta: Cymbal Starlight. Chodzi o to, że dokument dotyczący fikcyjnego samochodu nie powinien być częścią wiedzy modelu. Jeśli Gemini będzie w stanie poprawnie odpowiedzieć na pytania dotyczące tego samochodu, będzie to oznaczać, że podejście RAG działa: potrafi przeszukiwać dokument.

Wdrażanie czatbota

Przyjrzyjmy się, jak zbudować podejście dwufazowe: najpierw z przetwarzaniem dokumentu, a potem z czasem zapytania (zwanym też „fazą pobierania”), gdy użytkownicy zadają pytania dotyczące dokumentu.

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

W tym przykładzie użyjemy też bazy danych wektorów w pamięci. W rzeczywistym scenariuszu produkcyjnym fazy pozyskiwania i wykonywania zapytań byłyby rozdzielone na 2 osobne aplikacje, a wektory byłyby przechowywane w samodzielnej bazie danych.

Przetwarzanie dokumentów

Pierwszym krokiem etapu wczytywania dokumentu jest znalezienie pliku PDF dotyczącego naszego fikcyjnego samochodu 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 najpierw tworzyć zwykły model językowy do czatu, tworzysz instancję modelu osadzania. Jest to konkretny 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 potrzebujesz kilku klas, które będą ze sobą współpracować, aby:

  • Wczytaj i podziel 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, bazy danych wektorowych w pamięci, do przechowywania wektorów dystrybucyjnych.

Dokument jest dzielony na fragmenty dzięki klasie DocumentSplitters. Podzieli on tekst pliku PDF na fragmenty po 500 znaków, z nakładaniem się 100 znaków (z następnym fragmentem, aby uniknąć dzielenia słów lub zdań na części).

Moduł do wprowadzania danych do sklepu łączy moduł dzielenia dokumentów, model osadzania do obliczania wektorów i bazę danych wektorów w pamięci. Następnie metoda ingest() zajmie się przetwarzaniem.

Pierwszy etap dobiegł końca. Dokument został przekształcony w bloki tekstu z powiązanymi wektorami dystrybucyjnymi i zapisany w bazie danych wektorowych.

Zadawanie pytań

Czas zadać pytania. Aby rozpocząć rozmowę, utwórz model czatu:

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

Potrzebujesz też klasy pobierającej, aby połączyć bazę danych wektorów (w zmiennej embeddingStore) z modelem wektora dystrybucyjnego. Jego zadaniem jest wysyłanie zapytań do bazy danych wektorowych przez obliczanie wektora dystrybucyjnego dla zapytania użytkownika w celu znalezienia podobnych wektorów w bazie danych:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Utwórz interfejs reprezentujący asystenta eksperta ds. samochodów, czyli interfejs, który klasa AiServices zaimplementuje, aby umożliwić Ci interakcję z modelem:

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

Interfejs CarExpert zwraca odpowiedź w formie ciągu znaków opakowanego w klasę Result LangChain4j. Dlaczego warto używać tego modułu? Nie tylko otrzymasz odpowiedź, ale też będziesz mieć możliwość sprawdzenia fragmentów z bazy danych zwróconych przez narzędzie do wyszukiwania treści. Dzięki temu możesz wyświetlać użytkownikowi źródła dokumentów, które zostały wykorzystane do przygotowania ostatecznej odpowiedzi.

Na tym etapie 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:

  • Model języka czatu skonfigurowany wcześniej.
  • Pamięć czatu, która śledzi rozmowę.
  • Wyszukiwarka porównuje wektor dystrybucyjny zapytania 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}}
            """))
        .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 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 Twoje 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

W niektórych sytuacjach warto, aby LLM miał dostęp do systemów zewnętrznych, takich jak zdalny interfejs API, który pobiera informacje lub wykonuje działanie, albo usługi, które przeprowadzają 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 notowania giełdowe czy pomiary z czujników IoT.
  • wysłać e-maila,

Narzędzia obliczeniowe:

  • Kalkulator do bardziej zaawansowanych zadań matematycznych.
  • Interpretacja kodu do uruchamiania kodu, gdy duże modele językowe potrzebują logiki rozumowania.
  • Konwertowanie żądań w języku naturalnym na zapytania SQL, aby LLM mógł wysyłać zapytania do bazy danych.

Wywoływanie funkcji (czasami nazywane narzędziami lub korzystaniem z narzędzi) to możliwość wysyłania przez model żądań wykonania w jego imieniu co najmniej jednego wywołania funkcji, aby mógł prawidłowo odpowiedzieć na prompt użytkownika, korzystając z aktualniejszych danych.

Na podstawie konkretnego promptu użytkownika i znajomości istniejących funkcji, które mogą być istotne w danym kontekście, LLM może odpowiedzieć prośbą o wywołanie funkcji. Aplikacja integrująca LLM może następnie wywołać funkcję w jego imieniu, a potem odpowiedzieć LLM-owi, który zinterpretuje odpowiedź i zwróci odpowiedź tekstową.

4 kroki wywoływania funkcji

Przyjrzyjmy się przykładowi wywoływania funkcji: uzyskiwaniu informacji o prognozie pogody.

Jeśli zapytasz Gemini lub innego LLM-a o pogodę w Paryżu, odpowie on, że nie ma informacji o aktualnej prognozie pogody. Jeśli chcesz, aby LLM miał dostęp do danych pogodowych w czasie rzeczywistym, musisz zdefiniować funkcje, o które może prosić.

Przyjrzyj się temu diagramowi:

31e0c2aba5e6f21c.png

1️⃣ Najpierw użytkownik pyta o pogodę w Paryżu. Aplikacja chatbot (korzystająca z LangChain4j) wie, że ma do dyspozycji co najmniej jedną funkcję, która pomoże LLM w odpowiedzi na zapytanie. Chatbot wysyła zarówno początkowy prompt, jak i listę funkcji, które można wywołać. W tym przypadku funkcja o nazwie getWeather() przyjmuje parametr ciągu znaków określający lokalizację.

8863be53a73c4a70.png

Model LLM nie zna prognozy pogody, więc zamiast odpowiadać tekstowo, wysyła żądanie wykonania funkcji. Chatbot musi wywołać funkcję getWeather() z parametrem lokalizacji "Paris".

2️⃣ Czatbot wywołuje tę funkcję w imieniu LLM i pobiera odpowiedź funkcji. Załóżmy, że odpowiedź to {"forecast": "sunny"}.

d1367cc69c07b14d.png

3️⃣ Aplikacja chatbota odsyła odpowiedź JSON do LLM.

73a5f2ed19f47d8.png

4️⃣ Duży model językowy analizuje odpowiedź w formacie JSON, interpretuje te informacje i ostatecznie odpowiada tekstem, że w Paryżu jest słonecznie.

20832cb1ee6fbfeb.png

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-2.0-flash")
    .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();

Zdefiniowano nazwę funkcji oraz nazwę i typ parametru, ale zarówno funkcja, jak i parametry mają opisy. Opisy są bardzo ważne i pomagają modelowi LLM zrozumieć, co może robić funkcja, a tym samym ocenić, czy należy ją wywołać w kontekście rozmowy.

Zacznijmy od kroku 1, czyli wysłania pierwszego pytania o pogodę 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 ma użyć model, a model odpowiada żądaniem wykonania narzędzia:

// 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, którą funkcję LLM chce wywołać. W kodzie nie wykonujemy prawdziwego wywołania zewnętrznego interfejsu API, tylko zwracamy 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);

W kroku 4 model LLM dowiaduje się o wyniku wykonania funkcji i może następnie wygenerować 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());

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

Dane wyjściowe powinny być 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.

W danych wyjściowych powyżej widać żądanie wykonania narzędzia oraz odpowiedź.

12. LangChain4j obsługuje wywoływanie funkcji

W poprzednim kroku pokazaliśmy, jak przeplatają się normalne interakcje z pytaniami i odpowiedziami w formie tekstowej oraz interakcje z prośbami o wykonanie funkcji i odpowiedziami na nie. W międzyczasie podaliśmy bezpośrednio odpowiedź na prośbę o wykonanie funkcji, bez wywoływania rzeczywistej funkcji.

LangChain4j oferuje jednak również abstrakcję wyższego poziomu, która może obsługiwać wywołania funkcji w sposób przejrzysty, a jednocześnie obsługiwać 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 utwórz klasę zawierającą rzeczywistą 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 oznaczona adnotacją @Tool, która odpowiada opisowi funkcji, o której wywołanie może poprosić model.

Parametry funkcji (w tym przypadku tylko jeden) są również oznaczone, 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 bardziej złożonych scenariuszach.

W tej klasie zwracasz niektóre gotowe odpowiedzi, ale jeśli chcesz wywołać prawdziwą zewnętrzną usługę prognozy pogody, to w treści tej metody musisz wywołać tę usługę.

Jak widzieliśmy podczas tworzenia ToolSpecification w poprzednim podejściu, ważne jest, aby dokumentować działanie funkcji i opisywać, do czego odnoszą się parametry. Dzięki temu model będzie wiedział, jak i kiedy można używać tej funkcji.

Następnie LangChain4j umożliwia udostępnienie interfejsu odpowiadającego 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żesz też używać bardziej złożonych sygnatur, które obejmują UserMessage (w przypadku wiadomości użytkownika) lub AiMessage (w przypadku odpowiedzi modelu) z LangChain4j, a nawet TokenStream, jeśli chcesz obsługiwać bardziej zaawansowane sytuacje, ponieważ te bardziej skomplikowane obiekty zawierają też dodatkowe informacje, takie jak liczba zużytych tokenów itp. Jednak dla uproszczenia przyjmiemy ciąg znaków jako dane wejściowe i ciąg znaków jako dane wyjściowe.

Na koniec omówmy 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-2.0-flash")
        .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?"));
    System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}

Jak zwykle skonfiguruj model rozmowy z Gemini. Następnie tworzysz instancję usługi prognozy pogody, która zawiera „funkcję”, o której wywołanie poprosi nas model.

Teraz ponownie użyj klasy AiServices, aby powiązać model czatu, pamięć czatu i narzędzie (czyli usługę prognozy pogody z jej funkcją). AiServices zwraca obiekt, który implementuje zdefiniowany przez Ciebie interfejs WeatherAssistant. Pozostało tylko wywołać metodę chat() tego asystenta. Podczas wywoływania tej funkcji zobaczysz tylko odpowiedzi tekstowe, ale żądania wywołania funkcji i odpowiedzi na nie nie będą widoczne dla dewelopera. Żądania te będą obsługiwane automatycznie i w sposób przejrzysty. Jeśli Gemini uzna, że należy wywołać funkcję, odpowie żądaniem wywołania funkcji, a LangChain4j wywoła lokalną funkcję w Twoim imieniu.

Uruchom przykład:

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

Dane wyjściowe powinny być podobne do tych:

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

It is warmer in Paris (20 degrees) than in London (15 degrees).

To był przykład pojedynczej funkcji.

Wiele wywołań funkcji

Możesz też mieć wiele funkcji i pozwolić LangChain4j na obsługę wielu wywołań funkcji w Twoim imieniu. Przykład użycia wielu funkcji znajdziesz w sekcji 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 do pobierania 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, która stosuje procent 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 i klasę MultiTools i zadać pytania takie jak „Ile wynosi 10% ceny akcji AAPL po przeliczeniu z USD na EUR?”.

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

Uruchom go w ten sposób:

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

Powinny się 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.

Do pracowników obsługi klienta

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

Tworząc tak zaawansowanych agentów, należy zachować odpowiedzialność. Przed podjęciem automatycznych działań warto rozważyć proces z udziałem człowieka. Podczas projektowania agentów opartych na LLM, którzy wchodzą 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 jej mniejszy model.

Gemma to rodzina lekkich, zaawansowanych otwartych modeli, które powstały na podstawie tych samych badań i technologii, które zostały wykorzystane do stworzenia modeli Gemini. Najnowszy model Gemma to Gemma 3, który jest dostępny w 4 rozmiarach: 1B (tylko tekst), 4B, 12B i 27B. Ich wagi są dostępne bezpłatnie, a małe rozmiary oznaczają, że możesz uruchomić je samodzielnie, nawet na laptopie lub w Cloud Shell.

Jak uruchomić model Gemma?

Model Gemma można uruchamiać na wiele sposobów: w chmurze, w Vertex AI jednym kliknięciem lub w GKE z kilkoma procesorami GPU, ale można go też uruchomić lokalnie.

Dobrym sposobem na lokalne uruchomienie Gemy jest Ollama, narzędzie, które pozwala uruchamiać małe modele, takie jak Llama, Mistral i wiele innych, na komputerze lokalnym. Jest podobny do Dockera, ale przeznaczony dla dużych modeli językowych.

Zainstaluj Ollamę, postępując zgodnie z instrukcjami dla Twojego systemu operacyjnego.

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

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

Po zainstalowaniu lokalnym możesz uruchamiać polecenia, aby pobrać model:

ollama pull gemma3:1b

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

Uruchom model:

ollama run gemma3:1b

Teraz możesz korzystać z modelu:

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

Aby zamknąć wiersz poleceń, naciśnij Ctrl+D.

Uruchamianie Gemy w Ollamie na TestContainers

Zamiast instalować i uruchamiać Ollamy lokalnie, możesz używać jej w kontenerze obsługiwanym przez TestContainers.

TestContainers przydaje się nie tylko do testowania, ale też do wykonywania kontenerów. Możesz nawet skorzystać z OllamaContainer.

Oto pełny obraz:

2382c05a48708dfd.png

Implementacja

Przyjrzyjmy się poszczególnym elementom GemmaWithOllamaContainer.java.

Najpierw musisz utworzyć kontener pochodny Ollama, który pobiera model Gemma. Ten obraz już istnieje (z poprzedniego uruchomienia) lub zostanie utworzony. Jeśli obraz już istnieje, poinformuj TestContainers, że chcesz zastąpić domyślny obraz Ollamy wariantem opartym na Gemmie:

    private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
    public static final String GEMMA_3 = "gemma3:1b";

    // Creating an Ollama container with Gemma 3 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_GEMMA3)
            .exec();

        if (listImagesCmd.isEmpty()) {
            System.out.println("Creating a new Ollama container with Gemma 3 image...");
            OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
            System.out.println("Starting Ollama...");
            ollama.start();
            System.out.println("Pulling model...");
            ollama.execInContainer("ollama", "pull", GEMMA_3);
            System.out.println("Committing to image...");
            ollama.commitToImage(TC_OLLAMA_GEMMA3);
            return ollama;
        }

        System.out.println("Ollama image substitution...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA3)
                .asCompatibleSubstituteFor("ollama/ollama"));
    }

Następnie utwórz i uruchom testowy kontener Ollama, a potem 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_3)
        .build();

    String response = model.generate("Why is the sky blue?");

    System.out.println(response);
}

Uruchom go w ten sposób:

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

Pierwsze uruchomienie potrwa dłużej, ponieważ trzeba utworzyć i uruchomić kontener. Po zakończeniu tego procesu powinna pojawić się odpowiedź Gemy:

INFO: Container ollama/ollama:0.7.1 started in PT7.228339916S
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 już 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 LangChain4j i interfejsu Gemini API. Po drodze odkrywasz, że wielomodowe duże modele językowe są dość zaawansowane i mogą wykonywać różne zadania, takie jak odpowiadanie na pytania (nawet na podstawie własnej dokumentacji), wyodrębnianie danych, interakcje z zewnętrznymi interfejsami API i inne.

Co dalej?

Teraz Twoja kolej, aby wzbogacić aplikacje o zaawansowane integracje z LLM.

Więcej informacji

Dokumentacja