Oparty na generatywnej AI czat z użytkownikami i dokumentami w języku Java z PaLM i LangChain4J

1. Wprowadzenie

Ostatnia aktualizacja: 5.02.2024

Co to jest generatywna AI

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

Generatywna AI korzysta z modeli podstawowych (dużych modeli AI), które mogą wykonywać wiele zadań jednocześnie i wykonywać gotowe zadania, w tym podsumowywanie, pytania i odpowiedzi, klasyfikację i nie tylko. Ponadto modele podstawowe można dostosowywać do docelowych przypadków użycia z bardzo małą ilością przykładowych danych, a ich trenowanie jest ograniczone.

Jak działa generatywna AI?

Generatywna AI wykorzystuje model systemów uczących się do poznawania wzorców i relacji w zbiorze danych z treściami utworzonymi przez człowieka. Wykorzystuje nauczone wzorce do generowania nowych treści.

Najczęstszym sposobem trenowania modelu generatywnej AI jest użycie uczenia nadzorowanego – model otrzymuje zbiór treści stworzonych przez człowieka i odpowiadających im etykiet. Następnie uczy się generować treści podobne do tych stworzonych przez człowieka i oznaczone tymi samymi etykietami.

Jakie są typowe zastosowania generatywnej AI?

Generatywna AI przetwarza ogromne treści, tworząc statystyki i odpowiedzi za pomocą tekstu, obrazów i przyjaznych dla użytkowników formatów. Generatywnej AI można używać do:

  • Popraw komunikację z klientami dzięki ulepszonemu czatowi i wyszukiwaniu
  • Eksploruj ogromne ilości nieuporządkowanych danych za pomocą interfejsów konwersacyjnych i podsumowań
  • Pomagaj w powtarzalnych zadaniach, takich jak odpowiadanie na zapytania ofertowe, tłumaczenie treści marketingowych w 5 językach czy sprawdzanie umów z klientami pod kątem zgodności z zasadami itp.

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 – nie musisz nawet znać się na ML. Uzyskaj dostęp do modeli podstawowych w bazie modeli, dostrajaj modele za pomocą prostego interfejsu w Generative AI Studio lub używaj modeli w notatniku do badania danych.

Usługa Vertex AI Search and Conversation zapewnia deweloperom najszybszy sposób na tworzenie wyszukiwarek i czatbotów opartych na generatywnej AI.

Duet AI to oparta na AI usługa wspomagająca, która jest dostępna w Google Cloud i IDE, dzięki czemu wykonasz więcej zadań szybciej.

Na czym się skupiają te ćwiczenia z programowania?

To ćwiczenia w programie skupiają się na dużym modelu językowym PaLM 2 (LLM) hostowanego w Vertex AI Google Cloud. Obejmuje on wszystkie produkty i usługi systemów uczących się.

Do interakcji z interfejsem PaLM API będziesz używać Javy w połączeniu z administratorem platformy LLM LangChain4J. Poznasz różne konkretne przykłady, które pozwolą Ci wykorzystać LLM do odpowiadania na pytania, generowania pomysłów, wyodrębniania encji i ustrukturyzowanych treści oraz podsumowywania.

Opowiedz mi więcej o platformie LangChain4J.

Platforma LangChain4J to biblioteka typu open source do integrowania dużych modeli językowych (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), programy ładujące i dzielniki dokumentów (do analizowania dokumentów i uczenia się na nich), parsery wyjściowe i nie tylko.

c6d7f7c3fd0d2951.png

Czego się nauczysz

  • Jak skonfigurować projekt w języku Java pod kątem PaLM i LangChain4J
  • Jak wyodrębniać przydatne informacje z nieuporządkowanych treści (wyodrębnianie encji lub słów kluczowych, dane wyjściowe w formacie JSON)
  • Jak rozpocząć rozmowę z użytkownikami
  • Jak używać modelu czatu do zadawania pytań na własnej dokumentacji

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

  1. 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ć.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • 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.
  1. 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

  1. W konsoli Cloud kliknij Aktywuj Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

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.

d95252b003979716.png

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

