1. はじめに
この Codelab では、Google Cloud の Vertex AI でホストされている Gemini 大規模言語モデル(LLM)に焦点を当てます。Vertex AI は、Google Cloud 上のすべての ML プロダクト、サービス、モデルを網羅するプラットフォームです。
Java を使用して、LangChain4j フレームワークで Gemini API を操作します。具体的な例を通して、LLM を活用した質問応答、アイデアの生成、エンティティと構造化コンテンツの抽出、検索拡張生成、関数呼び出しについて説明します。
生成 AI とは
生成 AI とは、人工知能を活用してテキスト、画像、音楽、音声、動画などの新しいコンテンツを作成することを指します。
生成 AI は、要約、Q&A、分類など、すぐに使える複数のタスクを同時に実行できる大規模言語モデル(LLM)を利用しています。ほんのわずかなサンプルデータで必要最小限のトレーニングを行うだけで、基盤モデルを対象のユースケースに適応させることができます。
生成 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 を統合するためのオープンソース ライブラリです。
このプロジェクトは 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 Shell(Cloud 上で動作するコマンドライン環境)を使用します。
Cloud Shell をアクティブにする
- Cloud Console で、[Cloud Shell をアクティブにする]
をクリックします。
Cloud Shell を初めて起動する場合、その内容を説明する中間画面が表示されます。中間画面が表示された場合は、[続行] をクリックします。
Cloud Shell のプロビジョニングと接続に少し時間がかかる程度です。
この仮想マシンには、必要な開発ツールがすべて用意されています。仮想マシンは Google Cloud で稼働し、永続的なホーム ディレクトリが 5 GB 用意されているため、ネットワークのパフォーマンスと認証が大幅に向上しています。この 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 で最も重要な API は 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 エディタを開いて設定する
Cloud Shell の Cloud Code Editor でコードを開きます。
Cloud Code エディタで、File
-> Open Folder
を選択し、codelab ソースフォルダ(例: /home/username/gemini-workshop-for-java-developers/
)を指定できます。
環境変数を設定する
Terminal
-> New Terminal
を選択して、Cloud Code エディタで新しいターミナルを開きます。コードサンプルの実行に必要な 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-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 への最初の呼び出しが完了しました。
レスポンスのストリーミング
レスポンスが数秒後にまとめて返されたことに気付きましたか?ストリーミング レスポンス バリアントを使用することで、レスポンスを段階的に取得することもできます。ストリーミング レスポンスの場合、モデルはレスポンスが利用可能になり次第、断片的に返します。
この 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 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
を静的インポートする必要があります。これは、Java ラムダ式でストリーミング ハンドラを作成する StreamingResponseHandler
を提供する便利なメソッドです。
今回は、generate()
メソッドのシグネチャが少し異なります。戻り値の型は文字列ではなく void です。プロンプトに加えて、ストリーミング レスポンス ハンドラを渡す必要があります。ここでは、前述の静的インポートにより、onNext()
メソッドに渡すラムダ式を定義できます。ラムダ式は、レスポンスの新しい部分が利用可能になるたびに呼び出されますが、後者はエラーが発生した場合にのみ呼び出されます。
次のコマンドを実行します。
./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)
- 1 回あたりのリクエスト割り当てを超えた場合や、プラットフォームに技術的な問題が発生した場合に、モデルが呼び出しを 3 回再試行できるようにします。
ここまでは Gemini に 1 つの質問をしましたが、複数のターンの会話も可能です。次のセクションでは、その方法について説明します。
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-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
次のような 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 に 1 ターンの質問をしたり、複数ターンの会話をしたりできますが、現時点では入力はテキストのみです。画像はどうなりますか?画像については、次のステップで説明します。
6. Gemini を使用したマルチモダリティ
Gemini はマルチモーダル モデルです。テキストだけでなく、画像や動画も入力として使用できます。このセクションでは、テキストと画像を組み合わせるユースケースについて説明します。
Gemini はこの猫を認識できると思いますか?
Wikipedia から取得した雪の中の猫の写真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-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. 非構造化テキストから構造化された情報を抽出する
レポート ドキュメント、メール、その他の長いテキストで、重要な情報が非構造化形式で提供される場合がよくあります。理想的には、非構造化テキストに含まれる重要な詳細情報を構造化オブジェクトの形式で抽出できるとよいでしょう。方法を見てみましょう。
たとえば、人物の経歴、履歴書、説明からその人物の名前と年齢を抽出するとします。巧みに調整されたプロンプトを使用して、非構造化テキストから JSON を抽出するように LLM に指示できます(これは一般に「プロンプト エンジニアリング」と呼ばれます)。
ただし、次の例では、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
インスタンスに unmarshal されます。
次に、main()
メソッドの内容を見てみましょう。
- チャットモデルが構成され、インスタンス化されます。モデルビルダー クラスの 2 つの新しいメソッド(
responseMimeType()
とresponseSchema()
)を使用しています。最初のフラグは、出力で有効な JSON を生成するよう Gemini に指示します。2 つ目の方法では、返される 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
レコードなどの具体的なクラスを操作し、Person
インスタンスを返す extractPerson()
メソッドを持つ PersonExtractor
オブジェクトがあります。LLM の概念は抽象化され、Java デベロッパーは、この PersonExtractor
インターフェースを使用するときに、通常のクラスとオブジェクトを操作するだけです。
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
モデルを構成します。温度を高くしてクリエイティビティを高め、topP と topK の値も高くします。次に、プロンプトの文字列を渡して from()
静的メソッドで PromptTemplate
を作成し、二重中かっこのプレースホルダ変数 {{dish}}
と {{ingredients}}
を使用します。
最終的なプロンプトを作成するには、apply()
を呼び出します。この関数は、プレースホルダの名前と、プレースホルダに置き換える文字列値を表す Key-Value ペアのマップを受け取ります。
最後に、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 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
ビルダーを使用してスキーマ定義の形式を確認します。
モデルが構成されたら、LangChain4j の AiServices
が LLM を使用して実装する SentimentAnalysis
インターフェースを作成します。このインターフェースには、analyze()
という 1 つのメソッドが含まれています。分析するテキストを入力として受け取り、Sentiment
列挙型値を返します。認識された感情のクラスを表す強力な型のオブジェクトのみを操作します。
次に、モデルに分類作業を促す「少数ショットのサンプル」を提供するために、チャット メモリを作成して、テキストとそれに関連する感情を表すユーザー メッセージと AI レスポンスのペアを渡します。
SentimentAnalysis
インターフェース、使用するモデル、少数ショット サンプルを含むチャット メモリを渡して、すべてを AiServices.builder()
メソッドでバインドしましょう。最後に、分析するテキストを指定して analyze()
メソッドを呼び出します。
サンプルを実行する
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
単語が表示されます。
POSITIVE
いちごが好きという感情はポジティブな感情のようです。
10. 検索拡張生成
LLM は大量のテキストでトレーニングされます。ただし、その知識はトレーニング中に学習した情報に限られます。モデルのトレーニングの締め切り日後に新しい情報が公開された場合、その詳細はモデルで使用できません。そのため、モデルは、見たことのない情報に関する質問には回答できません。
そのため、このセクションで説明する検索拡張生成(RAG)などのアプローチは、ユーザーのリクエストを満たすために LLM が知っておく必要のある追加情報を提供したり、より最新の情報や、トレーニング時にアクセスできない機密情報に関する回答を返したりするために役立ちます。
会話に戻りましょう。今回は、書類について質問できます。ドキュメントが小さな部分(「チャンク」)に分割されたデータベースから関連情報を取得できる chatbot を構築します。この情報は、トレーニングに含まれる知識にのみ依存するのではなく、モデルが回答を検証するために使用されます。
RAG には 2 つのフェーズがあります。
- 取り込みフェーズ - ドキュメントがメモリに読み込まれ、小さなチャンクに分割されます。ベクトル エンベディング(チャンクの高次元ベクトル表現)が計算され、セマンティック検索が可能なベクトル データベースに保存されます。この取り込みフェーズは通常、ドキュメント コーパスに新しいドキュメントを追加する必要がある場合に 1 回だけ行われます。
- クエリフェーズ - ユーザーはドキュメントに関する質問をすることができます。質問もベクトルに変換され、データベース内の他のすべてのベクトルと比較されます。最も類似したベクトルは通常、意味的に関連しており、ベクトル データベースから返されます。次に、LLM に会話のコンテキストと、データベースから返されたベクトルに対応するテキストのチャンクが提供され、それらのチャンクを見て回答を根拠づけるよう求められます。
書類を準備する
この新しい例では、架空の自動車メーカーである Cymbal Starlight の架空の自動車モデルについて質問します。架空の自動車に関するドキュメントは、モデルの知識の一部であってはなりません。したがって、Gemini がこの車に関する質問に正しく回答できる場合、RAG アプローチが機能し、ドキュメントを検索できることを意味します。
chatbot を実装する
2 フェーズ アプローチを構築する方法について説明します。まず、ドキュメントの取り込みを行い、次に、ユーザーがドキュメントについて質問するクエリ時間(「取得フェーズ」とも呼ばれます)を行います。
この例では、両方のフェーズが同じクラスで実装されています。通常、取り込みを処理するアプリケーションと、ユーザーに chatbot インターフェースを提供するアプリケーションを用意します。
また、この例ではインメモリ ベクトル データベースを使用します。実際の本番環境シナリオでは、取り込みフェーズとクエリフェーズは 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
変数内の)ベクトル データベースをエンベディング モデルにリンクする Retriever クラスも必要です。ユーザーのクエリのベクトル エンベディングを計算してベクトル データベースにクエリを実行し、データベース内の類似ベクトルを検索します。
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 に外部システム(情報を取得またはアクションを実行するリモート Web API や、なんらかの計算を実行するサービスなど)へのアクセスを許可する必要がある場合があります。次に例を示します。
リモート ウェブ API:
- お客様の注文を追跡、更新する。
- 問題トラッカーでチケットを検索または作成します。
- 株価や IoT センサーの測定値などのリアルタイム データを取得します。
- メールを送信する
計算ツール:
- 高度な数学の問題を解くための電卓。
- LLM が推論ロジックを必要とするときにコードを実行するためのコード解釈。
- LLM がデータベースをクエリできるように、自然言語リクエストを SQL クエリに変換します。
関数呼び出し(ツールまたはツールの使用と呼ばれることもあります)は、モデルが自身に代わって 1 つ以上の関数呼び出しをリクエストする機能です。これにより、モデルはより新しいデータを使用してユーザーのプロンプトに適切に回答できます。
ユーザーからの特定のプロンプトと、そのコンテキストに関連する既存の関数の知識が与えられた場合、LLM は関数呼び出しリクエストで返信できます。LLM を統合したアプリケーションは、その代わりに関数を呼び出して、レスポンスを LLM に返信できます。LLM は、テキスト形式の回答を返すことで解釈を返します。
関数呼び出しの 4 つのステップ
関数呼び出しの例として、天気予報に関する情報を取得する関数を見てみましょう。
Gemini や他の LLM にパリの天気を尋ねると、現在の天気予報に関する情報がないことを返信します。LLM が気象データにリアルタイムでアクセスできるようにするには、使用をリクエストできる関数を定義する必要があります。
次の図をご覧ください。
1️⃣ まず、ユーザーがパリの天気を尋ねます。チャットボット アプリ(LangChain4j を使用)は、LLM がクエリを実行するために利用できる関数が 1 つ以上あることを認識しています。チャットボットは、最初のプロンプトと、呼び出せる関数のリストの両方を送信します。ここでは、ロケーションの文字列パラメータを受け取る getWeather()
という関数を作成します。
LLM は天気予報を認識しないため、テキストで返信する代わりに、関数実行リクエストを返します。チャットボットは、"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-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();
関数名、パラメータの名前と型が定義されていますが、関数とパラメータの両方に説明が付けられています。説明は非常に重要で、LLM が関数の機能を正確に理解し、会話のコンテキストでこの関数を呼び出す必要があるかどうかを判断するのに役立ちます。
ステップ 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. この時点で、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
を詳しく見てみましょう。
まず、関数のレスポンス データ構造を表すレコードを作成します。
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);
}
}
}
このクラスには 1 つの関数しか含まれていませんが、モデルが呼び出しをリクエストできる関数の説明に対応する @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-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 もあります。
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
を詳しく見てみましょう。
まず、Gemma モデルを pull する派生 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.
Gemma が Cloud Shell で実行されています。
14. 完了
これで、LangChain4j と Gemini API を使用して、Java で最初の生成 AI チャット アプリケーションを作成できました。マルチモーダル大規模言語モデルは非常に強力で、独自のドキュメント、データ抽出、外部 API とのやり取りなど、質問/回答などのさまざまなタスクを処理できることがわかりました。
次のステップ
強力な LLM 統合でアプリケーションを強化しましょう。