再帰アドバイザー

再帰アドバイザーとは何ですか ?

Advisors Recursive 再帰アドバイザは、下流のアドバイザチェーンを複数回ループできる特殊な型のアドバイザです。このパターンは、たとえば次のような特定の条件が満たされるまで LLM を繰り返し呼び出す必要がある場合に便利です。

  • 呼び出す必要がなくなるまでツール呼び出しをループで実行する

  • 構造化された出力を検証し、検証が失敗した場合は再試行する

  • リクエストを変更して評価ロジックを実装する

  • リクエストを変更して再試行ロジックを実装する

CallAdvisorChain.copy(CallAdvisor after) 法は、再帰アドバイザパターンを可能にする重要なユーティリティです。この手法は、元のチェーンで指定されたアドバイザの後に続くアドバイザのみを含む新しいアドバイザチェーンを作成し、再帰アドバイザが必要に応じてこのサブチェーンを呼び出すことを可能にします。このアプローチにより、以下のことが保証されます。

  • 再帰アドバイザーはチェーン内の残りのアドバイザーをループすることができる

  • チェーンの他のアドバイザーは各反復を観測しインターセプトすることができる

  • アドバイザーチェーンは適切な順序と観測性を維持する

  • 再帰アドバイザーはそれ以前に実行されたアドバイザーを再実行しない

組み込みの再帰アドバイザー

Spring AI には、このパターンを示す 2 つの組み込み再帰アドバイザーが用意されています。

ToolCallingAdvisor

ToolCallingAdvisor は、モデル内部のツール実行に依存するのではなく、アドバイザーチェーンの一部としてツール呼び出しループを実装します。これにより、チェーン内の他のアドバイザーがツール呼び出しプロセスをインターセプトして監視できるようになります。

主な機能:

  • ToolExecutionEligibilityChecker がツール呼び出しがなくなったと報告するまで、アドバイザーチェーンをループします。

  • 「直接返す」機能をサポート - ツール実行に returnDirect=true がある場合、ツール呼び出しループを中断し、ツール実行結果を LLM に送り返すのではなく、クライアントアプリケーションに直接返します。

  • callAdvisorChain.copy(this) を使用して再帰呼び出しのサブチェーンを作成します

  • conversationHistoryEnabled による設定可能な会話履歴管理をサポート

  • ループの反復処理をカスタマイズするためのプラグイン可能な ToolExecutionEligibilityChecker をサポートします

使用例:

var toolCallingAdvisor = ToolCallingAdvisor.builder()
    .toolCallingManager(toolCallingManager)
    .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
    .build();

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(toolCallingAdvisor)
    .build();

会話履歴管理

ToolCallingAdvisor には、ツール呼び出しの反復中に会話履歴を管理する方法を制御する conversationHistoryEnabled 構成オプションが含まれています。

デフォルト(conversationHistoryEnabled=true)では、アドバイザーはツール呼び出しの反復中に完全な会話履歴を内部的に保持します。つまり、ツール呼び出しループ内の後続の LLM 呼び出しには、以前のすべてのメッセージ(ユーザーメッセージ、アシスタントのレスポンス、ツールのレスポンス)が含まれます。

デフォルトでは、メモリアドバイザー(DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = HIGHEST_PRECEDENCE + 200)はツール呼び出しループの外側に配置されます。ToolCallingAdvisor (HIGHEST_PRECEDENCE + 300)は、反復処理全体にわたって会話履歴を内部的に管理します。メモリアドバイザーは、ループ開始前に一度履歴をロードし、ループ終了後には最後のユーザー / アシスタント間のやり取りのみを永続化します。ほとんどの ChatMemoryRepository 実装ではツール呼び出しメッセージ型がサポートされていないため、この設定が推奨されます。

// Default setup: memory advisor is outside the tool-call loop (no explicit order needed)
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
// DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = HIGHEST_PRECEDENCE + 200 < ToolCallingAdvisor.DEFAULT_ORDER (+300)
// ToolCallingAdvisor manages its own conversation history across tool-call iterations

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(chatMemoryAdvisor)
    .build();

