一个被忽略的问题

Spring AI 2.0 GA 升级清单 一文中,我提到了 Advisor 是 Spring AI 2.0 的核心抽象。但有一件事当时没有展开——Advisor 的顺序。

用 Spring AI 的 ChatClient 写一个带对话记忆 + 工具调用的 AI 助手,你可能会这样写:

1
2
3
4
5
6
7
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
new ToolCallingAdvisor(toolCallbackProvider),
new QuestionAnswerAdvisor(vectorStore)
)
.build();

看起来没问题。但这个顺序——Memory → Tool → QA——真的是对的吗?

顺序不同,结果可能完全不同。

Advisor 链是怎么工作的

先理解 Spring AI 的 Advisor 机制。

Spring AI 的 Advisor 链本质上是一个 责任链模式(Chain of Responsibility)。每个 Advisor 在请求发往 LLM 之前和响应返回之后各有一次介入机会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户请求

[Advisor A] → before: 处理请求

[Advisor B] → before: 处理请求

[Advisor C] → before: 处理请求

LLM 调用

[Advisor C] → after: 处理响应

[Advisor B] → after: 处理响应

[Advisor A] → after: 处理响应

返回用户

before 阶段从外到内执行,after 阶段从内到外执行。 就像洋葱模型——一层一层进去,再一层一层出来。

这意味着:排在前面的 Advisor 先处理请求,排在后面的 Advisor 后处理请求。 排在最后面的 Advisor 最接近 LLM。

Memory 为什么要在 Tool 前面

场景:用户说”帮我再查一次上个月的数据”

如果 Memory 在前面:

1
2
3
4
5
6
1. Memory Advisor(before): 从历史中取出之前的对话上下文
→ "用户之前查询了 6 月份的销售数据"
2. Tool Advisor(before): 根据请求 + 历史上下文判断需要调用什么工具
→ 理解"再查一次"指的是 6 月份销售数据
→ 调用 salesQueryTool(month=202606)
3. LLM 调用

如果 Tool 在前面:

1
2
3
4
5
6
7
1. Tool Advisor(before): 只看当前请求"帮我再查一次上个月的数据"
→ "再查一次"是什么?查什么?
→ 没有上下文,可能错误理解或无法匹配工具
→ 可能调用错误的工具或返回"我不理解"
2. Memory Advisor(before): 从历史中取出上下文
→ 但工具已经被调用(或调用失败)了
3. LLM 调用

Memory 必须在 Tool 前面,因为工具的选择依赖对话历史。 没有记忆的工具调用就是”听不见上下文的执行者”——它能调工具,但不知道该调哪个。

场景:用户说”换个方式再回答一遍”

如果 Memory 在前面:

1
2
3
1. Memory Advisor: 取出历史 → "之前回答了 RAG 实现方案"
2. Tool Advisor: "换个方式"不需要调工具,是纯对话
3. LLM 调用 → 基于历史重新组织回答

如果 Tool 在前面:

1
2
3
4
1. Tool Advisor: "换个方式再回答一遍" → 可能触发搜索工具
→ 完全不需要的搜索
2. Memory Advisor: 取出历史
3. LLM 调用

Memory 提供上下文,Tool 做决策。 顺序反了,Tool 做的决策就是错的。

QuestionAnswerAdvisor(RAG)应该在哪里

QuestionAnswerAdvisor 的作用是在请求 LLM 之前,先去向量库检索相关文档,把检索结果拼到 prompt 里。

它的位置取决于你的设计意图:

选项 1:Memory → RAG → Tool(推荐)

1
2
3
4
1. Memory: 取对话历史
2. RAG: 检索相关文档
3. Tool: 决定是否需要调工具
4. LLM

好处:RAG 检索时可以结合对话历史(用户之前说了什么 → 更准确地理解当前问题)。Tool 决策时有记忆 + 检索结果双重上下文。

选项 2:Memory → Tool → RAG(当工具调用频繁时)

1
2
3
1. Memory: 取对话历史
2. Tool: 决定是否调工具 → 如果调了,可能不需要 RAG
3. RAG: 只在工具不适用时检索

好处:如果大部分请求不需要文档检索(纯工具调用场景),RAG 可以跳过,省 token。

