Gemini di Java dengan Vertex AI dan LangChain4j

1. Pengantar

Codelab ini berfokus pada Model Bahasa Besar (LLM) Gemini yang dihosting di Vertex AI di Google Cloud. Vertex AI adalah platform yang mencakup semua produk, layanan, dan model machine learning di Google Cloud.

Anda akan menggunakan Java untuk berinteraksi dengan Gemini API menggunakan framework LangChain4j. Anda akan mempelajari contoh konkret cara memanfaatkan LLM untuk menjawab pertanyaan, pembuatan ide, ekstraksi entitas dan konten terstruktur, pembuatan versi yang ditingkatkan saat mengambil, dan panggilan fungsi.

Apa itu AI Generatif?

AI generatif mengacu pada penggunaan kecerdasan buatan untuk membuat konten baru, seperti teks, gambar, musik, audio, dan video.

AI generatif didukung oleh model bahasa besar (LLM) yang dapat melakukan beberapa tugas sekaligus dan melakukan tugas siap pakai seperti perangkuman, tanya jawab, klasifikasi, dan sebagainya. Dengan pelatihan minimal, model dasar dapat diadaptasikan untuk kasus penggunaan tertarget dengan data contoh yang sangat sedikit.

Bagaimana cara kerja AI Generatif?

AI generatif bekerja menggunakan model Machine Learning (ML) untuk mempelajari pola dan hubungan dalam set data konten buatan manusia. Sistem ini kemudian menggunakan pola-pola yang telah dipelajarinya untuk membuat konten baru.

Cara yang paling umum digunakan untuk melatih model AI generatif adalah menggunakan supervised learning. Model ini diberi sekumpulan konten buatan manusia dan label yang sesuai. AI generatif kemudian belajar membuat konten yang serupa dengan konten buatan manusia.

Apa saja aplikasi AI Generatif yang umum?

AI generatif dapat digunakan untuk:

  • Tingkatkan interaksi pelanggan melalui chat dan pengalaman penelusuran yang ditingkatkan kualitasnya.
  • Eksplorasi sejumlah besar data tidak terstruktur melalui antarmuka percakapan dan perangkuman.
  • Bantu tugas berulang seperti membalas permintaan proposal, melokalkan konten pemasaran dalam berbagai bahasa dan memeriksa kontrak pelanggan terkait kepatuhan, dan lainnya.

Penawaran AI Generatif apa yang dimiliki Google Cloud?

Dengan Vertex AI, Anda dapat berinteraksi dengan model dasar, menyesuaikan model tersebut, dan menyematkannya ke aplikasi Anda dengan sedikit atau tanpa keahlian ML. Anda dapat mengakses model dasar di Model Garden, menyesuaikan berbagai model melalui UI yang sederhana di Vertex AI Studio, atau menggunakan model dalam notebook data science.

Vertex AI Search and Conversation menawarkan cara tercepat bagi para developer untuk membangun mesin telusur dan chatbot yang didukung teknologi AI generatif.

Dengan dukungan Gemini, Gemini untuk Google Cloud adalah kolaborator dengan teknologi AI yang tersedia di Google Cloud dan IDE untuk membantu Anda menyelesaikan lebih banyak hal dengan lebih cepat. Gemini Code Assist menyediakan penyelesaian kode, pembuatan kode, penjelasan kode, dan memungkinkan Anda melakukan chat dengannya untuk mengajukan pertanyaan teknis.

Apa itu Gemini?

Gemini adalah rangkaian model AI generatif yang dikembangkan oleh Google DeepMind dan dirancang untuk kasus penggunaan multimodal. Multimodal artinya dapat memproses dan menghasilkan berbagai jenis konten seperti teks, kode, gambar, dan audio.

b9913d011999e7c7.png

Gemini tersedia dalam berbagai variasi dan ukuran:

  • Gemini Ultra: Versi terbesar dan paling canggih untuk tugas-tugas kompleks.
  • Gemini Flash: Tercepat dan paling hemat biaya, dioptimalkan untuk tugas bervolume tinggi.
  • Gemini Pro: Berukuran sedang, dioptimalkan untuk penskalaan di berbagai tugas.
  • Gemini Nano: Yang paling efisien dan dirancang untuk tugas di perangkat.

Fitur Utama:

  • Multimodalitas: Kemampuan Gemini untuk memahami dan menangani berbagai format informasi merupakan langkah yang signifikan melampaui model bahasa tradisional yang hanya menampilkan teks.
  • Performa: Gemini Ultra mengungguli performa tercanggih saat ini dalam banyak tolok ukur dan merupakan model pertama yang mengungguli pakar manusia dalam tolok ukur MMLU (Massive Multitask Language Understanding).
  • Fleksibilitas: Ukuran Gemini yang berbeda membuatnya dapat disesuaikan untuk berbagai kasus penggunaan, mulai dari riset skala besar hingga deployment di perangkat seluler.

Bagaimana cara berinteraksi dengan Gemini di Vertex AI dari Java?

Ada dua opsi:

  1. Library Vertex AI Java API untuk Gemini resmi.
  2. LangChain4j.

Dalam codelab ini, Anda akan menggunakan framework LangChain4j.

Apa itu framework LangChain4j?

Framework LangChain4j adalah library open source untuk mengintegrasikan LLM di aplikasi Java, dengan mengorkestrasi berbagai komponen, seperti LLM itu sendiri, serta alat lain seperti database vektor (untuk penelusuran semantik), loader dokumen, dan pemisah (untuk menganalisis dokumen dan belajar darinya), parser output, dan banyak lagi.

Project ini terinspirasi oleh project Python LangChain, tetapi bertujuan melayani developer Java.

bb908ea1e6c96ac2.png

Yang akan Anda pelajari

  • Cara menyiapkan project Java untuk menggunakan Gemini dan LangChain4j
  • Cara mengirim perintah pertama Anda ke Gemini secara terprogram
  • Cara mengalirkan respons dari Gemini
  • Cara membuat percakapan antara pengguna dan Gemini
  • Cara menggunakan Gemini dalam konteks multimodal dengan mengirim teks dan gambar
  • Cara mengekstrak informasi terstruktur yang berguna dari konten yang tidak terstruktur
  • Cara memanipulasi template perintah
  • Cara melakukan klasifikasi teks seperti analisis sentimen
  • Cara melakukan chat dengan dokumen Anda sendiri (Pengambilan Augmented Generation)
  • Cara memperluas chatbot Anda dengan panggilan fungsi
  • Cara menggunakan Gemma secara lokal dengan Ollama dan TestContainers

