主题
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 会:
- 照常执行工具调用。
- 在
ToolExecutionResult中检测到returnDirect标志。 - 跳出循环。
- 把工具执行结果直接作为
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(...) 传入的工具会被发给模型,但响应中的工具调用不会被自动执行。于是你进入用户控制模式。
另见
- 工具调用:工具调用循环 —— 概念概览
- 工具调用:记忆与工具循环 —— 内部/外部排序
- 工具调用:扩展循环 —— 供自定义子类用的自动配置扩展点
- 工具搜索工具 ——
ToolSearchToolCallingAdvisor,一个实现渐进式工具披露的具体子类 - 递归顾问 —— 底层的递归顾问模式
- 顾问 —— 更广泛的顾问系统