1. Wprowadzenie
To ćwiczenia w programie skupiają się na dużym modelu językowym Gemini (LLM), który jest hostowany w Vertex AI w Google Cloud. Vertex AI to platforma, która obejmuje wszystkie produkty, usługi i modele systemów uczących się dostępne w Google Cloud.
Do interakcji z interfejsem Gemini API będziesz używać języka Java za pomocą platformy LangChain4j. Poznasz konkretne przykłady, które pozwolą Ci wykorzystać ten model do odpowiadania na pytania, generowania pomysłów, wyodrębniania encji i treści uporządkowanych, generowania rozszerzonego przez pobieranie oraz wywoływania funkcji.
Co to 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 i wykonywać gotowe zadania, takie jak podsumowywanie, pytania i odpowiedzi, klasyfikacja i nie tylko. Dzięki minimalnej liczbie treningów modele podstawowe można dostosować do docelowych przypadków użycia z bardzo małą ilością przykładowych danych.
Jak działa generatywna AI?
Generatywna AI działa na podstawie modelu systemów uczących się (ML), aby poznawać wzorce i relacje w zbiorze danych z treściami utworzonymi przez człowieka. Wykorzystuje nauczone wzorce do generowania nowych treści.
Najpopularniejszym sposobem trenowania generatywnej AI jest użycie uczenia nadzorowanego. Model otrzyma zbiór treści stworzonych przez człowieka wraz z odpowiadającymi im etykietami. Następnie uczy się generować treści podobne do treści stworzonych przez człowieka.
Jakie są typowe zastosowania generatywnej AI?
Generatywnej AI można używać do:
- Usprawnij interakcję z klientami dzięki ulepszonemu czatowi i wyszukiwaniu.
- Eksploruj ogromne ilości nieuporządkowanych danych za pomocą interfejsów konwersacyjnych i podsumowań.
- Pomagaj m.in. w odpowiadaniu na zapytania ofertowe, lokalizowaniu treści marketingowych w różnych językach i sprawdzaniu umów z klientami pod kątem zgodności z przepisami.
Jakie rozwiązania w zakresie generatywnej AI oferuje Google Cloud?
Dzięki Vertex AI możesz wchodzić w interakcje z modelami podstawowymi, dostosowywać je i umieszczać w swoich aplikacjach, nawet jeśli nie masz zbyt dużego doświadczenia z systemami uczącymi się. Możesz uzyskać dostęp do modeli podstawowych w Bazie modeli, dostrajać modele za pomocą prostego interfejsu w Vertex AI Studio lub używać modeli w notatniku badania danych.
Usługa Vertex AI Search and Conversation zapewnia deweloperom najszybszy sposób na tworzenie wyszukiwarek i czatbotów opartych na generatywnej AI.
Gemini w Google Cloud to oparta na AI usługa wspomagająca, która jest oparta na Gemini i jest dostępna w Google Cloud oraz IDE. Dzięki niej zrobisz więcej w krótszym czasie. Gemini Code Assist umożliwia uzupełnianie kodu, generowanie kodu i jego wyjaśnienia, a także umożliwia rozmowy z tą osobą na czacie, w którym można zadawać pytania techniczne.
Co to jest Gemini?
Gemini to rodzina modeli generatywnej AI opracowana przez Google DeepMind, która została zaprojektowana z myślą o wielomodalnych zastosowaniach. Jest wielomodalny, więc może przetwarzać i generować różnego rodzaju treści, takie jak tekst, kod, obrazy i dźwięk.
Model 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 do zadań wymagających dużej liczby zasobów.
- Gemini Pro: średni rozmiar, zoptymalizowany pod kątem skalowania różnych zadań.
- Gemini Nano: najwydajniejszy, zaprojektowany z myślą o zadaniach na urządzeniu.
Najważniejsze funkcje:
- Multimodalność: zdolność Gemini do rozumienia i obsługiwania różnych formatów informacji to istotny krok w porównaniu z tradycyjnymi modelami językowymi opartymi na tekście.
- Wydajność: model Gemini Ultra w wielu testach porównawczych wyprzedza obecny stan urządzeń i był pierwszym modelem, który przewyższył oczekiwania ekspertów w testach porównawczych MMLU (Massive Multitask Language Reporting).
- Elastyczność: różne rozmiary Gemini pozwalają dostosować ją do różnych zastosowań, od badań na dużą skalę po wdrożenie na urządzeniach mobilnych.
Jak można korzystać z Gemini w Vertex AI z Javy?
Masz 2 możliwości:
- Oficjalna biblioteka Vertex AI Java API dla Gemini.
- LangChain4j.
W tym ćwiczeniu z programowania wykorzystasz platformę LangChain4j.
Czym jest platforma LangChain4j?
Platforma LangChain4j to biblioteka typu open source do integrowania modeli LLM z aplikacjami w Javie przez administrowanie różnymi komponentami, takimi jak sam LLM, ale też inne narzędzia, takie jak wektorowe bazy danych (do wyszukiwania semantycznego), narzędzia do wczytywania dokumentów i dzielniki (do analizowania dokumentów i uczenia się na nich), parsery wyjściowe i nie tylko.
Inspiracją dla projektu był projekt w języku Python LangChain, ale jego celem było pomaganie programistom Java.
Czego się nauczysz
- Jak skonfigurować projekt w języku Java, aby korzystać z Gemini i LangChain4j
- Jak automatycznie wysłać pierwszy prompt do Gemini
- Jak przesyłać odpowiedzi na bieżąco 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ębnić przydatne uporządkowane informacje z nieuporządkowanej treści
- Jak manipulować szablonami promptów
- Jak klasyfikować tekst, np. analizować nastawienia
- Jak czatować z własnymi dokumentami (Retrieval Augmented Generation)
- Jak rozszerzyć czatboty za pomocą wywołań funkcji
- Jak używać Gemma lokalnie w aplikacjach Ollama i TestContainers
Czego potrzebujesz
- Znajomość języka programowania Java
- Projekt Google Cloud
- przeglądarki, na przykład Chrome lub Firefox;
2. Konfiguracja i wymagania
Samodzielne konfigurowanie środowiska
- Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub wykorzystaj już istniejący. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.
- Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. To ciąg znaków, który nie jest używany przez interfejsy API Google. W każdej chwili możesz ją zaktualizować.
- Identyfikator projektu jest unikalny we wszystkich projektach Google Cloud i nie można go zmienić (po jego ustawieniu nie można go zmienić). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń w Codelabs musisz podać swój identyfikator projektu (zwykle identyfikowany jako
PROJECT_ID
). Jeśli nie podoba Ci się wygenerowany identyfikator, możesz wygenerować kolejny losowy. Możesz też spróbować własnych sił i sprawdzić, czy jest dostępna. Po wykonaniu tej czynności nie można jej już zmienić. Pozostanie ona przez cały czas trwania projektu. - Jest jeszcze trzecia wartość, numer projektu, z którego korzystają niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
- Następnie musisz włączyć płatności w Cloud Console, aby korzystać z zasobów Cloud/interfejsów API. Ukończenie tego ćwiczenia z programowania nic nie kosztuje. Aby wyłączyć zasoby w celu uniknięcia naliczania opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub projekt. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.
Uruchamianie Cloud Shell
Google Cloud można obsługiwać zdalnie z laptopa, ale w ramach tego ćwiczenia z programowania wykorzystasz Cloud Shell – środowisko wiersza poleceń działające w Cloud.
Aktywowanie Cloud Shell
- W konsoli Cloud kliknij Aktywuj Cloud Shell .
Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tej usługi. Jeśli wyświetlił się ekran pośredni, kliknij Dalej.
Uzyskanie dostępu do Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.
Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i uwierzytelnianie. Większość zadań w ramach tego ćwiczenia z programowania można wykonać w przeglądarce.
Po nawiązaniu połączenia z Cloud Shell powinno pojawić się potwierdzenie, że użytkownik jest uwierzytelniony, a projekt jest ustawiony na identyfikator Twojego projektu.
- Uruchom to polecenie w Cloud Shell, aby potwierdzić, że jesteś uwierzytelniony:
gcloud auth list
Dane wyjściowe polecenia
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- Uruchom to polecenie w Cloud Shell, aby sprawdzić, czy polecenie gcloud zna Twój projekt:
gcloud config list project
Dane wyjściowe polecenia
[core] project = <PROJECT_ID>
Jeśli tak nie jest, możesz go ustawić za pomocą tego polecenia:
gcloud config set project <PROJECT_ID>
Dane wyjściowe polecenia
Updated property [core/project].
3. Przygotowywanie środowiska programistycznego
W ramach tego ćwiczenia w programie użyjesz terminala Cloud Shell i edytora Cloud Shell, aby stworzyć programy w języku Java.
Włączanie interfejsów Vertex AI API
Upewnij się, że w konsoli Google Cloud nazwa Twojego projektu jest wyświetlana u góry konsoli Google Cloud. Jeśli nie, kliknij Wybierz projekt, aby otworzyć Selektor projektów, a następnie wybierz odpowiedni projekt.
Interfejsy Vertex AI API możesz włączyć w sekcji Vertex AI konsoli Google Cloud lub w terminalu Cloud Shell.
Aby włączyć usługę w konsoli Google Cloud, najpierw otwórz sekcję Vertex AI w menu konsoli Google Cloud:
W panelu Vertex AI kliknij Włącz wszystkie zalecane interfejsy API.
Spowoduje to włączenie kilku interfejsów API, ale najważniejszym z nich do ćwiczenia w Codelabs będzie aiplatform.googleapis.com
.
Ten interfejs API możesz też włączyć w terminalu Cloud Shell za pomocą tego polecenia:
gcloud services enable aiplatform.googleapis.com
Sklonowanie repozytorium GitHub
W terminalu Cloud Shell skopiuj repozytorium do tego ćwiczenia z programowania:
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ć polecenie „Hello World” programu.
Upewnij się, że jesteś w folderze najwyższego poziomu:
cd gemini-workshop-for-java-developers/
Utwórz kod Gradle:
gradle wrapper
Uruchom 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 za pomocą edytora Cloud Code z Cloud Shell:
W edytorze Cloud Code otwórz folder źródłowy ćwiczeń z programowania, wybierając File
-> Open Folder
i wskaż folder źródłowy ćwiczeń w Codelabs (np. /home/username/gemini-workshop-for-java-developers/
).
Instalowanie Gradle dla Javy
Aby umożliwić prawidłowe działanie edytora kodu w chmurze z Gradle, zainstaluj rozszerzenie Gradle for Java.
Najpierw przejdź do sekcji Projekty Java i naciśnij znak plusa:
Wybierz Gradle for Java
:
Wybierz wersję pakietu Install Pre-Release
:
Po zainstalowaniu przyciski Disable
i Uninstall
powinny być widoczne:
Na koniec wyczyść obszar roboczy, aby zastosować nowe ustawienia:
Poprosimy Cię o ponowne załadowanie i usunięcie warsztatu. Wybierz Reload and delete
:
Po otwarciu jednego z plików, np. App.java, powinien być widoczny edytor działający prawidłowo z wyróżnianiem składni:
Możesz teraz przetestować kilka próbek w Gemini.
Skonfiguruj zmienne środowiskowe
Otwórz nowy terminal w edytorze Cloud Code, wybierając Terminal
-> New Terminal
Skonfiguruj 2 zmienne środowiskowe wymagane do uruchomienia przykładowego kodu:
- 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
Po prawidłowym skonfigurowaniu projektu możesz wywołać interfejs Gemini API.
Spójrz na 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-1.5-flash-001")
.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 za pomocą konstruktora języka VertexAiGeminiChatModel
i określasz:
- Projekt
- Lokalizacja
- Nazwa modelu (
gemini-1.5-flash-001
).
Teraz, gdy model językowy jest gotowy, możesz wywołać metodę generate()
i przekazać do LLM swój prompt, pytanie lub instrukcje. W tym miejscu zadajesz proste pytanie o to, co sprawia, że niebo jest niebieskie.
Możesz go zmienić, 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
Zostaną wyświetlone 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, udało Ci się wykonać pierwsze połączenie z Gemini.
Odpowiedź na żądanie strumieniowania
Czy zauważyłeś, że odpowiedź została podawana za jednym razem po kilku sekundach? Możliwe jest też stopniowe uzyskiwanie odpowiedzi dzięki wariantowi przesyłania strumieniowego. W ramach strumieniowego przesyłania odpowiedzi model zwraca fragment odpowiedzi, gdy staje się dostępna.
W ramach tych ćwiczeń w programie pozostaniemy przy odpowiedzi niestrumieniowej, ale przyjrzyjmy się odpowiedziowi przesyłanej strumieniowo, aby sprawdzić, jak to zrobić.
Odpowiedź strumieniowania możesz zobaczyć w katalogu StreamQA.java
w katalogu app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import dev.langchain4j.model.StreamingResponseHandler;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.build();
model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
@Override
public void onNext(String text) {
System.out.println(text);
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});
}
}
Tym razem importujemy warianty klas strumieniowania VertexAiGeminiStreamingChatModel
, które implementują interfejs StreamingChatLanguageModel
. Potrzebujesz też StreamingResponseHandler
.
Tym razem podpis metody generate()
jest nieco inny. Zwracany typ nie zwraca ciągu znaków, ponieważ jest on nieważny. Oprócz promptu musisz przekazać moduł obsługi odpowiedzi na żądanie przesyłania strumieniowego. Tutaj implementujesz interfejs, tworząc anonimową klasę wewnętrzną z 2 metodami onNext(String text)
i onError(Throwable error)
. Pierwsza jest wywoływana za każdym razem, gdy dostępny jest nowy element odpowiedzi, a drugi jest wywoływany tylko w przypadku wystąpienia błędu.
Uruchomienie:
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
Otrzymasz odpowiedź podobną do 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 przypadku konfiguracji zdefiniowaliśmy tylko projekt, lokalizację i nazwę modelu, ale możesz określić dla modelu inne parametry:
temperature(Float temp)
– aby określić, jak kreatywna powinna być odpowiedź (0 oznacza mniej kreatywne, często bardziej rzeczowe, a 1 – dla większej liczby wyników kreacji).topP(Float topP)
– aby wybrać możliwe słowa, których całkowite prawdopodobieństwo sumuje się do liczby zmiennoprzecinkowej (od 0 do 1).topK(Integer topK)
– aby losowo wybrać słowo z maksymalnej liczby prawdopodobnych słów do ukończenia tekstu (od 1 do 40).maxOutputTokens(Integer max)
– aby określić maksymalną długość odpowiedzi udzielanej przez model (zazwyczaj 4 tokeny reprezentują około 3 słów).maxRetries(Integer retries)
– jeśli liczba żądań w czasie przekracza limit czasu lub na platformie występują problemy techniczne, model może 3 razy nawiązać połączenie.
Jak dotąd udało Ci się zadać Gemini jedno pytanie, ale możesz też prowadzić rozmowę wieloetapową. Te informacje omówimy w następnej sekcji.
5. Czatuj z Gemini
W poprzednim kroku zadaliśmy jedno pytanie. Nadszedł czas, aby przeprowadzić prawdziwą rozmowę między użytkownikiem a LLM. Każde pytanie i odpowiedź mogą opierać się na poprzednich i w ten sposób tworzyć prawdziwą dyskusję.
Rzuć okiem na 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-1.5-flash-001")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
W ramach tych zajęć dodaliśmy kilka nowych, interesujących importów:
MessageWindowChatMemory
– zajęcia, które pomogą Ci w obsłudze wieloetapowej rozmowy i zapamiętaniu poprzednich pytań i odpowiedzi.AiServices
– zajęcia, które łączą model czatu z pamięcią czatu
W metodzie głównej skonfigurujesz model, pamięć czatu i usługę AI. Model jest skonfigurowany w zwykły sposób z informacjami o projekcie, lokalizacji i nazwie modelu.
W przypadku pamięci czatu używamy kreatora MessageWindowChatMemory
do utworzenia wspomnienia, w którym wymieniamy 20 ostatnich wiadomości. To przesuwane okno nad rozmową, której kontekst jest przechowywany lokalnie w naszym kliencie klasowym w Javie.
Następnie utwórz obiekt AI service
, który wiąże model czatu ze wspomnieniem czatu.
Zwróć uwagę, że usługa AI wykorzystuje zdefiniowany przez nas niestandardowy interfejs ConversationService
i wdrożony przez LangChain4j interfejs, który pobiera zapytanie String
i zwraca odpowiedź String
.
Czas porozmawiać z Gemini. Najpierw wysyłane jest proste powitanie, a potem pierwsze pytanie o wieży Eiffla, aby dowiedzieć się, w którym kraju można ją znaleźć. Zwróć uwagę na to, że ostatnie zdanie jest związane z odpowiedzią na pierwsze pytanie, bo zastanawiasz się, ilu mieszkańców jest w kraju, w którym znajduje się wieża Eiffla, bez wyraźnego określenia kraju podanego w poprzedniej odpowiedzi. Pokazuje, że wcześniejsze pytania i odpowiedzi są wysyłane z każdym promptem.
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
Powinny wyświetlić 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 jednoetapowe lub prowadzić wieloetapowe rozmowy, ale na razie odpowiedź bazowała tylko na tekście. A co z obrazami? W następnym kroku przyjrzymy się bliżej obrazom.
6. Multimodalność w Gemini
Gemini to model multimodalny. Akceptuje on nie tylko tekst, ale także obrazy, a nawet filmy. W tej sekcji zobaczysz, jak można łączyć tekst i obrazy.
Myślisz, że Gemini rozpozna tego kota?
Zdjęcie kota na śniegu zrobione z Wikipediihttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg
Zobacz projekt 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-001")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
Podczas importowania można zauważyć rozróżnienie między różnymi rodzajami wiadomości i treści. Obiekt UserMessage
może zawierać zarówno obiekt TextContent
, jak i ImageContent
. W tym przypadku wielomodalność polega na łączeniu tekstu z obrazami. Model odsyła kolumnę Response
, która zawiera AiMessage
.
Następnie pobierasz AiMessage
z odpowiedzi za pomocą content()
, a następnie pobierasz tekst wiadomości dzięki funkcji text()
.
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
Nazwa obrazu z pewnością zdradziła Ci, co zawiera, ale wynik Gemini jest podobny do tej:
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 obrazów i promptów tekstowych otwiera ciekawe przypadki użycia. Możesz tworzyć aplikacje umożliwiające:
- Rozpoznawanie tekstu na obrazach.
- Sprawdź, czy obraz można bezpiecznie wyświetlić.
- Utwórz podpisy do obrazów.
- Przeszukuj bazę danych obrazów za pomocą deskryptorów zwykłego tekstu.
Oprócz wyodrębniania informacji z obrazów możesz też wyodrębniać informacje z tekstu nieuporządkowanego. Tego dowiesz się w następnej sekcji.
7. Wyodrębnianie uporządkowanych informacji z nieuporządkowanego tekstu
Jest wiele sytuacji, w których w dokumentach do raportu, e-mailach lub innych długich tekstach są podane nieuporządkowane informacje. Najlepiej, gdy chcesz wyodrębnić najważniejsze szczegóły zawarte w nieuporządkowanym tekście w postaci obiektów uporządkowanych. Zobaczmy, jak to zrobić.
Załóżmy, że chcesz wyodrębnić imię i nazwisko oraz wiek osoby na podstawie jej biografii lub opisu. Możesz polecić LLM wyodrębnianie kodu JSON z nieuporządkowanego tekstu za pomocą sprytnie dostosowanego promptu (jest to zwykle nazywane „inżynierią promptów”).
Zobacz kolekcję ExtractData.java
w języku: 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.UserMessage;
public class ExtractData {
static record Person(String name, int age) {}
interface PersonExtractor {
@UserMessage("""
Extract the name and age of the person described below.
Return a JSON document with a "name" and an "age" property, \
following this structure: {"name": "John Doe", "age": 34}
Return only JSON, without any markdown markup surrounding it.
Here is the document describing the person:
---
{{it}}
---
JSON:
""")
Person extractPerson(String text);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.temperature(0f)
.topK(1)
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
Person person = extractor.extractPerson("""
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
"""
);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
Przyjrzyjmy się różnym krokom w tym pliku:
- Rekord
Person
zawiera szczegółowe informacje o osobie ( imię i nazwisko oraz wiek). - Interfejs
PersonExtractor
jest zdefiniowany przy użyciu metody, która dla danego nieuporządkowanego ciągu tekstowego zwraca wystąpieniePerson
. - Element
extractPerson()
jest oznaczony adnotacją@UserMessage
, która wiąże z nim prompt. Model użyje tego promptu, aby wyodrębnić informacje i zwrócić je w formie dokumentu JSON, który zostanie przeanalizowany za Ciebie i rozpakowany do instancjiPerson
.
Przyjrzyjmy się teraz zawartości metody main()
:
- Tworzony jest model czatu. Zwróć uwagę, że używamy bardzo niskiej wartości
temperature
wynoszącej 0, atopK
wynoszącej tylko 1, aby zapewnić bardzo deterministyczną odpowiedź. Dzięki temu model będzie też lepiej przestrzegał instrukcji. W szczególności nie chcemy, aby usługa Gemini opakowała odpowiedź JSON dodatkowymi znacznikami Markdown. - Obiekt
PersonExtractor
jest tworzony dzięki klasieAiServices
LangChain4j. - Następnie możesz wywołać funkcję
Person person = extractor.extractPerson(...)
, aby wyodrębnić informacje o osobie z nieuporządkowanego tekstu i odzyskać wystąpieniePerson
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, jestem Anna i mają 23 lata.
Dzięki tej metodzie AiServices
możesz wykonywać operacje na obiektach o silnym typie. Nie wchodzisz w bezpośrednią interakcję z LLM. Zamiast tego pracujesz z konkretnymi klasami, takimi jak rekord Person
reprezentujący wyodrębnione dane osobowe, a masz obiekt PersonExtractor
z metodą extractPerson()
, która zwraca wystąpienie Person
. Pojęcie LLM zostało wycofane, a programista Java zajmuje się tylko zwykłymi klasami i obiektami.
8. Tworzenie struktury promptów za pomocą szablonów
Gdy wchodzisz w interakcję z LLM za pomocą wspólnego zestawu instrukcji lub pytań, część tego promptu nigdy się nie zmienia, podczas gdy inne części zawierają dane. Jeśli na przykład chcesz tworzyć przepisy, możesz użyć promptu „Jesteś utalentowanym szefem kuchni, utwórz przepis z tych składników: ...”, a następnie dopisz składniki na końcu tego tekstu. Właśnie do tego służą szablony promptów – podobnie jak interpolowane ciągi tekstowe w językach programowania. Szablon promptu zawiera zmienne, które możesz zastąpić odpowiednimi danymi dla określonego wywołania LLM.
Przyjrzyjmy się bliżej działaniu 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-1.5-flash-001")
.maxOutputTokens(500)
.temperature(0.8f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
Jak zwykle konfigurujesz model VertexAiGeminiChatModel
z wysokim poziomem kreatywności, wysoką temperaturą oraz wysokimi wartościami TopP i TopK. Następnie tworzysz pole PromptTemplate
za pomocą metody statycznej from()
, przekazując ciąg naszego promptu, i używasz zmiennych zastępczych podwójnych nawiasów klamrowych: {{dish}}
i {{ingredients}}
.
Ostatni prompt tworzysz, wywołując funkcję apply()
. Pobiera ona mapę par klucz-wartość reprezentujących nazwę obiektu zastępczego i wartość ciągu, którym zostanie on zastąpiony.
Na koniec wywołujesz metodę generate()
modelu Gemini, tworząc wiadomość dla użytkownika na podstawie tego promptu z instrukcją prompt.toUserMessage()
.
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt
Powinny się wyświetlić 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 dish
i ingredients
na mapie oraz dostosować temperaturę, topK
i tokP
, a następnie ponownie uruchomić kod. Pozwoli Ci to obserwować wpływ zmian tych parametrów na LLM.
Szablony promptów to dobry sposób na posiadanie instrukcji wielokrotnego użytku z możliwością konfigurowania parametrów w przypadku wywołań LLM. Możesz przekazywać dane i dostosowywać prompty pod kątem różnych wartości podanych przez użytkowników.
9. Klasyfikacja tekstu za pomocą promptów typu „few-shot”
LLM dość dobrze radzą sobie z klasyfikowaniem tekstu w różnych kategoriach. Możesz pomóc LLM w tym zadaniu, podając kilka przykładów tekstów i powiązanych z nimi kategorii. Ta metoda jest często nazywana promptem „few-shot”.
Przyjrzysz się zadaniu TextClassification.java
w katalogu app/src/main/java/gemini/workshop
, aby wykonać określony typ klasyfikacji tekstu – analizę nastawienia.
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.Map;
public class TextClassification {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.maxOutputTokens(10)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
Analyze the sentiment of the text below. Respond only with one word to describe the sentiment.
INPUT: This is fantastic news!
OUTPUT: POSITIVE
INPUT: Pi is roughly equal to 3.14
OUTPUT: NEUTRAL
INPUT: I really disliked the pizza. Who would use pineapples as a pizza topping?
OUTPUT: NEGATIVE
INPUT: {{text}}
OUTPUT:
""");
Prompt prompt = promptTemplate.apply(
Map.of("text", "I love strawberries!"));
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
Metoda main()
pozwala utworzyć model czatu z Gemini w zwykły sposób, ale z niewielką maksymalną liczbą tokenów wyjściowych, ponieważ oczekiwana jest tylko krótka odpowiedź: tekst to POSITIVE
, NEGATIVE
lub NEUTRAL
.
Następnie tworzysz szablon promptu wielokrotnego użytku z techniką promptów „few-shot”, instruując model o kilku przykładowych danych wejściowych i wyjściowych. Pomaga to też modelowi naśladować rzeczywiste dane wyjściowe. Gemini nie odpowiada wypowiadanym zdaniami, a jedynie musi odpowiedzieć jednym słowem.
Stosujesz zmienne za pomocą metody apply()
, aby zastąpić zmienną {{text}}
rzeczywistym parametrem ("I love strawberries"
) i przekształcić ten szablon w wiadomość dla użytkownika za pomocą tagu toUserMessage()
.
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
Powinno wyświetlić się jedno słowo:
POSITIVE
Wygląda na to, że ukochanie truskawek to pozytywne uczucie!
10. Retrieval-Augmented Generation
LLM są trenowane na dużej ilości tekstu. Wiedza ta obejmuje jednak tylko informacje, które zdołał zgromadzić podczas szkolenia. Jeśli po upływie ostatecznego terminu trenowania modelu zostaną udostępnione nowe informacje, nie będą one dostępne dla modelu. Dlatego model nie będzie w stanie odpowiedzieć na pytania dotyczące informacji, których nie widział.
Dlatego rozwiązania takie jak Retrieval Augmented Generation (RAG) pomagają dostarczać dodatkowe informacje, których LLM może potrzebować, aby spełniać prośby użytkowników, aby odpowiadać na bardziej aktualne lub prywatne informacje, które nie są dostępne w czasie trenowania.
Wróćmy do rozmów. Tym razem będzie można zadawać pytania na temat dokumentów. Tworzysz czatbota, który będzie w stanie pobierać istotne informacje z bazy danych zawierającej dokumenty podzielone na mniejsze części. Informacje te będą wykorzystywane przez model do ustalenia odpowiedzi, zamiast polegać wyłącznie na wiedzy zawartej w trenowaniu.
W RAG istnieją 2 fazy:
- Etap przetwarzania – dokumenty są ładowane do pamięci, dzielone na mniejsze fragmenty, a wektory dystrybucyjne wektorów (wysokiej, wielowymiarowej reprezentacji wektorowej fragmentów) są obliczane i przechowywane w wektorowej bazie danych zdolnej do wyszukiwania semantycznego. Ta faza pozyskiwania jest zwykle wykonywana raz, gdy do korpusu dokumentów trzeba dodać nowe dokumenty.
- Etap zapytania – użytkownicy mogą teraz zadawać pytania na temat dokumentów. Pytanie zostanie również przekształcone w wektor, a następnie porównywane ze wszystkimi innymi wektorami w bazie danych. Najbardziej podobne wektory są zwykle powiązane semantycznie i są zwracane do bazy danych wektorowych. Następnie LLM otrzymuje kontekst konwersacji, fragmenty tekstu odpowiadające wektorom zwracanym przez bazę danych i zostaje poproszony o ustalenie odpowiedzi na podstawie analizy tych fragmentów.
Przygotowywanie dokumentów
W ramach tej nowej wersji demonstracyjnej zadasz pytania „Potrzeba tylko uwagi” artykułu badawczego. Opisano w nim architekturę sieci neuronowej (transformatora) Google, w której obecnie wdrażane są wszystkie nowoczesne duże modele językowe.
Dokument został już pobrany do pliku attention-is-all-you-need.pdf w repozytorium.
Wdrażanie czatbota
Zobaczmy, jak utworzyć podejście dwufazowe: najpierw z przetwarzaniem dokumentów, a potem do czasu, w którym użytkownicy zadają pytania na temat dokumentu.
W tym przykładzie oba etapy są zaimplementowane w tej samej klasie. Zwykle składa się z jednej aplikacji do przetwarzania danych i drugiej oferującej interfejs czatbota.
W tym przykładzie wykorzystamy wektorową bazę danych działającą w pamięci. W rzeczywistym scenariuszu produkcyjnym fazy pozyskiwania danych i zapytań byłyby rozdzielane w 2 różnych aplikacjach, a wektory były pozostawiane w samodzielnej bazie danych.
Przetwarzanie dokumentów
Pierwszym krokiem w fazie przetwarzania dokumentów jest zlokalizowanie pobranego pliku PDF i przygotowanie PdfParser
do odczytu:
URL url = new URI("https://github.com/glaforge/gemini-workshop-for-java-developers/raw/main/attention-is-all-you-need.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());
Zamiast tworzyć zwykły model językowy czatu, możesz utworzyć instancję umieszczonego modelu. Jest to szczególny model, którego zadaniem jest tworzenie wektorowych reprezentacji fragmentów tekstu (słów, zdań, a nawet akapitów). Zwraca wektory liczb zmiennoprzecinkowych, zamiast zwracać 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("textembedding-gecko@003")
.maxRetries(3)
.build();
Następnie przygotujemy kilka zajęć, aby wspólnie z nimi współpracować:
- Wczytaj i podziel dokument PDF we fragmentach.
- Utwórz wektory dystrybucyjne wektorów 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);
Zostanie utworzona instancja InMemoryEmbeddingStore
– wektorowej bazy danych w pamięci, w której będą przechowywane wektory dystrybucyjne wektorów.
Dokument jest podzielony na fragmenty za pomocą klasy DocumentSplitters
. Tekst w pliku PDF zostanie podzielony na fragmenty składające się z 500 znaków i nakładające się na 100 znaków (z podanym niżej fragmentem, aby uniknąć wycinania słów lub zdań w częściach).
Moduł pozyskujący magazyn łączy funkcję podziału dokumentów, model wektorowy do obliczania wektorów i model wektorowy w pamięci. Następnie metodą ingest()
zajmie się przetwarzaniem.
Pierwszy etap dobiegł końca – dokument został przekształcony we fragmenty tekstu z powiązanymi z nimi wektorami dystrybucyjnymi i zapisany w bazie danych wektorowych.
Zadawanie pytań
Czas przygotować się 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-001")
.maxOutputTokens(1000)
.build();
Aby połączyć wektorową bazę danych (w zmiennej embeddingStore
) z modelem wektora dystrybucyjnego, potrzebujesz też klasy pobierania. Jego zadaniem jest wykonywanie zapytań do bazy danych wektorowych przez obliczenia wektorów dystrybucyjnych dla zapytania użytkownika w celu znalezienia w bazie podobnych wektorów:
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
Poza główną metodą utwórz interfejs reprezentujący asystenta eksperta LLM – interfejs, który klasa AiServices
wdroży, aby umożliwić Ci interakcję z modelem:
interface LlmExpert {
String ask(String question);
}
W tym momencie możesz skonfigurować nową usługę AI:
LlmExpert expert = AiServices.builder(LlmExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
Ta usługa łączy ze sobą:
- Skonfigurowany wcześniej model języka czatu.
- Wspomnienie na czacie ułatwiające śledzenie rozmowy.
- Moduł pobierania porównuje zapytanie wektora dystrybucyjnego wektorów z wektorami w bazie danych.
- Szablon promptu wyraźnie określa, że model czatu powinien udzielić odpowiedzi, opierając się na podanych informacjach (czyli na odpowiednich fragmentach dokumentacji, których wektory dystrybucyjne są podobne do wektora pytania użytkownika).
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in large language models,\s
you excel at explaining simply and clearly questions about LLMs.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
Możesz już zadawać pytania
List.of(
"What neural network architecture can be used for language models?",
"What are the different components of a transformer neural network?",
"What is attention in large language models?",
"What is the name of the process that transforms text into vectors?"
).forEach(query ->
System.out.printf("%n=== %s === %n%n %s %n%n", query, expert.ask(query)));
);
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 wyświetlić się odpowiedzi na Twoje pytania:
=== What neural network architecture can be used for language models? === Transformer architecture === What are the different components of a transformer neural network? === The different components of a transformer neural network are: 1. Encoder: The encoder takes the input sequence and converts it into a sequence of hidden states. Each hidden state represents the context of the corresponding input token. 2. Decoder: The decoder takes the hidden states from the encoder and uses them to generate the output sequence. Each output token is generated by attending to the hidden states and then using a feed-forward network to predict the token's probability distribution. 3. Attention mechanism: The attention mechanism allows the decoder to attend to the hidden states from the encoder when generating each output token. This allows the decoder to take into account the context of the input sequence when generating the output sequence. 4. Positional encoding: Positional encoding is a technique used to inject positional information into the input sequence. This is important because the transformer neural network does not have any inherent sense of the order of the tokens in the input sequence. 5. Feed-forward network: The feed-forward network is a type of neural network that is used to predict the probability distribution of each output token. The feed-forward network takes the hidden state from the decoder as input and outputs a vector of probabilities. === What is attention in large language models? === Attention in large language models is a mechanism that allows the model to focus on specific parts of the input sequence when generating the output sequence. This is important because it allows the model to take into account the context of the input sequence when generating each output token. Attention is implemented using a function that takes two sequences as input: a query sequence and a key-value sequence. The query sequence is typically the hidden state from the previous decoder layer, and the key-value sequence is typically the sequence of hidden states from the encoder. The attention function computes a weighted sum of the values in the key-value sequence, where the weights are determined by the similarity between the query and the keys. The output of the attention function is a vector of context vectors, which are then used as input to the feed-forward network in the decoder. The feed-forward network then predicts the probability distribution of the next output token. Attention is a powerful mechanism that allows large language models to generate text that is both coherent and informative. It is one of the key factors that has contributed to the recent success of large language models in a wide range of natural language processing tasks. === What is the name of the process that transforms text into vectors? === The process of transforming text into vectors is called **word embedding**. Word embedding is a technique used in natural language processing (NLP) to represent words as vectors of real numbers. Each word is assigned a unique vector, which captures its meaning and semantic relationships with other words. Word embeddings are used in a variety of NLP tasks, such as machine translation, text classification, and question answering. There are a number of different word embedding techniques, but one of the most common is the **skip-gram** model. The skip-gram model is a neural network that is trained to predict the surrounding words of a given word. By learning to predict the surrounding words, the skip-gram model learns to capture the meaning and semantic relationships of words. Once a word embedding model has been trained, it can be used to transform text into vectors. To do this, each word in the text is converted to its corresponding vector. The vectors for all of the words in the text are then concatenated to form a single vector, which represents the entire text. Text vectors can be used in a variety of NLP tasks. For example, text vectors can be used to train machine translation models, text classification models, and question answering models. Text vectors can also be used to perform tasks such as text summarization and text clustering.
11. Wywoływanie funkcji
Istnieją również sytuacje, w których LLM może mieć dostęp do systemów zewnętrznych, na przykład zdalny internetowy interfejs API pobierający informacje lub wykonujący działanie lub usługi wykonujące jakieś obliczenia. Na przykład:
Zdalne interfejsy API:
- Śledź i aktualizuj zamówienia klientów.
- Znajdź lub utwórz zgłoszenie w narzędziu Issue Tracker.
- Pobieraj dane w czasie rzeczywistym, takie jak notowania giełdowe czy pomiary z czujników IoT.
- Wyślij e-maila.
Narzędzia do obliczeń:
- Kalkulator do bardziej zaawansowanych zadań matematycznych.
- Interpretacja kodu na potrzeby uruchamiania kodu, gdy duże modele językowe wymagają logiki rozumowania.
- Przekonwertuj żądania w języku naturalnym na zapytania SQL, aby model LLM mógł wysyłać zapytania do bazy danych.
Wywołanie funkcji to zdolność modelu do zażądania co najmniej 1 wywołania funkcji w jego imieniu, co pozwala mu prawidłowo odpowiedzieć na prompt użytkownika przy użyciu bardziej aktualnych danych.
Biorąc pod uwagę konkretny prompt od użytkownika i znajomość istniejących funkcji, które mogą być istotne w tym kontekście, LLM może odpowiedzieć żądaniem wywołania funkcji. Aplikacja integrująca LLM może następnie wywołać tę funkcję, a następnie zwrócić LLM z odpowiedzią, która następnie zinterpretuje, wysyłając odpowiedź tekstową.
4 kroki wywoływania funkcji
Przyjrzyjmy się przykładowi wywoływania funkcji: pobierania informacji o prognozie pogody.
Gdy zapytamy Gemini lub inne duże modele językowe (LLM) o pogodę w Paryżu, odpowiedź odpowie, że nie ma informacji na temat prognozy pogody. Jeśli chcesz, aby LLM miał dostęp w czasie rzeczywistym do danych pogodowych, musisz zdefiniować funkcje, z których będzie mógł korzystać.
Spójrz na ten schemat:
1️⃣ Najpierw użytkownik pyta o pogodę w Paryżu. Aplikacja czatbot wie, że jest do dyspozycji co najmniej 1 funkcja, która pomoże LLM zrealizować zapytanie. Czatbot wysyła początkowy prompt oraz listę funkcji, które można wywołać. W tym przypadku funkcja o nazwie getWeather()
, która dla lokalizacji przyjmuje parametr w postaci ciągu znaków.
Ponieważ LLM nie wie o prognozach pogody, zamiast odpowiadać SMS-em, odsyła żądanie wykonania funkcji. Czatbot musi wywołać funkcję getWeather()
z parametrem lokalizacji "Paris"
.
2️⃣Czatbot wywołuje tę funkcję w imieniu LLM i pobiera odpowiedź. Zakładamy tutaj, że odpowiedź to {"forecast": "sunny"}
.
3️⃣ Aplikacja czatbota wysyła odpowiedź JSON z powrotem do LLM.
4️⃣ LLM analizuje odpowiedź JSON, interpretuje te informacje, a następnie odpowiada tekstowi z informacją, że pogoda w Paryżu jest słoneczna.
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-001")
.maxOutputTokens(100)
.build();
Należy podać specyfikację narzędzia zawierającą opis funkcji, którą można wywołać:
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeatherForecast")
.description("Get the weather forecast for a location")
.addParameter("location", JsonSchemaProperty.STRING,
JsonSchemaProperty.description("the location to get the weather forecast for"))
.build();
Nazwa funkcji jest zdefiniowana, a także nazwa i typ parametru. Pamiętaj jednak, że zarówno funkcja, jak i parametry mają opis. Opisy są bardzo ważne i pomagają LLM naprawdę zrozumieć, co może zrobić funkcja, a tym samym ocenić, czy tę funkcję trzeba wywołać w kontekście rozmowy.
Zacznijmy od kroku 1, wysyłając wstępne pytanie 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żywać model, a model odpowiada, wysyłając zbyt żądanie wykonania:
// 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ę LLM chciałbyś wywołać. W kodzie nie odwołujemy się do zewnętrznego interfejsu API, a jedynie bezpośrednio 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 LLM poznaje wynik wykonania funkcji, a potem 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 to:
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.
Odpowiedź zobaczysz w danych wyjściowych nad żądaniem wykonania narzędzia.
Pełny kod źródłowy znajduje się w katalogu FunctionCalling.java
w katalogu app/src/main/java/gemini/workshop
:
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling
Zostaną wyświetlone 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 zaobserwowaliśmy, jak zwykłe interakcje w tekście z pytaniami i odpowiedziami oraz żądaniami i odpowiedziami na funkcję są przeplatane. W międzyczasie wskazano odpowiedź żądanej funkcji bezpośrednio, bez wywoływania rzeczywistej funkcji.
LangChain4j oferuje też jednak wyższe poziomy abstrakcji, które pozwalają w przejrzysty sposób obsługiwać wywołania funkcji, a jednocześnie obsługiwać rozmowę w zwykły sposób.
Wywołanie pojedynczej funkcji
Przyjrzyjmy się krok po kroku FunctionCallingAssistant.java
.
Najpierw utwórz rekord, który będzie reprezentował strukturę danych odpowiedzi przez funkcję:
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 1 funkcję, ale jest ona oznaczona adnotacją @Tool
, która odpowiada opisowi funkcji, do której model może zażądać wywołania.
Parametry funkcji (pojedyncza jedna tutaj) również są opatrzone adnotacjami, ale z krótką adnotacją @P
, która też zawiera opis parametru. Możesz dodać dowolną liczbę funkcji, aby udostępnić je modelowi w bardziej złożonych scenariuszach.
W ramach tych zajęć zwracasz gotowe odpowiedzi, ale jeśli chcesz wywołać prawdziwą zewnętrzną usługę prognozowania pogody, musisz użyć tej metody w treści tej metody.
Jak widzieliśmy podczas tworzenia obiektu ToolSpecification
w poprzednim podejściu, ważne jest udokumentowanie działania funkcji i opisanie, do czego odpowiadają parametry. Pomaga to modelowi zrozumieć, jak i kiedy można używać tej funkcji.
Następnie LangChain4j umożliwia udostępnienie interfejsu zgodnego z umową, której chcesz używać do interakcji z modelem. Tutaj prosty interfejs, który pobiera ciąg znaków reprezentujący komunikat użytkownika i zwraca ciąg znaków odpowiadający odpowiedzi modelu:
interface WeatherAssistant {
String chat(String userMessage);
}
Można też używać bardziej złożonych podpisów, które obejmują UserMessage
(dla wiadomości użytkownika) lub AiMessage
(dla odpowiedzi modelu) lub TokenStream
, jeśli chcesz obsłużyć bardziej zaawansowane sytuacje, bo takie bardziej skomplikowane obiekty zawierają też dodatkowe informacje, na przykład liczbę wykorzystanych tokenów itp. Dla uproszczenia będziemy jednak podawać ciąg znaków w danych wejściowych i ciąg znaków w danych wyjściowych.
Zakończmy 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-flash-001")
.maxOutputTokens(100)
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
}
Jak zwykle konfigurujesz model czatu z Gemini. Następnie tworzysz instancję usługi prognozowania pogody, która zawiera „funkcję”. do którego model poprosi nas o wywołanie.
Ponownie użyj klasy AiServices
, aby powiązać model czatu, pamięć czatu i narzędzie (np. usługę prognozowania pogody z jej funkcją). Funkcja AiServices
zwraca obiekt, który implementuje zdefiniowany przez Ciebie interfejs WeatherAssistant
. Zostało tylko wywołanie metody chat()
tego asystenta. Podczas wywoływania zobaczysz tylko odpowiedzi tekstowe, ale żądania wywołania funkcji i odpowiedzi na nie nie będą widoczne dla programisty, a żądania te będą obsługiwane automatycznie i w przejrzysty sposób. Jeśli Gemini uzna, że funkcja powinna zostać wywołana, odpowie z żądaniem wywołania funkcji, a LangChain4j zajmie się wywoływaniem funkcji lokalnej w Twoim imieniu.
Uruchom przykład:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
Zostaną wyświetlone 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.
Wywołania wielofunkcyjne
Możesz także mieć wiele funkcji i pozwolić LangChain4j na obsługę wielu wywołań funkcji w Twoim imieniu. Przykład z wieloma funkcjami znajdziesz w tabeli MultiFunctionCallingAssistant.java
.
Służy do 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 pozwalająca uzyskać wartość 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 pozwalająca zastosować procent do określonej 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 oraz klasę Multi Tools i zadać pytania, np. „Ile wynosi 10% ceny akcji AAPL przeliczonej 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-1.5-flash-001")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
Uruchom go w ten sposób:
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
Powinno być widoczne kilka funkcji o nazwie:
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 rozszerzający duże modele językowe, takie jak Gemini. Pozwala nam to tworzyć bardziej złożone systemy, często nazywane „agentami”. czy „asystenci AI”. Te agenty mogą 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.).
Tworząc tak potężnych agentów, należy robić to odpowiedzialnie. Przed podjęciem automatycznych działań warto rozważyć proces z udziałem człowieka. Należy pamiętać o bezpieczeństwie podczas projektowania agentów opartych na LLM, które wchodzą w interakcje ze światem zewnętrznym.
13. Uruchamianie Gemmy z Ollamą i TestContainers
Do tej pory używamy Gemini, ale istnieje też Gemma, jej model siostry.
Gemma to rodzina lekkich, nowoczesnych modeli otwartych opartych na tych samych badaniach i technologii, które posłużyły do utworzenia modeli Gemini. Gra Gemma jest dostępna w 2 wersjach Gemma1 i Gemma2, a każda w różnych rozmiarach. Gra Gemma1 jest dostępna w 2 rozmiarach: 2B i 7B. Gra Gemma2 jest dostępna w 2 rozmiarach: 9B i 27B. Ich waga jest dostępna bezpłatnie, a mały rozmiar oznacza, że można uruchomić je samodzielnie, nawet na laptopie lub w Cloud Shell.
Jak uruchomić Gemmę?
Jest wiele sposobów uruchamiania Gemma: w chmurze, przez Vertex AI jednym kliknięciem, lub GKE z niektórymi GPU, ale można ją też uruchomić lokalnie.
Dobrym sposobem na lokalne uruchomienie Gemmy jest użycie narzędzia Ollama, które pozwala uruchamiać na komputerze małe modele, takie jak Llama 2 czy Mistral. Jest podobny do Dockera, ale w przypadku LLM.
Zainstaluj aplikację Ollama zgodnie z instrukcjami dotyczącymi Twojego systemu operacyjnego.
Jeśli używasz środowiska Linux, po zainstalowaniu musisz najpierw włączyć Ollama.
ollama serve > /dev/null 2>&1 &
Po zainstalowaniu modelu lokalnie możesz uruchomić polecenia w celu pobrania modelu:
ollama pull gemma:2b
Poczekaj na pobranie modelu. 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ąć prompt, naciśnij Ctrl + D
Uruchomienie Gemmy w Ollamie w plikach TestContainers
Zamiast instalować i uruchamiać Ollamę lokalnie, możesz jej używać w kontenerze obsługiwanym przez kontener TestContainers.
Kontener TestContainers przydaje się nie tylko do testowania, ale także do wykonywania kontenerów. Jest nawet OllamaContainer
, które może Ci pomóc.
Oto pełen obraz:
Implementacja
Przyjrzyjmy się krok po kroku GemmaWithOllamaContainer.java
.
Najpierw musisz utworzyć uzyskany kontener Ollama, który będzie pobierać model Gemma. Ten obraz istnieje już z poprzedniego uruchomienia lub zostanie utworzony. Jeśli obraz już istnieje, powiesz TestContainer, że chcesz zastąpić domyślny obraz Ollama wariantem wykorzystującym 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 potem utwórz model czatu Ollama, wskazując adres i port kontenera z modelem, którego chcesz używać. Na koniec tak jak zwykle wywołujesz model.generate(yourPrompt)
:
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);
}
Uruchom go w ten sposób:
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
Pierwsze uruchomienie może trochę potrwać, ale gdy to się stanie, Gemma odpowiada:
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.
Masz Gemma uruchomioną w Cloud Shell.
14. Gratulacje
Gratulujemy. Udało Ci się utworzyć pierwszą aplikację czatu z generatywną AI w języku Java, wykorzystując LangChain4j i interfejs Gemini API. Po drodze zauważyłeś, że multimodalne duże modele językowe są bardzo wydajne i radzą sobie z różnymi zadaniami, takimi jak pytania i udzielanie odpowiedzi, nawet w przypadku własnej dokumentacji, wyodrębniania danych czy interakcji z zewnętrznymi interfejsami API.
Co dalej?
Teraz Twoja kolej, aby ulepszyć swoje aplikacje za pomocą zaawansowanych integracji LLM.
Więcej informacji
- Typowe przypadki użycia generatywnej AI
- Zasoby szkoleniowe dotyczące generatywnej AI
- Interakcja z Gemini w Generative AI Studio
- Odpowiedzialna AI