メモリアドバイザーをツール呼び出しループ内に配置する場合(ToolCallingAdvisor.DEFAULT_ORDER よりも上位の順序)にのみ、.disableInternalConversationHistory() メソッドを使用してください。メモリアドバイザーは、各イテレーションで履歴を処理します。ツール呼び出しメッセージの永続化をサポートするのは InMemoryChatMemoryRepository のみであることに注意してください。その他のリポジトリでは、上記のデフォルトのループ外設定を使用する必要があります。

spring-ai-session [GitHub] (英語) コミュニティプロジェクトは、セッション対応のメモリ実装を提供しており、ツール呼び出しメッセージを完全にサポートし、あらゆるバックエンドのツール呼び出しループ内で安全に使用できます。spring-ai-session のドキュメント (英語) を参照してください。
var toolCallingAdvisor = ToolCallingAdvisor.builder()
    .toolCallingManager(toolCallingManager)
    .disableInternalConversationHistory()  // Memory advisor inside the loop handles history
    .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
    .build();

var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
    .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 400)  // Inside (after) ToolCallingAdvisor
    .build();

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(chatMemoryAdvisor, toolCallingAdvisor)
    .build();

ユーザー制御ツールの実行

デフォルトでは、ToolCallingAdvisor は自動登録され、ツール呼び出しループ全体を内部的に管理します。呼び出し元は最終的な LLM 応答のみを受け取ります。ループを完全に制御する必要がある場合(たとえば、中間進捗状況を UI にストリーミングしたり、カスタムの可観測性を追加したり、反復処理間に条件付きロジックを適用したりする場合)、自動登録されたアドバイザーを無効にして、ループを自分で制御できます。

AdvisorParams.toolCallingAdvisorAutoRegister(false) を使用して、呼び出しごとに自動登録を無効にする:

ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .build();

String question = "What is the weather in Amsterdam and Paris?";

// ToolCallingAdvisor is disabled — no tool loop runs automatically
ChatClientResponse response = chatClient.prompt()
    .user(question)
    .options(chatOptions)
    .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
    .call()
    .chatClientResponse();

Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);

// Drive the loop manually — each iteration can be forwarded to a UI as it happens
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
    prompt = new Prompt(result.conversationHistory(), chatOptions);
    response = chatClient.prompt()
        .messages(result.conversationHistory())
        .options(chatOptions)
        .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
        .call()
        .chatClientResponse();
}

ストリーミングパスを含む完全な例については、ユーザー制御ツールの実行 — ChatClient 付を参照してください。

ツール呼び出しループの観測

ループを手動で駆動する代わりに、ToolCallingAdvisor ループ内にカスタムアドバイザーを配置することができます。その際、ToolCallingAdvisor.DEFAULT_ORDER より大きいオーダー値(例: HIGHEST_PRECEDENCE + 400)を指定します。このようなアドバイザーは、ツール呼び出しループの各イテレーションで呼び出され、最後に一度だけ呼び出されるわけではありません。つまり、すべての中間メッセージにアクセスできます。

  • ストリーミングモード — ToolCallingAdvisor は、ツール呼び出しリクエストチャンクを含む、各イテレーションのモデルの生チャンクストリームを受信し、その後、送信ストリームからフィルタリングします。

  • 呼び出しパスにおいて — 後続の各リクエストで渡される会話履歴には、前回の反復からの ToolResponseMessage が含まれるため、アドバイザーはツール呼び出しリクエストとそのレスポンスの両方を監視できます。

このパターンを使用すると、ツール呼び出しループを中断することなく、中間チャンクをサイドチャネル(SSE、WebSocket、ログ)に転送できます。

public class ToolCallObservingAdvisor implements CallAdvisor, StreamAdvisor {

    private final Consumer<ChatClientResponse> observer;

    public ToolCallObservingAdvisor(Consumer<ChatClientResponse> observer) {
        this.observer = observer;
    }

    @Override
    public ChatClientResponse aroundCall(ChatClientRequest request, CallAdvisorChain chain) {
        // Inspect all messages on every iteration — subsequent requests include ToolResponseMessages
        request.prompt().getInstructions().forEach(msg -> log.debug("Message: {}", msg));
        ChatClientResponse response = chain.nextCall(request);
        observer.accept(response);
        return response;
    }

