はじめに
JavaアプリケーションからLLMを利用するための選択肢の一つに、Quarkus LangChain4jがあります。
Quarkus LangChain4jを使用すると、LLMとの通信処理を直接実装することなく、Javaインターフェースとして定義したAI ServiceからLLMを呼び出せます。
前回の記事では、Quarkus LangChain4jの基本として、AI Service、Prompt、Chat Modelの役割を整理し、サポートチケットの問い合わせ内容をLLMで分析するREST APIを作成しました。
前回の構成では、LLMが問い合わせ内容を分析し、自身の知識を基に回答を生成していました。一方、実際のアプリケーションでは、LLMによる回答生成に加えて、アプリケーションが持つ処理やデータを利用させたい場合があります。
そこで本記事では、前回作成した問い合わせ分析アプリケーションを発展させ、Quarkus LangChain4jのTool Callingを使用して、LLMからJavaメソッドを呼び出せるようにします。
なお、今回はTool Callingの基本的な仕組みに集中するため、Toolが参照するデータには固定のJSONファイルを使用します。また、データの登録や更新は行わず、参照系のToolのみを実装します。
実装には、前回と同様に、オープンソースのQuarkusをベースとするIBM Enterprise Build of Quarkusを使用します。
なお、本番運用向けには、IBMによるサポートと長期ライフサイクルが提供される製品版も用意されています。
Tool Callingとは
Tool Callingとは、LLMが利用可能な処理の中から必要なToolを選択し、そのToolに渡す引数を生成する仕組みです。
ここでいうToolとは、外部の処理やデータをLLMから利用できる形で定義したものです。本記事のサポートチケット分析を例にすると、次のような処理がToolに該当します。
- 対象サービスの稼働状況を確認する
- 過去の類似インシデントを検索する
- 担当チームの連絡先を取得する
Tool Callingの処理の流れを整理すると、次のようになります。
ユーザー
↓ 問い合わせ
LLM
↓ 使用するToolと引数を指定
Quarkus LangChain4j
↓ 対応するJavaメソッドを実行
Tool
↓ 実行結果
LLM
↓ Toolの結果を踏まえて回答
ユーザー
Tool Callingでは、LLM自身がJavaメソッドを直接実行するわけではありません。
LLMは、利用可能なToolの名前、説明、引数の情報を基に、問い合わせの処理に必要なToolを選択します。Quarkus LangChain4jは、その選択結果に基づいて対応するJavaメソッドを実行し、実行結果をLLMへ返します。LLMは、その結果を基に最終的な回答を生成します。
この仕組みにより、LLM単体の知識だけに依存せず、アプリケーションが持つ処理やデータを利用した回答が可能になります。
アプリケーションの実装
アプリケーション概要
今回は、前回作成した問い合わせ分析アプリケーション「AI Ticket Triage」を拡張します。
ユーザーがサポートチケットの問い合わせ内容を入力すると、LLMが内容を分析し、必要に応じて複数のToolを呼び出します。
Toolから取得した情報はLLMへ返され、最終的な調査結果としてまとめられます。
例えば、次のような問い合わせを入力します。
payment-service で Connection timeout が発生しています。
サービス状態と過去の類似インシデントを調査し、
担当チームの連絡先も確認してください。
この問い合わせに対して、LLMは必要なToolを選択し、次の情報を調査します。
-
payment-serviceの現在の稼働状況 -
Connection timeoutに関連する過去のインシデント -
payment-serviceを担当するチームの連絡先
今回のアプリケーションでは、1回の問い合わせに対して複数のToolが呼び出され、それぞれの実行結果を組み合わせた調査レポートが返却されます。
本記事で使用するソースコードは、次のGitHubリポジトリで公開しています。
前回のアプリケーションとの違い
前回の記事で作成したアプリケーションでは、LLMがユーザーから受け取った問い合わせ内容だけを基に、カテゴリー、優先度、推奨アクション、担当チームなどを分析していました。
問い合わせ
↓
LLM
↓
分析結果
今回はTool Callingを追加し、LLMがアプリケーションのJavaメソッドを介して、固定のJSONデータを参照できるようにします。
問い合わせ
↓
LLM
↓ Toolと引数を選択
Javaメソッド
↓ JSONデータを参照
LLM
↓
調査結果
違いを整理すると、次のとおりです。
| 項目 | 第1回 | 今回 |
|---|---|---|
| 主な目的 | 問い合わせ内容の分析 | Toolを使用した問い合わせの調査 |
| LLMが利用する情報 | 問い合わせに含まれる情報 | 問い合わせに含まれる情報とToolの実行結果 |
| Javaメソッドの呼び出し | なし | Quarkus LangChain4jを介して実行 |
| 外部データの参照 | なし | 固定のJSONファイルを参照 |
| RESTエンドポイント | POST /tickets/analyze |
POST /tickets/investigate |
RESTエンドポイントからAI Serviceを呼び出す基本構造は前回と同じです。
今回新たに追加するのは、AI ServiceへのTool登録と、ToolからApplication Serviceを介してJSONデータへアクセスする処理です。
なお、前回作成した/tickets/analyzeはそのまま残し、今回はTool Callingに対応した/tickets/investigateを追加します。
実装するTool
今回、LLMから利用できるToolとして、次の3つを実装します。
| Tool名 | 説明 |
|---|---|
checkServiceStatus |
対象サービスの現在の稼働状況を確認する |
searchSimilarIncidents |
サービス名とキーワードを基に、過去の類似インシデントを検索する |
getSupportTeamContact |
担当チームのメールアドレス、Slackチャンネル、オンコール電話番号を取得する |
LLMはすべてのToolを常に実行するのではなく、問い合わせ内容に応じて必要なToolだけを選択します。
例えば、「Inventory Teamの連絡先を教えてください」という問い合わせでは、サービス状態や類似インシデントを調査する必要がないため、getSupportTeamContactだけが呼び出されます。
プロジェクト構成
今回のアプリケーションは次のクラスで構成されています。
| クラス | 役割 |
|---|---|
TicketResource |
RESTエンドポイント。POST /tickets/investigate を公開する |
TicketAssistant |
Toolを利用するAI Service |
TicketInvestigationTools |
LLMへ公開するJavaメソッドをまとめたクラス |
IncidentInvestigationService |
調査処理を集約するApplication Service |
JsonServiceStatusDataSource / JsonIncidentDataSource / JsonSupportTeamDataSource
|
JSONファイルを読み込むDataSource |
TicketInvestigation |
最終回答を受け取るJava Record |
RESTエンドポイントからAI Serviceを呼び出す構造は前回と同じです。今回の追加要素は、AI ServiceにToolを登録し、ToolからApplication Serviceを通じてJSONデータへアクセスする部分です。
// TicketResource.java(抜粋)
@POST
@Path("/investigate")
public TicketInvestigation investigate(String ticket) {
return ticketAssistant.investigate(ticket);
}
Toolが参照するデータ
src/main/resources/data/
├── incidents.json # 過去のインシデント履歴
├── service-status.json # 各サービスの稼働状況
└── support-teams.json # サポートチームの連絡先
今回使用するJSONファイルは、Tool Callingの動作確認用に用意した固定データです。実務では、監視システム、インシデント管理システム、データベース、社内APIなどへの置き換えを想定します。
各JSONファイルはアプリケーション起動時にメモリへ読み込まれ、Toolからの検索に使用されます。例えば service-status.json には次のようなデータが含まれています。
[
{
"serviceName": "payment-service",
"status": "DEGRADED",
"message": "外部決済ゲートウェイへのレイテンシが増加しています",
"lastChecked": "2026-08-05T20:00:00+09:00",
"ownerTeam": "Payment Team"
},
...
]
Toolの実装
TicketInvestigationTools クラスに3つのToolを実装します。
@ApplicationScoped
public class TicketInvestigationTools {
@Inject
IncidentInvestigationService service;
@Tool(name = "checkServiceStatus",
value = "指定したサービスの現在の稼働状況を確認する。障害、停止、接続エラー、タイムアウトなどが疑われる場合に呼び出す。")
public ServiceStatus checkServiceStatus(
@P("稼働状況を確認したいサービスの名前(例: payment-service)") String serviceName) {
try {
return service.checkServiceStatus(serviceName);
} catch (Exception e) {
LOG.errorf(e, "checkServiceStatus でエラーが発生しました: serviceName=%s", serviceName);
return new ServiceStatus(serviceName, "UNKNOWN", "調査中にエラーが発生しました", null, "UNKNOWN");
}
}
@Tool(name = "searchSimilarIncidents",
value = "過去の類似インシデントを検索する。エラーコード、エラーメッセージ、具体的な症状がある場合に呼び出す。")
public List<IncidentRecord> searchSimilarIncidents(
@P("検索対象のサービス名(例: payment-service)") String serviceName,
@P("検索キーワード。エラーコードや症状を表す短い単語(例: timeout, connection refused)") String keyword) {
try {
return service.searchSimilarIncidents(serviceName, keyword);
} catch (Exception e) {
LOG.errorf(e, "searchSimilarIncidents でエラーが発生しました: serviceName=%s, keyword=%s", serviceName, keyword);
return Collections.emptyList();
}
}
@Tool(name = "getSupportTeamContact",
value = "担当チームの連絡先(メール、Slackチャンネル、オンコール電話番号)を取得する。担当チームを特定できた場合に呼び出す。")
public SupportTeam getSupportTeamContact(
@P("連絡先を取得したいチームの名前(例: Payment Team)") String teamName) {
try {
return service.getSupportTeamContact(teamName);
} catch (Exception e) {
LOG.errorf(e, "getSupportTeamContact でエラーが発生しました: teamName=%s", teamName);
return new SupportTeam("UNKNOWN", "", "", "");
}
}
}
ここでのポイントは次の5点です。
@Tool でJavaメソッドをToolとして公開する
@Tool アノテーションを付与したメソッドが、LLMから利用できるToolになります。name にTool名、value にToolの説明を記述します。
@P で引数の意味をLLMへ伝える
引数に @P アノテーションを付与することで、その引数の意味をLLMへ伝えられます。LLMはこの説明を参考に、問い合わせ内容から適切な引数を生成します。
Toolの説明に「何をするか」だけでなく「いつ使うか」も記述する
checkServiceStatus の説明には「障害、停止、接続エラー、タイムアウトなどが疑われる場合に呼び出す」という条件も含めています。LLMはTool名・説明・引数の説明を参考にToolを選択するため、利用条件を明示することで適切なToolが選ばれやすくなります。
ToolからApplication Serviceを呼び出す
ToolはCDI Beanとして動作するため、@Inject で他のBeanを注入できます。Toolに処理を直接書かず、IncidentInvestigationService へ委譲することで、Toolはアダプターの役割に専念できます。
Toolの戻り値をJava Recordとして構造化する
ServiceStatus、IncidentRecord、SupportTeam はJava Recordです。Toolの戻り値を構造化することで、LLMへ渡す情報の項目と意味が明確になります。
// ServiceStatus.java
public record ServiceStatus(
String serviceName,
String status,
String message,
String lastChecked,
String ownerTeam) {
}
// SupportTeam.java
public record SupportTeam(
String teamName,
String email,
String slackChannel,
String onCallPhone) {
}
// IncidentRecord.java
public record IncidentRecord(
String id,
String serviceName,
String title,
String description,
String errorMessage,
String resolution,
String team) {
}
AI ServiceへのTool登録
TicketAssistant インターフェースに @RegisterAiService を付与し、tools 属性で使用するToolクラスを指定します。
@RegisterAiService(tools = TicketInvestigationTools.class)
public interface TicketAssistant {
@SystemMessage("""
...
""")
@UserMessage("""
以下の問い合わせ内容を調査してください。
問い合わせ:
{{ticket}}
""")
TicketInvestigation investigate(String ticket);
}
tools 属性を指定することで、TicketInvestigationTools に定義した3つのToolが TicketAssistant から利用できるようになります。
Quarkus LangChain4jがToolの情報をLLMへ自動的に送信し、LLMが必要に応じてToolを選択できる状態になります。
Promptの定義
System Messageには、Toolを呼び出す条件と、情報を推測しないルールを定義します。
@SystemMessage("""
あなたはIT運用チームのシニアエンジニアです。
問い合わせ内容を調査し、最終的な調査結果を報告してください。
以下のToolを利用できます。必要に応じて複数のToolを組み合わせて調査してください。
- checkServiceStatus: サービスの稼働状況を確認する
- searchSimilarIncidents: 過去の類似インシデントを検索する
- getSupportTeamContact: 担当チームの連絡先を取得する
Toolを呼び出す条件:
- 障害、停止、接続エラー、タイムアウトがある場合は checkServiceStatus を呼び出す
- エラーコードやメッセージ、具体的な症状がある場合は searchSimilarIncidents を呼び出す
- 担当チームを特定できた場合は getSupportTeamContact を呼び出す
- 調査が不要な一般的な問い合わせにはToolを呼び出さない
注意事項:
- Toolで取得できなかった情報は推測しないこと
- 「該当する情報は確認できませんでした」と明示すること
- 回答は簡潔かつ実務的にすること
- 担当チームの連絡先を取得した場合は、メールアドレス、Slackチャンネル、オンコール電話番号をinvestigationResultに必ず含めること
...
""")
各条件の意図は次のとおりです。
| 状況 | 呼び出すTool |
|---|---|
| 障害やタイムアウトが疑われる場合 |
checkServiceStatus でサービス状態を確認する |
| 具体的なエラーや症状がある場合 |
searchSimilarIncidents で類似インシデントを検索する |
| 担当チームへの連絡が必要な場合 |
getSupportTeamContact で連絡先を取得する |
| Toolの結果にない情報 | 推測しない |
System MessageにToolの利用条件を明示することで、LLMが状況に応じてToolを選択しやすくなります。
アプリケーションの実行
application.properties にAPIキーを直接記述せず、環境変数から読み込む設定になっています。
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}
APIキーをソースコードや application.properties へ直接記述すると、リポジトリへの誤コミットなどの原因になるため、環境変数で管理することを推奨します。
次のコマンドでアプリケーションを起動します。
export OPENAI_API_KEY="your_api_key_here"
mvn quarkus:dev
Tool Callingの動作確認
複数のパターンでリクエストを送信し、Tool Callingの動作を確認します。
複数のToolを呼び出す
はじめに、サービスの稼働状況、過去の類似インシデント、担当チームの連絡先をまとめて確認する問い合わせを送信します。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary \
"payment-service で Connection timeout が発生しています。サービス状態と過去の 類似インシデントを調査し、担当チームの連絡先も確認してください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "payment-service で接続タイムアウトが発生しています。",
"category": "APPLICATION",
"priority": "HIGH",
"investigationResult": "payment-service の状態は DEGRADED で、外部決済ゲートウェイへのレイテンシが増加しています。過去の類似インシデントでは、接続タイムアウトが多発しており、接続プールの上限を拡張し、スロットリング設定を緩和した解決策が取られました。",
"recommendedAction": "接続プールの上限を再度確認し、外部決済ゲートウェイに対する負荷を見直すことをお勧めします。",
"assignedTeam": "Payment Team (連絡先: payment-team@example.com, Slack: #payment-support, オンコール: +81-3-0000-0001)"
}
レスポンスには、次の情報が含まれています。
- payment-service の現在の状態
- 過去の類似インシデントとその解決策
- Payment Teamの連絡先
この結果から、問い合わせに応じて複数のToolから取得した情報が、最終的な回答に反映されていることを確認できます。
なお、category は APPLICATION になっています。
決済サービスの接続タイムアウトという内容なので、現状のカテゴリー候補ではAPPLICATIONでも不自然ではありません。ただし、外部決済ゲートウェイとの通信障害としてNETWORKに分類される可能性もあります。
これはTool Callingの不具合ではなく、LLMによる分類上の揺れによるものです。
1つのToolだけを呼び出す
次に、担当チームの連絡先だけを確認する問い合わせを送信します。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary "Inventory Teamの連絡先を教えてください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "Inventory Teamの連絡先を取得しました。",
"category": "OTHER",
"priority": "LOW",
"investigationResult": "メールアドレス: inventory-team@example.com\nSlackチャンネル: #inventory-support\nオンコール電話番号: +81-3-0000-0004",
"recommendedAction": "必要に応じて上記の連絡先にお問い合わせください。",
"assignedTeam": "Inventory Team"
}
レスポンスにはInventory Teamの連絡先が含まれており、サービスの稼働状況や過去のインシデントに関する情報は含まれていません。
LLMは利用可能なToolを常にすべて使用するのではなく、問い合わせの処理に必要なToolを選択します。今回の問い合わせには障害やエラーに関する情報がないため、getSupportTeamContact が選択され、checkServiceStatus と searchSimilarIncidents は選択されなかったと考えられます。
データが見つからない場合
続いて、JSONファイルに登録されていないサービスの状態を問い合わせます。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary "xyz-service のステータスを確認してください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "xyz-service のステータスが不明です。",
"category": "OTHER",
"priority": "MEDIUM",
"investigationResult": "該当するサービスの情報は確認できませんでした。",
"recommendedAction": "サービスの所有チームに連絡してください。",
"assignedTeam": "該当チームは特定できませんでした。"
}
レスポンスから、データが見つからない場合に次のような応答となることを確認できました。
- サービス情報を推測していない
- サービスの状態を不明として扱っている
- 担当チームを特定していない
- 内部エラーの詳細をレスポンスへ露出していない
なお、priority は MEDIUM と判定されています。サービスの状態を確認できないことを、LLMが一定の対応を要する状況と判断した可能性があります。
これもTool Callingの不具合ではなく、LLMによる分類上の揺れによるものです。
既存APIへの回帰確認
今回はTool Callingを使用する新しいAPIとして、 /tickets/investigate を追加しましたが、前回実装した /tickets/analyze にはTool Callingを追加していません。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary "Webアプリケーションにログインできません。Database connection timeout と表示されています。" \
http://localhost:8080/tickets/analyze | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "Webアプリケーションへのログイン時にDatabase connection timeoutエラーが表示されている。",
"category": "APPLICATION",
"priority": "HIGH",
"recommendedAction": "データベースの接続状況を確認し、設定やネットワーク状況を調査する。",
"assignedTeam": "DATABASE"
}
既存の /tickets/analyze からも、これまでと同じ形式で問い合わせの分析結果が返されました。
以上の結果から、Tool Callingを使用する /tickets/investigate の追加後も、既存のAI Serviceを使用する /tickets/analyze が引き続き動作することを確認できました。
Tool Callingを使用する際の注意点
Toolの説明を具体的にする
LLMはTool名、説明、引数の説明を基に、使用するToolと引数を判断します。説明が曖昧だと意図しないToolが選ばれたり、引数が正しく生成されなかったりすることがあります。今回の実装では、@Tool の value 属性に「何をするか」だけでなく「いつ使うか」も記述しています。
Toolの結果にない情報を推測させない
LLMは確認できない情報を補完しようとする場合があります。System Messageで「Toolで取得できなかった情報は推測しないこと」「該当する情報は確認できませんでした、と明示すること」と指示することで、事実に基づかない回答を抑制できます。
更新系Toolは慎重に扱う
今回は参照系Toolだけを使用しています。登録、メール送信、サービス再起動などを行う場合は、認証・認可・実行前確認・監査ログが必要です。LLMが意図しないToolや引数を選択する可能性も考慮し、更新系の処理には入力値の検証、権限管理、利用者による承認、監査ログなどの安全策を設ける必要があります。
まとめ
今回は、Quarkus LangChain4jのTool Callingを使用し、LLMからJavaメソッドを呼び出すアプリケーションを作成しました。
今回のアプリケーションでは、問い合わせ内容に応じて、LLMが次のToolと引数を選択します。
- サービスの稼働状況を確認する
- 過去の類似インシデントを検索する
- 担当チームの連絡先を取得する
Tool Callingでは、LLMが問い合わせ内容に応じてToolと引数を選択し、Quarkus LangChain4jが対応するJavaメソッドを実行します。これにより、LLMはアプリケーションが持つ処理やデータを利用して回答を生成できます。
今回は固定のJSONファイルを参照しましたが、実際には監視システム、データベース、社内APIなどと連携する処理に置き換えられます。
今回のToolはアプリケーション内部で利用する構成でした。
次回は、QuarkusでMCP Serverを作成し、Javaの業務処理をMCP Toolとして外部へ公開します。併せて、MCP Clientからの接続方法、通常のTool Callingとの違い、MCP化に適した処理を整理します。