1. 簡介
本程式碼研究室著重於 Gemini 大型語言模型 (LLM),該模型託管於 Google Cloud 的 Vertex AI 上。Vertex AI 是一個平台,涵蓋 Google Cloud 上的所有機器學習產品、服務和模型。
您將使用 Java 搭配 LangChain4j 架構,與 Gemini API 互動。您將透過具體範例瞭解如何運用 LLM 回答問題、產生構想、擷取實體和結構化內容、檢索增強生成,以及呼叫函式。
什麼是生成式 AI?
生成式 AI 是指使用人工智慧技術創造新的文字、圖片、音樂、音訊和影片等內容。
生成式 AI 是由大型語言模型 (LLM) 驅動,可同時處理多項工作,並立即執行各種任務,包括提供摘要、問與答和分類等。只需極少的訓練,就能以極少的範例資料針對目標用途調整基礎模型。
生成式 AI 的運作方式為何?
生成式 AI 的運作方式是使用機器學習模型,根據一系列人類創作內容的資料來掌握相關模式與關係,再運用學到的模式生成新內容。
訓練生成式 AI 模型最常見的方式是使用監督式學習,模型會獲得一組人類創作內容和對應標籤。然後學習產生類似人類創作內容的內容。
常見的生成式 AI 應用程式有哪些?
生成式 AI 有助於:
- 透過強化的即時通訊和搜尋體驗,改善與顧客的互動方式。
- 透過對話式介面和摘要功能,探索大量非結構化資料。
- 協助處理重複性工作,例如回覆提案請求、以不同語言將行銷內容本地化,以及確認客戶合約是否符合相關法規等
Google Cloud 提供哪些生成式 AI 產品?
Vertex AI 能讓您將自訂的基礎模型嵌入應用程式中,並與應用程式互動,而且不需要具備機器學習專業知識,也能輕鬆上手。您可以在 Model Garden 中存取基礎模型、透過 Vertex AI Studio 的簡易使用者介面調整模型,或是運用數據資料學筆記本中的模型。
有了 Vertex AI Search and Conversation,開發人員就能在最短時間內,建構生成式 AI 技術輔助搜尋引擎和聊天機器人。
Google Cloud 專用 Gemini 是由 Gemini 提供技術支援的 AI 協作工具,適用於 Google Cloud 和 IDE,可協助您以更快的速度完成更多工作。Gemini Code Assist 提供程式碼補全、程式碼生成和程式碼說明功能,您也可以透過即時通訊功能向它提出技術問題。
Gemini 是什麼?
Gemini 是由 Google DeepMind 開發的一系列生成式 AI 模型,專為多模態用途而設計。多模態表示模型可處理及產生不同類型的內容,例如文字、程式碼、圖片和音訊。
Gemini 有不同變化版本和尺寸:
- Gemini Ultra:規模最大、性能最優異的模型,適合高難度工作。
- Gemini Flash:最快、最具成本效益的模型,專為大量工作量進行最佳化。
- Gemini Pro:中型模型,經過最佳化,可用於各種工作。
- Gemini Nano:處理裝置端工作最有效率的模型。
主要功能:
- 多模態:Gemini 能理解及處理多種資訊格式,這項功能是傳統純文字語言模型的重大進展。
- 效能:Gemini Ultra 在許多基準測試中表現優於目前的頂尖模型,也是第一個在艱難的 MMLU (Massive Multitask Language Understanding) 基準測試中超越人類專家的模型。
- 彈性:Gemini 的尺寸多元,可用於各種用途,從大規模研究到行動裝置部署皆適用。
如何透過 Java 與 Vertex AI 上的 Gemini 互動?
方法有以下兩種:
- 適用於 Gemini 的 Vertex AI Java API 官方程式庫。
- LangChain4j 架構。
在本程式碼研究室中,您將使用 LangChain4j 架構。
什麼是 LangChain4j 架構?
LangChain4j 框架是開放原始碼程式庫,可透過協調各種元件 (例如 LLM 本身),以及向量資料庫 (用於語意搜尋)、文件載入器和分割器 (用於分析文件並從中學習)、輸出剖析器等其他工具,在 Java 應用程式中整合 LLM。
這個專案的靈感來自 LangChain Python 專案,但目標是為 Java 開發人員提供服務。
課程內容
- 如何設定 Java 專案以使用 Gemini 和 LangChain4j
- 如何以程式設計方式向 Gemini 傳送第一個提示
- 如何串流顯示 Gemini 回覆
- 如何建立使用者與 Gemini 之間的對話
- 如何在多模態情境中使用 Gemini,同時傳送文字和圖片
- 如何從非結構化內容中擷取實用的結構化資訊
- 如何操作提示範本
- 如何進行文字分類,例如情緒分析
- 如何與自己的文件對話 (檢索增強生成)
- 如何透過函式呼叫擴充聊天機器人
- 如何在本機使用 Gemma 搭配 Ollama 和 TestContainers
軟硬體需求
- 具備 Java 程式設計語言知識
- Google Cloud 專案
- 瀏覽器,例如 Chrome 或 Firefox
2. 設定和需求
自助式環境設定
- 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果您還沒有 Gmail 或 Google Workspace 帳戶,請務必建立帳戶。
- 「Project name」是這個專案參與者的顯示名稱。這是 Google API 不會使用的字元字串。您隨時可以更新。
- 專案 ID 在所有 Google Cloud 專案中都是不重複的值,且無法變更 (設定後即無法變更)。Cloud 控制台會自動產生專屬字串,您通常不需要特別在意。在大多數程式碼研究室中,您都需要參照專案 ID (通常會以
PROJECT_ID
表示)。如果您不喜歡系統產生的 ID,可以隨機產生另一個 ID。或者,您也可以自行嘗試,看看是否可用。這項設定在這個步驟後就無法變更,並會在整個專案期間維持不變。 - 提醒您,有些 API 會使用第三個值「專案編號」。如要進一步瞭解這三個值,請參閱說明文件。
- 接下來,您需要在 Cloud 控制台中啟用帳單功能,才能使用 Cloud 資源/API。執行這個程式碼研究室不會產生太多費用,甚至可能完全不會產生費用。如要關閉資源,避免在本教學課程結束後繼續產生費用,您可以刪除建立的資源或專案。Google Cloud 新使用者可享有價值 $300 美元的免費試用期。
啟動 Cloud Shell
雖然 Google Cloud 可透過筆記型電腦遠端操作,但在本程式碼研究室中,您將使用 Cloud Shell,這是在雲端運作的指令列環境。
啟用 Cloud Shell
- 在 Cloud 控制台中,按一下「啟用 Cloud Shell」 圖示
。
如果這是您首次啟動 Cloud Shell,系統會顯示中介畫面,說明 Cloud Shell 的功能。如果您看到中介畫面,請按一下「繼續」。
佈建並連線至 Cloud Shell 的作業只需幾分鐘的時間。
這個虛擬機器會載入所有必要的開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,可大幅提升網路效能和驗證功能。您可以在瀏覽器中完成本程式碼研究室的大部分工作,甚至是全部工作。
連線至 Cloud Shell 後,您應會發現自己通過驗證,且專案已設為您的專案 ID。
- 在 Cloud Shell 中執行下列指令,確認您已通過驗證:
gcloud auth list
指令輸出
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- 在 Cloud Shell 中執行下列指令,確認 gcloud 指令知道您的專案:
gcloud config list project
指令輸出
[core] project = <PROJECT_ID>
如未設定,請輸入下列指令設定專案:
gcloud config set project <PROJECT_ID>
指令輸出
Updated property [core/project].
3. 準備開發環境
在本程式碼研究室中,您將使用 Cloud Shell 終端機和 Cloud Shell 編輯器來開發 Java 程式。
啟用 Vertex AI API
在 Google Cloud 控制台中,確認專案名稱顯示在 Google Cloud 控制台的頂端。如果不是,請按一下「選取專案」開啟專案選取器,然後選取所需專案。
您可以透過 Google Cloud 控制台的 Vertex AI 專區,或 Cloud Shell 終端機啟用 Vertex AI API。
如要透過 Google Cloud 控制台啟用,請先前往 Google Cloud 控制台選單的 Vertex AI 專區:
在 Vertex AI 資訊主頁中,按一下「啟用所有建議的 API」。
這麼做會啟用多個 API,但對本程式碼研究室而言,最重要的是 aiplatform.googleapis.com
。
或者,您也可以使用下列指令,從 Cloud Shell 終端機啟用這個 API:
gcloud services enable aiplatform.googleapis.com
複製 GitHub 存放區
在 Cloud Shell 終端機中,複製本程式碼研究室的存放區:
git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git
如要確認專案是否可供執行,您可以嘗試執行「Hello World」程式。
請確認您位於頂層資料夾:
cd gemini-workshop-for-java-developers/
建立 Gradle 包裝函式:
gradle wrapper
使用 gradlew
執行:
./gradlew run
您應該會看到以下的輸出內容:
.. > Task :app:run Hello World!
開啟及設定 Cloud 編輯器
使用 Cloud Shell 中的 Cloud Code Editor 開啟程式碼:
在 Cloud Code 編輯器中,依序選取「File
」->「Open Folder
」,然後指向程式碼研究室來源資料夾 (例如/home/username/gemini-workshop-for-java-developers/
)。
設定環境變數
在 Cloud Code 編輯器中,依序選取 Terminal
-> New Terminal
,開啟新的終端機。設定執行程式碼範例所需的兩個環境變數:
- PROJECT_ID:您的 Google Cloud 專案 ID
- LOCATION:Gemini 模型的部署區域
匯出變數的方式如下:
export PROJECT_ID=$(gcloud config get-value project) export LOCATION=us-central1
4. 第一次呼叫 Gemini 模型
專案設定完成後,您就可以呼叫 Gemini API。
請查看 app/src/main/java/gemini/workshop
目錄中的 QA.java
:
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?"));
}
}
在第一個範例中,您需要匯入實作 ChatModel
介面的 VertexAiGeminiChatModel
類別。
在 main
方法中,您可以使用 VertexAiGeminiChatModel
的建構工具設定聊天語言模型,並指定下列項目:
- 專案
- 位置
- 模型名稱 (
gemini-1.5-flash-002
)。
語言模型已準備就緒,您可以呼叫 generate()
方法,並將提示、問題或指示傳送至 LLM。您可以問一個簡單的問題,例如為什麼天空是藍色的。
您可以自行變更提示,嘗試不同的問題或工作。
在原始碼根資料夾中執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.QA
您應該會看到類似以下的輸出內容:
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.
恭喜,您已首次呼叫 Gemini!
串流回應
您是否注意到,回應是在幾秒後一次性提供?您也可以透過串流回應變化版本,逐步取得回應。串流回應:模型會在可用時逐一傳回回應。
在本程式碼研究室中,我們會使用非串流回應,但我們也會查看串流回應,瞭解如何執行這項操作。
在 app/src/main/java/gemini/workshop
目錄的 StreamQA.java
中,您可以查看串流回應的實際運作情形:
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));
}
}
這次我們會匯入實作 StreamingChatLanguageModel
介面的串流類別變數 VertexAiGeminiStreamingChatModel
。您也需要靜態匯入 LambdaStreamingResponseHandler.onNext
,這是提供 StreamingResponseHandler
的便利方法,可透過 Java lambda 運算式建立串流處理常式。
這次 generate()
方法的簽章稍有不同。傳回類型為 void,而非傳回字串。除了提示之外,您還必須傳遞串流回應處理常式。在本例中,由於我們使用了上述的靜態匯入功能,因此可以定義要傳遞至 onNext()
方法的 lambda 運算式。每次有新的回應片段可用時,系統就會呼叫 lambda 運算式,而只有在發生錯誤時才會呼叫後者。
執行作業:
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
您會得到與前一個類別相似的答案,但這次您會發現答案會逐步顯示在殼層中,而不會等待完整答案顯示。
其他設定
在設定方面,我們只定義了專案、位置和模型名稱,但您可以為模型指定其他參數:
temperature(Float temp)
:定義回覆的創意程度 (0 代表創意程度低,通常較為客觀,而 2 代表較有創意的輸出內容)topP(Float topP)
:選取可能的字詞,其總機率加總起來等於該浮點數 (介於 0 和 1 之間)topK(Integer topK)
:從文字完成功能的可能字詞中隨機選取一個字詞 (從 1 到 40 個)maxOutputTokens(Integer max)
:指定模型提供的答案長度上限 (一般而言,4 個符記大約代表 3 個字詞)maxRetries(Integer retries)
:如果您執行的次數超過每次要求配額,或是平台發生技術問題,您可以讓模型重試呼叫 3 次
到目前為止,你只向 Gemini 提出一個問題,但也可以進行多回合的對話。您將在下一節中瞭解這項功能。
5. 與 Gemini 對話
在上一個步驟中,您只問了一個問題。接下來,我們將示範使用者與大型語言模型之間的實際對話。每個問題和答案都能建立在先前的內容上,形成真正的討論。
請查看 app/src/main/java/gemini/workshop
資料夾中的 Conversation.java
:
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));
});
}
}
這個類別中有一些新的有趣匯入項目:
MessageWindowChatMemory
:這個類別可處理對話的多輪方面,並在本機記憶體中保留先前的問答AiServices
:較高層級的抽象類別,可將聊天模型和聊天記憶體連結在一起
在主要方法中,您將設定模型、聊天記憶體和 AI 服務。模型會按照慣例設定專案、位置和模型名稱資訊。
針對即時通訊記憶體,我們使用 MessageWindowChatMemory
的建構工具建立記憶體,保留最近 20 次訊息交換內容。這是對話的滑動視窗,其脈絡會保留在 Java 類別用戶端的本機中。
接著,您可以建立 AI service
,將聊天模型與聊天記憶體繫結。
請注意,AI 服務如何使用我們定義的 LangChain4j 實作自訂 ConversationService
介面,並接收 String
查詢並傳回 String
回應。
接下來,您可以與 Gemini 對話。首先,系統會傳送簡單的問候語,然後詢問使用者關於艾菲爾鐵塔的問題,以瞭解這個地標位於哪個國家/地區。請注意,最後一句話與第一個問題的答案有關,因為你想知道艾菲爾鐵塔所在國家/地區有多少居民,但並未明確提及先前答案中提供的國家/地區。這表示系統會在每個提示中傳送過去的問題和答案。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
畫面上會顯示類似以下的三個答案:
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.
你可以向 Gemini 提出單回合問題或進行多回合對話,但目前只能輸入文字。圖片呢?接下來,我們將探討圖片。
6. 採用 Gemini 的多模態功能
Gemini 是多模態模型。這項功能不僅可接受文字做為輸入內容,還可接受圖片,甚至是影片做為輸入內容。在本節中,您將看到混合文字和圖片的用途。
你認為 Gemini 會辨識這隻貓嗎?
在雪地中的貓咪相片,取自 Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg
請查看 app/src/main/java/gemini/workshop
目錄中的 Multimodal.java
:
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());
}
}
在匯入作業中,請注意我們會區分不同類型的訊息和內容。UserMessage
可同時包含 TextContent
和 ImageContent
物件。這就是多模態的應用:混合文字和圖片。我們不僅傳送簡單的字串提示,還會傳送更具結構化的物件,用於代表使用者訊息,並由圖片內容片段和文字內容片段組成。模型會傳回包含 AiMessage
的 Response
。
接著,您可以透過 content()
從回應中擷取 AiMessage
,然後再透過 text()
擷取訊息文字。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
圖片名稱確實會提供圖片內容的相關資訊,但 Gemini 的輸出內容類似於以下內容:
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.
混合使用圖片和文字提示,可開創有趣的應用情境。您可以建立可執行下列操作的應用程式:
- 辨識圖片中的文字。
- 檢查圖片是否安全。
- 建立圖片說明。
- 搜尋含有純文字描述的圖片資料庫。
除了從圖片擷取資訊,您也可以從非結構化文字擷取資訊。您將在下一節中瞭解這項作業。
7. 從非結構化文字中擷取結構化資訊
在許多情況下,報告文件、電子郵件或其他長篇文字中都會以非結構化方式提供重要資訊。理想情況下,您應該能夠以結構化物件的形式,擷取非結構化文字中包含的重要詳細資料。我們來看看如何操作。
假設您想從某人的傳記、履歷或描述中,擷取該人的姓名和年齡。您可以指示 LLM 透過巧妙調整的提示 (通常稱為「提示工程」),從非結構化文字中擷取 JSON。
但在以下範例中,我們不會設計說明 JSON 輸出的提示,而是會使用 Gemini 的強大功能,也就是稱為「結構化輸出」的功能 (有時稱為受限解碼),強制模型依照指定的 JSON 結構定義輸出有效的 JSON 內容。
請查看 app/src/main/java/gemini/workshop
中的 ExtractData.java
:
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
}
}
我們來看看這個檔案中的各個步驟:
Person
記錄定義為代表描述人物的詳細資料 (姓名和年齡)。PersonExtractor
介面是由方法定義,該方法會在給定非結構化文字字串時,傳回Person
例項。extractPerson()
會加上@SystemMessage
註解,將操作提示與其建立關聯。這是模型會用來引導擷取資訊的提示,並以 JSON 文件格式傳回詳細資料,系統會為您剖析這些資料,並將其解析為Person
例項。
接著來看看 main()
方法的內容:
- 對話模型已設定及實例化。我們會使用模型建構工具類別的 2 個新方法:
responseMimeType()
和responseSchema()
。第一個指令會指示 Gemini 在輸出內容中產生有效的 JSON。第二種方法則是定義應傳回的 JSON 物件結構定義。此外,後者會將權限委派給便利方法,該方法可將 Java 類別或記錄轉換為適當的 JSON 結構定義。 PersonExtractor
物件是透過 LangChain4j 的AiServices
類別建立。- 接著,您只需呼叫
Person person = extractor.extractPerson(...)
,即可從非結構化文字中擷取人物的詳細資料,並傳回包含姓名和年齡的Person
例項。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
您應該會看到以下的輸出內容:
Anna 23
是的,我是 Anna,他們今年 23 歲!
使用這個 AiServices
方法,您可以操作強型別物件。您並未直接與 LLM 互動。而是使用具體類別,例如 Person
記錄,用來代表已擷取的個人資訊,您也擁有 PersonExtractor
物件,其中包含 extractPerson()
方法,可傳回 Person
例項。LLM 的概念已抽離,因此當您使用這個 PersonExtractor
介面時,身為 Java 開發人員的您只需操作一般類別和物件。
8. 使用提示範本來建構提示
當您使用一組常見指示或問題與 LLM 互動時,提示的某部分會保持不變,而其他部分則包含資料。舉例來說,如果要建立食譜,可以使用「你是位優秀的廚師,請使用以下食材製作食譜:...」這類提示,然後在該文字結尾附加食材。這就是提示範本的用途,類似於程式設計語言中的插補字串。提示範本包含預留位置,您可以根據特定 LLM 呼叫替換為正確的資料。
具體來說,讓我們研究 app/src/main/java/gemini/workshop
目錄中的 TemplatePrompt.java
:
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());
}
}
您可以照常設定 VertexAiGeminiChatModel
模型,使用高水準的廣告素材,並設定較高的 temperature、topP 和 topK 值。接著,您可以透過傳遞提示的字串,使用 from()
靜態方法建立 PromptTemplate
,並使用雙圓括號占位符變數:{{dish}}
和 {{ingredients}}
。
您可以呼叫 apply()
來建立最終提示,該函式會採用鍵/值組合的對應,代表預留位置的名稱和要取代的字串值。
最後,您可以使用 prompt.toUserMessage()
指令,根據該提示建立使用者訊息,進而呼叫 Gemini 模型的 generate()
方法。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt
您應該會看到類似以下的產生輸出內容:
**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.
您可以自行變更地圖中的 dish
和 ingredients
值,並調整溫度 topK
和 tokP
,然後重新執行程式碼。這樣一來,您就能觀察變更這些參數對 LLM 的影響。
提示範本是為 LLM 呼叫提供可重複使用且可設定參數的指示的絕佳方法。您可以傳遞資料,並針對使用者提供的不同值自訂提示。
9. 使用少量樣本提示法進行文字分類
大型語言模型非常擅長將文字分類。您可以提供一些文字範例和相關類別,協助 LLM 完成這項工作。這種做法通常稱為「少量樣本提示」。
我們來開啟 app/src/main/java/gemini/workshop
目錄中的 TextClassification.java
,進行特定類型的文字分類:情緒分析。
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!"));
}
}
Sentiment
列舉會列出情緒的不同值:負面、中性或正面。
在 main()
方法中,您可以照常建立 Gemini 聊天模型,但請使用較少的最大輸出符記數,因為您只需要簡短的回覆:文字為 POSITIVE
、NEGATIVE
或 NEUTRAL
。為了限制模型只傳回這些值,您可以善用在「資料擷取」一節中所述的結構化輸出支援功能。因此我們會使用 responseSchema()
方法。這次您不會使用 SchemaHelper
的方便方法來推斷結構定義,而是改用 Schema
建構工具,瞭解結構定義的樣貌。
設定模型後,您可以建立 SentimentAnalysis
介面,LangChain4j 的 AiServices
會使用 LLM 為您實作這個介面。這個介面包含一個方法:analyze()
。這個方法會在輸入內容中分析文字,並傳回 Sentiment
列舉值。因此,您只會操作代表已辨識情緒類別的強型別物件。
接著,為了提供「少量範例」讓模型進行分類作業,您需要建立聊天記錄,以便傳遞一組使用者訊息和 AI 回應,代表文字和與文字相關的情緒。
讓我們透過 AiServices.builder()
方法,將所有內容與 SentimentAnalysis
介面、要使用的模型,以及包含少量範例的即時通訊記憶體綁在一起。最後,請使用要分析的文字呼叫 analyze()
方法。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
畫面上應會顯示單字:
POSITIVE
看來喜歡草莓是正面情緒!
10. 檢索增強生成
LLM 經過大量文字訓練,不過,它們的知識只涵蓋訓練期間看到的資訊。如果模型訓練截止日期過後有新資訊發布,模型就無法取得這些詳細資料。因此,模型無法回答未見過的資訊相關問題。
因此,本節將介紹檢索增強生成 (RAG) 等方法,協助提供 LLM 可能需要瞭解的額外資訊,以便滿足使用者的請求,回覆較新穎的資訊,或在訓練期間無法存取的私人資訊。
我們來談談對話。這次,你可以針對文件提出問題。您將建立聊天機器人,讓它能夠從資料庫中擷取相關資訊,這些資訊包含以較小片段 (「區塊」) 分割的文件,模型會使用這些資訊來回答問題,而非僅依賴訓練時所提供的知識。
RAG 有兩個階段:
- 攝入階段:系統會將文件載入記憶體,並將其分割為較小的分塊,接著計算向量嵌入 (分塊的高度多維向量表示法),然後儲存至可進行語意搜尋的向量資料庫。當需要將新文件加入文件集時,通常會執行這項擷取階段一次。
- 查詢階段:使用者現在可以針對文件提出問題。問題也會轉換為向量,並與資料庫中的所有其他向量進行比較。最相似的向量通常在語意上有相關性,並由向量資料庫傳回。接著,系統會提供對話內容的脈絡,以及與資料庫傳回的向量相對應的文字片段,並要求 LLM 根據這些片段提供答案。
準備文件
在這個新例子中,您將詢問有關虛構車廠 (同樣也是虛構的) 虛構車款 Cymbal Starlight 的問題!這表示關於虛構車輛的文件不應是模型知識的一部分。因此,如果 Gemini 能夠正確回答有關這輛車的問題,就表示 RAG 方法有效,也就是能夠搜尋文件。
導入聊天機器人
讓我們來瞭解如何建構 2 個階段的做法:首先是文件擷取,然後是使用者針對文件提出問題的查詢時間 (也稱為「擷取階段」)。
在這個範例中,這兩個階段都會在同一個類別中實作。通常,您會使用一個應用程式處理擷取作業,另一個應用程式則會為使用者提供聊天機器介面。
此外,在本範例中,我們會使用記憶體內向量資料庫。在實際的實際工作環境中,擷取和查詢階段會分開在兩個不同的應用程式中,且向量會保留在獨立資料庫中。
文件匯入
文件擷取階段的第一步,就是找出關於虛構汽車的 PDF 檔案,並準備 PdfParser
來讀取該檔案:
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());
您不必先建立一般聊天語言模型,而是建立嵌入模型的例項。這是一種特殊模型,其功能是為文字片段 (字詞、句子或段落) 建立向量表示法。它會傳回浮點數的向量,而非傳回文字回應。
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();
接下來,您需要幾個類別來協同合作,以便執行以下操作:
- 分塊載入及分割 PDF 文件。
- 為所有這些區塊建立向量嵌入。
InMemoryEmbeddingStore<TextSegment> embeddingStore =
new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 100))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
storeIngestor.ingest(document);
建立 InMemoryEmbeddingStore
例項 (記憶體內向量資料庫),用於儲存向量嵌入項目。
文件會因 DocumentSplitters
類別而分割成多個區塊。它會將 PDF 檔案的文字分割成 500 個字元的片段,並以 100 個字元重疊 (使用下列區塊,以免將字詞或句子切割成零碎片段)。
儲存器擷取器會連結文件分割器、用於計算向量的嵌入模型,以及記憶體內向量資料庫。接著,ingest()
方法會負責執行攝入作業。
第一階段結束後,文件已轉換為文字區塊,並附上相關的向量嵌入資料,然後儲存在向量資料庫中。
提問
準備好提問吧!建立即時通訊模型來展開對話:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(1000)
.build();
您也需要擷取器類別,才能將向量資料庫 (在 embeddingStore
變數中) 與嵌入模型連結。其工作是透過為使用者查詢計算向量嵌入項目,查詢向量資料庫,以便在資料庫中找出相似的向量:
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
建立代表汽車專家助理的介面,這是 AiServices
類別會實作的介面,可讓您與模型互動:
interface CarExpert {
Result<String> ask(String question);
}
CarExpert
介面會傳回以 LangChain4j 的 Result
類別包裝的字串回應。為什麼要使用這個包裝函式?因為這不僅會提供答案,還可讓您檢查內容擷取工具傳回的資料庫區塊。這樣一來,您就能向使用者顯示用於提供最終答案的文件來源。
此時,您可以設定新的 AI 服務:
CarExpert expert = AiServices.builder(CarExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
這項服務會將下列項目綁定在一起:
- 您先前設定的聊天語言模型。
- 即時通訊記錄,用於追蹤對話。
- retriever會將向量嵌入查詢與資料庫中的向量進行比對。
.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())
你終於可以開始提問了!
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());
});
完整原始碼位於 app/src/main/java/gemini/workshop
目錄中的 RAG.java
中。
執行範例:
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
輸出結果應會顯示問題的答案:
=== 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. 函式呼叫
在某些情況下,您可能會希望 LLM 能夠存取外部系統,例如用於擷取資訊或執行動作的遠端網頁 API,或是執行某種運算的服務。例如:
遠端網路 API:
- 追蹤及更新客戶訂單。
- 在問題追蹤工具中找出或建立問題單。
- 擷取即時資料,例如股票報價或 IoT 感應器測量值。
- 傳送電子郵件。
運算工具:
- 可用於解答較進階的數學問題。
- 當 LLM 需要推理邏輯時,用於執行程式碼的程式碼解讀。
- 將自然語言要求轉換為 SQL 查詢,以便 LLM 查詢資料庫。
函式呼叫 (有時稱為工具或工具使用) 是指模型可以要求代為執行一或多個函式呼叫的功能,以便使用較新鮮的資料正確回答使用者的提示。
當使用者提出特定提示,且大型語言模型已知與該脈絡相關的現有函式時,即可傳回函式呼叫要求。整合大型語言模型的應用程式可代為呼叫函式,然後回覆大型語言模型的回應,而大型語言模型則會回覆文字答案。
函式呼叫的四個步驟
我們來看看函式呼叫的範例:取得天氣預報資訊。
如果你向 Gemini 或其他 LLM 詢問巴黎的天氣,他們會回覆說,目前沒有關於天氣預報的資訊。如果您希望 LLM 能即時存取天氣資料,就必須定義可供 LLM 要求使用的函式。
請查看下圖:
1️⃣ 首先,使用者詢問巴黎的天氣。使用 LangChain4j 的聊天機器人應用程式會知道,有一個或多個可用函式可協助 LLM 執行查詢。聊天機器人會傳送初始提示,以及可呼叫的函式清單。此處的函式名為 getWeather()
,會使用位置的字串參數。
由於 LLM 不瞭解天氣預報,因此不會透過文字回覆,而是傳回函式執行要求。聊天機器人必須以 "Paris"
做為位置參數呼叫 getWeather()
函式。
2️⃣ 聊天機器人會代表大型語言模型呼叫該函式,並擷取函式回應。假設回應為 {"forecast": "sunny"}
。
3️⃣ 聊天機器人應用程式將 JSON 回應傳回至 LLM。
4️⃣ LLM 會查看 JSON 回應,解讀該資訊,並最終回覆文字,指出巴黎的天氣為晴。
每個步驟皆為程式碼
首先,您會按照一般方式設定 Gemini 模型:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(100)
.build();
您可以定義工具規格,說明可呼叫的函式:
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();
函式名稱已定義,參數的名稱和類型也已定義,但請注意,函式和參數都已提供說明。說明非常重要,可協助大型語言模型真正瞭解函式可執行的功能,進而判斷是否需要在對話內容中呼叫此函式。
讓我們開始步驟 1,傳送關於巴黎天氣的初始問題:
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);
在步驟 2 中,我們會傳遞要讓模型使用的工具,而模型會回覆工具執行要求:
// 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());
步驟 3:此時,我們知道大型語言模型希望我們呼叫的函式。在程式碼中,我們並未實際呼叫外部 API,而是直接傳回假設的天氣預報:
// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
"{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);
在步驟 4 中,LLM 會瞭解函式執行結果,然後合成文字回應:
// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());
輸出內容如下:
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.
您可以在上方輸出內容中看到工具執行要求和答案。
完整原始碼位於 app/src/main/java/gemini/workshop
目錄中的 FunctionCalling.java
:
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling
畫面會顯示類似以下的輸出:
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 會處理函式呼叫作業
在前一個步驟中,您已瞭解如何交錯使用一般文字問題/答案和函式要求/回應互動,並在其中直接提供要求的函式回應,而無須呼叫實際函式。
不過,LangChain4j 也提供更高層級的抽象化功能,可為您透明地處理函式呼叫,同時照常處理對話。
單一函式呼叫
我們來逐一看看 FunctionCallingAssistant.java
。
首先,您需要建立代表函式回應資料結構的記錄:
record WeatherForecast(String location, String forecast, int temperature) {}
回應會包含位置、預測和溫度相關資訊。
接著,您可以建立包含要提供給模型的實際函式的類別:
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);
}
}
}
請注意,這個類別包含單一函式,但會加上 @Tool
註解,對應模型可要求呼叫的函式說明。
函式的參數 (此處為單一參數) 也已加上註解,但使用簡短的 @P
註解,同時提供參數說明。您可以視需要新增任意數量的函式,讓模型可用於更複雜的情況。
在這個類別中,您會傳回一些預先設定的回應,但如果您想呼叫實際的外部天氣預報服務,則會在該方法的內容中呼叫該服務。
如同先前方法建立 ToolSpecification
時所示,您必須記錄函式的作用,並說明參數對應的項目。這有助模型瞭解如何使用這項功能,以及何時使用。
接下來,LangChain4j 會讓您提供與要用於與模型互動的合約相對應的介面。以下是簡單的介面,可接收代表使用者訊息的字串,並傳回與模型回應相對應的字串:
interface WeatherAssistant {
String chat(String userMessage);
}
如果您想處理更進階的情況,也可以使用涉及 LangChain4j 的 UserMessage
(針對使用者訊息) 或 AiMessage
(針對模型回應) 等更複雜的簽章,甚至是 TokenStream
,因為這些較複雜的物件也包含額外資訊,例如已使用的符記數量等。但為了簡單起見,我們只會在輸入內容中使用字串,並在輸出內容中使用字串。
讓我們以 main()
方法結束,這個方法會將所有部分連結在一起:
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?"));
}
您可以照常設定 Gemini 聊天模型。接著,您要將天氣預報服務例項化,其中包含模型要求我們呼叫的「函式」。
現在,您可以再次使用 AiServices
類別來繫結聊天模型、聊天記憶體和工具 (即天氣預報服務及其函式)。AiServices
會傳回實作您定義的 WeatherAssistant
介面的物件。剩下的工作就是呼叫該助理的 chat()
方法。在叫用時,您只會看到文字回應,但開發人員無法看到函式呼叫要求和函式呼叫回應,且系統會自動且透明地處理這些要求。如果 Gemini 認為應呼叫函式,就會回覆函式呼叫要求,而 LangChain4j 會代您呼叫本機函式。
執行範例:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
畫面會顯示類似以下的輸出:
OK. The weather in Paris is sunny with a temperature of 20 degrees.
這是單一函式的範例。
多次函式呼叫
您也可以擁有多個函式,並讓 LangChain4j 代為處理多個函式呼叫。請參閱 MultiFunctionCallingAssistant.java
的多函式範例。
它具有轉換貨幣的函式:
@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;
}
另一個用於取得股票價值的函式:
@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;
}
另一個函式可將百分比套用至指定金額:
@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;
}
接著,您可以結合所有這些函式和 MultiTools 類別,並提出類似「AAPL 股票價格從美元轉換成歐元後,10% 的價格是多少?」的提問。
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?"));
}
請按照下列指示執行:
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
您應該會看到多個函式呼叫:
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.
服務專員
函式呼叫是 Gemini 等大型語言模型的絕佳擴充機制。這可讓我們建構更複雜的系統,通常稱為「代理程式」或「AI 助理」。這些代理程式可透過外部 API 與外部世界互動,以及與可能對外部環境產生副作用的服務互動 (例如傳送電子郵件、建立支援單等)。
建立這類功能強大的服務專員時,請務必負起責任。請先考量人機迴圈,再採取自動動作。設計與外部世界互動的 LLM 輔助代理程式時,請務必考量安全性。
13. 使用 Ollama 和 TestContainers 執行 Gemma
到目前為止,我們一直使用 Gemini,但也有 Gemma,這是 Gemini 的姊妹模型。
Gemma 是一系列先進的輕量級開放式模型,採用與建立 Gemini 模型時相同的研究成果和技術。Gemma 有兩種變化版本,分別是 Gemma1 和 Gemma2,每種版本都有不同的尺寸。Gemma1 有 2B 和 7B 兩種尺寸,Gemma 2 有兩種尺寸:9B 和 27B。這些模型的權重可供免費使用,而且體積小,您可以自行執行,甚至可以在筆記型電腦或 Cloud Shell 中執行。
如何執行 Gemma?
您可以透過多種方式執行 Gemma,包括在雲端透過 Vertex AI 點按按鈕,或在 GKE 中使用部分 GPU 執行,也可以在本機執行。
Ollama 是本機執行 Gemma 的理想選擇,這項工具可讓您在本機電腦上執行小型模型,例如 Llama 2、Mistral 和其他許多模型。這與 Docker 類似,但適用於 LLM。
按照適用於您作業系統的操作說明安裝 Ollama。
如果您使用的是 Linux 環境,請先在安裝 Ollama 後啟用。
ollama serve > /dev/null 2>&1 &
在本機安裝完成後,您可以執行指令來提取模型:
ollama pull gemma:2b
等待模型下載完成。這項作業可能需要一點時間。
執行模型:
ollama run gemma:2b
您現在可以與模型互動:
>>> Hello! Hello! It's nice to hear from you. What can I do for you today?
如要退出提示,請按下 Ctrl + D
在 TestContainers 上執行 Ollama 中的 Gemma
您不必在本機安裝及執行 Ollama,而是可以在容器中使用 Ollama,由 TestContainers 處理。
TestContainers 不僅可用於測試,還可用於執行容器。甚至還有專屬的 OllamaContainer
可供你使用!
以下是完整的圖表:
導入
我們來逐一看看 GemmaWithOllamaContainer.java
。
首先,您需要建立衍生 Ollama 容器,以便匯入 Gemma 模型。這個映像檔可能已在先前的執行作業中存在,也可能會在執行時建立。如果映像檔已存在,您只需告訴 TestContainers,您想用 Gemma 支援的變化版本取代預設 Ollama 映像檔:
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"));
}
}
接著,您將建立並啟動 Ollama 測試容器,然後使用要使用的模型,指向容器的地址和連接埠,建立 Ollama 聊天模型。最後,您只要照常叫用 model.generate(yourPrompt)
即可:
public static void main(String[] args) throws IOException, InterruptedException {
OllamaContainer ollama = createGemmaOllamaContainer();
ollama.start();
ChatLanguageModel model = OllamaChatModel.builder()
.baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
.modelName("gemma:2b")
.build();
String response = model.generate("Why is the sky blue?");
System.out.println(response);
}
請按照下列指示執行:
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
第一次執行時,系統需要花點時間建立及執行容器,但完成後,您應該會看到 Gemma 回應:
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.
您已在 Cloud Shell 中執行 Gemma!
14. 恭喜
恭喜,您已成功使用 LangChain4j 和 Gemini API,以 Java 建構第一個生成式 AI 聊天應用程式!您在過程中發現,多模態大型語言模型相當強大,能夠處理各種工作,例如問題/答案 (甚至是您自己的文件)、資料擷取、與外部 API 互動等等。
後續步驟
您可以使用強大的 LLM 整合功能,為應用程式增添新功能!