Yang Anda butuhkan

  • Pengetahuan tentang bahasa pemrograman Java
  • Project Google Cloud
  • Browser, seperti Chrome atau Firefox

2. Penyiapan dan persyaratan

Penyiapan lingkungan mandiri

  1. Login ke Google Cloud Console dan buat project baru atau gunakan kembali project yang sudah ada. Jika belum memiliki akun Gmail atau Google Workspace, Anda harus membuatnya.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Project name adalah nama tampilan untuk peserta project ini. String ini adalah string karakter yang tidak digunakan oleh Google API. Anda dapat memperbaruinya kapan saja.
  • Project ID bersifat unik di semua project Google Cloud dan tidak dapat diubah (tidak dapat diubah setelah ditetapkan). Cloud Console otomatis membuat string unik; biasanya Anda tidak mementingkan kata-katanya. Di sebagian besar codelab, Anda harus merujuk Project ID-nya (umumnya diidentifikasi sebagai PROJECT_ID). Jika tidak suka dengan ID yang dibuat, Anda dapat membuat ID acak lainnya. Atau, Anda dapat mencobanya sendiri, dan lihat apakah ID tersebut tersedia. ID tidak dapat diubah setelah langkah ini dan tersedia selama durasi project.
  • Sebagai informasi, ada nilai ketiga, Project Number, yang digunakan oleh beberapa API. Pelajari lebih lanjut ketiga nilai ini di dokumentasi.
  1. Selanjutnya, Anda harus mengaktifkan penagihan di Konsol Cloud untuk menggunakan resource/API Cloud. Menjalankan operasi dalam codelab ini tidak akan memakan banyak biaya, bahkan mungkin tidak sama sekali. Guna mematikan resource agar tidak menimbulkan penagihan di luar tutorial ini, Anda dapat menghapus resource yang dibuat atau menghapus project-nya. Pengguna baru Google Cloud memenuhi syarat untuk mengikuti program Uji Coba Gratis senilai $300 USD.

Mulai Cloud Shell

Meskipun Google Cloud dapat dioperasikan secara jarak jauh dari laptop Anda, dalam codelab ini Anda akan menggunakan Cloud Shell, yakni lingkungan command line yang berjalan di Cloud.

Mengaktifkan Cloud Shell

  1. Dari Cloud Console, klik Aktifkan Cloud Shell 853e55310c205094.pngS.

3c1dabeca90e44e5.pngS

Jika ini pertama kalinya Anda memulai Cloud Shell, Anda akan melihat layar perantara yang menjelaskan apa itu Cloud Shell. Jika Anda melihat layar perantara, klik Lanjutkan.

9c92662c6a846a5c.pngS

Perlu waktu beberapa saat untuk penyediaan dan terhubung ke Cloud Shell.

9f0e51b578fecce5.pngS

Mesin virtual ini dimuat dengan semua alat pengembangan yang diperlukan. Layanan ini menawarkan direktori beranda tetap sebesar 5 GB dan beroperasi di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Sebagian besar pekerjaan Anda dalam codelab ini dapat dilakukan dengan browser.

Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda telah diautentikasi dan project sudah ditetapkan ke project ID Anda.

  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
gcloud auth list

Output perintah

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa perintah gcloud mengetahui project Anda:
gcloud config list project

Output perintah

[core]
project = <PROJECT_ID>

Jika tidak, Anda dapat menyetelnya dengan perintah ini:

gcloud config set project <PROJECT_ID>

Output perintah

Updated property [core/project].

3. Menyiapkan lingkungan pengembangan Anda

Dalam codelab ini, Anda akan menggunakan terminal Cloud Shell dan editor Cloud Shell untuk mengembangkan program Java.

Mengaktifkan Vertex AI API

Di konsol Google Cloud, pastikan nama project Anda ditampilkan di bagian atas Konsol Google Cloud. Jika tidak, klik Select a project untuk membuka Project Selector, lalu pilih project yang Anda inginkan.

Anda dapat mengaktifkan Vertex AI API dari bagian Vertex AI di Konsol Google Cloud atau dari terminal Cloud Shell.

Untuk mengaktifkan dari konsol Google Cloud, pertama, buka bagian Vertex AI di menu Konsol Google Cloud:

451976f1c8652341.pngS

Klik Enable All Recommended APIs di dasbor Vertex AI.

Tindakan ini akan mengaktifkan beberapa API, tetapi yang paling penting untuk codelab adalah aiplatform.googleapis.com.

Atau, Anda juga dapat mengaktifkan API ini dari terminal Cloud Shell dengan perintah berikut:

gcloud services enable aiplatform.googleapis.com

Meng-clone repositori GitHub

Di terminal Cloud Shell, clone repositori untuk codelab ini:

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

Untuk memeriksa apakah project siap dijalankan, Anda dapat mencoba menjalankan aplikasi "Hello World" (Halo Dunia) program ini.

Pastikan Anda berada di folder tingkat atas:

cd gemini-workshop-for-java-developers/ 

Buat wrapper Gradle:

gradle wrapper

Jalankan dengan gradlew:

./gradlew run

Anda akan melihat output berikut:

..
> Task :app:run
Hello World!

Membuka dan menyiapkan Cloud Editor

Buka kode dengan Cloud Code Editor dari Cloud Shell:

42908e11b28f4383.pngS

Di Cloud Code Editor, buka folder sumber codelab dengan memilih File -> Open Folder dan arahkan kursor ke folder sumber codelab (misalnya, /home/username/gemini-workshop-for-java-developers/).

Menginstal Gradle untuk Java

Agar editor kode cloud berfungsi dengan baik dengan Gradle, instal ekstensi Gradle untuk Java.

Pertama, buka bagian Java Projects dan tekan tanda plus:

84d15639ac61c197.pngS

Pilih Gradle for Java:

34d6c4136a3cc9ff.pngS

