概述
递归 Advisor(Recursive Advisors) 是一种特殊类型的 Advisor,可以多次循环遍历下游 Advisor 链。这种模式在需要重复调用大语言模型(LLM)直到满足特定条件时非常有用,例如:
- 循环执行工具调用,直到无需再调用任何工具
- 验证结构化输出,并在验证失败时重试
- 通过修改请求来实现评估(Evaluation)逻辑
- 通过修改请求来实现重试逻辑
核心机制
CallAdvisorChain.copy(CallAdvisor after) 方法是实现递归 Advisor 模式的关键工具。它创建一个新的 Advisor 链,该链仅包含原始链中指定 Advisor 之后的 Advisor,并允许递归 Advisor 根据需要调用此子链。
这种方法确保了:
- 递归 Advisor 可以循环遍历链中剩余的 Advisor
- 链中的其他 Advisor 可以观察和拦截每次迭代
- Advisor 链保持正确的执行顺序和可观测性
- 递归 Advisor 不会重新执行它之前的 Advisor
内置的递归 Advisor
Spring AI 提供了两个演示此模式的内置递归 Advisor:
1. ToolCallingAdvisor(工具调用 Advisor)
ToolCallingAdvisor 将工具调用循环作为 Advisor 链的一部分来实现,而不是依赖模型内部的工具执行。这使得链中的其他 Advisor 能够拦截和观察工具调用的过程。
核心特性
- 循环遍历 Advisor 链,直到
ToolExecutionEligibilityChecker报告没有更多工具调用需要执行 - 支持 “return direct” 功能 —— 当工具执行设置了
returnDirect=true时,它会中断工具调用循环,并直接将工具执行结果返回给客户端应用程序,而不是将其发送回 LLM - 使用
callAdvisorChain.copy(this)创建用于递归调用的子链 - 通过
conversationHistoryEnabled支持可配置的对话历史管理 - 支持可插拔的
ToolExecutionEligibilityChecker,以自定义循环迭代的条件
使用示例
1 | // 示例:将 ToolCallingAdvisor 注册到 ChatClient |
对话历史管理
ToolCallingAdvisor 包含一个 conversationHistoryEnabled 配置选项,用于控制工具调用迭代期间对话历史的管理方式。
默认行为(conversationHistoryEnabled = true)
Advisor 在工具调用迭代期间会在内部维护完整的对话历史。这意味着工具调用循环中的每次后续 LLM 调用都会包含所有之前的消息(用户消息、助手回复、工具回复)。
默认情况下,记忆 Advisor(DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = HIGHEST_PRECEDENCE + 200)被放置在工具调用循环的外部。ToolCallingAdvisor(位于 HIGHEST_PRECEDENCE + 300)在迭代期间内部管理对话历史。记忆 Advisor 在循环前加载一次历史,并在循环结束后仅持久化最终的用户/助手交互。这是推荐的配置,因为大多数 ChatMemoryRepository 实现不支持工具调用消息类型。
将记忆 Advisor 放入循环内部
仅当需要将记忆 Advisor 放置在工具调用循环内部时(order 高于 ToolCallingAdvisor.DEFAULT_ORDER),才使用 .disableInternalConversationHistory() 方法。此时,记忆 Advisor 会在每次迭代时处理历史记录。请注意,只有 InMemoryChatMemoryRepository 支持持久化工具调用消息;其他存储库应使用上述默认的外部循环设置。
1 | // 禁用内部对话历史,将记忆管理交给循环内的记忆 Advisor |
手动控制工具调用循环
默认情况下,ToolCallingAdvisor 是自动注册的,并在内部管理整个工具调用循环 —— 调用者仅接收最终的 LLM 答案。
当你需要完全控制循环时(例如,向 UI 流式传输中间进度、添加自定义可观测性、或在迭代之间应用条件逻辑),你可以选择退出自动注册,并手动驱动循环。
按调用禁用自动注册:
使用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 在每次调用时禁用自动注册:
1 | // 手动驱动工具调用循环 |
在循环内部放置自定义 Advisor:
作为手动驱动循环的替代方案,你可以通过将自定义 Advisor 的 order 值设置为大于 ToolCallingAdvisor.DEFAULT_ORDER(例如 HIGHEST_PRECEDENCE + 400),将其放置在 ToolCallingAdvisor 循环内部。这样的 Advisor 会在工具调用循环的每次迭代中被调用,而不仅仅是在结束时调用一次,这意味着它可以访问所有中间消息:
- 在流式模式下 —— 它在
ToolCallingAdvisor从出站流中过滤掉工具调用请求块之前,接收每次迭代中模型的原始块流(包括工具调用请求块)。 - 在调用路径中 —— 每次后续请求中传递的对话历史包含上一次迭代的
ToolResponseMessage,因此该 Advisor 可以观察到工具调用请求及其响应。
这种模式允许你将中间块转发到辅助通道(SSE、WebSocket、日志),而不会中断工具调用循环:
1 | // 定义一个观察 Advisor,在每次工具调用迭代时执行 |
注册观察 Advisor 和自动注册的 ToolCallingAdvisor:
1 | ChatClient chatClient = ChatClient.builder(chatModel) |
由于 ToolCallObservingAdvisor 的 order 为 HIGHEST_PRECEDENCE + 400,它被插入到自动注册的 ToolCallingAdvisor(order 为 HIGHEST_PRECEDENCE + 300)之后,因此它会参与每次工具调用迭代。ToolCallingAdvisor 仍然会从返回给外部链的内容中过滤掉工具调用块,因此主调用者仅接收最终答案 —— 而观察 Advisor 负责辅助通道的发射。
这种方法使工具调用循环完全由框架管理,同时仍能让你完全可见地观察每个中间步骤,并且它与对话历史管理中描述的 conversationHistoryEnabled 和记忆 Advisor 模式自然配对。
Return Direct 功能
“return direct” 功能允许工具绕过 LLM,将其结果直接返回给客户端应用程序。这在以下情况下非常有用:
- 工具的输出就是最终答案,不需要 LLM 处理
- 希望通过避免额外的 LLM 调用来减少延迟
- 工具结果应按原样返回,无需解释
当工具执行设置了 returnDirect=true 时,ToolCallingAdvisor 将:
- 正常执行工具调用
- 在
ToolExecutionResult中检测到returnDirect标志 - 跳出工具调用循环
- 将工具执行结果作为
ChatResponse(以工具的输出作为生成内容)直接返回给客户端应用程序
2. StructuredOutputValidationAdvisor(结构化输出验证 Advisor)
StructuredOutputValidationAdvisor 根据 JSON Schema 验证结构化的 JSON 输出,并在验证失败时重试调用,最多重试指定次数。
核心特性
- 从期望的输出类型派生 JSON Schema,或接受预先提供的 Schema 字符串
- 根据 Schema 验证 LLM 的响应
- 如果验证失败则重试调用,最多可配置重试次数(默认:3 次)
- 在重试尝试时,将验证错误消息附加到 Prompt 中,以帮助 LLM 纠正其输出
- 使用
callAdvisorChain.copy(this)创建用于递归调用的子链 - 可选地支持自定义
JsonMapper进行 JSON 处理
该 Advisor 可以通过 outputType(自动派生 Schema)或 outputJsonSchema(预先提供的 Schema 字符串)进行配置;这两个选项是互斥的。
使用示例
使用 outputType:
1 | // 根据期望的输出类型自动派生 JSON Schema |
使用预先提供的 JSON Schema 字符串:
1 | // 提供预先定义好的 JSON Schema 字符串 |
在 entity() 调用上直接启用 Schema 验证:
作为配置 Advisor 的替代方案,你可以使用 EntityParamSpec 在 entity() 调用上直接启用 Schema 验证,而无需手动配置 Advisor:
1 | // 在 entity() 调用中直接启用验证 |
总结
递归 Advisor 是 Spring AI 中一个强大的扩展机制,它允许 Advisor 在链中多次循环执行。通过 CallAdvisorChain.copy() 方法,递归 Advisor 可以创建子链并重复调用,直到满足特定条件。ToolCallingAdvisor 和 StructuredOutputValidationAdvisor 是两个典型的内置实现,分别解决了工具调用循环和结构化输出验证重试的常见需求。