このバージョンはまだ開発中であり、まだ安定しているとは見なされていません。最新の安定バージョンについては、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 が適切なデフォルト値で自動的に登録します。デフォルト以外の設定が必要な場合、またはカスタムサブクラスに置き換える場合にのみ、手動で構築してください。 |
ビルダーオプション
| オプション | 説明 | デフォルト |
|---|---|---|
| ツール呼び出しを実行するために使用される | 自動構築されたインスタンス |
| モデルレスポンスによって別のツール呼び出しの反復処理をトリガーするかどうかを決定する述語。デフォルトでは |
|
| チェーン内でアドバイザーが適用される順序。 |
|
| アドバイザーがイテレーション間で会話履歴を内部的に保持するかどうか。 |
|
|
| — |
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);| Hook | When and what for |
|---|---|
| Once, before the first iteration. Use to set up session-scoped state (indexes, caches, augmented prompts). |
| 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. |
| After each iteration’s model response. Use to record per-iteration observations or transform the response before the loop decides whether to continue. |
| Once, after the loop ends. Use to emit aggregate metrics, clean up session state, or attach a final transformation. |
| Decides what messages the next iteration sends to the model. The default behavior depends on |
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
ToolCallingAdvisorcounts as the one.If you register a second
ToolAdvisor-implementing advisor (for example, a custom subclass),DefaultChatClientskips 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:
Executes the tool call as normal.
Detects the
returnDirectflag in theToolExecutionResult.Breaks out of the loop.
Returns the tool execution result directly to the caller as a
ChatResponsewhose 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=false1 回の呼び出しのみ無効にするには:
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.
関連事項
ツール呼び出し: The Tool Calling Loop — conceptual overview
ツール呼び出し: Memory and the Tool Loop — inside/outside ordering
ツール呼び出し: ループを拡張する — auto-configuration extension point for custom subclasses
ツール検索ツール —
ToolSearchToolCallingAdvisor, a concrete subclass implementing progressive tool disclosure再帰アドバイザー — the underlying recursive advisor pattern
アドバイザー — the broader advisor system