Pilih versi Install Pre-Release:

3b044fb450cccb7.pngS

Setelah diinstal, Anda akan melihat tombol Disable dan Uninstall:

46410fe86d777f9c.pngS

Terakhir, bersihkan ruang kerja untuk menerapkan setelan baru:

31e27e9bb61d975d.pngS

Tindakan ini akan meminta Anda memuat ulang dan menghapus workshop. Lanjutkan dan pilih Reload and delete:

d6303bc49e391dc.png

Jika membuka salah satu file, misalnya App.java, Anda kini akan melihat editor berfungsi dengan benar dengan penyorotan sintaksis:

fed1b1b5de0dff58.png

Anda sekarang siap untuk menjalankan beberapa contoh terhadap Gemini.

Menyiapkan variabel lingkungan

Buka terminal baru di Cloud Code Editor dengan memilih Terminal -> New Terminal. Siapkan dua variabel lingkungan yang diperlukan untuk menjalankan contoh kode:

  • PROJECT_ID — ID project Google Cloud Anda
  • LOCATION — Region tempat model Gemini di-deploy

Ekspor variabel sebagai berikut:

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

4. Panggilan pertama ke model Gemini

Setelah project disiapkan dengan benar, saatnya untuk memanggil Gemini API.

Lihat QA.java di direktori 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?"));
    }
}

Pada contoh pertama ini, Anda harus mengimpor class VertexAiGeminiChatModel, yang menerapkan antarmuka ChatModel.

Dalam metode main, Anda mengonfigurasi model bahasa chat menggunakan builder untuk VertexAiGeminiChatModel dan menentukan:

  • Project
  • Lokasi
  • Nama model (gemini-1.5-flash-001).

Setelah model bahasa siap, Anda dapat memanggil metode generate() dan meneruskan perintah, pertanyaan, atau petunjuk Anda untuk dikirim ke LLM. Di sini, Anda mengajukan pertanyaan sederhana tentang apa yang membuat langit berwarna biru.

Jangan ragu untuk mengubah {i>prompt<i} ini untuk mencoba pertanyaan atau tugas yang berbeda.

Jalankan contoh di folder root kode sumber:

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

Anda akan melihat output yang mirip dengan output ini:

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.

Selamat, Anda telah melakukan panggilan pertama ke Gemini.

Respons streaming

Apakah Anda memperhatikan bahwa respons diberikan dalam satu sesi, setelah beberapa detik? Berkat varian respons streaming, Anda juga bisa mendapatkan respons secara bertahap. Respons streaming, model akan menampilkan respons sepotong demi sepotong, saat tersedia.

Dalam codelab ini, kita akan tetap menggunakan respons non-streaming, tetapi mari kita lihat respons streaming untuk melihat cara melakukannya.

Di StreamQA.java di direktori app/src/main/java/gemini/workshop, Anda dapat melihat cara kerja respons streaming:

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

Kali ini, kita akan mengimpor varian class streaming VertexAiGeminiStreamingChatModel yang menerapkan antarmuka StreamingChatLanguageModel. Anda juga memerlukan StreamingResponseHandler.

Kali ini, tanda tangan metode generate() sedikit berbeda. Sebagai ganti menampilkan string, jenis nilai yang ditampilkan menjadi batal. Selain prompt, Anda harus meneruskan pengendali respons streaming. Di sini, Anda akan menerapkan antarmuka dengan membuat class dalam anonim, dengan dua metode onNext(String text) dan onError(Throwable error). Yang pertama dipanggil setiap kali bagian baru dari respons tersedia, sementara yang kedua hanya dipanggil jika terjadi error.

Jalankan:

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

Anda akan mendapatkan jawaban yang serupa dengan class sebelumnya, tetapi kali ini, Anda akan melihat bahwa jawabannya muncul secara bertahap di shell Anda, bukan menunggu tampilan jawaban lengkap.

Konfigurasi tambahan

Untuk konfigurasi, kita hanya menentukan project, lokasi, dan nama model, tetapi ada parameter lain yang dapat Anda tetapkan untuk model tersebut:

  • temperature(Float temp) — untuk menentukan seberapa kreatifkah respons yang Anda inginkan (0 jika materi iklan rendah dan sering kali lebih faktual, sedangkan 1 untuk output materi iklan yang lebih banyak)
  • topP(Float topP) — untuk memilih kemungkinan kata yang total probabilitasnya menjumlahkan bilangan floating point tersebut (antara 0 dan 1)
  • topK(Integer topK) — untuk memilih kata secara acak dari jumlah maksimum kata yang mungkin untuk pelengkapan teks (dari 1 sampai 40)
  • maxOutputTokens(Integer max) — untuk menentukan panjang maksimum jawaban yang diberikan oleh model (umumnya, 4 token mewakili sekitar 3 kata)
  • maxRetries(Integer retries) — jika Anda melampaui kuota permintaan per waktu, atau platform mengalami beberapa masalah teknis, Anda dapat meminta model untuk mencoba kembali panggilan tersebut 3 kali

Sejauh ini, Anda mengajukan satu pertanyaan kepada Gemini, tetapi Anda juga dapat melakukan percakapan bolak-balik. Itulah yang akan Anda pelajari di bagian berikutnya.

5. Mulai percakapan dengan Gemini

Di langkah sebelumnya, Anda mengajukan satu pertanyaan. Sekarang saatnya melakukan percakapan nyata antara pengguna dan LLM. Setiap pertanyaan dan jawaban dapat dibangun berdasarkan pertanyaan dan jawaban sebelumnya untuk membentuk diskusi yang nyata.

Lihat Conversation.java di folder 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));
        });
    }
}

Beberapa impor baru yang menarik di class ini:

  • MessageWindowChatMemory — class yang akan membantu menangani aspek percakapan multi-giliran, serta menyimpan pertanyaan dan jawaban sebelumnya dalam memori lokal
  • AiServices — kelas yang akan menggabungkan model chat dan memori chat

Di metode utama, Anda akan menyiapkan model, memori chat, dan layanan AI. Model dikonfigurasi seperti biasa dengan informasi project, lokasi, dan nama model.

