このバージョンはまだ開発中であり、まだ安定しているとは見なされていません。最新の安定バージョンについては、Spring AI 2.0.1 を使用してください!

ToolCallingAdvisor

ToolCallingAdvisor は、Spring AI 2.0 におけるツール実行ライフサイクルを管理する再帰アドバイザーです。DefaultChatClient によって自動的に登録され、モデルがツール呼び出しなしでレスポンスを生成するまで、リクエスト / レスポンスループを駆動します。

このページは、ビルダー API、設定オプション、フックメソッド、拡張パターンのリファレンスです。ループの概念的な概要については、The Tool Calling Loop を参照してください。より広範な再帰アドバイザーパターンについては、再帰アドバイザーを参照してください。

概要

ToolCallingAdvisor は、CallAdvisor と StreamAdvisor の両方に加え、ToolAdvisor マーカーインターフェースを実装しています。マーカーインターフェースは、DefaultChatClient がチェーンにツールアドバイザーが正確に 1 つだけ存在することを強制するために使用するものです(Single-ToolAdvisor Invariant を参照)。

デフォルトの ToolCallingAdvisor.DEFAULT_ORDER は Ordered.HIGHEST_PRECEDENCE + 300 です。これは、デフォルトでメモリアドバイザをツールループの外に配置するデフォルトの MessageChatMemoryAdvisor オーダー (HIGHEST_PRECEDENCE + 200) よりも高い値です。Memory and the Tool Loop を参照してください。

ビルダー

ToolCallingAdvisor.builder() を介して ToolCallingAdvisor を構築する:

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

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(toolCallingAdvisor)
    .build();
ほとんどのアプリケーションは ToolCallingAdvisor を直接構築しません。DefaultChatClient が適切なデフォルト値で自動的に登録します。デフォルト以外の設定が必要な場合、またはカスタムサブクラスに置き換える場合にのみ、手動で構築してください。

ビルダーオプション

オプション 説明 デフォルト

toolCallingManager(ToolCallingManager)

ツール呼び出しを実行するために使用される ToolCallingManager インスタンス。

自動構築されたインスタンス

toolExecutionEligibilityChecker(ToolExecutionEligibilityChecker)

モデルレスポンスによって別のツール呼び出しの反復処理をトリガーするかどうかを決定する述語。デフォルトでは chatResponse.hasToolCalls() をチェックします。プロバイダ固有の停止理由ロジックを適用するには、オーバーライドしてください(ツール呼び出しの有無に加えて、終了理由フィールドをチェックするなど)。

chatResponse → chatResponse != null && chatResponse.hasToolCalls()

advisorOrder(int)

チェーン内でアドバイザーが適用される順序。BaseAdvisor.HIGHEST_PRECEDENCE と BaseAdvisor.LOWEST_PRECEDENCE の間に指定する必要があります。ループの内側で実行されるアドバイザーと外側で実行されるアドバイザーを決定します。

HIGHEST_PRECEDENCE + 300

conversationHistoryEnabled(boolean)

アドバイザーがイテレーション間で会話履歴を内部的に保持するかどうか。true (デフォルト)の場合、ループ内の各 LLM 呼び出しは、アドバイザー自身によって管理される、以前のツール呼び出しとレスポンスの完全な履歴を受け取ります。false の場合、アドバイザーは最新のメッセージのみを転送します。これは、ループ内の MemoryAdvisor が履歴管理を引き継ぐ場合に便利です。

true

disableInternalConversationHistory()

conversationHistoryEnabled(false) のショートカット。

—

ToolExecutionEligibilityChecker

ToolExecutionEligibilityChecker は関数インターフェースです。

@FunctionalInterface
public interface ToolExecutionEligibilityChecker {
    boolean isToolCallResponse(@Nullable ChatResponse chatResponse);
}

デフォルトのチェッカーは、レスポンスにツール呼び出しが含まれるたびに次のイテレーションを実行します。プロバイダ固有の動作を設定するには、これをオーバーライドしてください。

ToolExecutionEligibilityChecker strictChecker = response ->
    response != null
        && response.hasToolCalls()
        && "tool_calls".equals(response.getMetadata().getFinishReason());

var advisor = ToolCallingAdvisor.builder()
    .toolExecutionEligibilityChecker(strictChecker)
    .build();

会話履歴の行動

デフォルトでは、ToolCallingAdvisor はループ内で会話履歴全体(ユーザーメッセージ、モデルレスポンス、ツール呼び出しリクエスト、ツールレスポンス)を保持します。後続の各イテレーションでは、完全な履歴がモデルに送信されます。

