Gemini trong Java với Vertex AI và LangChain4j

1. Giới thiệu

Lớp học lập trình này tập trung vào Mô hình ngôn ngữ lớn (LLM) Gemini, được lưu trữ trên Vertex AI trên Google Cloud. Vertex AI là một nền tảng bao gồm tất cả các sản phẩm, dịch vụ và mô hình học máy trên Google Cloud.

Bạn sẽ sử dụng Java để tương tác với Gemini API bằng khung LangChain4j. Bạn sẽ xem các ví dụ cụ thể để tận dụng LLM cho việc trả lời câu hỏi, tạo ý tưởng, trích xuất thực thể và nội dung có cấu trúc, tạo nội dung tăng cường truy xuất và gọi hàm.

AI tạo sinh là gì?

AI tạo sinh là hình thức sử dụng trí tuệ nhân tạo để tạo ra nội dung mới, chẳng hạn như văn bản, hình ảnh, nhạc, âm thanh và video.

AI tạo sinh được hỗ trợ bởi các mô hình ngôn ngữ lớn (LLM) có thể thực hiện nhiều tác vụ và các tác vụ không có sẵn như tóm tắt, hỏi đáp, phân loại, v.v. Với quá trình huấn luyện tối thiểu, các mô hình cơ bản có thể được điều chỉnh cho các trường hợp sử dụng được nhắm mục tiêu với rất ít dữ liệu mẫu.

AI tạo sinh hoạt động như thế nào?

AI tạo sinh hoạt động bằng cách sử dụng mô hình Học máy (ML) để tìm hiểu các mẫu và mối quan hệ trong tập dữ liệu về nội dung do con người tạo. Sau đó, công cụ này sẽ sử dụng các mẫu đã học để tạo nội dung mới.

Cách phổ biến nhất để huấn luyện mô hình AI tạo sinh là sử dụng phương pháp học có giám sát. Mô hình được cung cấp một tập hợp nội dung do con người tạo và các nhãn tương ứng. Sau đó, công nghệ này sẽ học cách tạo nội dung tương tự như nội dung do con người tạo.

AI tạo sinh có những ứng dụng phổ biến nào?

AI tạo sinh có thể được dùng để:

  • Cải thiện hoạt động tương tác của khách hàng thông qua trải nghiệm trò chuyện và tìm kiếm nâng cao.
  • Khám phá lượng lớn dữ liệu không có cấu trúc thông qua các giao diện trò chuyện và bản tóm tắt.
  • Hỗ trợ các nhiệm vụ lặp đi lặp lại như trả lời yêu cầu đề xuất, bản địa hoá nội dung tiếp thị bằng nhiều ngôn ngữ, kiểm tra hợp đồng của khách hàng để đảm bảo tuân thủ, v.v.

Google Cloud có những giải pháp AI tạo sinh nào?

Với Vertex AI, bạn có thể tương tác, tuỳ chỉnh và nhúng các mô hình nền tảng vào ứng dụng của mình mà không cần có nhiều kiến thức chuyên môn về máy học. Bạn có thể truy cập vào các mô hình nền tảng trên Model Garden, điều chỉnh mô hình thông qua giao diện người dùng đơn giản trên Vertex AI Studio hoặc sử dụng mô hình trong sổ tay khoa học dữ liệu.

Vertex AI Search and Conversation cung cấp cho nhà phát triển cách nhanh nhất để tạo công cụ tìm kiếm và chatbot dựa trên AI tạo sinh.

Gemini cho Google Cloud là một công cụ cộng tác dựa trên AI, được cung cấp trên Google Cloud và các IDE để giúp bạn hoàn thành nhiều việc hơn một cách nhanh chóng hơn. Gemini Code Assist cung cấp tính năng hoàn tất mã, tạo mã, giải thích mã và cho phép bạn trò chuyện với công cụ này để đặt câu hỏi kỹ thuật.

Gemini là gì?

Gemini là một nhóm mô hình AI tạo sinh do Google DeepMind phát triển, được thiết kế cho các trường hợp sử dụng đa phương thức. Đa phương thức có nghĩa là mô hình này có thể xử lý và tạo nhiều loại nội dung như văn bản, mã, hình ảnh và âm thanh.

b9913d011999e7c7.png

Gemini có nhiều biến thể và kích thước:

  • Gemini Ultra: Phiên bản lớn nhất, mạnh nhất để xử lý các tác vụ phức tạp.
  • Gemini Flash: Nhanh nhất và tiết kiệm chi phí nhất, được tối ưu hoá cho các tác vụ có khối lượng lớn.
  • Gemini Pro: Kích thước trung bình, được tối ưu hoá để mở rộng quy mô trên nhiều tác vụ.
  • Gemini Nano: Hiệu quả nhất, được thiết kế để xử lý các tác vụ trên thiết bị.

Các tính năng chính:

  • Đa phương thức: Khả năng của Gemini trong việc hiểu và xử lý nhiều định dạng thông tin là một bước tiến đáng kể so với các mô hình ngôn ngữ chỉ có văn bản truyền thống.
  • Hiệu suất: Gemini Ultra vượt trội so với các công nghệ hiện đại trên nhiều điểm chuẩn và là mô hình đầu tiên vượt qua các chuyên gia con người về điểm chuẩn MMLU (Hiểu ngôn ngữ đa nhiệm khổng lồ) đầy thách thức.
  • Linh hoạt: Các kích thước khác nhau của Gemini giúp bạn có thể điều chỉnh cho nhiều trường hợp sử dụng, từ nghiên cứu trên quy mô lớn đến triển khai trên thiết bị di động.

Làm cách nào để tương tác với Gemini trên Vertex AI từ Java?

Bạn có hai lựa chọn:

  1. Thư viện Vertex AI Java API cho Gemini chính thức.
  2. Khung LangChain4j.

Trong lớp học lập trình này, bạn sẽ sử dụng khung LangChain4j.

Khung LangChain4j là gì?

Khung LangChain4j là một thư viện nguồn mở để tích hợp LLM vào các ứng dụng Java của bạn, bằng cách điều phối nhiều thành phần, chẳng hạn như chính LLM, nhưng cũng có các công cụ khác như cơ sở dữ liệu vectơ (dành cho tìm kiếm ngữ nghĩa), trình tải và trình chia tài liệu (để phân tích tài liệu và học hỏi từ các tài liệu đó), trình phân tích cú pháp đầu ra, v.v.

Dự án này được lấy cảm hứng từ dự án Python LangChain nhưng với mục tiêu phục vụ các nhà phát triển Java.

bb908ea1e6c96ac2.png

Kiến thức bạn sẽ học được

  • Cách thiết lập dự án Java để sử dụng Gemini và LangChain4j
  • Cách gửi câu lệnh đầu tiên đến Gemini theo phương thức lập trình
  • Cách truyền trực tuyến câu trả lời từ Gemini
  • Cách tạo cuộc trò chuyện giữa người dùng và Gemini
  • Cách sử dụng Gemini trong ngữ cảnh đa phương thức bằng cách gửi cả văn bản và hình ảnh
  • Cách trích xuất thông tin có cấu trúc hữu ích từ nội dung không có cấu trúc
  • Cách thao tác với mẫu câu lệnh
  • Cách phân loại văn bản, chẳng hạn như phân tích cảm xúc
  • Cách trò chuyện với tài liệu của riêng bạn (Tạo dữ liệu tăng cường truy xuất)
  • Cách mở rộng chatbot bằng lệnh gọi hàm
  • Cách sử dụng Gemma cục bộ với Ollama và TestContainers

Bạn cần có

  • Có kiến thức về ngôn ngữ lập trình Java
  • Một dự án trên Google Cloud
  • Một trình duyệt, chẳng hạn như Chrome hoặc Firefox

2. Cách thiết lập và yêu cầu