Untuk memori chat, kita menggunakan builder MessageWindowChatMemory untuk membuat memori yang menyimpan 20 pesan terakhir yang dipertukarkan. Ini adalah jendela geser di atas percakapan yang konteksnya disimpan secara lokal di klien class Java kita.

Anda kemudian membuat AI service yang mengikat model chat dengan memori chat.

Perhatikan cara layanan AI menggunakan antarmuka ConversationService kustom yang telah kita tentukan, yang diimplementasikan oleh LangChain4j, dan menggunakan kueri String serta menampilkan respons String.

Sekarang, saatnya berdiskusi dengan Gemini. Pertama, sapaan sederhana dikirim, lalu pertanyaan pertama tentang menara Eiffel untuk mengetahui di negara mana sapaan itu dapat ditemukan. Perhatikan bahwa kalimat terakhir berkaitan dengan jawaban pertanyaan pertama, saat Anda bertanya-tanya berapa jumlah penduduk di negara tempat menara Eiffel berada, tanpa menyebutkan secara eksplisit negara yang diberikan pada jawaban sebelumnya. Bagian ini menunjukkan bahwa pertanyaan dan jawaban sebelumnya dikirim dengan setiap perintah.

Jalankan contoh:

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

Anda akan melihat tiga jawaban yang serupa dengan berikut ini:

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.

Anda dapat mengajukan pertanyaan satu giliran atau melakukan percakapan bolak-balik dengan Gemini, tetapi sejauh ini, inputnya hanya berupa teks. Bagaimana dengan gambar? Mari kita pelajari gambar pada langkah berikutnya.

6. Multimodalitas dengan Gemini

Gemini adalah model multimodal. Sistem ini tidak hanya menerima teks sebagai input, tetapi juga menerima gambar, atau bahkan video sebagai input. Di bagian ini, Anda akan melihat kasus penggunaan untuk mencampur teks dan gambar.

Apa menurutmu Gemini akan mengenali kucing ini?

af00516493ec9ade.png

Gambar kucing di salju yang diambil dari Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

Lihat Multimodal.java di direktori 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());
    }
}

Dalam impor, perhatikan bahwa kita membedakan antara berbagai jenis pesan dan konten. UserMessage dapat berisi objek TextContent dan ImageContent. Inilah multimodalitas yang berfungsi: mencampur teks dan gambar. Model mengirimkan kembali Response yang berisi AiMessage.

Anda kemudian mengambil AiMessage dari respons melalui content(), kemudian teks pesan berkat text().

Jalankan contoh:

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

Nama gambar itu tentu memberi Anda petunjuk tentang isi gambar tersebut, tetapi output Gemini-nya mirip dengan yang berikut ini:

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.

Mencampur perintah gambar dan teks akan membuka kasus penggunaan yang menarik. Anda dapat membuat aplikasi yang dapat:

  • Mengenali teks dalam gambar.
  • Memeriksa apakah gambar aman untuk ditampilkan.
  • Membuat teks gambar.
  • Menelusuri database gambar dengan deskripsi teks biasa.

Selain mengekstrak informasi dari gambar, Anda juga dapat mengekstrak informasi dari teks yang tidak terstruktur. Itulah yang akan Anda pelajari di bagian berikutnya.

7. Mengekstrak informasi terstruktur dari teks yang tidak terstruktur

Ada banyak situasi di mana informasi penting diberikan dalam dokumen laporan, di email, atau teks panjang lainnya dengan cara yang tidak terstruktur. Idealnya, Anda ingin dapat mengekstrak detail kunci yang terkandung dalam teks yang tidak terstruktur, dalam bentuk objek terstruktur. Mari kita lihat cara melakukannya.

Misalkan Anda ingin mengekstrak nama dan usia seseorang, berdasarkan biografi atau deskripsi orang tersebut. Anda dapat menginstruksikan LLM untuk mengekstrak JSON dari teks tidak terstruktur dengan prompt yang diubah secara cerdas (ini biasa disebut "prompt engineering").

Lihat ExtractData.java di 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
    }
}

Mari kita lihat berbagai langkah dalam file ini:

  • Data Person ditentukan untuk mewakili detail yang mendeskripsikan seseorang ( nama dan usia).
  • Antarmuka PersonExtractor ditentukan dengan metode yang diberi string teks tidak terstruktur, dan menampilkan instance Person.
  • extractPerson() dianotasi dengan anotasi @UserMessage yang mengaitkan perintah dengannya. Itulah perintah yang akan digunakan model untuk mengekstrak informasi, dan menampilkan detail dalam bentuk dokumen JSON, yang akan diurai untuk Anda, dan dibatalkan susunannya menjadi instance Person.

Sekarang mari kita lihat konten metode main():

  • Model chat dibuat instance-nya. Perhatikan bahwa kita menggunakan temperature yang sangat rendah dari nol, dan topK dari hanya satu, untuk memastikan jawaban yang sangat determenistik. Hal ini juga membantu model mengikuti petunjuk dengan lebih baik. Secara khusus, kita tidak ingin Gemini menggabungkan respons JSON dengan markup Markdown tambahan.
  • Objek PersonExtractor dibuat berkat class AiServices LangChain4j.
  • Kemudian, Anda cukup memanggil Person person = extractor.extractPerson(...) untuk mengekstrak detail orang dari teks yang tidak terstruktur, dan mendapatkan kembali instance Person dengan nama dan usia.

Jalankan contoh:

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

Anda akan melihat output berikut:

Anna
23

Ya, ini Anna dan mereka berusia 23 tahun!

Dengan pendekatan AiServices ini, Anda beroperasi dengan objek dengan sistem yang kuat. Anda tidak berinteraksi langsung dengan LLM. Sebagai gantinya, Anda akan menggunakan class konkret, seperti data Person untuk menampilkan informasi pribadi yang diekstrak, dan Anda memiliki objek PersonExtractor dengan metode extractPerson() yang menampilkan instance Person. Konsep LLM diabstraksi, dan sebagai developer Java, Anda hanya memanipulasi class dan objek normal.

8. Membuat struktur prompt dengan template perintah

