首页 / Spring AI 入门教程 / Recursive Advisor 与调用链编排

Spring AI 入门教程

Recursive Advisor 与调用链编排

本教程共 45 篇 · 第 6 篇 · 更新于 2026-08-16 · 约 7 分钟阅读

Spring AIAdvisorToolCallingAdvisor工具调用结构化输出校验调用链Java

本节目标:理解 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();
Note

order 关系:记忆 advisor 默认在 HIGHEST_PRECEDENCE + 200ToolCallingAdvisor+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=trueToolCallingAdvisor 会中断循环,把工具结果直接作为响应返回给客户端。

适合 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 类型自动推导 schema
  • outputJsonSchema(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 布局:

orderadvisor位置
HIGHEST_PRECEDENCE + 200MessageChatMemoryAdvisor循环外,加载与持久化历史
+300ToolCallingAdvisor驱动循环
+400自定义观察 advisor循环内,每轮执行

记忆 advisor 在循环外、观察 advisor 在循环内,靠的就是这个数字差。设计自己的递归 advisor 时,先想清楚子链从哪开始、每轮要执行哪些逻辑,再定 order。

6.5 小结

Recursive Advisor 把”循环调用模型”变成了链内能力。copy() 提供子链,order 决定嵌套位置。工具循环和输出校验是它的两个典型场景。先跑通默认配置,再按需调整 order 和循环边界,就能组合出可控的调用链。