Thiết lập môi trường theo tốc độ của riêng bạn

  1. Đăng nhập vào Google Cloud Console rồi tạo một dự án mới hoặc sử dụng lại một dự án hiện có. Nếu chưa có tài khoản Gmail hoặc Google Workspace, bạn phải tạo một tài khoản.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Tên dự án là tên hiển thị cho người tham gia dự án này. Đây là một chuỗi ký tự không được API của Google sử dụng. Bạn luôn có thể cập nhật thông tin này.
  • Mã dự án là duy nhất trên tất cả các dự án Google Cloud và không thể thay đổi (không thể thay đổi sau khi đặt). Cloud Console sẽ tự động tạo một chuỗi duy nhất; thường thì bạn không cần quan tâm đến chuỗi này. Trong hầu hết các lớp học lập trình, bạn sẽ cần tham chiếu đến Mã dự án (thường được xác định là PROJECT_ID). Nếu không thích mã được tạo, bạn có thể tạo một mã ngẫu nhiên khác. Ngoài ra, bạn có thể thử dùng email của riêng mình để xem có thể sử dụng hay không. Bạn không thể thay đổi thông tin này sau bước này và thông tin này sẽ được giữ nguyên trong suốt thời gian diễn ra dự án.
  • Xin lưu ý rằng có một giá trị thứ ba là Mã dự án mà một số API sử dụng. Tìm hiểu thêm về cả ba giá trị này trong tài liệu.
  1. Tiếp theo, bạn cần bật tính năng thanh toán trong Cloud Console để sử dụng các tài nguyên/API trên Cloud. Việc tham gia lớp học lập trình này sẽ không tốn kém nhiều chi phí, nếu có. Để tắt các tài nguyên nhằm tránh bị tính phí sau khi hoàn tất hướng dẫn này, bạn có thể xoá các tài nguyên đã tạo hoặc xoá dự án. Người dùng mới của Google Cloud đủ điều kiện tham gia chương trình Dùng thử miễn phí 300 USD.

Khởi động Cloud Shell

Mặc dù có thể điều khiển Google Cloud từ xa trên máy tính xách tay, nhưng trong lớp học lập trình này, bạn sẽ sử dụng Cloud Shell, một môi trường dòng lệnh chạy trên đám mây.

Kích hoạt Cloud Shell

  1. Trên Cloud Console, hãy nhấp vào Kích hoạt Cloud Shell 853e55310c205094.png.

3c1dabeca90e44e5.png

Nếu đây là lần đầu tiên bạn khởi động Cloud Shell, bạn sẽ thấy một màn hình trung gian mô tả về Cloud Shell. Nếu bạn thấy một màn hình trung gian, hãy nhấp vào Tiếp tục.

9c92662c6a846a5c.png

Quá trình cấp phép và kết nối với Cloud Shell chỉ mất vài phút.

9f0e51b578fecce5.png

Máy ảo này được tải sẵn tất cả các công cụ phát triển cần thiết. Ứng dụng này cung cấp một thư mục gốc 5 GB ổn định và chạy trong Google Cloud, giúp cải thiện đáng kể hiệu suất mạng và xác thực. Bạn có thể thực hiện hầu hết (nếu không phải tất cả) công việc trong lớp học lập trình này bằng trình duyệt.

Sau khi kết nối với Cloud Shell, bạn sẽ thấy mình đã được xác thực và dự án được đặt thành mã dự án của bạn.

  1. Chạy lệnh sau trong Cloud Shell để xác nhận rằng bạn đã được xác thực:
gcloud auth list

Kết quả của lệnh

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Chạy lệnh sau trong Cloud Shell để xác nhận rằng lệnh gcloud biết về dự án của bạn:
gcloud config list project

Kết quả của lệnh

[core]
project = <PROJECT_ID>

Nếu không, bạn có thể đặt giá trị này bằng lệnh sau:

gcloud config set project <PROJECT_ID>

Kết quả của lệnh

Updated property [core/project].

3. Chuẩn bị môi trường phát triển

Trong lớp học lập trình này, bạn sẽ sử dụng cửa sổ dòng lệnh Cloud Shell và trình soạn thảo Cloud Shell để phát triển các chương trình Java.

Bật API Vertex AI

Trong Google Cloud Console, hãy đảm bảo tên dự án của bạn xuất hiện ở đầu Google Cloud Console. Nếu không, hãy nhấp vào Chọn dự án để mở Trình chọn dự án rồi chọn dự án bạn muốn.

Bạn có thể bật API Vertex AI từ mục Vertex AI trong Google Cloud Console hoặc từ thiết bị đầu cuối Cloud Shell.

Để bật từ bảng điều khiển Google Cloud, trước tiên, hãy chuyển đến phần Vertex AI trong trình đơn bảng điều khiển Google Cloud:

451976f1c8652341.png

Nhấp vào Bật tất cả API được đề xuất trong trang tổng quan của Vertex AI.

Thao tác này sẽ bật một số API, nhưng API quan trọng nhất cho lớp học lập trình này là aiplatform.googleapis.com.

Ngoài ra, bạn cũng có thể bật API này từ dòng lệnh Cloud Shell bằng lệnh sau:

gcloud services enable aiplatform.googleapis.com

Sao chép kho lưu trữ GitHub

Trong dòng lệnh Cloud Shell, hãy sao chép kho lưu trữ cho lớp học lập trình này:

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

Để kiểm tra xem dự án đã sẵn sàng chạy hay chưa, bạn có thể thử chạy chương trình "Hello World".

Đảm bảo bạn đang ở thư mục cấp cao nhất:

cd gemini-workshop-for-java-developers/ 

Tạo trình bao bọc Gradle:

gradle wrapper

Chạy bằng gradlew:

./gradlew run

Bạn sẽ thấy kết quả sau đây:

..
> Task :app:run
Hello World!

Mở và thiết lập Cloud Editor

Mở mã bằng Trình soạn thảo mã trên đám mây từ Cloud Shell:

42908e11b28f4383.png

Trong Cloud Code Editor, hãy mở thư mục nguồn của lớp học lập trình bằng cách chọn File -> Open Folder rồi trỏ đến thư mục nguồn của lớp học lập trình (ví dụ: /home/username/gemini-workshop-for-java-developers/).

Thiết lập biến môi trường

Mở một cửa sổ dòng lệnh mới trong Cloud Code Editor bằng cách chọn Terminal -> New Terminal. Thiết lập hai biến môi trường cần thiết để chạy các ví dụ về mã:

  • PROJECT_ID – Mã dự án của bạn trên Google Cloud
  • LOCATION – Khu vực triển khai mô hình Gemini

Xuất các biến như sau:

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

4. Lệnh gọi đầu tiên đến mô hình Gemini

Giờ đây, khi dự án đã được thiết lập đúng cách, đã đến lúc gọi Gemini API.

Hãy xem QA.java trong thư mục 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-002")
            .build();

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

Trong ví dụ đầu tiên này, bạn cần nhập lớp VertexAiGeminiChatModel để triển khai giao diện ChatModel.

Trong phương thức main, bạn định cấu hình mô hình ngôn ngữ trò chuyện bằng cách sử dụng trình tạo cho VertexAiGeminiChatModel và chỉ định:

  • Dự án
  • Vị trí
  • Tên mô hình (gemini-1.5-flash-002).

Giờ đây, khi mô hình ngôn ngữ đã sẵn sàng, bạn có thể gọi phương thức generate() và truyền câu lệnh, câu hỏi hoặc hướng dẫn để gửi đến LLM. Ở đây, bạn đặt một câu hỏi đơn giản về lý do khiến bầu trời có màu xanh dương.

Bạn có thể thay đổi câu lệnh này để thử các câu hỏi hoặc nhiệm vụ khác.

Chạy mẫu tại thư mục gốc của mã nguồn:

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

