Java×Spring AIで始めるAIプログラミングの入門連載。前回は、Spring AIを導入するまでの流れを紹介しました。今回は、Spring AIの主な特徴であるAIチャットを行う上での基本的な流れと、その理解に必要となる基本的な概念を解説します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
Spring AIでのプログラミングにおいて基本となるクラスにChatClientがあります。Spring AIのレファレンスなどを見ると、チャットのためのクラスにChatModelもあります。ChatModelはChatClient内部で利用されているので、ChatModelでの事例の多くはChatClientを使っても実現可能です。さらに、ChatModelを利用するコードが紹介されているケースでも、ほとんどはChatModelではなくChatClientを利用する方が便利です。そのため本記事では、ChatClientを中心に紹介していきます。
ChatClientは、以下のようなクラスを利用して、質問の意味を解釈し、その回答をAI(人工知能)モデルから取得します。
図1は、これらの関係をまとめたものです。まずは、具体的なコードを見る前に、大ざっぱなクラスの関係を把握してください。
それでは、具体的なチャットプログラムを作成してみましょう。リスト1は、簡単なテキストによる質問とそれに回答するAIチャットプログラムの例です。
// ・・(省略) @ShellMethod(key = "chat-client") public String chatClientPrompt(String message){ // (1) 最適なChatModelの取得 ChatModel model = context.getBean(ChatModel.class); // (2) ChatClientインスタンスの作成 ChatClient client = ChatClient.builder(model).build(); // (3) メッセージの作成 UserMessage userMessage = new UserMessage(message); // (4) プロンプトの作成 Prompt prompt = new Prompt(userMessage); // (5) リクエストデータの作成 ChatClient.ChatClientRequestSpec request = client.prompt(prompt); // (6) 処理の実行 ChatClient.CallResponseSpec response = request.call(); // (7) 処理結果の取得 return response.content(); }
(1)では、ChatModelを取得します。ここでのChatModelとは、「どのAIベンダーや言語モデルを使用するか」といった、API実行時の各種パラメーターと、実際にAPIを実行するためのインスタンスを保持するためのインスタンスと理解するとよいでしょう。
なお、Spring Frameworkの利用経験がない方だと、context.getBeanが何をしているのか疑問に思うかもしれません。ここでいうcontextとは、SpringにおけるApplicationContextです。ApplicationContextは、アプリケーション全体でインスタンスを管理しており、引数にChatModel.classを指定することで、このアプリケーションに登録されているChatModelのインスタンスが取得できます。
これらのインスタンスを実際に作成しているのは、Spring Bootフレームワークです。前回、導入時に必要な設定ファイルとしてapplication.propertiesの記述方法など紹介しましたが、その設定に従ってSpring Bootが最適なインスタンスを作成します。そのため、プログラムの利用側はgetBeanメソッドだけで適切なインスタンスを取得できます。
ChatModelを取得できたら、これを基にChatClientのインスタンスを作成できるので(2)、Message(3)、Prompt(4)インスタンスを生成、リクエストを組み立てます(5)。
リクエストを実際に送信するのはcallメソッドの役割です(6)。リクエストへの応答はCallResponseSpecインスタンスとして返されます。CallResponseSpecインスタンスからはcontentメソッドで結果(文字列)を取得できます(7)。
以上を理解したら、サンプルを実行してみましょう。以下は、リスト1を呼び出すコマンドと、その結果例です。
shell:>chat-client 地球の温暖化の原因には主にどんな原因が考えられますか?簡潔に50文字以内で教えて下さい 地球温暖化の主な原因は、温室効果ガスの排出増加です。
以上、Spring AIによるコードの基本的な流れを理解できたところで、ここからはメッセージ、プロンプトなど、リクエストを構成する個別の要素について見ていきます。
Spring AIでは、プロンプトに入力される質問や指示などのデータを役割で分類しています。そして、それらの役割(Role)を持つデータをMessageと呼んでいます。Messageそのものはインタフェースであり、実際には以下のような実装クラスを利用することになります。
これ以外にも外部ツールとの連携に利用できるToolResponseMessageのようなクラスもありますが、こちらはついては次回以降で改めて紹介します。取りあえず、ここではUSER、SYSTEM、ASSISTANTメッセージに絞って、それぞれの役割を理解します。
多くのチャットシステムでは、AIと利用者がコミュニケーションを取る上で共通の前提が設けられています。例えば図2であれば、(1)がその前提となるシステムへの指示です。(2)のように回答できない質問が送られてきた場合にも、(1)の条件に基づいて「分かりません」という回答を促すことができます。
特定の目的を持ったAIシステムでは、必ずしも利用者の全ての質問にAIモデルが答える必要はありません。サービスの範囲外の質問や指示には答えてほしくないといったケースもあります。そのような場合は、(1)の条件や指示は、より詳細に設定されるでしょう。
そして、それぞれの利用者からの入力に応じて(2)〜(5)のようにユーザーとシステムとがメッセージを交換することになります。
このように、メッセージに役割を持たせることで、「それぞれのメッセージをどのように扱うか」「再利用が必要な場合にどうすればよいか」といったことを、ChatClientが判断できるようになるわけです。
では、これらのメッセージを利用した例を見てみましょう(リスト2)。
// ・・(省略) @ShellMethod(key = "message-create") public String createMessage(String userText){ // (1) SYSTEMのメッセージを作成 SystemMessage systemMessage = new SystemMessage("あなたは専門家のためのAIアシスタントです。曖昧な回答や不確実な場合には「わかりません」と答えてください。"); // (2) USERのメッセージを作成 UserMessage userMessage = new UserMessage(userText); // (3) プロンプトを作成 Prompt prompt = Prompt.builder() .messages(systemMessage,userMessage).build(); ChatModel model = context.getBean(ChatModel.class); ChatClient client = ChatClient.builder(model).build(); ChatClient.CallResponseSpec response = client.prompt(prompt).call(); var result = response.chatResponse().getResult(); if(result != null){ // (4) ASSISTANTのメッセージを取得 AssistantMessage message = result.getOutput(); return message.getText(); } else{ return "回答エラー"; } }
実行すると、以下のようになります。
shell:>message-create 日本は今後インフレになりますか?デフレになりますか わかりません。インフレやデフレの将来の動向は、さまざまな経済要因や政策、国際的な状況によって影響されるため、具体的な予測は難しいです。
(1)ではSYSTEM型のメッセージを、(2)では利用者からの入力を扱うUSER型のメッセージを、それぞれ作成しています。そして(3)では、これらの複数のメッセージを扱うためのPromptを、Prompt.Builderでまとめます。
なお、ここではAIからの応答をcontentメソッドではなく、getResultメソッドで取得しています。getResultメソッドの戻り値はChatResult型――応答メッセージはもちろん、応答に伴うメタ情報を含んだオブジェクトです。ここでは、そのgetOutputメソッドでメッセージ(AssistantMessageオブジェクト)を取得しています。
画像を元に質問したい場合もあります。その場合には、Mediaをメッセージに追加します。
例えば、リスト3は指定した画像に対して質問できるようにした例です。
@ShellMethod(key = "message-image") public String imageMessage(String filename,String userText){ File file = new File(filename); ChatModel model = context.getBean(ChatModel.class); ChatClient client = ChatClient.builder(model).build(); // (1) 画像ファイルをMediaに変換 FileSystemResource resource = new FileSystemResource(file); Media media = new Media(MimeTypeUtils.IMAGE_PNG, resource); // (2) UserMessageに画像(メディア)を添付 UserMessage userMessage = UserMessage.builder().text(userText).media(media).build(); // (3) Promptを作成 Prompt prompt = Prompt.builder() .messages(userMessage) .build(); return client.prompt(prompt).call().content(); }
ポイントは以下の通りです。
画像ファイルを利用するには、Springフレームワークの共通型であるResource型に変換した上で、型を宣言したMediaオブジェクトを作成します。
ただし、どんな型(MIMEタイプ)でも指定できるわけではありません。例えばOpenAIの場合、画像であれば問題ありませんでしたが、PDF(Portable Document Format)は利用できませんでした。そのため、PDFデータを画像に変換する、もしくはテキストデータに変換するなどの処理を開発者が賄う必要があります。
なお、今回の例ではOpenAIを利用していますが、利用できるメディアの制限は、利用するLLMモデルによっても異なる場合があります。具体的な制限については、Spring AIのドキュメントも参照してください。
続いて、準備したMediaオブジェクトに基づいて、UserMessageオブジェクトを作成します。先ほどはtextメソッドでユーザーからのメッセージを指定するだけでしたが、ここではmediaメソッドで画像を束ねています。
あとは、Promptに対してUserMessageを組み込んで終了です。前述したように、Builderクラスを使ってインスタンスを作成することで、さまざまな型の引数が必要な場合でもインスタンスが作りやすくなっています。
以上を理解したら、サンプルを実行してみましょう。以下は、実行コマンドとその結果です。
shell:>message-image image.png この画像で記されている特徴は何ですか この画像は、Quick2PDFというサービスの紹介ページのようです。主な特徴は以下の通りです: // (省略) このサービスは、企業が効率的にPDFを生成する手助けをすることを目的としています。
画像内の文字データなども取得でき、AI-OCRのような使い方もできます。ただし、その場合は、結果がAIベンダーごとのOCR(Optical Character Recognition)の性能にも左右される可能性があります。用途に応じてAIベンダーを使い分けるなどの必要が出てくるかもしれません。
ここまでは、単発の問いかけを前提としたコードを紹介しましたが、例えば画像ファイルなどは毎回指定するのは面倒ですし、そもそもシステムメッセージは初回に一度だけ実行されればよいはずです。
そこで本節では、履歴を加味した会話の例を紹介します。これには、Advisor機能の一つであるMessageChatMemoryAdvisorを使います。
Advisorとは、チャット全体の会話を記憶するときのルールを管理するための基盤です。Advisorを設定することで、あらかじめ用意されたルールを前提とした回答を作成してくれるようになります。MessageChatMemoryAdvisorは、まさにチャットの内容を記憶するためのルールです。
では、具体的な例を見ていきます(リスト4)。
@ShellMethod(key = "client-start") public String startClient(String systemMessage){ ChatModel model = context.getBean(ChatModel.class); // (1) ChatMemoryインスタンスを取得 ChatMemory chatMemory = context.getBean(ChatMemory.class); // (2) 履歴を保存するAdvisorインスタンスを作成 MessageChatMemoryAdvisor chatAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); this.client= ChatClient.builder(model) .defaultAdvisors(chatAdvisor) // (3) ChatClientに設定 .build(); var response = client.prompt(new Prompt(new SystemMessage(systemMessage))).call(); return response.content(); } @ShellMethod(key = "client-user") public String textPrompt(String message){ return client.prompt(new Prompt(new UserMessage(message))).call().content(); }
チャット情報を保存するのは、ChatMemoryオブジェクトの役割です。これまでと同じく、getBeanメソッド経由でインスタンスを取得します。Spring Bootでは、デフォルトでMessageWindowChatMemoryオブジェクトを取得します。MessageWindowChatMemoryはチャット履歴を管理するためのクラスです。
あとは、作成したChatMemoryを基に、Advisor(MessageChatMemoryAdvisor)を作成し(2)、ChatClientに組み込むだけです(3)。
以上を理解したら、サンプルを実行してみましょう。以下は実行コマンドと、その結果です。
shell:>client-start あなたは簡単な算数を教えるAIボットです はい、簡単な算数について教えることができます。どんな問題やトピックについて学びたいですか?足し算、引き算、掛け算、割り算、または他のことでも大丈夫です。 shell:>client-user 1+1は 1 + 1 は 2 です。何か他の算数の問題があれば、お知らせください! shell:>client-user その結果に3を掛けてください 2 に 3 を掛けると、2 × 3 = 6 です。何か他に計算したいことがあれば教えてください! shell:>client-user その結果に4を掛けてください 6 に 4 を掛けると、6 × 4 = 24 です。さらに計算したいことがあれば、お知らせください! shell:>client-user 最初の足し算結果に2を加えてください 最初の足し算の結果は 2 でしたので、そこに 2 を加えると、2 + 2 = 4 です。他に計算したいことがあれば教えてください!
ちなみに、MessageWindowChatMemoryクラスではデフォルトで20個のメッセージを履歴として保存します。この値を変更するには、リスト5のようなコードをSpringの設定ファイルに対して追加します。
// (1) Spring Framework用の設定クラス @Configuration public class ChatConfiguration { // (2) Beanとして設定 @Bean ChatMemory chatMemory(){ return MessageWindowChatMemory.builder() .maxMessages(2) // (3) 最大履歴保持数を変更 .build(); } }
@Configurationアノテーション(1)は、そのクラスがSpringの設定ファイルであることを示すものです。
あとは、Spring Context上で扱えるBeanインスタンスとして、MessageWindowChatMemoryを登録します。この例では、MessageWindowChatMemoryの履歴保持数を2に設定しています(3)。
このようにConfiguration用のクラスを別に定義することで、先ほどのコードChatMemoryを利用するコードには何も変更を加えず、実際の動作を変更することができます。
以上を理解したら、もう一度同じ質問で実行してみましょう。以下は、実行コマンドとその結果です。
shell:>client-start あなたは簡単な算数を教えるAIボットです こんにちは!簡単な算数を教えるお手伝いをします。どんな問題を解決したいですか?足し算、引き算、掛け算、割り算の中から選んでください。また、具体的な問題があれば教えてくださいね。 shell:>client-user 1+1は 1 + 1 は 2 です!他に質問があれば教えてくださいね。 shell:>client-user その結果に3を掛けてください 1 + 1 の結果である 2 に 3 を掛けると、2 × 3 = 6 になります。 shell:>client-user その結果に4を掛けてください 前回の結果 6 に 4 を掛けると、6 × 4 = 24 になります。 shell:>client-user 最初の足し算結果に2を加えてください 最初の足し算の結果は 6 でしたので、それに 2 を加えると、6 + 2 = 8 になります。
このように、履歴を2つしか持たないため、意図した結果にならずに、「最初の足し算結果」に2つ前の結果である「6」という値を使ってしまっています。ただし、最初に指定したSYSTEMメッセージは保持されますので安心してください。このように、Messageの役割によって振る舞いを変えるなどの実装がAdvisorによって行われます。
今回は、AIチャットに質問や指示を送るまでの流れについて紹介しました。しかし、実際のシステムではAIモデルが返すテキストをそのまま使うと不都合が生じることがあります。
そこで次回は、このような結果をプログラムが扱いやすいように、そのフォーマットをカスタマイズするための利用方法を中心に解説します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.