Recursive Advisor 与调用链编排
本教程共 45 篇 · 第 6 篇 · 更新于 2026-08-16 · 约 7 分钟阅读
本节目标:理解 Recursive Advisor 的循环机制,学会用 ToolCallingAdvisor 编排工具调用循环,用 StructuredOutputValidationAdvisor 做输出校验与重试。
第 5 章讲过,Advisor 能拦截请求和响应。普通 Advisor 只拦一次,请求沿着链走一趟就结束。Recursive Advisor 不一样,它可以反复遍历调用链,直到某个条件满足。
想想工具调用:模型说要查天气,应用查完把结果喂回去,模型看了结果可能又要查另一个城市。这种多轮交互不能靠单次调用完成。递归模式就是为这类需求准备的。
官方文档列出的典型场景:
- 循环执行工具调用,直到没有工具需要调用
- 校验结构化输出,失败就重试
- 实现带请求修改的评估逻辑
- 实现带请求修改的重试逻辑
6.1 子链:CallAroundAdvisorChain.copy
Recursive Advisor 的关键工具是 CallAroundAdvisorChain.copy(CallAroundAdvisor after)。它创建一条新链,只包含原链中位于指定 advisor 之后的那些 advisor。
递归 advisor 拿着这条子链,想循环几次就调几次。这个机制带来几个保证:
- 递归 advisor 只循环下游的 advisor,不会重复执行自己前面的部分
- 子链里每个 advisor 都能观察和拦截每一轮迭代
- 调用链的顺序和可观测性保持不变
一句话概括:把”循环调用模型”变成链内行为,而不是散落在业务代码里。循环的每一轮都走完整的下游链,观察、日志、限流这些横切逻辑不用重写。
6.2 ToolCallingAdvisor:工具调用循环
工具调用的完整过程包含多轮。模型说要调工具,应用执行工具,把结果喂回去,模型可能又要调下一个。在 Spring AI 2.0 里,这个循环默认由 ToolCallingAdvisor 驱动。
它把工具调用循环搬进了 advisor 链。链上其他 advisor 因此能看到每一轮发生了什么。核心特性:
- 循环遍历链,直到
ToolExecutionEligibilityChecker报告没有更多工具调用 - 支持 returnDirect,工具结果可以跳过模型直接返回
- 通过
callAdvisorChain.copy(this)构造递归子链 - 可配置对话历史管理,循环判定器可插拔
循环何时结束?每轮调用后,判定器检查模型的输出。模型不再请求工具,循环就停。整个过程对调用方透明,拿到的是最终答案。
ToolCallingAdvisor 默认自动注册,大部分场景不用配。手动注册的写法:
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(toolCallingAdvisor)
.build();
对话历史管理
循环里每次调用模型,都要带上之前所有消息:用户消息、助手消息、工具响应。ToolCallingAdvisor 默认自己管理这份历史(conversationHistoryEnabled=true)。
默认布局下,记忆 advisor 在循环外面。它加载一次历史,循环结束后只持久化最后一轮对话。这种设计是刻意的:大多数 ChatMemoryRepository 实现不支持工具消息类型。
// 默认布局:记忆 advisor 在循环外,无需显式设置 order
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
Noteorder 关系:记忆 advisor 默认在
HIGHEST_PRECEDENCE + 200,ToolCallingAdvisor在+300。200 在 300 之前,所以记忆 advisor 在循环外。
大部分ChatMemoryRepository实现不支持持久化工具消息(InMemoryChatMemoryRepository是支持的特例),其他实现请保持默认布局。
想把记忆 advisor 放进循环内,要调用 .disableInternalConversationHistory(),再给记忆 advisor 更大的 order:
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.disableInternalConversationHistory() // 循环内的历史交给记忆 advisor
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // 在循环内
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor, toolCallingAdvisor)
.build();
观察循环的每一轮
想监控中间过程,不需要手动接管循环。给自定义 advisor 一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的 order 值,它就会在每一轮迭代中被调用。
public class ToolCallObservingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
private final Consumer<AdvisedResponse> observer;
public ToolCallObservingAdvisor(Consumer<AdvisedResponse> observer) {
this.observer = observer;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
// 每一轮的请求里都能看到工具响应消息
request.prompt().getInstructions().forEach(msg -> log.debug("Message: {}", msg));
AdvisedResponse response = chain.nextCall(request);
observer.accept(response);
return response;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest request, StreamAroundAdvisorChain chain) {
// 每一轮的分片流都能观察到
return chain.nextStream(request).doOnNext(observer);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 400; // 在 ToolCallingAdvisor(300)之后
}
}
注册后,这个 advisor 位于循环内部,每一轮都执行。可以把中间结果转发到 SSE、WebSocket 或日志,不影响循环本身。调用方收到的仍是最终答案,工具调用的中间分片由观察者转发到旁路通道。
returnDirect:跳过模型直接返回
有的工具输出就是最终答案,不需要模型再加工。给工具执行设置 returnDirect=true,ToolCallingAdvisor 会中断循环,把工具结果直接作为响应返回给客户端。
适合 returnDirect 的场景:工具输出是最终答案、想降低延迟、工具结果需要原样返回。好处是省掉一次模型调用,用户等得更短。
手动驱动循环
默认情况下调用方只拿到最终答案。想完全控制循环,比如边执行边把进度推给前端 UI,可以按次关闭自动注册:
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
AdvisedResponse response = chatClient.prompt()
.user(question)
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
// 自己驱动循环,每一轮都能拿到中间结果
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
response = chatClient.prompt()
.messages(result.conversationHistory())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
}
循环条件看 hasToolCalls(),每一轮的执行结果通过 result.conversationHistory() 带回下一轮。这个模式适合流式展示中间过程的自定义 UI。
6.3 StructuredOutputValidationAdvisor:输出校验重试
模型的 JSON 输出偶尔不合规。StructuredOutputValidationAdvisor 用 JSON Schema 校验输出,不合格就重试,默认最多 3 次。重试时会把校验错误拼进提示词,引导模型修正。
两种配置方式,二选一:
outputType(Class):从 Java 类型自动推导 schemaoutputJsonSchema(String):直接提供 schema
前者省事,适合大多数场景。后者适合 schema 已经由前后端约定好的情况,比如接口文档里定了 JSON 结构,直接用那份定义。
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputType(MyResponseType.class)
.maxRepeatAttempts(3)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(validationAdvisor)
.build();
预先提供 schema 的写法:
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputJsonSchema(myConverter.getJsonSchema())
.build();
不手动配置也行。.entity() 调用时直接开启 schema 校验:
ActorFilms actorFilms = chatClient.prompt()
.user("随机选一位演员,列出他的 5 部电影")
.call()
.entity(ActorFilms.class, spec -> spec.schemaValidation());
这个 advisor 内部同样用 callAdvisorChain.copy(this) 构造子链,是递归模式的第二个官方示例。它和 BeanOutputConverter 配合,正好补上”模型不守规矩”的短板。第 8 章会详细讲转换器。
6.4 组合与嵌套:order 决定执行顺序
Recursive Advisor 没有改变 order 规则:值越小越先执行。嵌套发生在循环内部,循环的每一轮都会重新走一遍子链。
典型的 order 布局:
| order | advisor | 位置 |
|---|---|---|
| HIGHEST_PRECEDENCE + 200 | MessageChatMemoryAdvisor | 循环外,加载与持久化历史 |
| +300 | ToolCallingAdvisor | 驱动循环 |
| +400 | 自定义观察 advisor | 循环内,每轮执行 |
记忆 advisor 在循环外、观察 advisor 在循环内,靠的就是这个数字差。设计自己的递归 advisor 时,先想清楚子链从哪开始、每轮要执行哪些逻辑,再定 order。
6.5 小结
Recursive Advisor 把”循环调用模型”变成了链内能力。copy() 提供子链,order 决定嵌套位置。工具循环和输出校验是它的两个典型场景。先跑通默认配置,再按需调整 order 和循环边界,就能组合出可控的调用链。