はじめに
第4弾
では、プロンプトテンプレートを使って「口調(トーン)を指定できる」ようにしました。実装したテンプレートは、以下のように「LLLへの指示」と「ユーザーからの質問」が1つの文字列の中に混在した形になっていました。
private static final String QUESTION_PROMPT_TEMPLATE = """
次の質問に、{tone}な口調で答えてください。
質問:{question}
""";
今回は、この「指示」と「質問」を、Spring AIのメッセージロールという仕組みを使ってはっきり分離してみます。プロジェクトは「SpringAISample04」を拡張した「SpringAISample05」として進めます。
前提環境
前回と同様です。
- Eclipse Version: 2025-12 (4.38.0)(Spring Tools がインストール済み)
- JDK 21
- Spring Boot 4.1.1 / Spring AI 2.0.1
- ビルドツール:Gradle
1. メッセージロールとは
多くのLLMは、プロンプトを1つの文章としてではなく、「誰の立場のメッセージか」を表すロール付きの複数メッセージとして受け取れるようになっています。代表的なロールは次の3つです。
| ロール | 意味 |
|---|---|
User |
アプリケーションの利用者からの発言・質問 |
System |
アプリケーション側からLLMへの指示(振る舞いの指定など) |
Assistant |
LLMからの応答(会話履歴を扱う際に登場します) |
これまでの実装では、.user(...) だけを使い、「口調を指定する指示文」も「ユーザーの質問」も、まとめて User ロールのメッセージとして送信していました。しかし、本来「口調を指定する」というのはアプリケーション側からの指示であり、ユーザー自身の発言ではありません。ここを System ロールに分離することで、LLMに対して「どちらがアプリケーションの指示で、どちらがユーザーの発言か」をより明確に伝えられます。
2. テンプレートをSystemメッセージ側に移動する
SpringAiSample04Service を元に、テンプレートを System メッセージとして送るように書き換えます。
package com.example.springaisample;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SpringAiSample05Service implements SpringAiService {
private static final String SYSTEM_PROMPT_TEMPLATE = """
あなたは親切なアシスタントです。
ユーザーからの質問に、{tone}な口調で答えてください。
""";
private final ChatClient chatClient;
public SpringAiSample05Service(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()
.system(systemSpec -> systemSpec
.text(SYSTEM_PROMPT_TEMPLATE)
.param("tone", tone))
.user(question.question())
.call()
.content();
return new Answer(answerText);
}
}
コードのポイント
- 第4弾では
{question}というプレースホルダーにユーザーの質問文まで埋め込んでいましたが、今回はテンプレート(SYSTEM_PROMPT_TEMPLATE)から{question}を削除し、質問そのものは.user(question.question())として別枠で渡すようにしています -
.system(Consumer<SystemSpec>)の書き方は、.user(Consumer<UserSpec>)とほぼ同じ形です。.text()でテンプレートを、.param()でプレースホルダーへの値を指定します -
.system(...)と.user(...)を呼び出す順序に決まりはありません。どちらを先に書いても、最終的にLLMへ送られるリクエストには両方のメッセージが含まれます
ソースコードはこちら
3. 実行
第4弾と同じように、curl でリクエストを送ってみます。
curl localhost:8080/ask \
-H"Content-type: application/json" \
-d'{"question":"なぜ、犬はおなかを見せるのでしょうか?","tone":"カジュアル"}'
見た目のレスポンスは前回とほぼ変わりませんが、内部的にはLLMへ送られるリクエストの中身が変わっています。以前は1つの User メッセージにすべてが詰め込まれていましたが、今回は「指示(System)」と「質問(User)」の、2つの独立したメッセージとして送信されています。
Tips:実際に送信されているリクエストの中身を覗いてみたくなるところですが、OpenAI向けの解説でよく紹介される「Logbook」というロギングライブラリは、Spring AIのHTTPクライアント(
RestClient)を前提にしているため、Google公式SDKを使うGoogle GenAI(Gemini)統合では利用できません。この点については、機会があれば別の記事で扱いたいと思います。
まとめ
今回は、これまで User メッセージに混在させていた「口調の指示」と「質問」を、System ロールと User ロールに分離しました。
- Spring AIでは
User/System/Assistantといったメッセージロールを使い分けられる -
.system(Consumer<SystemSpec>)は.user(Consumer<UserSpec>)とほぼ同じ書き方で使え、.text()と.param()でテンプレートを扱える - アプリケーションからの指示は
System、利用者の発言はUserと役割を分けることで、プロンプトの意図がより明確になる
参考