Skip to content
Spring AI 文档版本 2.1.0-SNAPSHOT 源快照 903cc6e1 更新于 2026-10-16

ToolCallingAdvisor

ToolCallingAdvisor 是 Spring AI 2.0 中拥有工具执行生命周期的递归顾问。它由 DefaultChatClient 自动注册,并驱动请求/响应循环,直到模型产出一个不含工具调用的响应。

本页是构建器 API、配置选项、钩子方法与扩展模式的参考。关于该循环的概念概览,参见工具调用循环。关于更广泛的递归顾问模式,参见递归顾问。

概述

ToolCallingAdvisor 实现了 CallAdvisor 和 StreamAdvisor,外加 ToolAdvisor 标记接口。DefaultChatClient 正是用这个标记接口来强制链中恰好存在一个工具顾问——参见 单个 ToolAdvisor 不变式。

默认的 ToolCallingAdvisor.DEFAULT_ORDER 是 Ordered.HIGHEST_PRECEDENCE + 300。它高于默认的 MessageChatMemoryAdvisor 顺序(HIGHEST_PRECEDENCE + 200),从而默认把记忆顾问置于工具循环之外。参见记忆与工具循环。

构建器

通过 ToolCallingAdvisor.builder() 构造一个 ToolCallingAdvisor:

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

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

NOTE

多数应用并不直接构造 ToolCallingAdvisor——DefaultChatClient 会用合理的默认值自动注册一个。只有当你需要非默认设置,或要用自定义子类替换它时,才手动构造。

构建器选项

选项描述默认值
toolCallingManager(ToolCallingManager)用于执行工具调用的 ToolCallingManager 实例。自动构建的实例
toolExecutionEligibilityChecker(ToolExecutionEligibilityChecker)一个判定模型响应是否应触发下一次工具调用迭代的谓词。默认检查 chatResponse.hasToolCalls()。可覆盖以施加服务商特定的停止原因逻辑(例如除工具调用存在外还检查 finish-reason 字段)。chatResponse -> chatResponse != null && chatResponse.hasToolCalls()
advisorOrder(int)顾问在链中被应用的顺序。必须介于 BaseAdvisor.HIGHEST_PRECEDENCE 与 BaseAdvisor.LOWEST_PRECEDENCE 之间。它决定哪些其他顾问在循环内部 vs 外部运行。HIGHEST_PRECEDENCE + 300
conversationHistoryEnabled(boolean)顾问是否在迭代间在内部维护对话历史。为 true(默认)时,循环内每次 LLM 调用都收到先前工具调用与响应的完整历史,由顾问自己管理。为 false 时,顾问只转发最新一条消息——当循环内的 MemoryAdvisor 接管了历史管理时很有用。true
disableInternalConversationHistory()conversationHistoryEnabled(false) 的简写。—

ToolExecutionEligibilityChecker

ToolExecutionEligibilityChecker 是一个函数式接口:

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

默认检查器只要响应携带工具调用就触发下一次迭代。为服务商特定行为覆盖它:

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

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

对话历史行为

默认情况下,ToolCallingAdvisor 在其循环内保留完整的对话历史(用户消息、模型响应、工具调用请求、工具响应)。每次后续迭代都向模型发送完整历史。

当记忆位于循环之外时这是正确的行为,因为外层记忆顾问只会看到最终的用户/助手交换——逐迭代的历史是 ToolCallingAdvisor 的私事。

在以下情形设置 disableInternalConversationHistory():

  • 你把一个 MemoryAdvisor 放在循环之内(它会自己管理逐迭代历史)。
  • 你在自行驱动该循环,且只想转发最新一条消息。
java
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();

TIP

对于自动注册的 ToolCallingAdvisor,DefaultChatClient 会检测放在循环之内的任何 MemoryAdvisor 并自动禁用内部历史——你无需自己调用 disableInternalConversationHistory()。只有当你直接构造 ToolCallingAdvisor 时才需手动调用。参见记忆:循环之内。

钩子方法

ToolCallingAdvisor 在循环中定义良好的点暴露受保护的钩子方法。子类通过覆盖这些钩子来定制行为,而无需重新实现循环本身。

有两组并行的家族——一组用于 call(阻塞)路径,一组用于 stream(响应式)路径。自定义子类应覆盖相应的一对以同时处理两种模式。

Call 路径钩子

