アドバイザー API

Spring AI アドバイザー API は、Spring アプリケーションで AI 主導のインタラクションをインターセプト、変更、強化するための柔軟で強力な方法を提供します。アドバイザー API を活用することで、開発者はより洗練され、再利用可能で、保守しやすい AI コンポーネントを作成できます。

主な利点としては、繰り返し発生する生成 AI パターンのカプセル化、大規模言語モデル (LLM) との間で送受信されるデータの変換、さまざまなモデルやユースケース間での移植性の提供などが挙げられます。

次の例に示すように、ChatClient API を使用して既存のアドバイザーを構成できます。

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        new MessageChatMemoryAdvisor(chatMemory), // chat-memory advisor
        new QuestionAnswerAdvisor(vectorStore)    // RAG advisor
    )
    .build();

String response = this.chatClient.prompt()
    // Set advisor parameters at runtime
    .advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
            .param("chat_memory_response_size", 100))
    .user(userText)
    .call()
	.content();

ビルダーの defaultAdvisors() メソッドを使用して、ビルド時にアドバイザーを登録することをお勧めします。

アドバイザーは Observability スタックにも参加するため、その実行に関連するメトリクスとトレースを表示できます。

コアコンポーネント

API は、非ストリーミングシナリオ用の CallAroundAdvisor と CallAroundAdvisorChain、およびストリーミングシナリオ用の StreamAroundAdvisor と StreamAroundAdvisorChain で構成されています。また、封印されていないプロンプトリクエストを表す AdvisedRequest、チャット補完レスポンスを表す AdvisedResponse も含まれます。どちらもアドバイザーチェーン全体で状態を共有するために advise-context を保持しています。

Advisors API Classes

nextAroundCall() と nextAroundStream() は主要なアドバイザーメソッドであり、通常は、封印されていないプロンプトデータの調査、プロンプトデータのカスタマイズと拡張、アドバイザーチェーン内の次のエンティティの呼び出し、オプションでのリクエストのブロック、チャット補完レスポンスの調査、処理エラーを示す例外のスローなどのアクションを実行します。

さらに、getOrder() メソッドは チェーン内のアドバイザーの順序を決定し、getName() は一意のアドバイザー名を提供します。

Spring AI フレームワークによって作成されたアドバイザーチェーンを使用すると、getOrder() 値順に複数のアドバイザーを順番に呼び出すことができます。値の低いアドバイザーが最初に実行されます。自動的に追加された最後のアドバイザーは、リクエストを LLM に送信します。

次のフロー図は、アドバイザーチェーンとチャットモデル間のやり取りを示しています。

Advisors API Flow
  1. Spring AI フレームワークは、ユーザーの Prompt と空の AdvisorContext オブジェクトから AdvisedRequest を作成します。

  2. チェーンの各アドバイザーはリクエストを処理し、場合によってはリクエストを変更します。または、次のエンティティを呼び出す呼び出しを行わないことでリクエストをブロックすることもできます。後者の場合、アドバイザーはレスポンスを入力する責任があります。

  3. フレームワークによって提供される最終アドバイザーは、リクエストを Chat Model に送信します。

  4. チャットモデルのレスポンスはアドバイザーチェーンを介して返され、AdvisedResponse に変換されます。その後、共有 AdvisorContext インスタンスが含まれます。

  5. 各アドバイザーはレスポンスを処理または変更できます。

  6. 最終的な AdvisedResponse は、ChatCompletion を抽出することによってクライアントに返されます。

アドバイザーのオーダー

チェーンのアドバイザーの実行順序は、getOrder() メソッドによって決定されます。理解すべき重要なポイント:

  • 順序値の低いアドバイザーが最初に実行されます。

  • アドバイザーチェーンはスタックとして動作します。

    • チェーンの最初のアドバイザーが最初にリクエストを処理します。

    • また、レスポンスを処理するのも最後です。

  • 実行順序を制御するには:

    • アドバイザーが チェーンで最初に実行されるようにするには (リクエスト処理の場合は最初に、レスポンス処理の場合は最後に)、順序を Ordered.HIGHEST_PRECEDENCE に近い値に設定します。

    • アドバイザーが チェーンで最後に実行されるようにするには (リクエスト処理の場合は最後に、レスポンス処理の場合は最初に)、順序を Ordered.LOWEST_PRECEDENCE に近い値に設定します。

  • 値が大きいほど優先度は低くなると解釈されます。

  • 複数のアドバイザーが同じ順序値を持つ場合、実行順序は保証されません。

順序と実行シーケンスの間に矛盾があるように見えるのは、アドバイザーチェーンのスタックのような性質によるものです。* 優先順位が最も高い (順序値が最も低い) アドバイザーがスタックの一番上に追加されます。* スタックが巻き戻されるときに、最初にリクエストを処理します。* スタックが巻き戻されるときに、最後にレスポンスを処理します。