Saat Anda berinteraksi dengan LLM menggunakan serangkaian petunjuk atau pertanyaan umum, ada bagian dari perintah tersebut yang tidak pernah berubah, sementara bagian lainnya berisi data. Misalnya, jika ingin membuat resep, Anda dapat menggunakan perintah seperti "Anda seorang koki berbakat, tolong buat resep dengan bahan-bahan berikut: ...", lalu tambahkan bahan-bahan di akhir teks tersebut. Itulah fungsi template prompt, mirip dengan string interpolasi dalam bahasa pemrograman. Template perintah berisi placeholder yang dapat Anda ganti dengan data yang tepat untuk panggilan tertentu ke LLM.

Lebih jelasnya, mari kita pelajari TemplatePrompt.java di direktori 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());
    }
}

Seperti biasa, Anda mengonfigurasi model VertexAiGeminiChatModel, dengan tingkat kreativitas yang tinggi dengan suhu yang tinggi serta nilai topP dan topK yang tinggi. Kemudian, Anda membuat PromptTemplate dengan metode statis from(), dengan meneruskan string perintah, dan menggunakan variabel placeholder tanda kurung kurawal ganda: {{dish}} dan {{ingredients}}.

Anda membuat perintah akhir dengan memanggil apply() yang menggunakan peta key-value pair yang mewakili nama placeholder dan nilai string yang akan menggantikannya.

Terakhir, Anda memanggil metode generate() model Gemini dengan membuat pesan pengguna dari perintah tersebut, menggunakan petunjuk prompt.toUserMessage().

Jalankan contoh:

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

Anda akan melihat output yang dihasilkan dan terlihat mirip dengan output ini:

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

Jangan ragu untuk mengubah nilai dish dan ingredients di peta dan menyesuaikan suhu, topK dan tokP, lalu menjalankan ulang kodenya. Hal ini akan memungkinkan Anda untuk mengamati efek perubahan parameter ini pada LLM.

Template perintah adalah cara yang baik untuk memiliki petunjuk yang dapat digunakan kembali dan yang dapat diparameterisasi untuk panggilan LLM. Anda dapat meneruskan data dan menyesuaikan dialog untuk berbagai nilai yang diberikan oleh pengguna.

9. Klasifikasi teks dengan few-shot prompting

LLM cukup bagus dalam mengklasifikasikan teks ke dalam berbagai kategori. Anda dapat membantu LLM dalam melakukan tugas tersebut dengan memberikan beberapa contoh teks dan kategori terkaitnya. Pendekatan ini sering disebut few shot prompting.

Lihat TextClassification.java di direktori app/src/main/java/gemini/workshop, untuk melakukan jenis klasifikasi teks tertentu: analisis sentimen.

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

Dalam metode main(), Anda membuat model chat Gemini seperti biasa, tetapi dengan jumlah token output maksimum yang kecil, karena Anda hanya menginginkan respons singkat: teksnya adalah POSITIVE, NEGATIVE, atau NEUTRAL.

Kemudian, Anda akan membuat template prompt yang dapat digunakan kembali dengan teknik few-shot prompting, dengan memberi petunjuk kepada model tentang beberapa contoh input dan output. Hal ini juga membantu model mengikuti output aktual. Gemini tidak akan membalas dengan kalimat lengkap, tetapi diminta untuk membalas hanya dengan satu kata.

Anda akan menerapkan variabel dengan metode apply(), untuk mengganti placeholder {{text}} dengan parameter asli ("I love strawberries"), dan mengubah template tersebut menjadi pesan pengguna dengan toUserMessage().

Jalankan contoh:

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

Anda akan melihat satu kata:

POSITIVE

Sepertinya mencintai stroberi adalah sentimen positif!

10. Pembuatan Augmented Reality

LLM dilatih dengan teks dalam jumlah besar. Namun, pengetahuan mereka hanya mencakup informasi yang dilihat selama pelatihan. Jika ada informasi baru yang dirilis setelah batas waktu pelatihan model, detail tersebut tidak akan tersedia untuk model. Dengan demikian, model tidak akan mampu menjawab pertanyaan tentang informasi yang belum dilihat.

Itulah sebabnya pendekatan seperti Retrieval Augmented Generation (RAG) membantu memberikan informasi tambahan yang mungkin perlu diketahui LLM untuk memenuhi permintaan penggunanya, untuk membalas dengan informasi yang mungkin lebih baru atau berdasarkan informasi pribadi yang tidak dapat diakses pada waktu pelatihan.

Mari kita kembali ke percakapan. Kali ini, Anda akan dapat mengajukan pertanyaan tentang dokumen Anda. Anda akan membangun chatbot yang mampu mengambil informasi yang relevan dari database yang berisi bagian dokumen yang lebih kecil ("bagian") dan informasi tersebut akan digunakan oleh model untuk mendasarkan jawabannya, alih-alih hanya mengandalkan pengetahuan yang terdapat dalam pelatihannya.

Dalam RAG, terdapat dua fase:

  1. Fase penyerapan — Dokumen dimuat dalam memori, dibagi menjadi potongan-potongan yang lebih kecil, dan embedding vektor (representasi vektor multidimensi tinggi dari potongan tersebut) dihitung dan disimpan dalam database vektor yang mampu melakukan penelusuran semantik. Fase penyerapan ini biasanya dilakukan satu kali, saat dokumen baru perlu ditambahkan ke korpus dokumen.

cd07d33d20ffa1c8.png

  1. Fase kueri — Pengguna kini dapat mengajukan pertanyaan tentang dokumen. Pertanyaan itu juga akan diubah menjadi vektor dan dibandingkan dengan semua vektor lain dalam {i>database<i}. Vektor yang paling mirip biasanya terkait secara semantik dan ditampilkan oleh database vektor. Kemudian, LLM diberi konteks percakapan, potongan-potongan teks yang sesuai dengan vektor yang ditampilkan oleh {i>database<i}, dan diminta untuk mendasarkan jawabannya dengan melihat potongan-potongan tersebut.

a1d2e2deb83c6d27.png

Menyiapkan dokumen

Untuk demo baru ini, Anda akan mengajukan pertanyaan tentang panduan "Attention is all you need" makalah penelitian. Studi tersebut menjelaskan arsitektur jaringan neural transformator, yang dipelopori oleh Google, dan merupakan cara penerapan semua model bahasa besar modern saat ini.

Makalah ini sudah didownload ke attention-is-all-you-need.pdf di repositori.