Bạn sẽ thấy kết quả tương tự như sau:

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.

Xin chúc mừng! Bạn đã thực hiện cuộc gọi đầu tiên đến Gemini!

Phản hồi truyền trực tuyến

Bạn có nhận thấy rằng phản hồi được đưa ra ngay sau vài giây không? Bạn cũng có thể nhận được phản hồi theo từng phần nhờ biến thể phản hồi truyền trực tuyến. Phản hồi truyền trực tuyến, mô hình trả về từng phần phản hồi khi có sẵn.

Trong lớp học lập trình này, chúng ta sẽ sử dụng phản hồi không truyền trực tuyến, nhưng hãy xem phản hồi truyền trực tuyến để biết cách thực hiện.

Trong StreamQA.java trong thư mục app/src/main/java/gemini/workshop, bạn có thể thấy phản hồi truyền trực tuyến đang hoạt động:

package gemini.workshop;

import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;

import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;

public class StreamQA {
    public static void main(String[] args) {
        StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(4000)
            .build();

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

Lần này, chúng ta nhập các biến thể lớp truyền trực tuyến VertexAiGeminiStreamingChatModel triển khai giao diện StreamingChatLanguageModel. Bạn cũng cần nhập tĩnh LambdaStreamingResponseHandler.onNext. Đây là một phương thức tiện lợi cung cấp StreamingResponseHandler để tạo trình xử lý truyền trực tuyến bằng biểu thức lambda Java.

Lần này, chữ ký của phương thức generate() có một chút khác biệt. Thay vì trả về một chuỗi, loại dữ liệu trả về là rỗng. Ngoài lời nhắc, bạn phải truyền trình xử lý phản hồi phát trực tuyến. Tại đây, nhờ tính năng nhập tĩnh mà chúng ta đã đề cập ở trên, chúng ta có thể xác định một biểu thức lambda mà bạn truyền vào phương thức onNext(). Biểu thức lambda được gọi mỗi khi có một phần phản hồi mới, trong khi phần sau chỉ được gọi nếu có lỗi xảy ra.

Chạy:

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

Bạn sẽ nhận được câu trả lời tương tự như lớp trước, nhưng lần này, bạn sẽ nhận thấy câu trả lời xuất hiện dần trong shell thay vì chờ câu trả lời đầy đủ xuất hiện.

Cấu hình bổ sung

Đối với cấu hình, chúng ta chỉ xác định dự án, vị trí và tên mô hình, nhưng bạn có thể chỉ định các tham số khác cho mô hình:

  • temperature(Float temp) – để xác định mức độ sáng tạo mà bạn muốn phản hồi (0 là mức độ sáng tạo thấp và thường mang tính thực tế hơn, còn 2 là để tạo ra nhiều kết quả sáng tạo hơn)
  • topP(Float topP) – để chọn các từ có thể có tổng xác suất bằng số dấu phẩy động đó (từ 0 đến 1)
  • topK(Integer topK) – để chọn ngẫu nhiên một từ trong số tối đa từ có thể dùng để hoàn thành văn bản (từ 1 đến 40)
  • maxOutputTokens(Integer max) – để chỉ định độ dài tối đa của câu trả lời do mô hình đưa ra (thường thì 4 mã thông báo đại diện cho khoảng 3 từ)
  • maxRetries(Integer retries) – trong trường hợp bạn đã vượt quá hạn mức yêu cầu mỗi thời gian hoặc nền tảng đang gặp phải một số vấn đề kỹ thuật, bạn có thể yêu cầu mô hình thử lại lệnh gọi 3 lần

Cho đến nay, bạn đã đặt một câu hỏi cho Gemini, nhưng bạn cũng có thể trò chuyện với Gemini theo nhiều lượt. Đó là những gì bạn sẽ khám phá trong phần tiếp theo.

5. Trò chuyện với Gemini

Ở bước trước, bạn đã đặt một câu hỏi. Giờ là lúc để có một cuộc trò chuyện thực sự giữa người dùng và LLM. Mỗi câu hỏi và câu trả lời có thể dựa trên các câu hỏi và câu trả lời trước đó để tạo thành một cuộc thảo luận thực sự.

Hãy xem Conversation.java trong thư mục 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-002")
            .build();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
            .maxMessages(20)
            .build();

        interface ConversationService {
            String chat(String message);
        }

        ConversationService conversation =
            AiServices.builder(ConversationService.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        List.of(
            "Hello!",
            "What is the country where the Eiffel tower is situated?",
            "How many inhabitants are there in that country?"
        ).forEach( message -> {
            System.out.println("\nUser: " + message);
            System.out.println("Gemini: " + conversation.chat(message));
        });
    }
}

Một vài lệnh nhập mới thú vị trong lớp này:

  • MessageWindowChatMemory – một lớp giúp xử lý khía cạnh nhiều lượt của cuộc trò chuyện và lưu giữ các câu hỏi và câu trả lời trước đó trong bộ nhớ cục bộ
  • AiServices – một lớp trừu tượng cấp cao hơn sẽ liên kết mô hình trò chuyện và bộ nhớ trò chuyện

Trong phương thức chính, bạn sẽ thiết lập mô hình, bộ nhớ trò chuyện và dịch vụ AI. Mô hình được định cấu hình như bình thường với thông tin về dự án, vị trí và tên mô hình.

Đối với bộ nhớ trò chuyện, chúng ta sử dụng trình tạo của MessageWindowChatMemory để tạo bộ nhớ lưu giữ 20 tin nhắn đã trao đổi gần đây nhất. Đây là một cửa sổ trượt trên cuộc trò chuyện có ngữ cảnh được lưu trữ cục bộ trong ứng dụng lớp Java của chúng ta.

Sau đó, bạn tạo AI service liên kết mô hình trò chuyện với bộ nhớ trò chuyện.

Hãy lưu ý cách dịch vụ AI sử dụng giao diện ConversationService tuỳ chỉnh mà chúng ta đã xác định, mà LangChain4j triển khai, đồng thời lấy truy vấn String và trả về phản hồi String.

Giờ là lúc trò chuyện với Gemini. Trước tiên, ứng dụng sẽ gửi một lời chào đơn giản, sau đó là câu hỏi đầu tiên về tháp Eiffel để biết tháp này nằm ở quốc gia nào. Lưu ý rằng câu cuối cùng liên quan đến câu trả lời của câu hỏi đầu tiên, vì bạn thắc mắc có bao nhiêu cư dân ở quốc gia nơi có tháp Eiffel, mà không đề cập rõ ràng đến quốc gia được đưa ra trong câu trả lời trước đó. Điều này cho thấy rằng các câu hỏi và câu trả lời trước đây được gửi cùng với mọi câu lệnh.

Chạy mẫu:

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

Bạn sẽ thấy 3 câu trả lời tương tự như sau:

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.

Bạn có thể đặt câu hỏi một lượt hoặc trò chuyện nhiều lượt với Gemini, nhưng cho đến nay, dữ liệu đầu vào chỉ là văn bản. Còn hình ảnh thì sao? Hãy cùng khám phá hình ảnh trong bước tiếp theo.

6. Khả năng đa phương thức với Gemini

Gemini là một mô hình đa phương thức. Không chỉ chấp nhận văn bản làm dữ liệu đầu vào, mà còn chấp nhận hình ảnh hoặc thậm chí là video làm dữ liệu đầu vào. Trong phần này, bạn sẽ thấy một trường hợp sử dụng để kết hợp văn bản và hình ảnh.

Bạn có nghĩ Gemini sẽ nhận ra chú mèo này không?

af00516493ec9ade.png

Hình ảnh một chú mèo trên tuyết được lấy từ Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

Hãy xem Multimodal.java trong thư mục app/src/main/java/gemini/workshop:

package gemini.workshop;

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

public class Multimodal {

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


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

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

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

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

Trong các lệnh nhập, hãy lưu ý chúng ta phân biệt giữa các loại thông báo và nội dung. UserMessage có thể chứa cả đối tượng TextContentImageContent. Đây là tính năng đa phương thức: kết hợp văn bản và hình ảnh. Chúng ta không chỉ gửi một lời nhắc chuỗi đơn giản mà còn gửi một đối tượng có cấu trúc hơn đại diện cho thông báo của người dùng, bao gồm một phần nội dung hình ảnh và một phần nội dung văn bản. Mô hình sẽ gửi lại một Response chứa AiMessage.

Sau đó, bạn truy xuất AiMessage từ phản hồi thông qua content(), rồi truy xuất văn bản của thông báo nhờ text().

Chạy mẫu:

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

Tên của bức ảnh chắc chắn đã cho bạn biết nội dung của bức ảnh, nhưng kết quả của Gemini tương tự như sau:

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.

Việc kết hợp hình ảnh và lời nhắc văn bản sẽ mở ra nhiều trường hợp sử dụng thú vị. Bạn có thể tạo các ứng dụng có thể:

  • Nhận dạng văn bản trong ảnh.
  • Kiểm tra xem hình ảnh có an toàn để hiển thị hay không.
  • Tạo chú thích hình ảnh.
  • Tìm kiếm thông qua cơ sở dữ liệu hình ảnh có nội dung mô tả bằng văn bản thuần tuý.

Ngoài việc trích xuất thông tin từ hình ảnh, bạn cũng có thể trích xuất thông tin từ văn bản không có cấu trúc. Đó là những gì bạn sẽ tìm hiểu trong phần tiếp theo.

7. Trích xuất thông tin có cấu trúc từ văn bản không có cấu trúc

Có nhiều trường hợp thông tin quan trọng được cung cấp trong tài liệu báo cáo, trong email hoặc văn bản dài khác theo cách không có cấu trúc. Lý tưởng nhất là bạn có thể trích xuất các thông tin chi tiết chính có trong văn bản không có cấu trúc dưới dạng các đối tượng có cấu trúc. Hãy xem cách thực hiện.

Giả sử bạn muốn trích xuất tên và tuổi của một người, dựa trên tiểu sử, CV hoặc nội dung mô tả về người đó. Bạn có thể hướng dẫn LLM trích xuất JSON từ văn bản không có cấu trúc bằng một câu lệnh được điều chỉnh khéo léo (đây thường được gọi là "kỹ thuật câu lệnh").

Tuy nhiên, trong ví dụ bên dưới, thay vì tạo một lời nhắc mô tả đầu ra JSON, chúng ta sẽ sử dụng một tính năng mạnh mẽ của Gemini có tên là đầu ra có cấu trúc, hoặc đôi khi là giải mã có điều kiện, buộc mô hình chỉ xuất ra nội dung JSON hợp lệ, tuân theo giản đồ JSON đã chỉ định.

Hãy xem ExtractData.java trong app/src/main/java/gemini/workshop:

package gemini.workshop;

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

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

public class ExtractData {

    record Person(String name, int age) { }

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

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

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

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

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

Hãy cùng xem các bước trong tệp này:

  • Bản ghi Person được xác định để biểu thị thông tin chi tiết mô tả một người (tên và tuổi).
  • Giao diện PersonExtractor được xác định bằng một phương thức trả về một thực thể Person, với một chuỗi văn bản không có cấu trúc.
  • extractPerson() được chú thích bằng chú thích @SystemMessage liên kết lời nhắc hướng dẫn với chú thích đó. Đó là lời nhắc mà mô hình sẽ sử dụng để hướng dẫn trích xuất thông tin và trả về thông tin chi tiết ở dạng tài liệu JSON. Tài liệu này sẽ được phân tích cú pháp cho bạn và được chuyển đổi không mã hoá thành một thực thể Person.

Bây giờ, hãy xem nội dung của phương thức main():

  • Mô hình trò chuyện được định cấu hình và tạo bản sao. Chúng ta đang sử dụng 2 phương thức mới của lớp trình tạo mô hình: responseMimeType()responseSchema(). Lệnh đầu tiên yêu cầu Gemini tạo JSON hợp lệ trong kết quả. Phương thức thứ hai xác định giản đồ của đối tượng JSON cần được trả về. Hơn nữa, phương thức sau uỷ quyền cho một phương thức tiện lợi có thể chuyển đổi một lớp hoặc bản ghi Java thành giản đồ JSON thích hợp.
  • Đối tượng PersonExtractor được tạo nhờ lớp AiServices của LangChain4j.
  • Sau đó, bạn chỉ cần gọi Person person = extractor.extractPerson(...) để trích xuất thông tin chi tiết về người đó từ văn bản không có cấu trúc và lấy lại một thực thể Person có tên và tuổi.

Chạy mẫu:

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

Bạn sẽ thấy kết quả sau đây:

Anna
23

Vâng, tôi là Anna và tôi 23 tuổi!

Với phương pháp AiServices này, bạn sẽ thao tác với các đối tượng có kiểu mạnh. Bạn không tương tác trực tiếp với LLM. Thay vào đó, bạn đang làm việc với các lớp cụ thể, chẳng hạn như bản ghi Person để biểu thị thông tin cá nhân đã trích xuất và bạn có một đối tượng PersonExtractor với phương thức extractPerson() trả về một thực thể Person. Khái niệm LLM được trừu tượng hoá và với tư cách là nhà phát triển Java, bạn chỉ thao tác với các lớp và đối tượng thông thường khi sử dụng giao diện PersonExtractor này.

8. Sắp xếp câu lệnh bằng mẫu câu lệnh

Khi bạn tương tác với LLM bằng một bộ hướng dẫn hoặc câu hỏi phổ biến, một phần của câu lệnh đó sẽ không bao giờ thay đổi, trong khi các phần khác chứa dữ liệu. Ví dụ: nếu muốn tạo công thức nấu ăn, bạn có thể sử dụng câu lệnh như "Bạn là một đầu bếp tài năng, vui lòng tạo một công thức nấu ăn với các nguyên liệu sau: ...", sau đó thêm các nguyên liệu vào cuối văn bản đó. Đó là mục đích của các mẫu lời nhắc — tương tự như các chuỗi nội suy trong ngôn ngữ lập trình. Mẫu lời nhắc chứa phần giữ chỗ mà bạn có thể thay thế bằng dữ liệu phù hợp cho một lệnh gọi cụ thể đến LLM.

Cụ thể hơn, hãy nghiên cứu TemplatePrompt.java trong thư mục 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-002")
            .maxOutputTokens(500)
            .temperature(1.0f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

        PromptTemplate promptTemplate = PromptTemplate.from("""
            You're a friendly chef with a lot of cooking experience.
            Create a recipe for a {{dish}} with the following ingredients: \
            {{ingredients}}, and give it a name.
            """
        );

        Map<String, Object> variables = new HashMap<>();
        variables.put("dish", "dessert");
        variables.put("ingredients", "strawberries, chocolate, and whipped cream");

        Prompt prompt = promptTemplate.apply(variables);

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

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

Như thường lệ, bạn định cấu hình mô hình VertexAiGeminiChatModel, với mức độ sáng tạo cao, nhiệt độ cao và các giá trị topP và topK cao. Sau đó, bạn tạo một PromptTemplate bằng phương thức tĩnh from(), bằng cách truyền chuỗi của lời nhắc và sử dụng các biến phần giữ chỗ dấu ngoặc nhọn đôi: {{dish}}{{ingredients}}.

Bạn tạo lời nhắc cuối cùng bằng cách gọi apply(). Phương thức này sẽ lấy một bản đồ gồm các cặp khoá/giá trị đại diện cho tên của phần giữ chỗ và giá trị chuỗi để thay thế phần giữ chỗ đó.

Cuối cùng, bạn gọi phương thức generate() của mô hình Gemini bằng cách tạo thông báo cho người dùng từ lời nhắc đó, với hướng dẫn prompt.toUserMessage().

Chạy mẫu:

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

Bạn sẽ thấy kết quả được tạo có dạng như sau:

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

Bạn có thể thay đổi giá trị của dishingredients trong bản đồ và điều chỉnh nhiệt độ, topKtokP rồi chạy lại mã. Điều này cho phép bạn quan sát hiệu quả của việc thay đổi các tham số này đối với LLM.

Mẫu câu lệnh là một cách hay để có các hướng dẫn có thể sử dụng lại và có thể định tham số cho các lệnh gọi LLM. Bạn có thể truyền dữ liệu và tuỳ chỉnh lời nhắc cho các giá trị khác nhau do người dùng cung cấp.

9. Phân loại văn bản bằng câu lệnh dựa trên một vài ví dụ

LLM khá giỏi trong việc phân loại văn bản thành nhiều danh mục. Bạn có thể giúp LLM thực hiện nhiệm vụ đó bằng cách cung cấp một số ví dụ về văn bản và danh mục liên quan. Phương pháp này thường được gọi là nhắc một vài lần.

Hãy mở TextClassification.java trong thư mục app/src/main/java/gemini/workshop để thực hiện một loại phân loại văn bản cụ thể: phân tích cảm xúc.

package gemini.workshop;

import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import java.util.List;

public class TextClassification {

    enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(10)
            .maxRetries(3)
            .responseSchema(Schema.newBuilder()
                .setType(Type.STRING)
                .addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
                .build())
            .build();


        interface SentimentAnalysis {
            @SystemMessage("""
                Analyze the sentiment of the text below.
                Respond only with one word to describe the sentiment.
                """)
            Sentiment analyze(String text);
        }

        MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
        memory.add(UserMessage.from("This is fantastic news!"));
        memory.add(AiMessage.from(Sentiment.POSITIVE.name()));

        memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
        memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));

        memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
        memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));

        SentimentAnalysis sentimentAnalysis =
            AiServices.builder(SentimentAnalysis.class)
                .chatLanguageModel(model)
                .chatMemory(memory)
                .build();

        System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
    }
}

Enum Sentiment liệt kê các giá trị khác nhau cho một cảm xúc: tiêu cực, trung lập hoặc tích cực.

Trong phương thức main(), bạn tạo mô hình trò chuyện Gemini như bình thường, nhưng với số lượng mã thông báo đầu ra tối đa nhỏ, vì bạn chỉ muốn một câu trả lời ngắn: văn bản là POSITIVE, NEGATIVE hoặc NEUTRAL. Và để hạn chế mô hình chỉ trả về các giá trị đó, bạn có thể tận dụng tính năng hỗ trợ đầu ra có cấu trúc mà bạn đã khám phá trong phần trích xuất dữ liệu. Đó là lý do phương thức responseSchema() được sử dụng. Lần này, bạn không sử dụng phương thức thuận tiện từ SchemaHelper để suy luận định nghĩa giản đồ, mà sẽ sử dụng trình tạo Schema để hiểu định nghĩa giản đồ trông như thế nào.

Sau khi định cấu hình mô hình, bạn sẽ tạo một giao diện SentimentAnalysisAiServices của LangChain4j sẽ triển khai cho bạn bằng cách sử dụng LLM. Giao diện này chứa một phương thức: analyze(). Phương thức này sẽ lấy văn bản để phân tích trong dữ liệu đầu vào và trả về một giá trị enum Sentiment. Vì vậy, bạn chỉ thao tác với một đối tượng được xác định kiểu mạnh đại diện cho lớp cảm xúc được nhận dạng.

Sau đó, để cung cấp "một số ví dụ ngắn" nhằm thúc đẩy mô hình thực hiện công việc phân loại, bạn tạo một bộ nhớ trò chuyện để truyền các cặp tin nhắn của người dùng và câu trả lời của AI đại diện cho văn bản và cảm xúc liên quan đến văn bản đó.

Hãy liên kết mọi thứ với nhau bằng phương thức AiServices.builder(), bằng cách truyền giao diện SentimentAnalysis, mô hình cần sử dụng và bộ nhớ trò chuyện bằng các ví dụ về một số lần chụp. Cuối cùng, hãy gọi phương thức analyze() bằng văn bản cần phân tích.

Chạy mẫu:

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

Bạn sẽ thấy một từ duy nhất:

POSITIVE

Có vẻ như việc yêu thích dâu tây là một cảm xúc tích cực!

10. Tạo tăng cường truy xuất

LLM được huấn luyện dựa trên một lượng lớn văn bản. Tuy nhiên, kiến thức của chúng chỉ bao gồm thông tin mà chúng đã thấy trong quá trình huấn luyện. Nếu có thông tin mới được phát hành sau ngày kết thúc huấn luyện mô hình, thì mô hình sẽ không có thông tin chi tiết đó. Do đó, mô hình sẽ không thể trả lời các câu hỏi về thông tin mà nó chưa thấy.

Đó là lý do các phương pháp như Tạo dữ liệu tăng cường truy xuất (RAG) sẽ được đề cập trong phần này giúp cung cấp thêm thông tin mà LLM có thể cần biết để thực hiện các yêu cầu của người dùng, để trả lời bằng thông tin có thể mới hơn hoặc về thông tin riêng tư không thể truy cập được tại thời điểm huấn luyện.

Hãy quay lại với các cuộc trò chuyện. Lần này, bạn có thể đặt câu hỏi về giấy tờ của mình. Bạn sẽ xây dựng một chatbot có thể truy xuất thông tin liên quan từ một cơ sở dữ liệu chứa các tài liệu được chia thành các phần nhỏ hơn ("chunk"). Mô hình sẽ sử dụng thông tin đó để đưa ra câu trả lời, thay vì chỉ dựa vào kiến thức có trong quá trình huấn luyện.

Trong RAG, có hai giai đoạn:

  1. Giai đoạn truyền dẫn – Tài liệu được tải vào bộ nhớ, chia thành các phần nhỏ hơn và các vectơ nhúng (biểu diễn vectơ đa chiều cao của các phần) được tính toán và lưu trữ trong cơ sở dữ liệu vectơ có khả năng thực hiện các lượt tìm kiếm ngữ nghĩa. Giai đoạn nhập này thường được thực hiện một lần, khi cần thêm tài liệu mới vào tập hợp tài liệu.

cd07d33d20ffa1c8.png

  1. Giai đoạn truy vấn – Giờ đây, người dùng có thể đặt câu hỏi về các tài liệu. Câu hỏi cũng sẽ được chuyển đổi thành một vectơ và so sánh với tất cả các vectơ khác trong cơ sở dữ liệu. Các vectơ tương tự nhất thường có liên quan về ngữ nghĩa và được cơ sở dữ liệu vectơ trả về. Sau đó, LLM được cung cấp ngữ cảnh của cuộc trò chuyện, các đoạn văn bản tương ứng với các vectơ do cơ sở dữ liệu trả về và được yêu cầu đưa ra câu trả lời bằng cách xem xét các đoạn đó.

a1d2e2deb83c6d27.png

Chuẩn bị giấy tờ

Trong ví dụ mới này, bạn sẽ đặt câu hỏi về một mẫu xe hư cấu của một nhà sản xuất xe cũng hư cấu: xe Cymbal Starlight! Ý tưởng là tài liệu về một chiếc xe hư cấu không được nằm trong kiến thức về mô hình. Vì vậy, nếu Gemini có thể trả lời chính xác các câu hỏi về chiếc xe này, thì tức là phương pháp RAG hoạt động hiệu quả: Gemini có thể tìm kiếm trong tài liệu của bạn.