念のため、Spring Ordered インターフェースのセマンティクスを以下に示します。

public interface Ordered {

    /**
     * Constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Get the order value of this object.
     * <p>Higher values are interpreted as lower priority. As a consequence,
     * the object with the lowest value has the highest priority (somewhat
     * analogous to Servlet {@code load-on-startup} values).
     * <p>Same order values will result in arbitrary sort positions for the
     * affected objects.
     * @return the order value
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}

入力側と出力側の両方で チェーンの先頭にする必要のあるユースケースの場合:

  1. それぞれの側に別々のアドバイザーを使用します。

  2. 異なる順序値で設定します。

  3. アドバイザーコンテキストを使用して、それらの間で状態を共有します。

API の概要

主要なアドバイザーインターフェースはパッケージ org.springframework.ai.chat.client.advisor.api にあります。独自のアドバイザーを作成するときに使用する主要なインターフェースは次のとおりです。

public interface Advisor extends Ordered {

	String getName();

}

同期アドバイザーとリアクティブアドバイザーの 2 つのサブインターフェースは

public interface CallAroundAdvisor extends Advisor {

	/**
	 * Around advice that wraps the ChatModel#call(Prompt) method.
	 * @param advisedRequest the advised request
	 * @param chain the advisor chain
	 * @return the response
	 */
	AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);

}

および

public interface StreamAroundAdvisor extends Advisor {

	/**
	 * Around advice that wraps the invocation of the advised request.
	 * @param advisedRequest the advised request
	 * @param chain the chain of advisors to execute
	 * @return the result of the advised request
	 */
	Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);

}

アドバイスの チェーンを継続するには、アドバイスの実装で CallAroundAdvisorChain と StreamAroundAdvisorChain を使用します。

インターフェースは

public interface CallAroundAdvisorChain {

	AdvisedResponse nextAroundCall(AdvisedRequest advisedRequest);

}

および

public interface StreamAroundAdvisorChain {

	Flux<AdvisedResponse> nextAroundStream(AdvisedRequest advisedRequest);

}

アドバイザーの導入

アドバイザーを作成するには、CallAroundAdvisor または StreamAroundAdvisor (あるいはその両方) を実装します。実装する主要な方法は、非ストリーミングアドバイザーの場合は nextAroundCall()、ストリーミングアドバイザーの場合は nextAroundStream() です。

サンプル

ユースケースを観察および拡張するためのアドバイザーを実装する方法を説明するために、いくつかの実践的な例を紹介します。

ログ記録アドバイザー

チェーンの次のアドバイザーへの呼び出し前の AdvisedRequest と呼び出し後の AdvisedResponse をログに記録する、シンプルなログアドバイザーを実装できます。アドバイザーはリクエストとレスポンスを監視するだけで、変更は行わないことに注意してください。この実装は、非ストリーミングシナリオとストリーミングシナリオの両方をサポートします。

public class SimpleLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

	private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);

	@Override
	public String getName() { (1)
		return this.getClass().getSimpleName();
	}

	@Override
	public int getOrder() { (2)
		return 0;
	}

	@Override
	public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

		logger.debug("BEFORE: {}", advisedRequest);

		AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);

		logger.debug("AFTER: {}", advisedResponse);

		return advisedResponse;
	}

	@Override
	public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

		logger.debug("BEFORE: {}", advisedRequest);

		Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);

        return new MessageAggregator().aggregateAdvisedResponse(advisedResponses,
                    advisedResponse -> logger.debug("AFTER: {}", advisedResponse)); (3)
	}
}
1 アドバイザーに一意の名前を提供します。
2 順序値を設定することで実行順序を制御できます。値が小さいほど先に実行されます。
3MessageAggregator は、Flux レスポンスを 1 つの AdvisedResponse に集約するユーティリティクラスです。これは、ストリーム内の個々の項目ではなくレスポンス全体を監視するログ記録やその他の処理に役立ちます。MessageAggregator は読み取り専用操作であるため、レスポンスを変更できないことに注意してください。

再読(Re2)アドバイザー

"再読により大規模言語モデルの推論機能が向上する (英語) " の記事では、大規模言語モデルの推論機能を向上させる Re-Reading (Re2) と呼ばれる手法を紹介しています。Re2 手法では、次のように入力プロンプトを拡張する必要があります。

{Input_Query}
Read the question again: {Input_Query}

Re2 テクニックをユーザーの入力クエリに適用するアドバイザーを実装するには、次のようにします。