java
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);
钩子何时以及用途
doInitializeLoop一次,在首次迭代之前。用于设置会话作用域的状态(索引、缓存、增强的提示词)。
doBeforeCall每次迭代之前。用于注入或移除工具、修改选项,或添加逐迭代上下文。返回的请求就是发给模型的内容。
doAfterCall在每次迭代的模型响应之后。用于记录逐迭代观测,或在循环决定是否继续之前转换响应。
doFinalizeLoop一次,在循环结束之后。用于发出聚合指标、清理会话状态,或附加最后一次转换。
doGetNextInstructionsForToolCall决定下一次迭代向模型发送哪些消息。默认行为取决于 conversationHistoryEnabled:为 true 时返回完整对话历史;为 false 时只返回系统消息和最新的工具响应。这只影响为链中其余部分转发的内容——工具调用限制总是针对当前回合的完整历史评估,与此设置无关。

Stream 路径钩子

java
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);

流式变体遵循与 call 变体相同的语义。doAfterStream 对聚合了某次迭代各块的响应进行操作;doFinalizeLoopStream 可以转换整个输出 Flux。

子类示例

ToolSearchToolCallingAdvisor 是 ToolCallingAdvisor 子类的具体例子。它覆盖 doInitializeLoop 和 doInitializeLoopStream 以在会话开始时索引工具集并增强系统消息,并覆盖 doBeforeCall 和 doBeforeStream 以在每次迭代只注入迄今发现的工具。循环的其余部分继承自基类。

java
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);
        }
    }
}

自引用的泛型模式(Builder<T extends Builder<T>>)让子类的构建器能链式调用继承的 setter 而不丢失子类类型。若你需要支持 DefaultChatClient 用于逐调用调整的复制语义,请覆盖 newCopy() 和 copy()。

单个 ToolAdvisor 不变式

ToolAdvisor 是一个标记接口。DefaultChatClient 用它来强制在任何给定的顾问链中恰好存在一个工具顾问。这一不变式避免了堆叠两个工具调用顾问所带来的微妙重复执行 bug。

实际上:

  • 自动注册的 ToolCallingAdvisor 就算那一个。
  • 若你注册了第二个实现 ToolAdvisor 的顾问(例如一个自定义子类),DefaultChatClient 会跳过默认注册并使用你的——不变式得以保持。
  • 若你同时注册两个实现 ToolAdvisor 的自定义顾问,链的构建会以一个明确错误快速失败。

要在 Spring Boot 应用中替换默认的那个,通过自动配置注册你的子类——参见自定义 ToolAdvisor:自动配置集成。

用户控制的流式

用户控制的工具执行 涵盖阻塞变体;本节涵盖流式。

当用 .stream() 手动驱动该循环时,每次迭代产出一个块 Flux。你用 ChatClientMessageAggregator 聚合各块以做工具调用检测,同时仍把原始流转发给你的下游订阅者(例如一个 SSE 端点):

java
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();
}

这种模式很啰嗦。多数情况下你应更倾向于把一个自定义顾问放在循环之内——你保留框架的循环,只拦截块流。

观察工具调用循环

对于多数观察用例——把中间进度流式转发到 UI、把工具调用事件转发到审计日志、记录逐迭代指标——无需禁用自动注册或自行驱动该循环。通过给你的自定义顾问一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的顺序,把它放在循环之内:

java
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 一并注册:

java
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 在每次迭代上运行;主调用方仍只收到最终答案,因为 ToolCallingAdvisor 会把工具调用块从其返回的流中过滤掉。

直接返回

当某个工具的 ToolMetadata 带 returnDirect = true 时,ToolCallingAdvisor 会:

  1. 照常执行工具调用。
  2. 在 ToolExecutionResult 中检测到 returnDirect 标志。
  3. 跳出循环。
  4. 把工具执行结果直接作为 ChatResponse 返回给调用方,其 generation 内容即为工具的输出。

模型永远看不到工具结果——往返被跳过。当工具的输出就是最终答案(例如一次 RAG 检索),或当工具应终止智能体的推理循环时,这很有用。

NOTE

如果模型在单次迭代中请求多个工具调用,只有当所有被调用的工具都有 returnDirect = true 时,returnDirect 才被采纳。否则结果会被发回模型,循环继续。关于在工具定义上声明该标志,参见直接返回。

退出自动注册

要全局禁用 ToolCallingAdvisor 自动注册(针对自动配置的 ChatClient 发出的每次调用):

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

要仅对单次调用禁用它:

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

自动注册关闭后,通过 .tools(...) 传入的工具会被发给模型,但响应中的工具调用不会被自动执行。于是你进入用户控制模式。

另见

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。