0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Spring AI 第5回】Spring AIで「指示」と「質問」を分ける ― System/Userロールを使い分けてみた

0
Posted at

はじめに

第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 メッセージとして送るように書き換えます。

SpringAiSample05Service.java
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 と役割を分けることで、プロンプトの意図がより明確になる

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?