public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {


	private AdvisedRequest before(AdvisedRequest advisedRequest) { (1)

		Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
		advisedUserParams.put("re2_input_query", advisedRequest.userText());

		return AdvisedRequest.from(advisedRequest)
			.userText("""
			    {re2_input_query}
			    Read the question again: {re2_input_query}
			    """)
			.userParams(advisedUserParams)
			.build();
	}

	@Override
	public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { (2)
		return chain.nextAroundCall(this.before(advisedRequest));
	}

	@Override
	public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) { (3)
		return chain.nextAroundStream(this.before(advisedRequest));
	}

	@Override
	public int getOrder() { (4)
		return 0;
	}

    @Override
    public String getName() { (5)
		return this.getClass().getSimpleName();
	}
}
1before メソッドは、再読み取りテクニックを適用してユーザーの入力クエリを拡張します。
2aroundCall メソッドは、非ストリーミングリクエストをインターセプトし、再読み取りテクニックを適用します。
3aroundStream メソッドはストリーミングリクエストをインターセプトし、再読み取りテクニックを適用します。
4 順序値を設定することで実行順序を制御できます。値が小さいほど先に実行されます。
5 アドバイザーに一意の名前を提供します。

Spring AI 組み込みアドバイザー

Spring AI フレームワークには、AI のインタラクションを強化するための組み込みアドバイザーがいくつか用意されています。利用可能なアドバイザーの概要は次のとおりです。

チャットメモリアドバイザー

これらのアドバイザーは、チャットメモリストア内の会話履歴を管理します。

  • MessageChatMemoryAdvisor

    メモリを取得し、それをメッセージのコレクションとしてプロンプトに追加します。このアプローチでは、会話履歴の構造が維持されます。すべての AI モデルがこのアプローチをサポートしているわけではないことに注意してください。

  • PromptChatMemoryAdvisor

    メモリを取得し、プロンプトのシステムテキストに組み込みます。

  • VectorStoreChatMemoryAdvisor

    VectorStore からメモリを取得し、プロンプトのシステムテキストに追加します。このアドバイザーは、大規模なデータセットから関連情報を効率的に検索および取得できます。

質問回答アドバイザー
  • QuestionAnswerAdvisor

    このアドバイザーは、ベクトルストアを使用して質問応答機能を提供し、RAG (Retrieval-Augmented Generation) パターンを実装します。

コンテンツセーフティアドバイザー
  • SafeGuardAdvisor

    モデルが有害または不適切なコンテンツを生成しないように設計されたシンプルなアドバイザー。

ストリーミング vs 非ストリーミング

Advisors Streaming vs Non-Streaming Flow
  • 非ストリーミングアドバイザーは、完全なリクエストとレスポンスを扱います。

  • ストリーミングアドバイザーは、リアクティブプログラミングの概念 (レスポンスの場合は Flux など) を使用して、リクエストとレスポンスを連続ストリームとして処理します。

@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

    return  Mono.just(advisedRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // This can be executed by blocking and non-blocking Threads.
                // Advisor before next section
            })
            .flatMapMany(request -> chain.nextAroundStream(request))
            .map(response -> {
                // Advisor after next section
            });
}

ベストプラクティス

  1. モジュール性を高めるために、アドバイザーが特定のタスクに集中できるようにします。

  2. 必要に応じて、adviseContext を使用してアドバイザー間で状態を共有します。

  3. 柔軟性を最大限に高めるには、アドバイザーのストリーミングバージョンと非ストリーミングバージョンの両方を実装します。

  4. 適切なデータフローを確保するために、チェーン内のアドバイザーの順序を慎重に検討してください。

下位互換性

AdvisedRequest クラスは新しいパッケージに移動されました。

破壊的な API 変更

Spring AI アドバイザーチェーンは、バージョン 1.0 M2 から 1.0 M3 にかけて大幅な変更が行われました。主な変更点は次のとおりです。

アドバイザーインターフェース

  • 1.0 M2 には、RequestAdvisor と ResponseAdvisor のインターフェースが別々にありました。

    • RequestAdvisor は、ChatModel.call メソッドと ChatModel.stream メソッドの前に呼び出されました。

    • これらのメソッドの後に ResponseAdvisor が呼び出されました。

  • 1.0 M3 では、これらのインターフェースは次のように置き換えられました。

    • CallAroundAdvisor

    • StreamAroundAdvisor

  • 以前は ResponseAdvisor の一部であった StreamResponseMode は削除されました。

コンテキストマップの処理

  • 1.0 M2 の場合:

    • コンテキストマップは別のメソッド引数でした。

    • マップは変更可能であり、チェーンに沿って渡されました。

  • 1.0 M3 の場合:

    • コンテキストマップは、AdvisedRequest および AdvisedResponse レコードの一部になりました。

    • マップは不変です。

    • コンテキストを更新するには、更新されたコンテンツを含む新しい変更不可能なマップを作成する updateContext メソッドを使用します。

1.0 M3 のコンテキストを更新する例:

@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

    this.advisedRequest = advisedRequest.updateContext(context -> {
        context.put("aroundCallBefore" + getName(), "AROUND_CALL_BEFORE " + getName());  // Add multiple key-value pairs
        context.put("lastBefore", getName());  // Add a single key-value pair
        return context;
    });

    // Method implementation continues...
}