Mengimplementasikan chatbot

Mari pelajari cara membangun pendekatan 2 fase: pertama dengan penyerapan dokumen, lalu waktu kueri saat pengguna mengajukan pertanyaan tentang dokumen.

Dalam contoh ini, kedua fase diimplementasikan di class yang sama. Biasanya, Anda memiliki satu aplikasi yang menangani penyerapan, dan aplikasi lain yang menawarkan antarmuka chatbot kepada pengguna.

Selain itu, dalam contoh ini kita akan menggunakan database vektor dalam memori. Dalam skenario produksi yang sebenarnya, fase penyerapan dan kueri akan dipisahkan dalam dua aplikasi yang berbeda, dan vektor tersebut disimpan dalam database mandiri.

Penyerapan dokumen

Langkah pertama dari fase penyerapan dokumen adalah menemukan file PDF yang sudah didownload, dan menyiapkan PdfParser untuk membacanya:

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

Alih-alih membuat model bahasa chat biasa, Anda harus membuat instance model penyematan. Ini adalah model tertentu yang perannya adalah membuat representasi vektor dari potongan teks (kata, kalimat, atau bahkan paragraf). Metode ini mengembalikan vektor bilangan floating point, bukan menampilkan respons teks.

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

Selanjutnya, Anda memerlukan beberapa kelas untuk dikerjakan bersama guna:

  • Muat dan bagi dokumen PDF dalam beberapa bagian.
  • Membuat embedding vektor untuk semua potongan ini.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

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

Instance InMemoryEmbeddingStore, yang merupakan database vektor dalam memori, dibuat untuk menyimpan embedding vektor.

Dokumen ini dibagi menjadi beberapa bagian berkat class DocumentSplitters. Ini akan membagi teks file PDF menjadi cuplikan 500 karakter, dengan tumpang tindih 100 karakter (dengan potongan berikut, untuk menghindari pemotongan kata atau kalimat, menjadi per bagian).

Penyerapan penyimpanan menghubungkan pemisah dokumen, model embedding untuk menghitung vektor, dan database vektor dalam memori. Kemudian, metode ingest() akan menangani penyerapan.

Sekarang, fase pertama selesai, dokumen telah diubah menjadi potongan teks dengan embedding vektor terkait, dan disimpan dalam database vektor.

Mengajukan pertanyaan

Saatnya bersiap mengajukan pertanyaan! Buat model chat untuk memulai percakapan:

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

Anda juga memerlukan class retriever untuk menautkan database vektor (dalam variabel embeddingStore) dengan model embedding. Tugasnya adalah mengkueri database vektor dengan menghitung embedding vektor untuk kueri pengguna, untuk menemukan vektor serupa dalam database:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Di luar metode utama, buat antarmuka yang merepresentasikan asisten pakar LLM, yaitu antarmuka yang akan diimplementasikan oleh class AiServices agar Anda dapat berinteraksi dengan model:

interface LlmExpert {
    String ask(String question);
}

Pada tahap ini, Anda dapat mengonfigurasi layanan AI baru:

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

Layanan ini mengikat bersama:

  • Model bahasa chat yang Anda konfigurasi sebelumnya.
  • Memori chat untuk melacak percakapan.
  • Pengambil membandingkan kueri penyematan vektor dengan vektor dalam database.
  • Template perintah secara eksplisit menyatakan bahwa model chat harus membalas dengan mendasarkan responsnya pada informasi yang diberikan (yaitu cuplikan relevan dari dokumentasi yang penyematan vektornya mirip dengan vektor pertanyaan pengguna).
.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())

Akhirnya Anda siap mengajukan pertanyaan!

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

Kode sumber lengkap ada di RAG.java di direktori app/src/main/java/gemini/workshop:

Jalankan contoh:

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

Di output, Anda akan melihat jawaban atas pertanyaan Anda:

=== 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. Panggilan fungsi

Ada juga situasi saat Anda ingin LLM memiliki akses ke sistem eksternal, seperti API web jarak jauh yang mengambil informasi atau memiliki tindakan, maupun layanan yang melakukan semacam komputasi. Contoh:

API web jarak jauh:

  • Melacak dan memperbarui pesanan pelanggan.
  • Temukan atau buat tiket di issue tracker.
  • Ambil data real-time seperti harga saham atau pengukuran sensor IoT.
  • Kirim email.

Alat komputasi:

  • Kalkulator untuk soal matematika tingkat lanjut.
  • Interpretasi kode untuk menjalankan kode saat LLM memerlukan logika penalaran.
  • Mengonversi permintaan bahasa alami menjadi kueri SQL sehingga LLM dapat melakukan kueri database.

Panggilan fungsi adalah kemampuan model untuk meminta satu atau beberapa panggilan fungsi dilakukan atas namanya, sehingga dapat menjawab perintah pengguna dengan tepat menggunakan data yang lebih baru.

Dengan mempertimbangkan perintah tertentu dari pengguna, dan pengetahuan fungsi yang ada yang mungkin relevan dengan konteks tersebut, LLM dapat membalas dengan permintaan panggilan fungsi. Aplikasi yang mengintegrasikan LLM kemudian dapat memanggil fungsi, lalu membalas LLM dengan respons, kemudian LLM kemudian menafsirkan kembali dengan memberikan jawaban tekstual.

Empat langkah untuk melakukan panggilan fungsi

Mari kita lihat contoh panggilan fungsi: mendapatkan informasi tentang prakiraan cuaca.

Jika Anda bertanya kepada Gemini atau LLM lain tentang cuaca di Paris, mereka akan menjawab dengan mengatakan bahwa Gemini atau LLM lain tidak memiliki informasi tentang perkiraan cuaca. Jika ingin LLM memiliki akses real time ke data cuaca, Anda perlu menentukan beberapa fungsi yang dapat digunakannya.

Lihat diagram berikut:

31e0c2aba5e6f21c.pngS

1️⃣ Pertama, pengguna bertanya tentang cuaca di Paris. Aplikasi chatbot mengetahui bahwa ada satu atau beberapa fungsi yang dapat digunakan untuk membantu LLM memenuhi kueri. Chatbot mengirimkan perintah awal, serta daftar fungsi yang dapat dipanggil. Di sini, fungsi bernama getWeather() yang menggunakan parameter string untuk lokasi.

