1. はじめに
この Codelab では、Google Cloud の Vertex AI でホストされている Gemini 大規模言語モデル(LLM)に焦点を当てます。Vertex AI は、Google Cloud のすべての ML プロダクト、サービス、モデルを網羅するプラットフォームです。
LangChain4j フレームワークを使用して Gemini API とやり取りするために Java を使用します。具体的な例を参考に、質問応答、アイデアの生成、エンティティと構造化コンテンツの抽出、検索拡張生成、関数呼び出しに LLM を活用します。
生成 AI とは
生成 AI とは、人工知能を使用してテキスト、画像、音楽、音声、動画などの新しいコンテンツを作成することを指します。
生成 AI は、さまざまなタスクに対応した大規模言語モデル(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 2.0 Flash: 最新の次世代機能と機能強化。
- Gemini 2.0 Flash-Lite: 費用対効果と低レイテンシを重視して最適化された Gemini 2.0 Flash モデル。
- Gemini 2.5 Pro: Google 史上最も高度な推論モデル。
- Gemini 2.5 Flash: バランスの取れた機能を提供する思考モデル。価格とパフォーマンスのバランスを取るように設計されています。
主な機能:
- マルチモーダル: Gemini は、複数の情報形式を理解して処理できるため、従来のテキストのみの言語モデルから大きく進歩しています。
- パフォーマンス: Gemini 2.5 Pro は、多くのベンチマークで現在の最先端のモデルを上回っており、難しい 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 用意されているため、ネットワークのパフォーマンスと認証が大幅に向上しています。このコードラボで行う作業のほとんどはブラウザから実行できます。
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 エディタを開いて設定する
Cloud Shell から Cloud コードエディタでコードを開きます。

Cloud Code エディタで、File -> Open Folder を選択して、codelab のソースフォルダ(例: /home/username/gemini-workshop-for-java-developers/)を指定できます。
環境変数を設定する
Cloud Code エディタで新しいターミナルを開くには、Terminal -> 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-2.0-flash")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
最初の例では、ChatModel インターフェースを実装する VertexAiGeminiChatModel クラスをインポートする必要があります。
main メソッドで、VertexAiGeminiChatModel のビルダーを使用してチャット言語モデルを構成し、次のものを指定します。
- プロジェクト
- 場所
- モデル名(
gemini-2.0-flash)。
言語モデルの準備ができたので、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-2.0-flash")
.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)- 時間あたりのリクエスト数の割り当てを超えている場合や、プラットフォームで技術的な問題が発生している場合は、モデルに呼び出しを 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-2.0-flash")
.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 サービスが、定義したカスタム ConversationService インターフェースを使用していることに注目してください。このインターフェースは LangChain4j が実装し、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 はこの猫を認識すると思いますか?

雪の中の猫の写真(Wikipedia より)
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-2.0-flash")
.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-2.0-flash")
.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()を使用しています。最初のプロンプトは、出力で有効な 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-2.0-flash")
.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}} を使用します。
最終的なプロンプトは、プレースホルダの名前と置き換える文字列値を表す 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 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-2.0-flash")
.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 インターフェースを作成します。このインターフェースには 1 つのメソッド(analyze())が含まれています。入力で分析するテキストを受け取り、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 つがあります。
また、この例ではインメモリ ベクトル データベースを使用します。実際の本番環境のシナリオでは、取り込みフェーズとクエリフェーズは 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() メソッドが取り込みを行います。
これで、第 1 フェーズが終了し、ドキュメントが関連するベクトル エンベディングを含むテキスト チャンクに変換され、ベクトル データベースに保存されました。
質問する
質問の準備をしましょう。会話を開始するチャットモデルを作成します。
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.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();
このサービスは、次のものをバインドします。
- 前に構成したチャット言語モデル。
- 会話を追跡するためのチャット メモリ。
- リトリーバーは、ベクトル エンベディング クエリをデータベース内のベクトルと比較します。
.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}}
"""))
.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 がデータベースをクエリできるようにします。
関数呼び出し(ツールまたはツール使用とも呼ばれます)は、モデルがユーザーのプロンプトに最新のデータで適切に回答できるように、モデルが 1 つ以上の関数呼び出しを代行して行うようリクエストする機能です。
ユーザーからの特定のプロンプトと、そのコンテキストに関連する既存の関数の知識に基づいて、LLM は関数呼び出しリクエストで応答できます。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-2.0-flash")
.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 は関数が何を実行できるかを正確に理解し、会話のコンテキストでこの関数を呼び出す必要があるかどうかを判断できます。
まず、パリの天気についての最初の質問を送信します。
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());
完全なソースコードは、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-2.0-flash")
.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?"));
System.out.println(assistant.chat("Is it warmer in London or 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.
It is warmer in Paris (20 degrees) than in London (15 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-2.0-flash")
.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 モデルは Gemma3 で、1B(テキストのみ)、4B、12B、27B の 4 つのサイズがあります。重みは自由に利用でき、サイズが小さいため、ノートパソコンや Cloud Shell などで独自に実行できます。
Gemma はどのように実行されますか?
Gemma の実行方法は多数あります。クラウドで実行する、Vertex AI でボタンをクリックして実行する、GKE で一部の GPU を使用して実行するなど、ローカルで実行することもできます。
Gemma をローカルで実行するのに適したオプションの 1 つに Ollama があります。これは、Llama や Mistral などの小さなモデルをローカルマシンで実行できるツールです。Docker に似ていますが、LLM 用です。
オペレーティング システムの手順に沿って Ollama をインストールします。
Linux 環境を使用している場合は、Ollama をインストールした後で、まず Ollama を有効にする必要があります。
ollama serve > /dev/null 2>&1 &
ローカルにインストールしたら、コマンドを実行してモデルを pull できます。
ollama pull gemma3:1b
モデルが pull されるまで待ちます。この処理には時間がかかることがあります。
モデルを実行します。
ollama run gemma3:1b
これで、モデルを操作できます。
>>> 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 モデルをプルする派生 Ollama コンテナを作成する必要があります。このイメージは、以前の実行で作成されたものがすでに存在するか、作成されます。イメージがすでに存在する場合は、デフォルトの Ollama イメージを Gemma を搭載したバリアントに置き換えることを TestContainers に伝えるだけです。
private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
public static final String GEMMA_3 = "gemma3:1b";
// Creating an Ollama container with Gemma 3 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_GEMMA3)
.exec();
if (listImagesCmd.isEmpty()) {
System.out.println("Creating a new Ollama container with Gemma 3 image...");
OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
System.out.println("Starting Ollama...");
ollama.start();
System.out.println("Pulling model...");
ollama.execInContainer("ollama", "pull", GEMMA_3);
System.out.println("Committing to image...");
ollama.commitToImage(TC_OLLAMA_GEMMA3);
return ollama;
}
System.out.println("Ollama image substitution...");
// Substitute the default Ollama image with our Gemma variant
return new OllamaContainer(
DockerImageName.parse(TC_OLLAMA_GEMMA3)
.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_3)
.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.7.1 started in PT7.228339916S
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 統合でアプリケーションを強化しましょう。