はじめに
このシリーズでは、これまで以下の3本を書いてきました。
これまでのコードでは、質問文をそのまま chatClient.prompt().user(question.question()) のようにLLMへ渡していました。単純な質問だけならこれで十分ですが、「回答のトーンを指定したい」「毎回同じ前置きを付けたい」といった要件が出てくると、文字列連結でプロンプトを組み立てるのはすぐに読みづらくなります。
今回は、Spring AIのプロンプトテンプレート機能を使って、プロンプトの「型」と「埋め込む値」を分離してみます。プロジェクトは「SpringAISample03」を拡張した「SpringAISample04」として進めます。
前提環境
前回と同様です。
- Eclipse Version: 2025-12 (4.38.0)(Spring Tools がインストール済み)
- JDK 21
- Spring Boot 4.1.1 / Spring AI 2.0.1
- ビルドツール:Gradle
1. なぜプロンプトテンプレートが必要か
「回答のトーンを丁寧にするかカジュアルにするか、リクエストで指定できるようにしたい」という要件を、素朴に文字列連結で実装すると、以下のようになります。
String prompt = "次の質問に、" + question.tone() + "な口調で答えてください。\n\n質問:" + question.question();
この程度ならまだ読めますが、条件分岐や項目が増えるたびに + だらけの文字列がどんどん長くなり、プロンプトの全体像が把握しづらくなっていきます。
Spring AIでは、{プレースホルダー} を含んだ文字列(StringTemplate形式)を定義しておき、実行時にそこへ値を埋め込む「プロンプトテンプレート」という仕組みが用意されています。これを使うと、プロンプトの「文面」と「埋め込む値」がコード上ではっきり分離できます。
2. Question にパラメータを追加する
まず、回答のトーンを指定できるように Question にプロパティを追加します。
package com.example.springaisample;
public record Question(String question, String tone) {
}
tone には「丁寧」「カジュアル」といった文字列を想定しています(未指定の場合はService側でデフォルト値を使うようにします)。
3. テンプレートを使うようにServiceを書き換える
SpringAiSample03Service を元に、テンプレートを使う SpringAiSample04Service を作成します。
package com.example.springaisample;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SpringAiSample04Service implements SpringAiService {
private static final String QUESTION_PROMPT_TEMPLATE = """
次の質問に、{tone}な口調で答えてください。
質問:{question}
""";
private final ChatClient chatClient;
public SpringAiSample04Service(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Override
public Answer askQuestion(Question question) {
var tone = (question.tone() == null || question.tone().isBlank())
? "丁寧"
: question.tone();
var answerText = chatClient.prompt()
.user(userSpec -> userSpec
.text(QUESTION_PROMPT_TEMPLATE)
.param("tone", tone)
.param("question", question.question()))
.call()
.content();
return new Answer(answerText);
}
}
コードのポイント
-
QUESTION_PROMPT_TEMPLATEは、{tone}と{question}という2つのプレースホルダーを含んだ、ただのString定数です。これがプロンプトテンプレートの実体になります - これまでの
.user(String)の代わりに、.user(Consumer<UserSpec> userSpec)という別のオーバーロードを使っています。ラムダ式の中で.text(テンプレート)によってテンプレートを指定し、.param(キー, 値)を呼ぶたびに、対応するプレースホルダーへ値が埋め込まれます -
toneが空の場合に「丁寧」をデフォルト値として使う処理は、Spring AIの機能ではなく、単なるJavaのコードです。テンプレート自体はあくまで「埋め込み先」を用意するだけで、値の妥当性チェックやデフォルト値の扱いは呼び出し側の責務になります
ソースコードはこちら
4. 実行
curl でトーンを指定してリクエストを送ってみます。
curl localhost:8080/ask \
-H"Content-type: application/json" \
-d'{"question":"なぜ、犬はおなかを見せるのでしょうか?","tone":"カジュアル"}'
カジュアルな口調の回答が返ってきます。
{"answer":"犬がおなかを見せるの、めちゃくちゃ可愛いよね!あれにはいくつか理由があるんだよ..."}
tone を省略すると、Service側のデフォルト値(丁寧)が使われます。
curl localhost:8080/ask \
-H"Content-type: application/json" \
-d'{"question":"なぜ、犬はおなかを見せるのでしょうか?"}'
{"answer":"犬がおなかを見せる行動には、主にいくつかの理由があります..."}
同じ質問でも、パラメータを変えるだけで口調が変わることが確認できます。
応用Tips:テンプレートを外部ファイルに切り出す
今回はテンプレートをJavaのString定数として定義しましたが、テンプレートが長くなってきたら、src/main/resources 以下に .st ファイルとして切り出すこともできます。
src/main/resources/promptTemplates/questionPromptTemplate.st
次の質問に、{tone}な口調で答えてください。
質問:{question}
Service側では、@Value を使ってこのファイルを Resource として注入します。
@Value("classpath:/promptTemplates/questionPromptTemplate.st")
Resource questionPromptTemplate;
.text() メソッドは String と Resource の両方を受け付けるオーバーロードになっているため、プロパティ名を QUESTION_PROMPT_TEMPLATE から questionPromptTemplate に変えるだけで、askQuestion() メソッド側のコードを変更せずに済みます。テンプレートの文面をソースコードと分離して管理したい場合に便利です。
ソースコードはこちら
まとめ
今回は、Spring AIのプロンプトテンプレート機能を使って、文字列連結だったプロンプト生成を、{プレースホルダー} を使った形に書き換えました。
- プロンプトテンプレートを使うと、プロンプトの「文面」と「埋め込む値」を分離でき、条件分岐が増えても見通しが良いまま保てる
-
.user(Consumer<UserSpec>)のラムダ式内で.text()と.param()を組み合わせることでテンプレートを利用できる - テンプレートが長くなったら、
.stファイルとして外部リソース化することもできる
次回は、今回追加した「トーンの指定」のような指示文を、ユーザーメッセージではなくシステムメッセージとして分離する回を予定しています。
参考