8863be53a73c4a70.png

Karena LLM tidak mengetahui prakiraan cuaca, LLM akan mengirim kembali permintaan eksekusi fungsi, alih-alih membalas melalui teks. Chatbot harus memanggil fungsi getWeather() dengan "Paris" sebagai parameter lokasi.

d1367cc69c07b14d.png

2️⃣ Chatbot memanggil fungsi tersebut atas nama LLM, mengambil respons fungsi. Di sini, kita bayangkan bahwa responsnya adalah {"forecast": "sunny"}.

73a5f2ed19f47d8.pngS

3️⃣ Aplikasi chatbot mengirimkan respons JSON kembali ke LLM.

20832cb1ee6fbfeb.pngS

4️⃣ LLM melihat respons JSON, menafsirkan informasi tersebut, dan akhirnya membalas dengan teks bahwa cuaca di Paris cerah.

Setiap langkah sebagai kode

Pertama, Anda akan mengonfigurasi model Gemini seperti biasa:

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

Anda menentukan spesifikasi alat yang menjelaskan fungsi yang dapat dipanggil:

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

Nama fungsi telah ditentukan, nama dan jenis parameternya, tetapi perhatikan bahwa baik fungsi maupun parameternya diberi deskripsi. Deskripsi sangat penting dan membantu LLM benar-benar memahami apa yang dapat dilakukan suatu fungsi, dan dengan demikian menilai apakah fungsi ini perlu dipanggil dalam konteks percakapan.

Mari kita mulai langkah #1, dengan mengirimkan pertanyaan awal tentang cuaca di Paris:

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

Pada langkah #2, kita meneruskan alat yang kita inginkan untuk digunakan model, dan model akan membalas dengan permintaan eksekusi yang terlalu tinggi:

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

Langkah #3. Pada tahap ini, kita tahu fungsi apa yang perlu dipanggil oleh LLM. Dalam kode, kita tidak membuat panggilan nyata ke API eksternal, kita hanya menampilkan hipotesis perkiraan cuaca secara langsung:

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

Dan di langkah #4, LLM mempelajari hasil eksekusi fungsi, lalu dapat menyintesis respons tekstual:

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

Outputnya adalah:

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.

Anda dapat melihat output di atas permintaan eksekusi fitur, beserta jawabannya.

Kode sumber lengkap ada di FunctionCalling.java di direktori app/src/main/java/gemini/workshop:

Jalankan contoh:

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

Anda akan melihat output yang mirip dengan berikut ini:

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 menangani panggilan fungsi

Pada langkah sebelumnya, Anda telah melihat bagaimana interaksi pertanyaan/jawaban teks normal dan interaksi permintaan/respons teks disisipi, dan di antaranya, Anda memberikan respons fungsi yang diminta secara langsung, tanpa memanggil fungsi sebenarnya.

Namun, LangChain4j juga menawarkan abstraksi tingkat lebih tinggi yang dapat menangani panggilan fungsi secara transparan untuk Anda, sambil menangani percakapan seperti biasa.

Panggilan fungsi tunggal

Mari kita lihat FunctionCallingAssistant.java, bagian demi bagian.

Pertama, Anda akan membuat data yang akan mewakili struktur data respons fungsi:

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

Respons akan berisi informasi tentang lokasi, perkiraan cuaca, dan suhu.

Kemudian, Anda membuat class yang berisi fungsi sebenarnya yang ingin Anda sediakan untuk model:

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

Perhatikan bahwa class ini berisi satu fungsi, tetapi dianotasi dengan anotasi @Tool yang sesuai dengan deskripsi fungsi yang dapat diminta oleh model untuk dipanggil.

Parameter fungsi (satu parameter di sini) juga dianotasi, tetapi dengan anotasi @P singkat ini, yang juga memberikan deskripsi parameter. Anda dapat menambahkan fungsi sebanyak yang diinginkan, membuatnya tersedia untuk model, untuk skenario yang lebih kompleks.

Di class ini, Anda akan menampilkan beberapa template pesan, tetapi jika ingin memanggil layanan prakiraan cuaca eksternal yang sebenarnya, layanan ini ada dalam isi metode yang akan Anda panggil ke layanan tersebut.

Seperti yang telah kita lihat saat membuat ToolSpecification dalam pendekatan sebelumnya, penting untuk mendokumentasikan fungsi fungsi, dan menjelaskan hal yang berkaitan dengan parameter tersebut. Hal ini membantu model memahami bagaimana dan kapan fungsi ini dapat digunakan.

Selanjutnya, LangChain4j memungkinkan Anda menyediakan antarmuka yang sesuai dengan kontrak yang ingin Anda gunakan untuk berinteraksi dengan model. Di sini, ini adalah antarmuka sederhana yang mengambil string yang mewakili pesan pengguna, dan menampilkan string yang sesuai dengan respons model:

interface WeatherAssistant {
    String chat(String userMessage);
}

Anda juga dapat menggunakan tanda tangan yang lebih kompleks yang melibatkan UserMessage LangChain4j (untuk pesan pengguna) atau AiMessage (untuk respons model), atau bahkan TokenStream, jika Anda ingin menangani situasi yang lebih canggih, karena objek yang lebih rumit tersebut juga berisi informasi tambahan seperti jumlah token yang dikonsumsi, dll. Namun, agar lebih mudah, kita hanya akan mengambil string dalam input, dan string dalam output.

Mari kita selesaikan dengan metode main() yang menggabungkan semua bagian:

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

Seperti biasa, Anda mengonfigurasi model percakapan Gemini. Lalu, buat instance layanan prakiraan cuaca yang berisi "fungsi" yang akan diminta model untuk kita panggil.

Sekarang, Anda akan menggunakan class AiServices lagi untuk mengikat model chat, memori chat, dan alat (yaitu layanan prakiraan cuaca dengan fungsinya). AiServices menampilkan objek yang mengimplementasikan antarmuka WeatherAssistant yang telah Anda tentukan. Satu-satunya hal yang tersisa adalah memanggil metode chat() asisten itu. Saat memanggilnya, Anda hanya akan melihat respons teks, tetapi permintaan panggilan fungsi dan respons panggilan fungsi tidak akan terlihat dari developer, dan permintaan tersebut akan ditangani secara otomatis dan transparan. Jika menganggap suatu fungsi harus dipanggil, Gemini akan membalas dengan permintaan panggilan fungsi, dan LangChain4j akan menangani panggilan fungsi lokal untuk Anda.