Triển khai chatbot

Hãy cùng tìm hiểu cách xây dựng phương pháp 2 giai đoạn: trước tiên là giai đoạn nhập tài liệu, sau đó là thời gian truy vấn (còn gọi là "giai đoạn truy xuất") khi người dùng đặt câu hỏi về tài liệu.

Trong ví dụ này, cả hai giai đoạn đều được triển khai trong cùng một lớp. Thông thường, bạn sẽ có một ứng dụng xử lý việc truyền dẫn và một ứng dụng khác cung cấp giao diện chatbot cho người dùng.

Ngoài ra, trong ví dụ này, chúng ta sẽ sử dụng cơ sở dữ liệu vectơ trong bộ nhớ. Trong trường hợp thực tế, các giai đoạn nhập và truy vấn sẽ được tách riêng trong hai ứng dụng riêng biệt và các vectơ được lưu trữ trong một cơ sở dữ liệu độc lập.

Quy trình nhập tài liệu

Bước đầu tiên của giai đoạn truyền dẫn tài liệu là xác định vị trí tệp PDF về chiếc xe hư cấu của chúng ta và chuẩn bị PdfParser để đọc tệp đó:

URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());

Thay vì tạo mô hình ngôn ngữ trò chuyện thông thường trước tiên, bạn sẽ tạo một thực thể của mô hình nhúng. Đây là một mô hình cụ thể có vai trò tạo ra các vectơ đại diện cho các đoạn văn bản (từ, câu hoặc thậm chí là đoạn văn). Phương thức này trả về các vectơ số dấu phẩy động thay vì trả về các phản hồi văn bản.

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .publisher("google")
    .modelName("text-embedding-005")
    .maxRetries(3)
    .build();

Tiếp theo, bạn sẽ cần một vài lớp để cộng tác với nhau nhằm:

  • Tải và chia tài liệu PDF thành các phần.
  • Tạo các vectơ nhúng cho tất cả các đoạn này.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

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

Một thực thể của InMemoryEmbeddingStore, một cơ sở dữ liệu vectơ trong bộ nhớ, được tạo để lưu trữ các vectơ nhúng.

Tài liệu được chia thành các phần nhờ lớp DocumentSplitters. Hàm này sẽ chia văn bản của tệp PDF thành các đoạn văn bản gồm 500 ký tự, với 100 ký tự trùng lặp (với đoạn văn bản sau đây, để tránh cắt các từ hoặc câu thành từng phần).

Trình nhập dữ liệu của cửa hàng liên kết trình chia tài liệu, mô hình nhúng để tính toán vectơ và cơ sở dữ liệu vectơ trong bộ nhớ. Sau đó, phương thức ingest() sẽ xử lý việc truyền dẫn.

Giờ đây, giai đoạn đầu tiên đã kết thúc, tài liệu đã được chuyển đổi thành các đoạn văn bản với các vectơ nhúng liên quan và được lưu trữ trong cơ sở dữ liệu vectơ.

Đặt câu hỏi

Đã đến lúc chuẩn bị đặt câu hỏi! Tạo mô hình trò chuyện để bắt đầu cuộc trò chuyện:

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

Bạn cũng cần một lớp truy xuất để liên kết cơ sở dữ liệu vectơ (trong biến embeddingStore) với mô hình nhúng. Nhiệm vụ của lớp này là truy vấn cơ sở dữ liệu vectơ bằng cách tính toán một vectơ nhúng cho truy vấn của người dùng, để tìm các vectơ tương tự trong cơ sở dữ liệu:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Tạo một giao diện đại diện cho trợ lý chuyên gia về ô tô. Đây là giao diện mà lớp AiServices sẽ triển khai để bạn tương tác với mô hình:

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

Giao diện CarExpert trả về một phản hồi chuỗi được gói trong lớp Result của LangChain4j. Tại sao nên sử dụng trình bao bọc này? Vì không chỉ cung cấp câu trả lời mà còn cho phép bạn kiểm tra các đoạn từ cơ sở dữ liệu mà trình truy xuất nội dung đã trả về. Bằng cách đó, bạn có thể hiển thị nguồn của(các) tài liệu được dùng để đưa ra câu trả lời cuối cùng cho người dùng.

Tại thời điểm này, bạn có thể định cấu hình một dịch vụ AI mới:

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

Dịch vụ này liên kết với nhau:

  • Mô hình ngôn ngữ trò chuyện mà bạn đã định cấu hình trước đó.
  • Bộ nhớ trò chuyện để theo dõi cuộc trò chuyện.
  • retriever so sánh truy vấn nhúng vectơ với các vectơ trong cơ sở dữ liệu.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

            Answer using the following information:
            {{contents}}
