ToolCallingAdvisor
ToolCallingAdvisor 是 Spring AI 2.0 中拥有工具执行生命周期的递归 advisor。它由 DefaultChatClient 自动注册,并驱动请求/响应循环,直到模型生成不包含工具调用的响应。
本页是有关 builder API、配置选项、钩子方法以及扩展模式的参考文档。关于该循环的概念性概述,请参阅 The Tool Calling Loop。关于更广泛的递归顾问(recursive-advisor)模式,请参阅 Recursive Advisors。
概述
ToolCallingAdvisor 实现了 CallAdvisor 和 StreamAdvisor,此外还实现了 ToolAdvisor 标记接口。该标记接口是 DefaultChatClient 用于强制要求链中必须存在且仅存在一个 tool advisor 的依据——参见 Single-ToolAdvisor 不变式。
默认的 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 会自动注册一个具有合理默认值的实例。只有在需要非默认配置,或者需要用自定义子类替换它时,才需要手动构造。
Builder 选项
| 选项 | 描述 | 默认值 |
|---|---|---|
toolCallingManager(ToolCallingManager) | 用于执行工具调用的 ToolCallingManager 实例。 | 自动构建的实例 |
toolExecutionEligibilityChecker(ToolExecutionEligibilityChecker) | 决定模型响应是否应触发另一次工具调用迭代的谓词。默认检查 chatResponse.hasToolCalls()。可以覆盖以实现特定于提供商的停止原因逻辑(例如,除了检查工具调用的存在性外,还检查 finish-reason 字段)。 | chatResponse → chatResponse != null && chatResponse.hasToolCalls() |
advisorOrder(int) | Advisor 在链中应用的顺序。必须介于 BaseAdvisor.HIGHEST_PRECEDENCE 和 BaseAdvisor.LOWEST_PRECEDENCE 之间。决定哪些其他 Advisor 在循环内部运行,哪些在循环外部运行。 | HIGHEST_PRECEDENCE + 300 |
conversationHistoryEnabled(boolean) | Advisor 是否在内部跨迭代维护对话历史。当为 true(默认值)时,循环内的每次 LLM 调用都会收到之前所有工具调用和响应的完整历史,由 Advisor 本身管理。当为 false 时,Advisor 仅转发最新的一条消息——当循环内的 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 在其循环中保留完整的对话历史(用户消息、模型响应、工具调用请求、工具响应)。每次后续迭代都会将完整的历史记录发送给模型。
当内存位于循环外部时,这种行为是正确的,因为外层的内存顾问只会看到最终的 user/assistant 交互——每次迭代的对话历史是 `ToolCallingAdvisor 的私有关注点。
在以下情况下设置 disableInternalConversationHistory():
- 您正在循环内部放置
MemoryAdvisor(它将自行管理每次迭代的历史记录)。 - 您自行控制循环,并且只希望转发最新的消息。
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();
当使用自动注册的 ToolCallingAdvisor 时,DefaultChatClient 会检测循环内放置的任何 MemoryAdvisor 并自动禁用内部历史——您无需自己调用 disableInternalConversationHistory()。只有在直接构建 ToolCallingAdvisor 时才需要手动调用。参见 Memory: Inside the Loop。
Hook 方法
ToolCallingAdvisor 在循环中的特定位置暴露了受保护的钩子方法。子类可以重写这些钩子来定制行为,而无需重新实现循环本身。
这里存在两个并行的体系:一个用于调用(阻塞)路径,另一个用于流(响应式)路径。自定义的子类应重写相应的一对方法,以同时处理这两种模式。
调用路径挂钩
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 | 时机与用途 |
|---|---|
doInitializeLoop | 仅执行一次,在第一轮迭代之前。用于初始化会话级别的上下文(如索引、缓存、增强提示词等)。 |
doBeforeCall | 在每轮迭代开始前。用于注入或移除工具、修改选项,或添加针对当前迭代的上下文。返回的请求对象即为发送给模型的内容。 |
doAfterCall | 在每轮迭代的模型响应之后。用于记录当前迭代的观测数据,或在循环决定是否继续之前对响应进行转换。 |
doFinalizeLoop | 仅执行一次,在循环结束后。用于输出聚合指标、清理会话状态,或附加最终转换逻辑。 |
doGetNextInstructionsForToolCall | 决定下一轮迭代向模型发送的消息内容。默认行为取决于 conversationHistoryEnabled:当为 true 时,返回完整的对话历史;当为 false 时,仅返回系统消息和最新的工具响应。此设置仅影响转发到链路其余部分的内容 —— 工具调用限制 始终基于当前轮次的完整历史进行评估,独立于此设置。 |
Stream-Path 钩子函数
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);
流变体遵循与调用变体相同的语义。doAfterStream 操作聚合自迭代中各个数据块(chunks)的响应;doFinalizeLoopStream 可以转换整个输出 Flux。
子类示例
ToolSearchToolCallingAdvisor 是 ToolCallingAdvisor 子类的一个具体示例。它重写了 doInitializeLoop 和 doInitializeLoopStream,用于在会话开始时对工具集进行索引并增强系统消息;同时重写了 doBeforeCall 和 doBeforeStream,用于在每次迭代中仅注入目前已发现的工具。循环的其余部分继承自基类。
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>>)允许子类的 Builder 在链式调用继承的 setter 方法时不丢失子类类型。如果你需要支持 DefaultChatClient 在每次调用时调整参数所使用的复制语义,请重写 newCopy() 和 copy() 方法。
单一 ToolAdvisor 不变量
ToolAdvisor 是一个标记接口。DefaultChatClient 使用它来确保在任何给定的顾问链中恰好存在一个工具顾问。这一不变式防止了因叠加两个工具调用顾问而产生的隐蔽的双次执行 bug。
在实践中:
-
自动注册的
ToolCallingAdvisor算作其中一个。 -
如果您注册了第二个实现了
ToolAdvisor的顾问(例如一个自定义子类),DefaultChatClient会跳过默认注册并使用您注册的顾问——该不变量得到保持。 -
如果您同时注册了两个实现了
ToolAdvisor的自定义顾问,链的构建会快速失败并给出清晰的错误信息。
要在 Spring Boot 应用中替换默认实现,请通过自动配置注册您的子类——详见 Custom ToolAdvisor:自动配置集成。
用户控制的流式传输
用户控制工具执行 涵盖了阻塞变体;本节介绍流式处理。
通过 .stream() 手动驱动循环时,每次迭代会产生一个 Flux 块。您可以使用 ChatClientMessageAggregator 聚合这些块以检测工具调用,同时仍将原始流转发给下游订阅者(例如 SSE 端点):
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();
}
这种写法较为冗长。在大多数情况下,建议使用在循环内放置自定义顾问——这样可以保留框架的循环,仅拦截 chunk 流。
观察工具调用循环
在大多数观察场景中——例如将中间进度流式传输到 UI、转发工具调用事件到审计日志、记录每次迭代的指标——无需禁用自动注册或自行驱动循环。通过设置一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的顺序值,可将自定义 advisor 放置在循环内部:
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 在每次迭代时都会执行;主调用方仍然只接收最终答案,因为 ToolCallingAdvisor 会从返回的流中过滤掉工具调用分片。
直接返回
当工具的 ToolMetadata 中 returnDirect = true 时,ToolCallingAdvisor 将:
-
正常执行工具调用。
-
检测
ToolExecutionResult中的returnDirect标志。 -
跳出循环。
-
直接将工具执行结果作为
ChatResponse返回给调用方,其生成内容为该工具的输出。
模型永远不会看到工具结果——往返过程被跳过。这在工具的输出即为最终答案时(例如 RAG 检索)或当工具应终止代理的推理循环时很有用。
如果模型在单次迭代中请求多个工具调用,只有当 所有 被调用的工具都设置为 returnDirect = true 时,returnDirect 才会生效。否则,结果将被发回给模型,循环将继续。有关在工具定义上声明该标志,请参阅 Return Direct。
拒绝自动注册
要在全局范围内禁用 ToolCallingAdvisor 的自动注册(针对来自自动配置的 ChatClient 的所有调用):
spring.ai.chat.client.tool-calling.enabled=false
若仅在单次调用中禁用:
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.content();
在关闭自动注册的情况下,通过 .tools(…) 传入的工具会被发送给模型,但响应中的工具调用 不会 自动执行。随后,您将进入用户控制模式。
另请参见
-
工具调用:工具调用循环 — 概念概述
-
工具调用:内存与工具循环 — 内部/外部顺序
-
工具调用:扩展循环 — 自定义子类的自动配置扩展点
-
工具搜索工具 —
ToolSearchToolCallingAdvisor,实现渐进式工具披露的具体子类 -
递归 Advisor — 基础的递归 Advisor 模式
-
Advisor — 更广泛的 Advisor 系统