7833d5e1c5d18f54.png

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.

  1. 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`
  1. 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 będziesz tworzyć programy w języku Java, używając terminala Cloud Shell i edytora kodu.

Włączanie interfejsów Vertex AI API

  1. 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.
  2. Jeśli nie jesteś w sekcji Vertex AI konsoli Google Cloud, wykonaj te czynności:
  3. W wyszukiwarce wpisz Vertex AI, a następnie zwróć
  4. W wynikach wyszukiwania kliknij Vertex AI. Pojawi się panel Vertex AI.
  5. W panelu Vertex AI kliknij Włącz wszystkie zalecane interfejsy API.

Spowoduje to włączenie kilku interfejsów API, ale najważniejszym z nich w ćwiczeniach z programowania jest interfejs aiplatform.googleapis.com, który możesz też włączyć z poziomu wiersza poleceń w terminalu Cloud Shell, uruchamiając to polecenie:

$ gcloud services enable aiplatform.googleapis.com

Tworzenie struktury projektu za pomocą Gradle

Aby utworzyć przykładowy kod w Javie, będziesz używać narzędzia do kompilacji Gradle i wersji 17 Javy. Aby skonfigurować projekt przy użyciu Gradle, w terminalu Cloud Shell utwórz katalog (tutaj, palm-workshop) i uruchom w tym katalogu polecenie gradle init:

$ mkdir palm-workshop
$ cd palm-workshop

$ gradle init

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] 

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 4

Project name (default: palm-workshop): 
Source package (default: palm.workshop): 

> Task :init
Get more help with your project: https://docs.gradle.org/7.4/samples/sample_building_java_applications.html

BUILD SUCCESSFUL in 51s
2 actionable tasks: 2 executed

Aplikację (opcja 2) utworzysz w języku Java (opcja 3), bez używania podprojektów (opcja 1), przy użyciu składni Groovy pliku kompilacji (opcja 1), nie używaj nowych funkcji kompilacji (opcja 1), generując testy za pomocą JUnit Jupaler (opcja 4), a w przypadku nazwy projektu możesz używać pakietu SDK i podobnej nazwy projektu dla pakietu „Palm-work”.

Struktura projektu będzie wyglądać tak:

├── gradle 
│   └── ...
├── gradlew 
├── gradlew.bat 
├── settings.gradle 
└── app
    ├── build.gradle 
    └── src
        ├── main
        │   └── java 
        │       └── palm
        │           └── workshop
        │               └── App.java
        └── test
            └── ...

Zaktualizujmy plik app/build.gradle, aby dodać niezbędne zależności. Możesz usunąć zależność guava, jeśli jest obecna, i zastąpić ją zależnościami projektu LangChain4J oraz biblioteką logowania, aby uniknąć denerwowania brakujących komunikatów rejestratora:

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'

    // Logging library
    implementation 'org.slf4j:slf4j-jdk14:2.0.9'

    // This dependency is used by the application.
    implementation 'dev.langchain4j:langchain4j-vertex-ai:0.24.0'
    implementation 'dev.langchain4j:langchain4j:0.24.0'
}

W przypadku LangChain4J istnieją 2 zależności:

  • w ramach głównego projektu,
  • a drugi dla dedykowanego modułu Vertex AI.

Aby do kompilowania i uruchamiania naszych programów używać języka Java 17, dodaj następujący blok pod blokiem plugins {}:

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Jeszcze jedna zmiana: zaktualizuj blok application obiektu app/build.gradle, aby umożliwić użytkownikom zastępowanie klasy głównej uruchamianej z poziomu wiersza poleceń podczas wywoływania narzędzia do kompilacji:

application {
    mainClass = providers.systemProperty('javaMainClass')
                         .orElse('palm.workshop.App')
}

Aby sprawdzić, czy plik kompilacji jest gotowy do uruchomienia aplikacji, możesz uruchomić domyślną klasę główną, która wyświetli prosty komunikat Hello World!:

$ ./gradlew run -DjavaMainClass=palm.workshop.App

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed

Teraz możesz zacząć programować duży model tekstowy PaLM przy użyciu projektu LangChain4J.

Poniżej znajdziesz przykładowy obraz pełnego pliku kompilacji app/build.gradle:

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}

java {
    toolchain {
        // Ensure we compile and run on Java 17
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'

    // This dependency is used by the application.
    implementation 'dev.langchain4j:langchain4j-vertex-ai:0.24.0'
    implementation 'dev.langchain4j:langchain4j:0.24.0'
    implementation 'org.slf4j:slf4j-jdk14:2.0.9'
}

application {
    mainClass = providers.systemProperty('javaMainClass').orElse('palm.workshop.App')
}

tasks.named('test') {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}

4. Nawiąż pierwsze połączenie z modelem czatu PaLM

Po prawidłowym skonfigurowaniu projektu możesz wywołać interfejs PaLM API.

Utwórz w katalogu app/src/main/java/palm/workshop nową klasę o nazwie ChatPrompts.java (oprócz domyślnej klasy App.java) i wpisz tę treść:

package palm.workshop;

import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.chain.ConversationalChain;

public class ChatPrompts {
    public static void main(String[] args) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(400)
            .maxRetries(3)
            .build();

        ConversationalChain chain = ConversationalChain.builder()
            .chatLanguageModel(model)
            .build();

        String message = "What are large language models?";
        String answer = chain.execute(message);
        System.out.println(answer);

        System.out.println("---------------------------");

        message = "What can you do with them?";
        answer = chain.execute(message);
        System.out.println(answer);

        System.out.println("---------------------------");

        message = "Can you name some of them?";
        answer = chain.execute(message);
        System.out.println(answer);
    }
}

W pierwszym przykładzie należy zaimportować klasę VertexAiChatModel i LangChain4J ConversationalChain, aby ułatwić sobie obsługę wieloetapowego aspektu rozmów.

Następnie w metodzie main skonfigurujesz model języka czatu, używając kreatora dla interfejsu VertexAiChatModel, aby określić:

  • i punktu końcowego.
  • nad projektem,
  • region
  • wydawca,
  • i nazwa modelu (chat-bison@001).

Gdy model językowy jest już gotowy, możesz przygotować ConversationalChain. Jest to abstrakcja na wyższym poziomie oferowana przez LangChain4J w celu skonfigurowania razem różnych komponentów do obsługi rozmowy, takich jak sam model języka czatu, ale potencjalnie także inne komponenty do obsługi historii rozmowy na czacie lub do podłączania innych narzędzi, takich jak retrievery, w celu pobierania informacji z wektorowych baz danych. Wrócimy do tego w późniejszej części tego ćwiczenia z programowania.

Następnie przeprowadzisz wieloetapową rozmowę z modelem czatu, aby zadać kilka powiązanych ze sobą pytań. Najpierw zastanawiasz się nad modelami LLM, a potem pytasz, co można z nimi zrobić i jakie są ich przykłady. Zwróć uwagę, że nie musisz się powtarzać. LLM wie, że oznacza LLM w kontekście tej rozmowy.

Aby wykonać tę wieloetapową rozmowę, wystarczy wywołać metodę execute() w łańcuchu, która zostanie dodana do kontekstu rozmowy, a model czatu wygeneruje odpowiedź i doda ją do historii czatu.

Aby uruchomić tę klasę, uruchom to polecenie w terminalu Cloud Shell:

./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

Zostaną wyświetlone dane wyjściowe podobne do tych:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts
Starting a Gradle Daemon, 2 incompatible and 2 stopped Daemons could not be reused, use --status for details

> Task :app:run
Large language models (LLMs) are artificial neural networks that are trained on massive datasets of text and code. They are designed to understand and generate human language, and they can be used for a variety of tasks, such as machine translation, question answering, and text summarization.
---------------------------
LLMs can be used for a variety of tasks, such as:

* Machine translation: LLMs can be used to translate text from one language to another.
* Question answering: LLMs can be used to answer questions posed in natural language.
* Text summarization: LLMs can be used to summarize text into a shorter, more concise form.
* Code generation: LLMs can be used to generate code, such as Python or Java code.
* Creative writing: LLMs can be used to generate creative text, such as poems, stories, and scripts.

LLMs are still under development, but they have the potential to revolutionize a wide range of industries. For example, LLMs could be used to improve customer service, create more personalized marketing campaigns, and develop new products and services.
---------------------------
Some of the most well-known LLMs include:

* GPT-3: Developed by OpenAI, GPT-3 is a large language model that can generate text, translate languages, write different kinds of creative content, and answer your questions in an informative way.
* LaMDA: Developed by Google, LaMDA is a large language model that can chat with you in an open-ended way, answering your questions, telling stories, and providing different kinds of creative content.
* PaLM 2: Developed by Google, PaLM 2 is a large language model that can perform a wide range of tasks, including machine translation, question answering, and text summarization.
* T5: Developed by Google, T5 is a large language model that can be used for a variety of tasks, including text summarization, question answering, and code generation.

These are just a few examples of the many LLMs that are currently being developed. As LLMs continue to improve, they are likely to play an increasingly important role in our lives.

BUILD SUCCESSFUL in 25s
2 actionable tasks: 2 executed

Usługa PaLM odpowiedział na 3 powiązane pytania.

Kreator VertexAIChatModel pozwala zdefiniować parametry opcjonalne, które mają już wartości domyślne, które możesz zastąpić. Oto przykłady:

  • .temperature(0.2) – 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).
  • .maxOutputTokens(50) – w tym przykładzie zażądano 400 tokenów (3 tokeny to mniej więcej 4 słowa), w zależności od tego, jak długo ma trwać wygenerowana odpowiedź.
  • .topK(20) – aby losowo wybrać słowo z maksymalnej liczby możliwych słów do uzupełnienia (od 1 do 40).
  • .topP(0.95) – aby wybrać możliwe słowa, których całkowite prawdopodobieństwo sumuje się do liczby zmiennoprzecinkowej (od 0 do 1).
  • .maxRetries(3) – jeśli liczba żądań w czasie przekracza limit, model może ponowić próbę, np. 3 razy.

5. Przydatny czatbot z charakterem.

W poprzedniej sekcji od razu zaczęto zadawać czatbotowi LLM bez podawania żadnego konkretnego kontekstu. Możesz jednak wyspecjalizować takiego czatbota, aby stał się ekspertem w konkretnej dziedzinie lub w konkretnej dziedzinie.

Jak to zrobić? Wstęp: objaśnij LLM, jakie jest zadanie, jaki jest kontekst, podaj kilka przykładów jego działania, osobowość, format i ton, w jakim chcesz otrzymywać odpowiedzi.

Ten artykuł o tworzeniu promptów dobrze ilustruje takie podejście na tej grafice:

8a4c67679dcbd085.png

https://medium.com/@eldatero/master-the-perfect-chatgpt-prompt-formula-c776adae8f19

Aby zilustrować ten punkt, zajrzyjmy na strony prompts.chat, gdzie znajdziesz mnóstwo ciekawych i ciekawych pomysłów na niestandardowe czatboty, które pozwolą Ci pełnić funkcję:

Istnieje przykład przekształcenia czatbota LLM w szachy. Zaimplementujmy ją.

Zaktualizuj klasę ChatPrompts w ten sposób:

package palm.workshop;

import dev.langchain4j.chain.ConversationalChain;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;

public class ChatPrompts {
    public static void main(String[] args) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(7)
            .maxRetries(3)
            .build();

        InMemoryChatMemoryStore chatMemoryStore = new InMemoryChatMemoryStore();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
            .chatMemoryStore(chatMemoryStore)
            .maxMessages(200)
            .build();

        chatMemory.add(SystemMessage.from("""
            You're an expert chess player with a high ELO ranking.
            Use the PGN chess notation to reply with the best next possible move.
            """
        ));


        ConversationalChain chain = ConversationalChain.builder()
            .chatLanguageModel(model)
            .chatMemory(chatMemory)
            .build();

        String pgn = "";
        String[] whiteMoves = { "Nf3", "c4", "Nc3", "e3", "Dc2", "Cd5"};
        for (int i = 0; i < whiteMoves.length; i++) {
            pgn += " " + (i+1) + ". " + whiteMoves[i];
            System.out.println("Playing " + whiteMoves[i]);
            pgn = chain.execute(pgn);
            System.out.println(pgn);
        }
    }
}

Przeanalizujmy je krok po kroku:

  • Do obsługi pamięci czatu są potrzebne nowe importy.
  • Tworzysz model czatu, ale masz niewielką liczbę tokenów (tutaj 7), ponieważ chcemy wygenerować następny ruch, a nie całą pracę na temat szachów.
  • Następnie utworzysz magazyn pamięci czatu, w którym będą zapisywane rozmowy na czacie.
  • Tworzysz w oknie wspomnienie czatu, aby zachować ostatnie ruchy.
  • W pamięci czatu dodajesz „system” komunikat informujący modelowi czatu o tym, kim ma być (np. mistrz szachowy). System wiadomość dodaje kontekst, a „użytkownik” i „AI” wiadomości stanowią prawdziwą dyskusję.
  • Tworzysz łańcuch konwersacyjny, który łączy pamięć i model czatu.
  • Dalej mamy listę ruchów białego powtarzania, które wykonujesz ponownie. Łańcuch jest za każdym razem wykonywany z następnym białym ruchem, a model czatu odpowiada kolejnym najlepszym ruchem.

Gdy uruchomisz tę klasę z tymi ruchami, powinny wyświetlić się takie dane wyjściowe:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :app:run
Playing Nf3
1... e5
Playing c4
2... Nc6
Playing Nc3
3... Nf6
Playing e3
4... Bb4
Playing Dc2
5... O-O
Playing Cd5
6... exd5 

O rany! PaLM wie, jak grać w szachy? Niezupełnie, ale podczas trenowania model musiał zapoznać się z komentarzami szachów lub nawet plikami PGN (Portable Game Notation) z wcześniejszych gier. Ten czatbot prawdopodobnie nie wygra jednak z AlphaZero (AI, która pokonuje najlepszych graczy w Go, Shogi i szachy), a rozmowa może się w dalszym ciągu pogarszać, a model nie zapamiętuje faktycznego stanu gry.

Modele czatu mają bardzo duże możliwości i umożliwiają rozbudowane interakcje z użytkownikami oraz wykonywanie różnych zadań kontekstowych. W następnej sekcji zajmiemy się przydatnym zadaniem: wyodrębnianiem uporządkowanych danych z tekstu.

6. Wyodrębnianie informacji z nieuporządkowanego tekstu

W poprzedniej sekcji utworzono rozmowy między użytkownikiem a modelem języka czatu. Jednak dzięki LangChain4J możesz też użyć modelu czatu, aby wyodrębnić uporządkowane informacje z nieuporządkowanego tekstu.

Załóżmy, że chcesz wyodrębnić imię i nazwisko oraz wiek osoby na podstawie jej biografii lub opisu. Duży model językowy (LLM) możesz poinstruować, aby generował struktury danych JSON za pomocą sprytnie dostosowanego promptu (jest to potocznie nazywane „inżynierią promptów”).

Zajęcia ChatPrompts zostaną zaktualizowane w ten sposób:

package palm.workshop;

import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;

public class ChatPrompts {

    static class 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) {
        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(300)
            .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);
        System.out.println(person.age);
    }
}

Przyjrzyjmy się różnym krokom w tym pliku:

  • Klasa Person jest zdefiniowana do rejestrowania informacji opisujących osobę (jej imię i nazwisko oraz wiek).
  • Interfejs PersonExtractor został utworzony za pomocą metody, która w przypadku nieuporządkowanego ciągu tekstowego zwraca wystąpienie wystąpienia Person w instancji.
  • 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 instancji Person.

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

  • Tworzony jest model czatu.
  • Obiekt PersonExtractor jest tworzony dzięki klasie AiServices 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ąpienie Person z imieniem i nazwiskiem oraz wiekiem.

Teraz uruchom te zajęcia za pomocą tego polecenia:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

> Task :app:run
Anna
23

Tak. Jestem Anna, która ma 23 lata.

Szczególnie interesujące w przypadku metody AiServices jest to, że używasz obiektów o dużym zapisie tekstu. Nie wchodzisz bezpośrednio w interakcję z LLM czatu. Zamiast tego pracujesz z konkretnymi klasami, takimi jak klasa Person do reprezentowania wyodrębnionych danych osobowych, a masz klasę PersonExtractor z metodą extractPerson(), która zwraca wystąpienie osoby. Pojęcie LLM zostało wycofane, a programista Java zajmuje się tylko zwykłymi klasami i obiektami.

7. Generowanie rozszerzone przez wyszukiwanie w zapisanych informacjach: czatowanie z dokumentami

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ć odpowiednie informacje z bazy danych z fragmentami dokumentów. Informacje te będą używane przez model do „przybywania” odpowiedzi, zamiast próbować generować odpowiedzi po trenowaniu. Ten wzorzec nosi nazwę RAG (Retrieval Augmented Generation).

W modelu Retrieval Augmented Generation (generowanie rozszerzonego wyszukiwania danych) dzieli się w skrócie 2 fazy:

  1. Etap przetwarzania – dokumenty są ładowane i dzielone na mniejsze fragmenty, a ich reprezentacja wektorowa („wektor wektorowy”) jest przechowywana w „bazie danych wektorowych” z możliwością wyszukiwania semantycznego.

6c5bb5cb2e3b8088.png

  1. Etap zapytań – użytkownicy mogą teraz zadawać czatbotowi pytania dotyczące dokumentacji. Pytanie zostanie również przekształcone w wektor i zostanie porównane ze wszystkimi innymi wektorami w bazie danych. Najbardziej podobne wektory są zwykle powiązane semantycznie i są zwracane do bazy danych wektorowych. Następnie otrzymuje kontekst konwersacji i fragmenty tekstu odpowiadające wektorom zwracanym przez bazę danych. Następnie otrzymuje prośbę o ustalenie odpowiedzi na podstawie tych fragmentów.

2c279c506d7606cd.png

Przygotowywanie dokumentów

Podczas tej nowej prezentacji zadamy pytania dotyczące architektury sieci neuronowych „transformer”, które zostały opracowane przez Google, a to właśnie w ten sposób wdrażane są wszystkie nowoczesne duże modele językowe obecnie.

Możesz pobrać artykuł badawczy opisujący tę architekturę („Uwaga to wszystko, czego potrzebujesz”) za pomocą polecenia wget do pobrania pliku PDF z internetu:

wget -O attention-is-all-you-need.pdf \
    https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf

Wdrażanie konwersacyjnego łańcucha pobierania

Zobaczmy krok po kroku, jak utworzyć podejście dwufazowe – najpierw z przetwarzaniem dokumentów, a następnie, w czasie, gdy użytkownicy zadają pytania na temat dokumentu.

Przetwarzanie dokumentów

Pierwszym krokiem w fazie przetwarzania dokumentów jest znalezienie pobieranego przez nas pliku PDF i przygotowanie PdfParser do odczytu:

PdfDocumentParser pdfParser = new PdfDocumentParser();
Document document = pdfParser.parse(
    new FileInputStream(new File("/home/YOUR_USER_NAME/palm-workshop/attention-is-all-you-need.pdf")));

Zamiast tworzyć standardowy model języka czatu, możesz utworzyć instancję modelu „umieszczanie”. Jest to konkretny model i punkt końcowy, których rola polega na tworzeniu wektorowych reprezentacji fragmentów tekstu (słów, zdań, a nawet akapitów).

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint("us-central1-aiplatform.googleapis.com:443")
    .project("YOUR_PROJECT_ID")
    .location("us-central1")
    .publisher("google")
    .modelName("textembedding-gecko@001")
    .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);

EmbeddingStoreRetriever retriever = EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

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

Sklep „Odbiorca” łączy narzędzie do dzielenia dokumentów, model wektora dystrybucyjnego do obliczania wektorów oraz działającą w pamięci bazę danych wektorowych. 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ę, można utworzyć standardowy model czatu:

VertexAiChatModel model = VertexAiChatModel.builder()
    .endpoint("us-central1-aiplatform.googleapis.com:443")
    .project("YOUR_PROJECT_ID")
    .location("us-central1")
    .publisher("google")
    .modelName("chat-bison@001")
    .maxOutputTokens(1000)
    .build();

Będziesz też potrzebować klasy "retriever", która będzie łączyć wektorową bazę danych (w zmiennej embeddingStore) z modelem wektora dystrybucyjnego. 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:

EmbeddingStoreRetriever retriever = 
    EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

W tym momencie możesz utworzyć instancję klasy ConversationalRetrievalChain (jest to po prostu inna nazwa wzorca Retrieval Augmented Generation):

ConversationalRetrievalChain rag = ConversationalRetrievalChain.builder()
    .chatLanguageModel(model)
    .retriever(retriever)
    .promptTemplate(PromptTemplate.from("""
        Answer to the following query the best as you can: {{question}}
        Base your answer on the information provided below:
        {{information}}
        """
    ))
    .build();

Ten „łańcuch” tworzy powiązania:

  • Skonfigurowany wcześniej model języka czatu.
  • 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).

Teraz możesz już w końcu zadawać pytania.

String result = rag.execute("What neural network architecture can be used for language models?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What are the different components of a transformer neural network?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What is attention in large language models?");
System.out.println(result);
System.out.println("------------");

result = rag.execute("What is the name of the process that transforms text into vectors?");
System.out.println(result);

Uruchom program, używając:

$ ./gradlew run -DjavaMainClass=palm.workshop.ChatPrompts

W danych wyjściowych powinna znajdować się odpowiedź na Twoje pytania:

The Transformer is a neural network architecture that can be used for 
language models. It is based solely on attention mechanisms, dispensing 
with recurrence and convolutions. The Transformer has been shown to 
outperform recurrent neural networks and convolutional neural networks on 
a variety of language modeling tasks.
------------
The Transformer is a neural network architecture that can be used for 
language models. It is based solely on attention mechanisms, dispensing 
with recurrence and convolutions. The Transformer has been shown to 
outperform recurrent neural networks and convolutional neural networks on a 
variety of language modeling tasks. The Transformer consists of an encoder 
and a decoder. The encoder is responsible for encoding the input sequence 
into a fixed-length vector representation. The decoder is responsible for 
decoding the output sequence from the input sequence. The decoder uses the 
attention mechanism to attend to different parts of the input sequence when 
generating the output sequence.
------------
Attention is a mechanism that allows a neural network to focus on specific 
parts of an input sequence. In the context of large language models, 
attention is used to allow the model to focus on specific words or phrases 
in a sentence when generating output. This allows the model to generate 
more relevant and informative output.
------------
The process of transforming text into vectors is called word embedding. 
Word embedding is a technique that represents words as vectors in a 
high-dimensional space. The vectors are typically learned from a large 
corpus of text, and they capture the semantic and syntactic relationships 
between words. Word embedding has been shown to be effective for a variety 
of natural language processing tasks, such as machine translation, question 
answering, and sentiment analysis.

Kompleksowe rozwiązanie

Aby ułatwić kopiowanie i wklejanie, oto pełna treść klasy ChatPrompts:

package palm.workshop;

import dev.langchain4j.chain.ConversationalRetrievalChain;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.PdfDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment; 
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.vertexai.VertexAiChatModel;
import dev.langchain4j.model.vertexai.VertexAiEmbeddingModel;
import dev.langchain4j.retriever.EmbeddingStoreRetriever;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ChatPrompts {
    public static void main(String[] args) throws IOException {
        PdfDocumentParser pdfParser = new PdfDocumentParser();
        Document document = pdfParser.parse(new FileInputStream(new File("/ABSOLUTE_PATH/attention-is-all-you-need.pdf")));

        VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("YOUR_PROJECT_ID")
            .location("us-central1")
            .publisher("google")
            .modelName("textembedding-gecko@001")
            .maxRetries(3)
            .build();

        InMemoryEmbeddingStore<TextSegment> embeddingStore = 
            new InMemoryEmbeddingStore<>();

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

        EmbeddingStoreRetriever retriever = EmbeddingStoreRetriever.from(embeddingStore, embeddingModel);

        VertexAiChatModel model = VertexAiChatModel.builder()
            .endpoint("us-central1-aiplatform.googleapis.com:443")
            .project("genai-java-demos")
            .location("us-central1")
            .publisher("google")
            .modelName("chat-bison@001")
            .maxOutputTokens(1000)
            .build();

        ConversationalRetrievalChain rag = ConversationalRetrievalChain.builder()
            .chatLanguageModel(model)
            .retriever(retriever)
            .promptTemplate(PromptTemplate.from("""
                Answer to the following query the best as you can: {{question}}
                Base your answer on the information provided below:
                {{information}}
                """
            ))
            .build();

        String result = rag.execute("What neural network architecture can be used for language models?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What are the different components of a transformer neural network?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What is attention in large language models?");
        System.out.println(result);
        System.out.println("------------");

        result = rag.execute("What is the name of the process that transforms text into vectors?");
        System.out.println(result);
    }
}

8. Gratulacje

Gratulujemy. Udało Ci się utworzyć pierwszą aplikację czatu z generatywną AI w języku Java przy użyciu LangChain4J i interfejsu PaLM API. Po drodze przekonaliście się, że duże modele czatu są bardzo wydajne i radzą sobie z różnymi zadaniami, takimi jak pytania i udzielanie odpowiedzi, nawet dzięki własnej dokumentacji czy wyodrębnianiu danych. Do pewnego stopnia potrafią nawet zagrać w szachy.

Co dalej?

Aby dowiedzieć się więcej o PaLM w języku Java, wykonaj te ćwiczenia z programowania:

Więcej informacji

Dokumenty referencyjne