はじめに
第5弾
では、指示(System)と質問(User)のメッセージロールを分離しました。ここまでのコードでは、LLMからの応答を毎回このように受け取っていました。
var answerText = chatClient.prompt()
// ...
.call()
.content();
return new Answer(answerText);
.content() でテキストを受け取り、自分で new Answer(answerText) を呼んで詰め替える――という流れです。Answer が String 1つだけのシンプルなレコードなので今のところ苦にはなりませんが、レコードのプロパティが増えていくと、この「受け取って、詰め替えて」という作業もだんだん煩雑になっていきます。
今回は、Spring AIの出力変換機能を使って、この詰め替え作業自体をSpring AIに任せてしまいます。プロジェクトは「SpringAISample05」を拡張した「SpringAISample06」として進めます。
前提環境
前回と同様です。
- Eclipse Version: 2025-12 (4.38.0)(Spring Tools がインストール済み)
- JDK 21
- Spring Boot 4.1.1 / Spring AI 2.0.1
- ビルドツール:Gradle
1. entity() で応答をオブジェクトとして受け取る
SpringAiSample05Service を元に、.content() の代わりに .entity(Answer.class) を使うように書き換えます。
package com.example.springaisample;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SpringAiSample06Service implements SpringAiService {
private static final String SYSTEM_PROMPT_TEMPLATE = """
あなたは親切なアシスタントです。
ユーザーからの質問に、{tone}な口調で答えてください。
""";
private final ChatClient chatClient;
public SpringAiSample06Service(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Override
public Answer askQuestion(Question question) {
var tone = (question.tone() == null || question.tone().isBlank())
? "丁寧"
: question.tone();
return chatClient.prompt()
.system(systemSpec -> systemSpec
.text(SYSTEM_PROMPT_TEMPLATE)
.param("tone", tone))
.user(question.question())
.call()
.entity(Answer.class);
}
}
変更点は、.call() の後ろが .content() から .entity(Answer.class) に変わり、return new Answer(answerText); という行がまるごと不要になったことです。askQuestion() メソッドの戻り値も、entity() の結果をそのまま返すだけになりました。
ソースコードはこちら
2. 裏側で何が起きているか
.entity(Answer.class) を呼び出すと、Spring AIは2つのことを行います。
-
プロンプト送信前:
Answerレコードの構造(プロパティ名や型)から、「こういうJSON形式で答えてください」という指示文を自動生成し、実際のプロンプトに追加してLLMへ送信する -
応答受信後:LLMから返ってきたテキストをJSONとして解析し、
Answerオブジェクトへ変換する
つまり、これまで自分たちで書いていた「詰め替え」の処理は、Spring AIが「LLMへの指示」と「応答の解析」という2つの処理に分けて肩代わりしてくれている、ということになります。
3. 実行
これまでと同じように curl でリクエストを送ってみます。
curl localhost:8080/ask \
-H"Content-type: application/json" \
-d'{"question":"なぜ、犬はおなかを見せるのでしょうか?","tone":"カジュアル"}'
見た目のレスポンスはこれまでと変わりません。
{"answer":"犬がおなかを見せるのは..."}
注意:出力変換は、LLMが「JSON形式で答えてください」という指示に確実に従ってくれることを前提にした機能です。Gemini(Google GenAI)は構造化出力への対応が比較的しっかりしているため大きな問題は出にくいですが、LLMによっては指示を無視して自由な形式で答えてしまうことがあります。その場合、Spring AIがJSON以外のテキストを
Answerに変換しようとしてJsonParseExceptionが発生します。「出力変換を使えば必ずJSONで返ってくる」と過信せず、あくまで「そうなるよう指示を送っている」だけだと理解しておくと安心です。
まとめ
今回は、LLMからの応答を手動で Answer オブジェクトに詰め替えていた処理を、Spring AIの出力変換機能(.entity(Answer.class))に置き換えました。
-
.entity(クラス)を使うと、応答の解析とオブジェクトへの詰め替えをSpring AIに任せられる - 裏側では、指定したクラスの構造からJSONスキーマ相当の指示文が自動生成され、プロンプトに追加されている
- LLMが必ずしも指示通りのJSON形式で返してくるとは限らないため、
JsonParseExceptionが発生する可能性があることは頭に入れておく
参考