但 Spring AI 2.0 的 Advisor 默认不提供”条件跳过”能力。 每个 Advisor 都会执行。如果你想”工具命中就跳过 RAG”,需要自定义 Advisor 实现。

一次真实的踩坑

实际开发中遇到过一个 bug:

用户对话流程中,Agent 突然”失忆”了——每轮对话都从零开始,不记得之前说过什么。

排查发现:ToolCallingAdvisor 排在了 MessageChatMemoryAdvisor 前面。

1
2
3
4
5
// 错误顺序
.defaultAdvisors(
new ToolCallingAdvisor(toolCallbackProvider), // 先执行
new MessageChatMemoryAdvisor(chatMemory) // 后执行
)

后果:

  1. Tool Advisor 先拿到用户请求,没有历史上下文
  2. 工具调用参数全凭当前这一句话,经常匹配错误
  3. Memory Advisor 后执行,把历史拼到 prompt 里
  4. LLM 收到的是”历史上下文 + 错误的工具调用结果”,产生幻觉

修复方法:交换顺序。

1
2
3
4
5
// 正确顺序
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 先执行:注入历史
new ToolCallingAdvisor(toolCallbackProvider) // 后执行:有上下文的工具调用
)

Advisor 顺序的通用原则

不是所有 Advisor 的顺序都像 Memory → Tool 这么明确。但有一个通用原则:

“信息提供者”在前面,”决策执行者”在后面。

Advisor 类型 角色 典型位置
MessageChatMemoryAdvisor 信息提供者(注入历史) 靠前
QuestionAnswerAdvisor (RAG) 信息提供者(注入文档) 中间
ToolCallingAdvisor 决策执行者(调工具) 靠后
SafeGuardAdvisor 过滤器(拦截不安全内容) 最前
SimpleLoggerAdvisor 观察者(记录请求/响应) 最前 + 最后
1
2
3
4
5
6
7
8
9
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new SimpleLoggerAdvisor(), // 最前:记录原始请求
new SafeGuardAdvisor(), // 安全检查
new MessageChatMemoryAdvisor(chatMemory), // 注入历史
new QuestionAnswerAdvisor(vectorStore), // 注入检索文档
new ToolCallingAdvisor(toolCallbackProvider) // 最后:有上下文的工具调用
)
.build();

一个容易混淆的点:Advisor 的 after 阶段

before 阶段的顺序容易理解——谁先谁后影响请求处理。

after 阶段的顺序是 反过来的:最后执行的 Advisor 在 after 阶段最先执行。

1
2
before: A → B → C → LLM
after: C → B → A

这意味着:

  • ToolCallingAdvisor 在 after 阶段最先执行 → 它可以先检查 LLM 的响应是否包含工具调用请求
  • MessageChatMemoryAdvisor 在 after 阶段较后执行 → 它在 Memory 中记录的是”最终处理后的响应”

如果你的 Logger 在最前面,它的 after 阶段最后执行 → 记录的是”经过所有 Advisor 处理后的最终响应”。这通常是你要的。

但如果你想记录”LLM 的原始响应”(未经任何 Advisor 处理),你需要把 Logger 放在链的最末尾——这样它的 before 最后执行(最接近 LLM),after 最先执行(拿到的是 LLM 原始响应)。

这意味着什么

  1. Advisor 顺序不是”怎么排都行”。 Memory 在 Tool 后面 = Agent 失忆 + 工具误调。这不是理论问题,是实际会遇到的 bug。

  2. “信息提供者在前,决策执行者在后”是通用原则。 但要理解为什么——不是背口诀,而是理解”洋葱模型”的 before/after 执行顺序。

  3. Spring AI 2.0 的 Advisor 链是声明式的,没有”条件跳过”能力。 如果你需要”工具命中就跳过 RAG”,需要自定义 Advisor 逻辑。这可能是 Spring AI 后续版本会补的能力。

  4. 调试 Advisor 顺序问题最简单的方法:打开 DEBUG 日志,看每个 Advisor 的 before/after 阶段实际收到的 prompt。你会立刻发现 Memory 有没有被注入、Tool 有没有拿到上下文。

参考链接