    @Override
    public Flux<ChatClientResponse> aroundStream(ChatClientRequest request, StreamAdvisorChain chain) {
        // Observe every chunk including tool-call request chunks that ToolCallingAdvisor will suppress upstream
        return chain.nextStream(request).doOnNext(observer);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 400; // inside ToolCallingAdvisor (order 300)
    }
}

自動登録された ToolCallingAdvisor に加えて、観測アドバイザーを登録してください。

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(new ToolCallObservingAdvisor(chunk -> forwardToSse(chunk)))
    .build();

String response = chatClient.prompt()
    .user("What is the weather in Amsterdam and Paris?")
    .tools(new WeatherTools())
    .call()
    .content();

ToolCallObservingAdvisor は HIGHEST_PRECEDENCE + 400 の順序にあるため、チェーン内で自動登録された ToolCallingAdvisor (HIGHEST_PRECEDENCE + 300 の順序)の後に挿入され、すべてのツール呼び出しの反復処理に参加します。ToolCallingAdvisor はツール呼び出しのチャンクをフィルタリングし、それを外側のチェーンに返します。そのため、メインの呼び出し元は最終的な回答のみを受け取り、サイドチャネルの放出は監視アドバイザーが処理します。

このアプローチでは、ツール呼び出しループはフレームワークによって完全に管理されつつ、すべての中間ステップを完全に可視化できます。また、会話履歴管理で説明されている conversationHistoryEnabled およびメモリアドバイザーパターンと自然に組み合わせることができます。

リターンダイレクト機能

"Return Direct" 機能を使用すると、ツールは LLM をバイパスし、結果をクライアントアプリケーションに直接返すことができます。これは以下の場合に便利です。

  • ツールの出力は最終的な答えであり、LLM 処理を必要としない。

  • 追加の LLM 呼び出しを回避することでレイテンシを削減したい

  • ツールの結果は解釈せずにそのまま返される必要があります

ツール実行に returnDirect=true がある場合、ToolCallingAdvisor は次のようになります。

  1. 通常通りツール呼び出しを実行します

  2. ToolExecutionResult 内の returnDirect フラグを検出する

  3. ツール呼び出しループからの脱出

  4. ツールの出力を生成コンテンツとして、ツール実行結果を ChatResponse としてクライアントアプリケーションに直接返す

StructuredOutputValidationAdvisor

StructuredOutputValidationAdvisor は、構造化された JSON 出力を JSON スキーマに対して検証し、検証に失敗した場合は、指定された回数まで呼び出しを再試行します。

主な機能:

  • 想定される出力型から JSON スキーマを生成するか、事前に指定されたスキーマ文字列を受け入れます。

  • LLM レスポンスをスキーマに対して検証する

  • 検証に失敗した場合、設定可能な試行回数まで呼び出しを再試行します。(default: 3)

  • LLM が出力を修正できるように、再試行時に検証エラーメッセージをプロンプトに追加します。

  • callAdvisorChain.copy(this) を使用して再帰呼び出しのサブチェーンを作成します

  • オプションで JSON 処理用のカスタム JsonMapper をサポート

アドバイザーは、outputType (スキーマを自動的に生成)または outputJsonSchema (事前に指定されたスキーマ文字列)のいずれかで構成できます。この 2 つのオプションは相互に排他的です。

outputType での使用例:

var validationAdvisor = StructuredOutputValidationAdvisor.builder()
    .outputType(MyResponseType.class)
    .maxRepeatAttempts(3)
    .build();

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(validationAdvisor)
    .build();

事前に用意された JSON スキーマを使用した使用例:

var validationAdvisor = StructuredOutputValidationAdvisor.builder()
    .outputJsonSchema(myConverter.getJsonSchema())
    .build();

あるいは、EntityParamSpec を使用すると、アドバイザーを手動で構成することなく、entity() 呼び出しでスキーマ検証を直接有効にすることができます。

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class, spec -> spec.schemaValidation());