Jalankan contoh:

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

Anda akan melihat output yang mirip dengan berikut ini:

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

Ini adalah contoh fungsi tunggal.

Beberapa panggilan fungsi

Anda juga dapat memiliki beberapa fungsi dan mengizinkan LangChain4j menangani beberapa panggilan fungsi atas nama Anda. Lihat MultiFunctionCallingAssistant.java untuk melihat beberapa contoh fungsi.

Ini memiliki fungsi untuk mengonversi mata uang:

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

Fungsi lain untuk mendapatkan nilai saham:

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

Fungsi lain untuk menerapkan persentase ke jumlah tertentu:

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

Anda kemudian dapat menggabungkan semua fungsi ini dan kelas MultiTools dan mengajukan pertanyaan seperti "Berapa 10% dari harga saham AAPL dikonversi dari USD ke 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?"));
}

Jalankan sebagai berikut:

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

Dan Anda akan melihat beberapa fungsi bernama:

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.

Kepada Agen

Panggilan fungsi adalah mekanisme ekstensi yang bagus untuk model bahasa besar seperti Gemini. Layanan ini memungkinkan kami membangun sistem yang lebih kompleks yang sering disebut "agen" atau "asisten AI". Agen ini dapat berinteraksi dengan dunia eksternal melalui API eksternal dan dengan layanan yang dapat memiliki efek samping terhadap lingkungan eksternal (seperti mengirim email, membuat tiket, dll.)

Saat membuat agen yang ampuh, Anda harus melakukannya secara bertanggung jawab. Anda harus mempertimbangkan interaksi manusia sebelum melakukan tindakan otomatis. Penting untuk mengingat keamanan saat mendesain agen berteknologi LLM yang berinteraksi dengan dunia eksternal.

13. Menjalankan Gemma dengan Ollama dan TestContainers

Sejauh ini, kami telah menggunakan Gemini, tetapi ada juga Gemma, yang merupakan model adik perempuannya.

Gemma adalah rangkaian model terbuka yang ringan dan canggih, dibangun dari riset dan teknologi yang sama dengan yang digunakan untuk membuat model Gemini. Gemma tersedia dalam dua variasi Gemma1 dan Gemma2 masing-masing dengan berbagai ukuran. Gemma1 tersedia dalam dua ukuran: 2B dan 7B. Gemma2 tersedia dalam dua ukuran: 9B dan 27B. Bobotnya tersedia gratis, dan ukurannya yang kecil berarti Anda dapat menjalankannya sendiri, bahkan di laptop Anda atau di Cloud Shell.

Bagaimana cara menjalankan Gemma?

Ada banyak cara untuk menjalankan Gemma: di cloud, melalui Vertex AI dengan sekali klik, atau GKE dengan beberapa GPU, tetapi Anda juga dapat menjalankannya secara lokal.

Salah satu opsi yang bagus untuk menjalankan Gemma secara lokal adalah dengan Ollama, alat yang memungkinkan Anda menjalankan model kecil, seperti Llama 2, Mistral, dan banyak lagi di komputer lokal Anda. Mirip dengan Docker, tetapi untuk LLM.

Instal Ollama dengan mengikuti petunjuk untuk Sistem Operasi Anda.

Jika menggunakan lingkungan Linux, Anda harus mengaktifkan Ollama terlebih dahulu setelah menginstalnya.

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

Setelah diinstal secara lokal, Anda dapat menjalankan perintah untuk mengambil model:

ollama pull gemma:2b

Tunggu hingga model ditarik. Proses ini dapat memerlukan waktu agak lama.

Jalankan model:

ollama run gemma:2b

Sekarang, Anda dapat berinteraksi dengan model:

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

Untuk keluar dari perintah, tekan Ctrl+D

Menjalankan Gemma di Ollama pada TestContainer

Daripada harus menginstal dan menjalankan Ollama secara lokal, Anda dapat menggunakan Ollama di dalam container, yang ditangani oleh TestContainers.

TestContainers tidak hanya berguna untuk pengujian, tetapi Anda juga dapat menggunakannya untuk menjalankan container. Bahkan ada OllamaContainer spesifik yang dapat Anda manfaatkan.

Berikut gambaran lengkapnya:

2382c05a48708dfd.png

Penerapan

Mari kita lihat GemmaWithOllamaContainer.java, bagian demi bagian.

Pertama, Anda perlu membuat container Ollama turunan yang menarik model Gemma. Gambar ini sudah ada dari proses sebelumnya atau akan dibuat. Jika image tersebut sudah ada, Anda hanya akan memberi tahu TestContainers bahwa Anda ingin mengganti image Ollama default dengan varian yang didukung 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"));
    }
}

Selanjutnya, Anda membuat dan memulai container pengujian Ollama, lalu membuat model chat Ollama, dengan mengarahkan ke alamat dan port container menggunakan model yang ingin Anda gunakan. Terakhir, Anda cukup memanggil model.generate(yourPrompt) seperti biasa:

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

Jalankan sebagai berikut:

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

Proses pertama akan memakan waktu beberapa saat untuk membuat dan menjalankan container, tetapi setelah selesai, Anda akan melihat Gemma merespons:

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.

Anda memiliki Gemma yang berjalan di Cloud Shell!

14. Selamat

Selamat, Anda telah berhasil membangun aplikasi chat AI Generatif pertama Anda di Java menggunakan LangChain4j dan Gemini API. Selama ini, Anda telah mendapati bahwa model bahasa besar multimodal cukup canggih dan mampu menangani berbagai tugas seperti tanya jawab, bahkan pada dokumentasi Anda sendiri, ekstraksi data, berinteraksi dengan API eksternal, dan banyak lagi.

Apa selanjutnya?

Sekarang giliran Anda untuk meningkatkan kualitas aplikasi dengan integrasi LLM yang canggih.

Bacaan lebih lanjut

Dokumen referensi