1. はじめに
この Codelab では、Google Cloud の Vertex AI でホストされる Gemini 大規模言語モデル(LLM)に焦点を当てます。Vertex AI は、Google Cloud 上の ML のプロダクト、サービス、モデルすべてを含むプラットフォームです。
Java で LangChain4j フレームワークを使って Gemini API を操作します。LLM を質問応答、アイデアの生成、エンティティと構造化コンテンツの抽出、検索拡張生成、関数呼び出しに活用できる具体的な例を説明します。
生成 AI とは
生成 AI とは、AI を使用してテキスト、画像、音楽、音声、動画などの新しいコンテンツを作成することを意味します。
生成 AI は大規模言語モデル(LLM)を基盤としており、マルチタスクが可能で、要約、Q&A、分類などのすぐに使えるタスクを実行できます。わずかなサンプルデータで、最小限のトレーニングで基盤モデルを対象のユースケースに適応させることができます。
生成 AI の仕組み
生成 AI は、機械学習(ML)モデルを使用して、人間が作成したコンテンツのデータセット内のパターンと関係を学習します。次に、学習したパターンを使用して新しいコンテンツを生成します。
生成 AI モデルをトレーニングする最も一般的な方法は、教師あり学習を使用することです。モデルには、人間が作成したコンテンツと対応するラベルのセットが与えられます。そして、人間が作成したコンテンツと類似したコンテンツを生成することを学習します。
一般的な生成 AI 用途とは
生成 AI は、以下のことに使用できます。
- チャットと検索のエクスペリエンスを改善することで、顧客対応を改善します。
- 会話インターフェースと要約を通じて、膨大な量の非構造化データを探索します。
- 提案依頼書への返信、マーケティング コンテンツのさまざまな言語へのローカライズ、顧客契約のコンプライアンス チェックなど、繰り返し作業を支援します。
Google Cloud にはどのような生成 AI サービスがありますか?
Vertex AI を使用すると、ML の専門知識がほとんどなくても、基盤モデルを操作、カスタマイズし、アプリケーションに埋め込むことができます。Model Garden で基盤モデルにアクセスするか、Vertex AI Studio のシンプルな UI からモデルをチューニングするか、データ サイエンス ノートブックでモデルを使用できます。
Vertex AI Search and Conversation は、生成 AI を活用した検索エンジンと chatbot を構築する最速の手段をデベロッパーに提供します。
Gemini を活用した Gemini for Google Cloud は、Google Cloud と IDE で利用できる AI を活用したコラボレーターで、より多くのことをより迅速にこなせるよう支援します。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 を操作するにはどうすればよいでしょうか。
次のどちらかの方法でお問い合わせください。
- Vertex AI Java API for Gemini の公式ライブラリ。
- LangChain4j フレームワーク。
この Codelab では、LangChain4j フレームワークを使用します。
LangChain4j フレームワークとは
LangChain4j フレームワークは、LLM を Java アプリケーションに統合するためのオープンソース ライブラリです。LLM 自体などのさまざまなコンポーネントだけでなく、ベクトル データベース(セマンティック検索用)、ドキュメント ローダとスプリッター(ドキュメントを分析して学習する)、出力パーサーなどをオーケストレートすることで、LLM を統合します。
このプロジェクトは LangChain Python プロジェクトから着想を得たもので、Java 開発者にサービスを提供することを目標としていました。
学習内容
- Gemini と LangChain4j を使用するように Java プロジェクトをセットアップする方法
- プログラムで最初のプロンプトを Gemini に送信する方法
- Gemini の回答をストリーミングする方法
- ユーザーと Gemini 間の会話を作成する方法
- テキストと画像の両方を送信して、マルチモーダル コンテキストで Gemini を使用する方法
- 非構造化コンテンツから有用な構造化情報を抽出する方法
- プロンプト テンプレートを操作する方法
- 感情分析などのテキスト分類の方法
- 独自のドキュメントとチャットする方法(検索拡張生成)
- 関数呼び出しで chatbot を拡張する方法
- Ollama と TestContainers を使用してローカルで Gemma を使用する方法
必要なもの
- Java プログラミング言語の知識
- Google Cloud プロジェクト
- Chrome や Firefox などのブラウザ
2. 設定と要件
セルフペース型の環境設定
- Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。
- プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。いつでも更新できます。
- プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常は、この内容を意識する必要はありません。ほとんどの Codelab では、プロジェクト ID(通常は
PROJECT_ID
と識別されます)を参照する必要があります。生成された ID が好みではない場合は、ランダムに別の ID を生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ ID になります。 - なお、3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
- 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に請求が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクトを削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。
Cloud Shell の起動
Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では Cloud 上で動作するコマンドライン環境である Cloud Shell を使用します。
Cloud Shell をアクティブにする
- Cloud Console で、[Cloud Shell をアクティブにする] をクリックします。
Cloud Shell を初めて起動する場合は、内容を説明する中間画面が表示されます。中間画面が表示されたら、[続行] をクリックします。
Cloud Shell のプロビジョニングと接続に少し時間がかかる程度です。
この仮想マシンには、必要なすべての開発ツールが読み込まれます。5 GB の永続的なホーム ディレクトリが用意されており、Google Cloud で稼働するため、ネットワークのパフォーマンスと認証が大幅に向上しています。この Codelab での作業のほとんどはブラウザを使って行うことができます。
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. 開発環境の準備
この Codelab では、Cloud Shell ターミナルと Cloud Shell エディタを使用して Java プログラムを開発します。
Vertex AI API を有効にする
Google Cloud コンソールで、Google Cloud コンソールの上部にプロジェクト名が表示されていることを確認します。選択されていない場合は、[プロジェクトを選択] をクリックして [Project Selector] を開き、目的のプロジェクトを選択します。
Vertex AI API は、Google Cloud コンソールの [Vertex AI] セクションまたは Cloud Shell ターミナルから有効にできます。
Google Cloud コンソールから有効にするには、まず Google Cloud コンソール メニューの [Vertex AI] セクションに移動します。
Vertex AI ダッシュボードで、[すべての推奨 API を有効化] をクリックします。
これにより複数の API が有効になりますが、この Codelab で最も重要なのは aiplatform.googleapis.com
です。
または、Cloud Shell ターミナルから次のコマンドを使用してこの API を有効にすることもできます。
gcloud services enable aiplatform.googleapis.com
GitHub リポジトリの クローンを作成する
Cloud Shell ターミナルで、この Codelab のリポジトリのクローンを作成します。
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 Editor を開いて設定する
Cloud Shell から Cloud Code エディタでコードを開きます。
Cloud Code エディタで、File
-> を選択して Codelab のソースフォルダを開きます。Open Folder
で、Codelab のソースフォルダ(/home/username/gemini-workshop-for-java-developers/
)を指定できます。
Gradle for Java をインストールする
Cloud Code エディタを Gradle で適切に動作させるには、Gradle for Java 拡張機能をインストールします。
まず、[Java Projects] セクションに移動し、プラス記号を押します。
Gradle for Java
を選択:
Install Pre-Release
のバージョンを選択します。
インストールが完了すると、Disable
ボタンと Uninstall
ボタンが表示されます。
最後に、ワークスペースをクリーンアップして新しい設定を適用します。
ワークショップを再読み込みして削除するよう求められます。Reload and delete
を選択してください。
App.java などのファイルを開くと、エディタが正常に動作し、構文がハイライト表示されているはずです。
これで、Gemini に対してサンプルを実行する準備が整いました。
環境変数を設定する
Terminal
-> を選択して、Cloud Code エディタで新しいターミナルを開きます。New Terminal
。コードサンプルの実行に必要な 2 つの環境変数を設定します。
- 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-001")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
この最初の例では、ChatModel
インターフェースを実装する VertexAiGeminiChatModel
クラスをインポートする必要があります。
main
メソッドで、VertexAiGeminiChatModel
のビルダーを使用してチャット言語モデルを構成し、以下を指定します。
- プロジェクト
- 場所
- モデル名(
gemini-1.5-flash-001
)。
言語モデルの準備ができたら、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 に初めて呼び出しました。
レスポンスのストリーミング
数秒後に、一度に応答していることに気付きましたか。レスポンスのストリーミング バリアントによって、レスポンスを段階的に取得することもできます。ストリーミング レスポンスの場合、モデルはレスポンスを 1 つずつ返します。
この Codelab では、非ストリーミング レスポンスについて取り上げますが、ストリーミング レスポンスを見てみましょう。
app/src/main/java/gemini/workshop
ディレクトリの StreamQA.java
で、ストリーミング レスポンスの動作を確認できます。
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import dev.langchain4j.model.StreamingResponseHandler;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.build();
model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
@Override
public void onNext(String text) {
System.out.println(text);
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});
}
}
今回は、StreamingChatLanguageModel
インターフェースを実装するストリーミング クラスのバリアント VertexAiGeminiStreamingChatModel
をインポートします。また、StreamingResponseHandler
も必要です。
今回は、generate()
メソッドのシグネチャが少し異なります。文字列を返すのではなく、戻り値の型は void です。プロンプトに加えて、ストリーミング レスポンス ハンドラを渡す必要があります。ここでは、2 つのメソッド onNext(String text)
と onError(Throwable error)
を使用して、匿名の内部クラスを作成することで、インターフェースを実装します。前者はレスポンスの新しい部分が利用可能になるたびに呼び出されますが、後者はエラーが発生したときにのみ呼び出されます。
次のコマンドを実行します。
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
前のクラスと同様の答えが得られますが、今回は、解答全体が表示されるのを待つのではなく、解答がシェルに段階的に表示されます。
追加設定
構成では、プロジェクト、ロケーション、モデル名のみを定義しましたが、モデルに指定できるパラメータは他にもあります。
temperature(Float temp)
- レスポンスに求めるクリエイティブを定義します(0 は創造性が低く、多くの場合は事実に即したもので、1 はよりクリエイティブな出力を表します)。topP(Float topP)
- 合計確率がその浮動小数点数(0 ~ 1)の合計になる単語を選択します。topK(Integer topK)
- テキスト補完として候補となる単語の最大数(1 ~ 40)からランダムに 1 つの単語を選択します。maxOutputTokens(Integer max)
- モデルからの回答の最大長を指定します(通常、4 個のトークンは約 3 語を表します)。maxRetries(Integer retries)
- 1 時間あたりのリクエスト数の割り当てを超過している場合や、プラットフォームに技術的な問題が発生している場合は、モデルに呼び出しを 3 回再試行させることができます。
ここまでは 1 つの質問を Gemini に投げかけましたが、マルチターンの会話を行うこともできます。これについては次のセクションで説明します。
5. Gemini と話そう
前のステップでは 1 つの質問をしました。それでは、ユーザーと LLM で実際に会話してみましょう。各質問と回答は、それ以前の質問と回答をベースにして、実際の会話を形成できます。
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-001")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
このクラスには、興味深いインポート機能がいくつかあります。
MessageWindowChatMemory
- 会話のマルチターンの側面を処理し、以前の質問と回答をローカルメモリに保持するクラスAiServices
- チャットモデルとチャットメモリを関連付けるクラス
main メソッドでは、モデル、チャットメモリ、AI サービスを設定します。モデルは、プロジェクト、ロケーション、モデル名の情報で通常どおり構成されます。
チャットのメモリには、MessageWindowChatMemory
のビルダーを使用して、最近やり取りした 20 件のメッセージを保持するメモリを作成します。これは会話上のスライディング ウィンドウであり、そのコンテキストは Java クラス クライアントのローカルに保持されます。
次に、チャットモデルをチャットメモリにバインドする AI service
を作成します。
AI サービスが、LangChain4j が実装した定義済みカスタム ConversationService
インターフェースをどのように利用し、String
クエリを受け取って String
レスポンスを返すことに注目してください。
次に、Gemini について説明します。最初に簡単な挨拶が送信された後、エッフェル塔がどこにあるかを把握するための最初の質問が送信されます。最後の文は、最初の質問の答えに関連しています。前の回答で出された国について明示的に言及せずに、エッフェル塔がある国には住民が何人いるのか不思議に思っています。過去の質問と回答がすべてのプロンプトとともに送信されていることがわかります。
サンプルを実行する
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
次のような 3 つの回答が表示されます。
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 はこの猫を認識するでしょうか?
雪の中にいる猫の画像(ウィキペディアより抜粋)https://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-001")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
インポートでは、さまざまな種類のメッセージとコンテンツが区別されていることがわかります。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. 非構造化テキストから構造化された情報を抽出する
レポート ドキュメント、メール、その他の長いテキストで重要な情報が構造化されていない方法で提供されていることは数多くあります。理想的には、非構造化テキストに含まれる主要な詳細を、構造化オブジェクトの形式で抽出できる必要があります。その方法を見てみましょう。
略歴または説明から、その人物の名前と年齢を抽出するとします。プロンプトを細かく調整して、非構造化テキストから JSON を抽出するように LLM に指示できます(これは一般に「プロンプト エンジニアリング」と呼ばれます)。
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.UserMessage;
public class ExtractData {
static record Person(String name, int age) {}
interface PersonExtractor {
@UserMessage("""
Extract the name and age of the person described below.
Return a JSON document with a "name" and an "age" property, \
following this structure: {"name": "John Doe", "age": 34}
Return only JSON, without any markdown markup surrounding it.
Here is the document describing the person:
---
{{it}}
---
JSON:
""")
Person extractPerson(String text);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.temperature(0f)
.topK(1)
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
Person person = extractor.extractPerson("""
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
"""
);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
このファイルのさまざまなステップを見てみましょう。
Person
レコードは、人物の詳細情報(名前と年齢)を表すために定義されます。PersonExtractor
インターフェースは、非構造化テキスト文字列を指定してPerson
インスタンスを返すメソッドで定義されます。extractPerson()
には、プロンプトを関連付ける@UserMessage
アノテーションが付けられます。これは、モデルが情報を抽出し、詳細を JSON ドキュメント形式で返すためにモデルが使用するプロンプトです。ドキュメントは自動で解析され、Person
インスタンスにマーシャリング解除されます。
次に、main()
メソッドの内容を見てみましょう。
- チャットモデルがインスタンス化されます。非常に低い
temperature
の 0 と 1 のtopK
を使用して、非常に決定的な回答にしています。これは、モデルが指示に適切に従うことにも役立ちます。特に、Gemini が JSON レスポンスを追加の Markdown マークアップでラップすることは望ましくありません。 PersonExtractor
オブジェクトは、LangChain4j のAiServices
クラスにより作成されます。- 次に、
Person person = extractor.extractPerson(...)
を呼び出すだけで、非構造化テキストから人物の詳細を抽出し、名前と年齢を含むPerson
インスタンスを取得できます。
サンプルを実行する
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
次の出力が表示されます。
Anna 23
はい、23 歳のアンナです。
この AiServices
アプローチでは、厳密に型指定されたオブジェクトで操作します。LLM と直接やり取りしていません。代わりに、抽出した個人情報を表す Person
レコードなどの具象クラスで作業し、Person
インスタンスを返す extractPerson()
メソッドを含む PersonExtractor
オブジェクトを用意します。LLM の概念は抽象化されており、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-001")
.maxOutputTokens(500)
.temperature(0.8f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
通常どおり、Temperature が高く、topP 値と topK 値も高く、高度な創造性で VertexAiGeminiChatModel
モデルを構成します。次に、プロンプトの文字列を渡して from()
静的メソッドで PromptTemplate
を作成し、二重中かっこのプレースホルダ変数({{dish}}
と {{ingredients}}
)を使用します。
プレースホルダの名前と置き換える文字列値を表す Key-Value ペアのマップを受け取る 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 はテキストをさまざまなカテゴリに分類するのを得意としています。テキストとそれに関連するカテゴリの例をいくつか提供することで、そのタスクで LLM を助けることができます。このアプローチは少数ショット プロンプトと呼ばれます。
特定のタイプのテキスト分類(感情分析)を行うには、app/src/main/java/gemini/workshop
ディレクトリの TextClassification.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;
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.Map;
public class TextClassification {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.maxOutputTokens(10)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
Analyze the sentiment of the text below. Respond only with one word to describe the sentiment.
INPUT: This is fantastic news!
OUTPUT: POSITIVE
INPUT: Pi is roughly equal to 3.14
OUTPUT: NEUTRAL
INPUT: I really disliked the pizza. Who would use pineapples as a pizza topping?
OUTPUT: NEGATIVE
INPUT: {{text}}
OUTPUT:
""");
Prompt prompt = promptTemplate.apply(
Map.of("text", "I love strawberries!"));
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
main()
メソッドで、通常どおり Gemini チャットモデルを作成しますが、短いレスポンスのみが必要なため、最大出力トークン数を小さくします。テキストは POSITIVE
、NEGATIVE
、または NEUTRAL
です。
次に、入力と出力の例をいくつかモデルに指示することで、再利用可能なプロンプト テンプレートを少数ショット プロンプトの手法で作成します。これは、モデルが実際の出力に従うことにも役立ちます。Gemini は全文で応答するのではなく、1 つの単語のみで応答するように指示されます。
apply()
メソッドで変数を適用し、{{text}}
プレースホルダを実際のパラメータ("I love strawberries"
)に置き換え、そのテンプレートを toUserMessage()
でユーザー メッセージに変換します。
サンプルを実行する
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
単語が 1 つ表示されます。
POSITIVE
イチゴ好きはポジティブな感情のようです。
10. 検索拡張生成
LLM は大量のテキストでトレーニングされている。しかし、彼らの知識は、トレーニング中に見た情報しかカバーしていません。モデルのトレーニングの締め切り日より後に新しい情報がリリースされた場合、それらの詳細情報はモデルに反映されません。そのため、モデルは、まだ見たことのない情報に関する質問に答えることはできません。
そのため、検索拡張生成(RAG)のようなアプローチは、LLM がユーザーのリクエストに応えるために知っておくべき追加情報を提供し、より新しい可能性のある情報や、トレーニング時にアクセスできない個人情報で応答するために役立ちます。
会話に戻りましょう。今回は、ドキュメントについて質問できるようになります。より小さな断片(「チャンク」)に分割されたドキュメントを含むデータベースから関連情報を取得できる chatbot を構築します。この情報は、トレーニングに含まれる知識のみに頼るのではなく、モデルが回答の根拠を示すために使用します。
RAG には次の 2 つのフェーズがあります。
- 取り込みフェーズ - ドキュメントはメモリに読み込まれ、小さなチャンクに分割されます。ベクトル エンベディング(チャンクの高多次元ベクトル表現)が計算されて、セマンティック検索に対応したベクトル データベースに保存されます。この取り込みフェーズは通常、新しいドキュメントをドキュメント コーパスに追加する必要があるときに 1 回実行されます。
- クエリフェーズ - ドキュメントについて質問できるようになりました。質問もベクトルに変換され、データベース内の他のすべてのベクトルと比較されます。通常、最も類似したベクトルは意味的に関連しており、ベクトル データベースによって返されます。次に、LLM に会話のコンテキスト、すなわちデータベースから返されたベクトルに対応する一連のテキストが与えられます。LLM は、これらのチャンクを見て回答の根拠を示すように求められます。
書類を準備する
この新しいデモでは、「Attention is all you need」について質問します。学びました。Google が開発した Transformer ニューラル ネットワーク アーキテクチャについて説明します。このアーキテクチャでは、現代の大規模言語モデルはすべて実装されています。
論文はすでにリポジトリの attention-is-all-you-need.pdf にダウンロードされています。
chatbot を実装する
2 段階のアプローチの構築方法を見てみましょう。まずドキュメントの取り込みで、次にユーザーがドキュメントについて質問したときのクエリ時間です。
この例では、両方のフェーズが同じクラスに実装されています。通常、取り込みを処理するアプリケーションと、ユーザーに chatbot インターフェースを提供する別のアプリケーションを用意します。
また、この例では、インメモリ ベクトル データベースを使用します。実際の本番環境シナリオでは、取り込みフェーズとクエリフェーズが 2 つの異なるアプリケーションに分離され、ベクトルはスタンドアロン データベースで保持されます。
ドキュメントの取り込み
ドキュメントの取り込みフェーズの最初のステップは、すでにダウンロードした PDF ファイルを見つけて、それを読み取るための PdfParser
を準備することです。
URL url = new URI("https://github.com/glaforge/gemini-workshop-for-java-developers/raw/main/attention-is-all-you-need.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());
通常のチャット言語モデルを作成する代わりに、エンベディング モデルのインスタンスを作成します。これは、テキスト断片(単語、文、さらには段落)のベクトル表現を作成する役割を持つ特定のモデルです。テキスト レスポンスではなく、浮動小数点数のベクトルを返します。
VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
.endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.publisher("google")
.modelName("textembedding-gecko@003")
.maxRetries(3)
.build();
次に、共同作業を行うクラスがいくつか必要になります。
- 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 ファイルのテキストを、100 文字の重複で 500 文字のスニペットに分割します(次のチャンクは、単語や文が断片に分割されるのを避けるため)。
ストア インジェクタは、ドキュメント スプリッター、ベクトル計算用のエンベディング モデル、インメモリ ベクトル データベースをリンクします。その後、ingest()
メソッドが取り込みを行います。
最初のフェーズが終わり、ドキュメントが関連するベクトル エンベディングによってテキスト チャンクに変換され、ベクトル データベースに格納されました。
質問する
それでは、質問を投げかける準備をしましょう。チャットモデルを作成して会話を開始します。
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.maxOutputTokens(1000)
.build();
また、ベクトル データベース(embeddingStore
変数内)をエンベディング モデルにリンクするための Retriever クラスが必要です。ユーザーのクエリに対するベクトル エンベディングを計算してベクトル データベースをクエリし、データベース内で類似のベクトルを見つけます。
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
main メソッドの外で、LLM エキスパート アシスタントを表すインターフェースを作成します。これは、モデルを操作するために AiServices
クラスが実装するインターフェースです。
interface LlmExpert {
String ask(String question);
}
この時点で、新しい AI サービスを構成できます。
LlmExpert expert = AiServices.builder(LlmExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
このサービスは次のものをバインドします。
- 前の手順で構成したチャット言語モデル。
- 会話を追跡するためのチャットメモリ。
- 取得ツールは、ベクトル エンベディングのクエリをデータベース内のベクトルと比較します。
- プロンプト テンプレートは、提供された情報(つまり、ベクトル エンベディングがユーザーの質問のベクトルに類似しているドキュメントの関連する抜粋)に基づいてチャットモデルが応答する必要があることを明示的に指定します。
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in large language models,\s
you excel at explaining simply and clearly questions about LLMs.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
質問を投げかける準備が整いました。
List.of(
"What neural network architecture can be used for language models?",
"What are the different components of a transformer neural network?",
"What is attention in large language models?",
"What is the name of the process that transforms text into vectors?"
).forEach(query ->
System.out.printf("%n=== %s === %n%n %s %n%n", query, expert.ask(query)));
);
完全なソースコードは app/src/main/java/gemini/workshop
ディレクトリの RAG.java
にあります。
サンプルを実行する
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
出力には、質問に対する回答が表示されます。
=== What neural network architecture can be used for language models? === Transformer architecture === What are the different components of a transformer neural network? === The different components of a transformer neural network are: 1. Encoder: The encoder takes the input sequence and converts it into a sequence of hidden states. Each hidden state represents the context of the corresponding input token. 2. Decoder: The decoder takes the hidden states from the encoder and uses them to generate the output sequence. Each output token is generated by attending to the hidden states and then using a feed-forward network to predict the token's probability distribution. 3. Attention mechanism: The attention mechanism allows the decoder to attend to the hidden states from the encoder when generating each output token. This allows the decoder to take into account the context of the input sequence when generating the output sequence. 4. Positional encoding: Positional encoding is a technique used to inject positional information into the input sequence. This is important because the transformer neural network does not have any inherent sense of the order of the tokens in the input sequence. 5. Feed-forward network: The feed-forward network is a type of neural network that is used to predict the probability distribution of each output token. The feed-forward network takes the hidden state from the decoder as input and outputs a vector of probabilities. === What is attention in large language models? === Attention in large language models is a mechanism that allows the model to focus on specific parts of the input sequence when generating the output sequence. This is important because it allows the model to take into account the context of the input sequence when generating each output token. Attention is implemented using a function that takes two sequences as input: a query sequence and a key-value sequence. The query sequence is typically the hidden state from the previous decoder layer, and the key-value sequence is typically the sequence of hidden states from the encoder. The attention function computes a weighted sum of the values in the key-value sequence, where the weights are determined by the similarity between the query and the keys. The output of the attention function is a vector of context vectors, which are then used as input to the feed-forward network in the decoder. The feed-forward network then predicts the probability distribution of the next output token. Attention is a powerful mechanism that allows large language models to generate text that is both coherent and informative. It is one of the key factors that has contributed to the recent success of large language models in a wide range of natural language processing tasks. === What is the name of the process that transforms text into vectors? === The process of transforming text into vectors is called **word embedding**. Word embedding is a technique used in natural language processing (NLP) to represent words as vectors of real numbers. Each word is assigned a unique vector, which captures its meaning and semantic relationships with other words. Word embeddings are used in a variety of NLP tasks, such as machine translation, text classification, and question answering. There are a number of different word embedding techniques, but one of the most common is the **skip-gram** model. The skip-gram model is a neural network that is trained to predict the surrounding words of a given word. By learning to predict the surrounding words, the skip-gram model learns to capture the meaning and semantic relationships of words. Once a word embedding model has been trained, it can be used to transform text into vectors. To do this, each word in the text is converted to its corresponding vector. The vectors for all of the words in the text are then concatenated to form a single vector, which represents the entire text. Text vectors can be used in a variety of NLP tasks. For example, text vectors can be used to train machine translation models, text classification models, and question answering models. Text vectors can also be used to perform tasks such as text summarization and text clustering.
11. 関数呼び出し
また、情報を取得するリモートウェブ API やなんらかの計算を実行するサービスなど、LLM に外部システムへのアクセスを許可したい場合もあります。例:
リモートウェブ API:
- 顧客の注文を追跡および更新する。
- Issue Tracker でチケットを検索または作成します。
- 株価や IoT センサーの測定値などのリアルタイム データを取得します。
- メールを送信します。
計算ツール:
- より高度な数学の問題向けの電卓。
- LLM に推論ロジックが必要な場合にコードを実行するためのコード解釈。
- 自然言語のリクエストを SQL クエリに変換して、LLM がデータベースにクエリを実行できるようにする。
関数呼び出しとは、モデルが自身の代わりに 1 つ以上の関数呼び出しを行うようリクエストする機能です。これにより、ユーザーのプロンプトに、より新しいデータで適切に応答できるようになります。
ユーザーからの特定のプロンプトと、そのコンテキストに関連する可能性のある既存の関数の知識があれば、LLM は関数呼び出しリクエストで応答できます。LLM を統合するアプリケーションは、関数を呼び出して、レスポンスで LLM に返信できます。LLM は、テキストで回答することで回答を解釈します。
関数呼び出しの 4 つのステップ
天気予報に関する情報を取得する関数呼び出しの例を見てみましょう。
Gemini やその他の LLM にパリの天気を尋ねると、天気予報に関する情報はないと答えるでしょう。LLM が気象データにリアルタイムでアクセスできるようにするには、使用できる関数を定義する必要があります。
次の図をご覧ください。
1️⃣ 最初に、ユーザーがパリの天気について質問します。chatbot アプリは、LLM がクエリを実行するために利用できる関数が 1 つ以上あることを知っています。chatbot は、初期プロンプトと呼び出し可能な関数のリストの両方を送信します。ここでは、ビジネスの文字列パラメータを受け取る getWeather()
という関数を使用しています。
LLM は天気予報を知らないため、テキストで応答するのではなく、関数の実行リクエストを送信します。chatbot は、位置パラメータとして "Paris"
を指定して getWeather()
関数を呼び出す必要があります。
2️⃣ チャットボットが LLM に代わってその関数を呼び出し、関数のレスポンスを取得します。ここでは、レスポンスが {"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-001")
.maxOutputTokens(100)
.build();
呼び出すことができる関数を記述するツール仕様を指定します。
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeatherForecast")
.description("Get the weather forecast for a location")
.addParameter("location", JsonSchemaProperty.STRING,
JsonSchemaProperty.description("the location to get the weather forecast for"))
.build();
関数の名前とパラメータの名前と型が定義されていますが、関数とパラメータの両方に説明があることに注意してください。説明は非常に重要であり、LLM が関数でできることを理解して、その関数を会話のコンテキストで呼び出す必要があるかどうかを判断するのに役立ちます。
まず、パリの天気に関する最初の質問を送信します。
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.この時点で、LLM がどの関数を呼び出すべきかは把握しています。このコードでは、外部 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
を 1 つずつ見ていきましょう。
まず、関数のレスポンス データ構造を表すレコードを作成します。
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
アノテーションが付けられていることに注意してください。
関数のパラメータ(ここでは 1 つ)にもアノテーションが付けられますが、この短い @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-flash-001")
.maxOutputTokens(100)
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
}
通常どおり、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-001")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
次のように実行します。
./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 もあります。
Gemma は、Gemini モデルの作成に使用されたものと同じ研究とテクノロジーに基づいて構築された、軽量で最先端のオープンモデルのファミリーです。Gemma には、さまざまなサイズの Gemma1 と Gemma2 の 2 つのバリエーションがあります。Gemma1 には、2B と 7B の 2 つのサイズがあります。Gemma2 には、9B と 27B の 2 つのサイズがあります。重みは自由に利用でき、サイズが小さいため、ノートパソコンや Cloud Shell でも単独で実行できます。
Gemma の実行方法
Gemma の実行には多くの方法があります。クラウドの場合、ボタンをクリックするだけで Vertex AI 経由で使用するか、GKE で GPU を使用して実行することもできますが、ローカルで実行することもできます。
Gemma をローカルで実行するのに適した選択肢の一つは、Ollama です。Ollama は、Llama 2、Mistral、その他の多くのモデルをローカルマシンで実行できるツールです。Docker に似ていますが、LLM 用です。
オペレーティング システムの手順に沿って Ollama をインストールします。
Linux 環境を使用している場合は、インストール後に Ollama を有効にする必要があります。
ollama serve > /dev/null 2>&1 &
ローカルにインストールしたら、次のコマンドを実行してモデルを pull できます。
ollama pull gemma:2b
モデルが pull されるまで待ちます。この処理には時間がかかることがあります。
モデルを実行します。
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 をローカルにインストールして実行するのではなく、TestContainers によって処理されるコンテナ内で Ollama を使用できます。
TestContainers はテストに役立つだけでなく、コンテナの実行にも使用できます。利用できる特定の OllamaContainer
もあります。
全体的な流れは次のとおりです。
実装
GemmaWithOllamaContainer.java
を 1 つずつ見ていきましょう。
まず、Gemma モデルを取り込む派生 Ollama コンテナを作成する必要があります。このイメージは、以前の実行ですでに存在するか、作成されます。イメージがすでに存在する場合は、デフォルトの Ollama イメージを Gemma を利用したバリアントに置き換えるように TestContainers に指示します。
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 統合を使用してアプリケーションを強化しましょう。