メモリがループの外にある場合、これは正しい動作です。なぜなら、外側のメモリアドバイザーは最終的なユーザー / アシスタントのやり取りしか見ることができず、イテレーションごとの履歴は ` ToolCallingAdvisor のプライベートな関心事です。

disableInternalConversationHistory() を設定するタイミング:

  • ループ内に MemoryAdvisor を配置しています(MemoryAdvisor はイテレーションごとの履歴を自動的に管理します)。

  • You’re driving the loop yourself and want only the latest message forwarded.

var toolCallingAdvisor = ToolCallingAdvisor.builder()
    .disableInternalConversationHistory()  // memory advisor inside the loop handles history
    .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
    .build();

var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
    .order(BaseAdvisor.HIGHEST_PRECEDENCE + 400)  // inside the loop
    .build();

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(chatMemoryAdvisor, toolCallingAdvisor)
    .build();
With the auto-registered ToolCallingAdvisor, DefaultChatClient detects any MemoryAdvisor placed inside the loop and disables internal history automatically — you don’t need to call disableInternalConversationHistory() yourself. The manual call is only needed when constructing ToolCallingAdvisor directly. See メモリ: Inside the Loop.

Hook Methods

ToolCallingAdvisor は、ループ内の明確に定義された箇所に保護されたフックメソッドを公開します。サブクラスはこれらのフックをオーバーライドすることで、ループ自体を再実装することなく動作をカスタマイズできます。

並行して 2 つのファミリーが存在します。1 つは呼び出し(ブロッキング)パス用、もう 1 つはストリーム(リアクティブ)パス用です。カスタムサブクラスは、両方のモードを処理するために、関連するペアをオーバーライドする必要があります。

コールパスフック

protected ChatClientRequest doInitializeLoop(
        ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

protected ChatClientRequest doBeforeCall(
        ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

protected ChatClientResponse doAfterCall(
        ChatClientResponse chatClientResponse, CallAdvisorChain callAdvisorChain);

protected ChatClientResponse doFinalizeLoop(
        ChatClientResponse chatClientResponse, CallAdvisorChain callAdvisorChain);

protected List<Message> doGetNextInstructionsForToolCall(
        ChatClientRequest chatClientRequest,
        ChatClientResponse chatClientResponse,
        ToolExecutionResult toolExecutionResult);
HookWhen and what for

doInitializeLoop

Once, before the first iteration. Use to set up session-scoped state (indexes, caches, augmented prompts).

doBeforeCall

Before each iteration. Use to inject or remove tools, mutate options, or add per-iteration context. The returned request is what gets sent to the model.

doAfterCall

After each iteration’s model response. Use to record per-iteration observations or transform the response before the loop decides whether to continue.

doFinalizeLoop

Once, after the loop ends. Use to emit aggregate metrics, clean up session state, or attach a final transformation.

doGetNextInstructionsForToolCall

Decides what messages the next iteration sends to the model. The default behavior depends on conversationHistoryEnabled: when true, returns the full conversation history; when false, returns only the system message and the latest tool response. This only affects what’s forwarded to the rest of the chain — tool call limits are always evaluated against the complete history for the current turn, independent of this setting.

Stream-Path Hooks

protected ChatClientRequest doInitializeLoopStream(
        ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

protected ChatClientRequest doBeforeStream(
        ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

protected ChatClientResponse doAfterStream(
        ChatClientResponse chatClientResponse, StreamAdvisorChain streamAdvisorChain);

protected Flux<ChatClientResponse> doFinalizeLoopStream(
        Flux<ChatClientResponse> chatClientResponseFlux, StreamAdvisorChain streamAdvisorChain);

protected List<Message> doGetNextInstructionsForToolCallStream(
        ChatClientRequest chatClientRequest,
        ChatClientResponse chatClientResponse,
        ToolExecutionResult toolExecutionResult);

The stream variants follow the same semantics as the call variants. doAfterStream operates on the response aggregated across the iteration’s chunks; doFinalizeLoopStream can transform the entire output Flux.

Subclass Example

ToolSearchToolCallingAdvisor is a concrete example of a ToolCallingAdvisor subclass. It overrides doInitializeLoop and doInitializeLoopStream to index the tool set at session start and augment the system message, and doBeforeCall and doBeforeStream to inject only the tools discovered so far on each iteration. The rest of the loop is inherited from the base class.

public class MyAuditingToolCallingAdvisor extends ToolCallingAdvisor {

    private final AuditService audit;

    @Override
    protected ChatClientResponse doAfterCall(
            ChatClientResponse response, CallAdvisorChain chain) {
        var toolCalls = response.chatResponse().getResult().getOutput().getToolCalls();
        for (var call : toolCalls) {
            audit.recordIntent(call.name(), call.arguments());
        }
        return response;
    }

    public static Builder<?> builder() {
        return new Builder<>();
    }

    public static class Builder<T extends Builder<T>> extends ToolCallingAdvisor.Builder<T> {

        private AuditService audit;

        public T audit(AuditService audit) {
            this.audit = audit;
            return self();
        }

        @Override
        public MyAuditingToolCallingAdvisor build() {
            // Use the inherited fields from ToolCallingAdvisor.Builder via the protected getters.
            return new MyAuditingToolCallingAdvisor(
                getToolCallingManager(),
                getToolExecutionEligibilityChecker(),
                getAdvisorOrder(),
                isConversationHistoryEnabled(),
                audit);
        }
    }
}

The self-referential generic pattern (Builder<T extends Builder<T>>) lets subclass builders chain inherited setter without losing the subclass type. Override newCopy() and copy() if you need to support the copy semantics used by DefaultChatClient for per-call adjustments.

Single- ToolAdvisor Invariant

ToolAdvisor is a marker interface. DefaultChatClient uses it to enforce that exactly one tool advisor is present in any given advisor chain. The invariant prevents subtle double-execution bugs from stacking two tool-calling advisors.

Practically:

  • The auto-registered ToolCallingAdvisor counts as the one.

  • If you register a second ToolAdvisor -implementing advisor (for example, a custom subclass), DefaultChatClient skips the default registration and uses yours — the invariant is preserved.

  • If you register two custom ToolAdvisor -implementing advisors at the same time, the chain construction fails fast with a clear error.

To replace the default in a Spring Boot application, register your subclass via auto-configuration — see カスタム ToolAdvisor: Auto-Configuration Integration.

User-Controlled Streaming

ユーザー制御によるツール実行 covers the blocking variant; this section covers streaming.

When driving the loop manually with .stream(), each iteration produces a chunk Flux. You aggregate the chunks for tool-call detection using ChatClientMessageAggregator, while still forwarding the raw stream to your downstream subscriber (e.g. an SSE endpoint):

ChatClient chatClient = ...
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?";
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);

AtomicReference<ChatClientResponse> ref = new AtomicReference<>();

new ChatClientMessageAggregator().aggregateChatClientResponse(
    chatClient.prompt()
        .messages(prompt.getInstructions())
        .options(chatOptions)
        .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
        .stream()
        .chatClientResponse()
        .doOnNext(chunk -> forwardToSse(chunk)),  // side-channel emission
    ref::set
).blockLast();

ChatClientResponse response = ref.get();

while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
    prompt = new Prompt(result.conversationHistory(), chatOptions);

    AtomicReference<ChatClientResponse> nextRef = new AtomicReference<>();
    new ChatClientMessageAggregator().aggregateChatClientResponse(
        chatClient.prompt()
            .messages(result.conversationHistory())
            .options(chatOptions)
            .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
            .stream()
            .chatClientResponse()
            .doOnNext(chunk -> forwardToSse(chunk)),
        nextRef::set
    ).blockLast();

    response = nextRef.get();
}

This pattern is verbose. In most cases you should prefer placing a custom advisor inside the loop — you keep the framework’s loop and only intercept the chunk stream.

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

For most observation use cases — streaming intermediate progress to a UI, forwarding tool-call events to an audit log, recording per-iteration metrics — there’s no need to disable auto-registration or drive the loop yourself. Place your custom advisor inside the loop by giving it an order greater than ToolCallingAdvisor.DEFAULT_ORDER:

public class ToolCallObservingAdvisor implements CallAdvisor, StreamAdvisor {

    private final Consumer<ChatClientResponse> observer;

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

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        // Each iteration's request includes ToolResponseMessages from prior iterations
        request.prompt().getInstructions().forEach(msg -> log.debug("Message: {}", msg));
        ChatClientResponse response = chain.nextCall(request);
        observer.accept(response);
        return response;
    }

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

自動登録された 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 runs on every iteration; the main caller still receives only the final answer because ToolCallingAdvisor filters tool-call chunks out of its returned stream.

直接 return

When a tool’s ToolMetadata has returnDirect = true, ToolCallingAdvisor:

  1. Executes the tool call as normal.

  2. Detects the returnDirect flag in the ToolExecutionResult.

  3. Breaks out of the loop.

  4. Returns the tool execution result directly to the caller as a ChatResponse whose generation content is the tool’s output.

The model never sees the tool result — the round-trip is skipped. This is useful when the tool’s output is the final answer (e.g. a RAG retrieval) or when the tool should terminate the agent’s reasoning loop.

If the model requests multiple tool calls in a single iteration, returnDirect is only honored if all the called tools have returnDirect = true. Otherwise the results are sent back to the model and the loop continues. See 直接 return for declaring the flag on tool definitions.

Opting Out of Auto-Registration

To disable ToolCallingAdvisor auto-registration globally (for every call from an auto-configured ChatClient):

spring.ai.chat.client.tool-calling.enabled=false

1 回の呼び出しのみ無効にするには:

chatClient.prompt("What day is tomorrow?")
    .tools(new DateTimeTools())
    .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
    .call()
    .content();

With auto-registration off, tools passed via .tools(…​) are sent to the model but tool calls in the response are not executed automatically. You’re then in user-controlled mode.

関連事項