the following information:
            {{contents}}
            """))
        .build())
    .contentRetriever(retriever)
    .build())

Cuối cùng, bạn đã sẵn sàng đặt câu hỏi!

List.of(
    "What is the cargo capacity of Cymbal Starlight?",
    "What's the emergency roadside assistance phone number?",
    "Are there some special kits available on that car?"
).forEach(query -> {
    Result<String> response = expert.ask(query);
    System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
    System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});

Toàn bộ mã nguồn nằm trong RAG.java trong thư mục app/src/main/java/gemini/workshop.

Chạy mẫu:

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

Trong kết quả, bạn sẽ thấy câu trả lời cho các câu hỏi của mình:

=== What is the cargo capacity of Cymbal Starlight? === 

 The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet.
 

SOURCE: Cargo
The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of
the vehicle.
To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell.
When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this
could affect the vehicle's handling and stability.
Luggage

=== What's the emergency roadside assistance phone number? === 

The emergency roadside assistance phone number is 1-800-555-1212.
 

SOURCE: Chapter 18: Emergencies
Roadside Assistance
If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside
assistance for help. Roadside assistance is available 24 hours a day, 7 days a week.
To call roadside assistance, dial the following number:
1-800-555-1212
When you call roadside assistance, be prepared to provide the following information:
Your name and contact information
Your vehicle's make, model, and year
Your vehicle's location

=== Are there some special kits available on that car? === 

 Yes, the Cymbal Starlight comes with a tire repair kit.
 

SOURCE: Lane keeping assist:  This feature helps to keep you in your lane by gently steering the vehicle back
into the lane if you start to drift.
Adaptive cruise control:  This feature automatically adjusts your speed to maintain a safe following
distance from the vehicle in front of you.
Forward collision warning:  This feature warns you if you are approaching another vehicle too
quickly.
Automatic emergency braking:  This feature can automatically apply the brakes to avoid a collision.

11. Lệnh gọi hàm

Có những trường hợp bạn muốn LLM có quyền truy cập vào các hệ thống bên ngoài, chẳng hạn như API web từ xa truy xuất thông tin hoặc thực hiện một hành động, hoặc các dịch vụ thực hiện một số loại phép tính. Ví dụ:

API web từ xa:

  • Theo dõi và cập nhật đơn đặt hàng của khách hàng.
  • Tìm hoặc tạo phiếu yêu cầu hỗ trợ trong công cụ theo dõi lỗi.
  • Tìm nạp dữ liệu theo thời gian thực như giá cổ phiếu hoặc số liệu đo lường của cảm biến IoT.
  • Gửi email.

Công cụ tính toán:

  • Một máy tính cho các bài toán toán học nâng cao hơn.
  • Diễn giải mã để chạy mã khi LLM cần logic suy luận.
  • Chuyển đổi các yêu cầu bằng ngôn ngữ tự nhiên thành truy vấn SQL để LLM có thể truy vấn cơ sở dữ liệu.

Lệnh gọi hàm (đôi khi được gọi là công cụ hoặc sử dụng công cụ) là khả năng mô hình yêu cầu thực hiện một hoặc nhiều lệnh gọi hàm thay mặt cho mô hình, nhờ đó mô hình có thể trả lời chính xác câu lệnh của người dùng bằng dữ liệu mới hơn.

Khi người dùng đưa ra một câu lệnh cụ thể và có kiến thức về các hàm hiện có có thể liên quan đến ngữ cảnh đó, LLM có thể trả lời bằng một yêu cầu gọi hàm. Sau đó, ứng dụng tích hợp LLM có thể thay mặt gọi hàm, rồi trả lời lại LLM bằng một phản hồi, sau đó LLM sẽ diễn giải lại bằng cách trả lời bằng câu trả lời dạng văn bản.

Bốn bước gọi hàm

Hãy xem ví dụ về cách gọi hàm: nhận thông tin về thông tin dự báo thời tiết.

Nếu bạn hỏi Gemini hoặc bất kỳ LLM nào khác về thời tiết ở Paris, chúng sẽ trả lời rằng không có thông tin về dự báo thời tiết hiện tại. Nếu muốn LLM có quyền truy cập theo thời gian thực vào dữ liệu thời tiết, bạn cần xác định một số hàm mà LLM có thể yêu cầu sử dụng.

Hãy xem sơ đồ sau:

31e0c2aba5e6f21c.png

1️⃣ Trước tiên, người dùng hỏi về thời tiết ở Paris. Ứng dụng chatbot (sử dụng LangChain4j) biết rằng có một hoặc nhiều hàm có sẵn để giúp LLM thực hiện truy vấn. Chatbot vừa gửi lời nhắc ban đầu, vừa gửi danh sách các hàm có thể gọi. Ở đây, một hàm có tên là getWeather() sẽ nhận tham số chuỗi cho vị trí.

8863be53a73c4a70.png

Vì LLM không biết về thông tin dự báo thời tiết, nên thay vì trả lời qua văn bản, lớp này sẽ gửi lại một yêu cầu thực thi hàm. Ứng dụng trò chuyện phải gọi hàm getWeather() với "Paris" làm tham số vị trí.

d1367cc69c07b14d.png

2️⃣ Ứng dụng trò chuyện thay mặt cho LLM gọi hàm đó, truy xuất phản hồi hàm. Ở đây, chúng ta giả định rằng phản hồi là {"forecast": "sunny"}.

73a5f2ed19f47d8.png

3️⃣ Ứng dụng chatbot gửi phản hồi JSON trở lại LLM.

20832cb1ee6fbfeb.png

4️⃣ LLM xem xét phản hồi JSON, diễn giải thông tin đó và cuối cùng trả lời bằng văn bản rằng thời tiết ở Paris đang nắng.

Mỗi bước dưới dạng mã

Trước tiên, bạn sẽ định cấu hình mô hình Gemini như bình thường:

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

Bạn xác định một thông số kỹ thuật của công cụ mô tả hàm có thể được gọi:

ToolSpecification weatherToolSpec = ToolSpecification.builder()
    .name("getWeather")
    .description("Get the weather forecast for a given location or city")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty(
            "location", 
            "the location or city to get the weather forecast for")
        .build())
    .build();

Tên của hàm được xác định, cũng như tên và loại của tham số, nhưng hãy lưu ý rằng cả hàm và tham số đều được mô tả. Nội dung mô tả rất quan trọng và giúp LLM thực sự hiểu được chức năng của một hàm, từ đó đánh giá xem có cần gọi hàm này trong bối cảnh cuộc trò chuyện hay không.

Hãy bắt đầu bước 1 bằng cách gửi câu hỏi ban đầu về thời tiết ở 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);

Ở bước 2, chúng ta truyền công cụ mà chúng ta muốn mô hình sử dụng và mô hình sẽ trả lời bằng một yêu cầu thực thi công cụ:

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

Bước #3. Tại thời điểm này, chúng ta biết LLM muốn chúng ta gọi hàm nào. Trong mã, chúng ta không thực hiện lệnh gọi thực sự đến một API bên ngoài mà chỉ trực tiếp trả về thông tin dự báo thời tiết giả định:

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

Và ở bước 4, LLM tìm hiểu về kết quả thực thi hàm, sau đó có thể tổng hợp một phản hồi bằng văn bản:

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

Kết quả xuất ra là:

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.

Bạn có thể thấy trong kết quả ở trên yêu cầu thực thi công cụ cũng như câu trả lời.

Toàn bộ mã nguồn nằm trong FunctionCalling.java trong thư mục app/src/main/java/gemini/workshop:

Chạy mẫu:

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

Bạn sẽ thấy kết quả tương tự như sau:

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 xử lý lệnh gọi hàm

Ở bước trước, bạn đã thấy cách các hoạt động tương tác câu hỏi/câu trả lời văn bản thông thường và yêu cầu/phản hồi hàm được xen kẽ với nhau, và ở giữa, bạn đã trực tiếp cung cấp phản hồi hàm được yêu cầu mà không cần gọi một hàm thực.

Tuy nhiên, LangChain4j cũng cung cấp một tính năng trừu tượng cấp cao hơn có thể xử lý các lệnh gọi hàm một cách minh bạch cho bạn, đồng thời xử lý cuộc trò chuyện như bình thường.

Lệnh gọi hàm đơn

Hãy cùng xem xét từng phần của FunctionCallingAssistant.java.

Trước tiên, bạn tạo một bản ghi sẽ đại diện cho cấu trúc dữ liệu phản hồi của hàm:

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

Nội dung phản hồi chứa thông tin về vị trí, thông tin dự báo và nhiệt độ.

Sau đó, bạn tạo một lớp chứa hàm thực tế mà bạn muốn cung cấp cho mô hình:

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

Xin lưu ý rằng lớp này chứa một hàm duy nhất, nhưng được chú thích bằng chú giải @Tool tương ứng với nội dung mô tả về hàm mà mô hình có thể yêu cầu gọi.

Các tham số của hàm (một tham số duy nhất ở đây) cũng được chú thích, nhưng bằng chú thích @P ngắn gọn này, cũng cung cấp nội dung mô tả về tham số. Bạn có thể thêm bao nhiêu hàm tuỳ thích để cung cấp cho mô hình, cho các tình huống phức tạp hơn.

Trong lớp này, bạn trả về một số câu trả lời được tạo sẵn, nhưng nếu muốn gọi một dịch vụ dự báo thời tiết bên ngoài thực sự, thì bạn sẽ thực hiện lệnh gọi đến dịch vụ đó trong phần nội dung của phương thức đó.

Như chúng ta đã thấy khi bạn tạo ToolSpecification trong phương pháp trước, điều quan trọng là phải ghi lại chức năng của hàm và mô tả tham số tương ứng. Điều này giúp mô hình hiểu được cách thức và thời điểm có thể sử dụng hàm này.

Tiếp theo, LangChain4j cho phép bạn cung cấp một giao diện tương ứng với hợp đồng mà bạn muốn sử dụng để tương tác với mô hình. Ở đây, đây là một giao diện đơn giản nhận một chuỗi đại diện cho thông báo của người dùng và trả về một chuỗi tương ứng với phản hồi của mô hình:

interface WeatherAssistant {
    String chat(String userMessage);
}

Bạn cũng có thể sử dụng các chữ ký phức tạp hơn liên quan đến UserMessage của LangChain4j (đối với thông báo của người dùng) hoặc AiMessage (đối với phản hồi của mô hình) hoặc thậm chí là TokenStream nếu muốn xử lý các tình huống nâng cao hơn, vì các đối tượng phức tạp hơn đó cũng chứa thêm thông tin như số lượng mã thông báo đã sử dụng, v.v. Nhưng để đơn giản, chúng ta sẽ chỉ lấy chuỗi đầu vào và chuỗi đầu ra.

Hãy kết thúc bằng phương thức main() liên kết tất cả các phần với nhau:

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

    WeatherForecastService weatherForecastService = new WeatherForecastService();

    WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .tools(weatherForecastService)
        .build();

    System.out.println(assistant.chat("What is the weather in Paris?"));
}

Như thường lệ, bạn sẽ định cấu hình mô hình trò chuyện Gemini. Sau đó, bạn tạo bản sao dịch vụ dự báo thời tiết chứa "hàm" mà mô hình sẽ yêu cầu chúng ta gọi.

Bây giờ, bạn sẽ sử dụng lại lớp AiServices để liên kết mô hình trò chuyện, bộ nhớ trò chuyện và công cụ (tức là dịch vụ dự báo thời tiết với chức năng của nó). AiServices trả về một đối tượng triển khai giao diện WeatherAssistant mà bạn đã xác định. Việc duy nhất còn lại là gọi phương thức chat() của trợ lý đó. Khi gọi, bạn sẽ chỉ thấy các phản hồi văn bản, nhưng các yêu cầu gọi hàm và phản hồi gọi hàm sẽ không hiển thị với nhà phát triển và các yêu cầu đó sẽ được xử lý tự động và minh bạch. Nếu Gemini cho rằng cần gọi một hàm, thì Gemini sẽ trả lời bằng yêu cầu gọi hàm và LangChain4j sẽ thay mặt bạn gọi hàm cục bộ.

Chạy mẫu:

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

Bạn sẽ thấy kết quả tương tự như sau:

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

Đây là ví dụ về một hàm duy nhất.

Nhiều lệnh gọi hàm

Bạn cũng có thể có nhiều hàm và để LangChain4j thay mặt bạn xử lý nhiều lệnh gọi hàm. Hãy xem MultiFunctionCallingAssistant.java để biết ví dụ về nhiều hàm.

Thư viện này có một hàm để chuyển đổi đơn vị tiền tệ:

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

Một hàm khác để lấy giá trị của một cổ phiếu:

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

Một hàm khác để áp dụng tỷ lệ phần trăm cho một số tiền nhất định:

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

Sau đó, bạn có thể kết hợp tất cả các hàm này và một lớp MultiTools để đặt câu hỏi như "10% giá cổ phiếu AAPL được chuyển đổi từ USD sang EUR là bao nhiêu?"

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

    MultiTools multiTools = new MultiTools();

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

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

Chạy như sau:

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

Và bạn sẽ thấy nhiều hàm được gọi:

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.

Hướng tới các trợ lý

Lệnh gọi hàm là một cơ chế mở rộng tuyệt vời cho các mô hình ngôn ngữ lớn như Gemini. Điều này cho phép chúng ta xây dựng các hệ thống phức tạp hơn, thường được gọi là "đại lý" hoặc "trợ lý AI". Các tác nhân này có thể tương tác với thế giới bên ngoài thông qua các API bên ngoài và với các dịch vụ có thể gây ra tác dụng phụ đối với môi trường bên ngoài (chẳng hạn như gửi email, tạo phiếu yêu cầu hỗ trợ, v.v.)

Khi tạo các tác nhân mạnh mẽ như vậy, bạn nên làm việc một cách có trách nhiệm. Bạn nên cân nhắc việc có sự tham gia của con người trước khi thực hiện các hành động tự động. Điều quan trọng là bạn phải chú ý đến sự an toàn khi thiết kế các tác nhân chạy bằng LLM tương tác với thế giới bên ngoài.

13. Chạy Gemma bằng Ollama và TestContainers

Cho đến nay, chúng ta đã sử dụng Gemini nhưng cũng có Gemma, mô hình nhỏ hơn của Gemini.

Gemma là một bộ mô hình mở, hiện đại và gọn nhẹ được xây dựng từ cùng một nghiên cứu và công nghệ dùng để tạo mô hình Gemini. Gemma có hai biến thể Gemma1 và Gemma2, mỗi biến thể có nhiều kích thước. Gemma1 có hai kích thước: 2B và 7B. Gemma2 có hai kích thước: 9B và 27B. Các gói này có trọng số miễn phí và kích thước nhỏ, vì vậy, bạn có thể tự chạy các gói này, ngay cả trên máy tính xách tay hoặc trong Cloud Shell.

Làm cách nào để chạy Gemma?

Có nhiều cách để chạy Gemma: trên đám mây, thông qua Vertex AI chỉ bằng một lần nhấp vào nút hoặc GKE với một số GPU, nhưng bạn cũng có thể chạy Gemma trên máy.

Một lựa chọn hay để chạy Gemma cục bộ là sử dụng Ollama, một công cụ cho phép bạn chạy các mô hình nhỏ, chẳng hạn như Llama 2, Mistral và nhiều mô hình khác trên máy cục bộ. Công cụ này tương tự như Docker nhưng dành cho LLM.

Cài đặt Ollama theo hướng dẫn dành cho Hệ điều hành của bạn.

Nếu đang sử dụng môi trường Linux, trước tiên, bạn cần bật Ollama sau khi cài đặt.

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

Sau khi cài đặt cục bộ, bạn có thể chạy các lệnh để lấy một mô hình:

ollama pull gemma:2b

Chờ mô hình được lấy. Quá trình này có thể mất chút thời gian.

Chạy mô hình:

ollama run gemma:2b

Bây giờ, bạn có thể tương tác với mô hình:

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

Để thoát khỏi lời nhắc, hãy nhấn tổ hợp phím Ctrl+D

Chạy Gemma trong Ollama trên TestContainers

Thay vì phải cài đặt và chạy Ollama trên máy, bạn có thể sử dụng Ollama trong một vùng chứa do TestContainers xử lý.

TestContainers không chỉ hữu ích cho việc kiểm thử mà bạn cũng có thể sử dụng nó để thực thi các vùng chứa. Thậm chí, bạn còn có thể tận dụng một OllamaContainer cụ thể!

Dưới đây là toàn bộ bức tranh:

2382c05a48708dfd.png

Triển khai

Hãy cùng xem xét từng phần của GemmaWithOllamaContainer.java.

Trước tiên, bạn cần tạo một vùng chứa Ollama phái sinh để lấy mô hình Gemma. Hình ảnh này đã tồn tại từ lần chạy trước hoặc sẽ được tạo. Nếu hình ảnh đã tồn tại, bạn chỉ cần cho TestContainers biết rằng bạn muốn thay thế hình ảnh Ollama mặc định bằng biến thể do Gemma cung cấp:

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

Tiếp theo, bạn tạo và khởi động một vùng chứa kiểm thử Ollama, sau đó tạo một mô hình trò chuyện Ollama bằng cách trỏ đến địa chỉ và cổng của vùng chứa bằng mô hình bạn muốn sử dụng. Cuối cùng, bạn chỉ cần gọi model.generate(yourPrompt) như bình thường:

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

Chạy như sau:

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

Lần chạy đầu tiên sẽ mất một chút thời gian để tạo và chạy vùng chứa, nhưng sau khi hoàn tất, bạn sẽ thấy Gemma phản hồi:

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.

Bạn đã chạy Gemma trong Cloud Shell!

14. Xin chúc mừng

Xin chúc mừng! Bạn đã tạo thành công ứng dụng trò chuyện AI tạo sinh đầu tiên của mình bằng Java bằng cách sử dụng LangChain4j và API Gemini! Trong quá trình tìm hiểu, bạn đã khám phá ra rằng các mô hình ngôn ngữ lớn đa phương thức khá mạnh mẽ và có thể xử lý nhiều nhiệm vụ như hỏi/trả lời, ngay cả trên tài liệu của riêng bạn, trích xuất dữ liệu, tương tác với các API bên ngoài, v.v.

Tiếp theo là gì?

Giờ là lúc bạn nâng cao ứng dụng của mình bằng cách tích hợp LLM mạnh mẽ!

Tài liệu đọc thêm

Tài liệu tham khảo