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

ChatModel ツール呼び出し

ChatModel は、チャットプロバイダへの低レベルのリクエスト / レスポンスインターフェースです。ツール定義を受け取り、モデルに送信し、モデルのレスポンスを返します。レスポンス内のツール呼び出しは自動的には実行されません。それは呼び出し元の責任です。

ほとんどのアプリケーションでは、ChatClient が推奨されるエントリポイントです。ChatClient は ToolCallingAdvisor を介してツール呼び出しループを処理し、メモリおよび可観測性アドバイザーと連携し、自動構成拡張ポイントをサポートします。このページは、意図的に低レベルのパスを使用したいユーザー向けです。

ChatModel を直接使用する場合

ChatModel は、次のような場合に最適な選択肢です。

  • アドバイザーチェーン(メモリアドバイザーなし、可観測性アドバイザーなし、ToolCallingAdvisor なし)は不要です。

  • 独自のループを持つカスタムオーケストレーター(ドメイン固有のワークフローエンジン、Spring 以外のエージェントフレームワークなど)にツール呼び出しを統合しようとしています。

  • Spring AI の上にインフラストラクチャを構築しています。たとえば、カスタムの ChatClient 実装や、独自の高レベル API を公開するライブラリなどです。

その他すべてについては — チャットアプリケーション、RAG、エージェント型ワークフロー、ツール多用型アシスタント— ChatClient を使用してください。

内部ツール実行なし

ChatModel はツール呼び出しを実行しません。ChatModel.call(prompt) と ChatModel.stream(prompt) は、ツール呼び出しリクエストを含むモデルの生のレスポンスを返しますが、ツール呼び出しは実行しません。リクエストされたツールを実行してモデルを再呼び出しするのは呼び出し元の責任です。Driving the Loop Manually を参照してください。

Spring AI 1.x から移行する場合、各 ChatModel が独自の内部ツール実行ループを実行していたため、変更点と移行方法については Upgrading Tool Calling from 1.x to 2.0 を参照してください。

ChatModel にツールを渡す

ツールは ToolCallingChatOptions.toolCallbacks(…​) を介して渡されます。オプションは List<ToolCallback> または ToolCallback[] を受け入れます。@Tool でアノテーションが付けられたオブジェクトをコールバックに変換するには、ToolCallbacks.from(…​) を使用します。

リクエストごとのツール

ChatModel chatModel = ...
ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools());

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .build();

Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
ChatResponse response = chatModel.call(prompt);

これにより、ツール定義がモデルに送信されます。モデルがツールを呼び出すことを決定した場合、レスポンスには呼び出しリクエストが含まれます。それを自分で実行します(Driving the Loop Manually を参照)。

デフォルトツール (Configured on the Model)

一部の ChatModel ビルダーはデフォルトオプションを受け入れます。デフォルトオプションで設定されたツールは、上書きされない限りすべてのリクエストに適用されます。

ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());

ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .options(ToolCallingChatOptions.builder()
        .toolCallbacks(dateTimeTools)
        .build())
    .build();
Default tools are sent to the model on every request issued through this ChatModel instance. This is convenient for tools that should always be available, but it can also be dangerous — risk-tier and destructive tools should typically be added per request, not as defaults.

オーバーライドセマンティクス

`ChatModel` のデフォルトオプションとリクエストごとのオプションの両方でツールが設定されている場合、リクエストごとのツールリストがデフォルトを完全に置き換えます。

ChatModel chatModel = OllamaChatModel.builder()
    .options(ToolCallingChatOptions.builder()
        .toolCallbacks(defaultTools)  // 5 default tools
        .build())
    .build();

ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(otherTool)          // 1 tool
    .build();

ChatResponse response = chatModel.call(new Prompt("...", runtimeOptions));
// The model sees ONLY otherTool — defaultTools were replaced, not appended.

To use defaults plus an extra tool on a single request, include the defaults explicitly in the runtime options:

List<ToolCallback> combined = new ArrayList<>(Arrays.asList(defaultTools));
combined.add(otherTool);

ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(combined)
    .build();
This override behavior is specific to the ChatModel API. When using ChatClient, per-call .tools(…​) appends to .defaultTools(…​) — the two layers compose naturally. See Passing Tools to ChatClient.

Driving the Loop Manually

When the model returns a response with tool calls, you execute the tools and call the model again with the results. This is the loop that ToolCallingAdvisor runs automatically for ChatClient. With ChatModel, you write it yourself.

ブロッキング

ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

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

Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);
ChatResponse response = chatModel.call(prompt);

while (response.hasToolCalls()) {
    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);

    if (result.returnDirect()) {
        // Tool's returnDirect=true — break out without sending back to the model
        return result.conversationHistory();
    }

    prompt = new Prompt(result.conversationHistory(), chatOptions);
    response = chatModel.call(prompt);
}

String finalAnswer = response.getResult().getOutput().getText();

Key components:

  • ToolCallingManager — executes tool calls. The default DefaultToolCallingManager is auto-configured by Spring Boot. Build one manually with ToolCallingManager.builder().build() when you’re not using auto-configuration.

  • response.hasToolCalls() — true if the model requested at least one tool.

  • toolCallingManager.executeToolCalls(prompt, response) — looks up each requested tool, executes it, and returns a ToolExecutionResult containing the updated conversation history.

  • result.returnDirect() — true if all the called tools have returnDirect = true. See 直接 return.

  • result.conversationHistory() — the original messages plus the assistant’s tool-call request and the tool responses. Use this as the next prompt’s messages.

ストリーミング

The streaming variant aggregates each iteration’s chunks via ChatClientMessageAggregator before checking for tool calls. You can forward the raw chunk stream to a downstream subscriber (e.g. an SSE endpoint) while aggregating:

ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

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

Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);

while (true) {
    AtomicReference<ChatResponse> aggregated = new AtomicReference<>();

    new MessageAggregator().aggregate(
        chatModel.stream(prompt).doOnNext(chunk -> forwardToSse(chunk)),
        aggregated::set
    ).blockLast();

    ChatResponse response = aggregated.get();
    if (!response.hasToolCalls()) {
        break;
    }

    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);
    if (result.returnDirect()) {
        break;
    }
    prompt = new Prompt(result.conversationHistory(), chatOptions);
}

For ChatClient -driven streaming with full advisor composition, see ToolCallingAdvisor: User-Controlled Streaming.

ツールコンテキスト

Tool context — non-model data passed to tool methods — works the same way with ChatModel as with ChatClient. Set it via ToolCallingChatOptions:

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(ToolCallbacks.from(new CustomerTools()))
    .toolContext(Map.of("tenantId", "acme"))
    .build();

Prompt prompt = new Prompt("Tell me about customer 42", chatOptions);
chatModel.call(prompt);

When both default and runtime toolContext are set, the resulting context is the merge of the two (unlike toolCallbacks, which is replaced) — runtime entries take precedence over defaults for matching keys.

See ツールコンテキスト for the tool-side API.

直接 return

returnDirect flags are honored by ToolCallingManager.executeToolCalls(…​). After execution, check ToolExecutionResult.returnDirect():

ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);

if (result.returnDirect()) {
    // Skip the next model call — return the tool result to the caller
    return result.conversationHistory();
}

If the model requested multiple tool calls in a single iteration, returnDirect() is true only if all the called tools have returnDirect = true. See 直接 return for declaring the flag on a tool.

関連事項