Spring AI 2.0 Advisor 链顺序:为什么 Memory 要在 Tool 前面
一个被忽略的问题
在 Spring AI 2.0 GA 升级清单 一文中,我提到了 Advisor 是 Spring AI 2.0 的核心抽象。但有一件事当时没有展开——Advisor 的顺序。
用 Spring AI 的 ChatClient 写一个带对话记忆 + 工具调用的 AI 助手,你可能会这样写:
1 | ChatClient chatClient = ChatClient.builder(chatModel) |
看起来没问题。但这个顺序——Memory → Tool → QA——真的是对的吗?
顺序不同,结果可能完全不同。
Advisor 链是怎么工作的
先理解 Spring AI 的 Advisor 机制。
Spring AI 的 Advisor 链本质上是一个 责任链模式(Chain of Responsibility)。每个 Advisor 在请求发往 LLM 之前和响应返回之后各有一次介入机会:
1 | 用户请求 |
before 阶段从外到内执行,after 阶段从内到外执行。 就像洋葱模型——一层一层进去,再一层一层出来。
这意味着:排在前面的 Advisor 先处理请求,排在后面的 Advisor 后处理请求。 排在最后面的 Advisor 最接近 LLM。
Memory 为什么要在 Tool 前面
场景:用户说”帮我再查一次上个月的数据”
如果 Memory 在前面:
1 | 1. Memory Advisor(before): 从历史中取出之前的对话上下文 |
如果 Tool 在前面:
1 | 1. Tool Advisor(before): 只看当前请求"帮我再查一次上个月的数据" |
Memory 必须在 Tool 前面,因为工具的选择依赖对话历史。 没有记忆的工具调用就是”听不见上下文的执行者”——它能调工具,但不知道该调哪个。
场景:用户说”换个方式再回答一遍”
如果 Memory 在前面:
1 | 1. Memory Advisor: 取出历史 → "之前回答了 RAG 实现方案" |
如果 Tool 在前面:
1 | 1. Tool Advisor: "换个方式再回答一遍" → 可能触发搜索工具 |
Memory 提供上下文,Tool 做决策。 顺序反了,Tool 做的决策就是错的。
QuestionAnswerAdvisor(RAG)应该在哪里
QuestionAnswerAdvisor 的作用是在请求 LLM 之前,先去向量库检索相关文档,把检索结果拼到 prompt 里。
它的位置取决于你的设计意图:
选项 1:Memory → RAG → Tool(推荐)
1 | 1. Memory: 取对话历史 |
好处:RAG 检索时可以结合对话历史(用户之前说了什么 → 更准确地理解当前问题)。Tool 决策时有记忆 + 检索结果双重上下文。
选项 2:Memory → Tool → RAG(当工具调用频繁时)
1 | 1. Memory: 取对话历史 |
好处:如果大部分请求不需要文档检索(纯工具调用场景),RAG 可以跳过,省 token。
但 Spring AI 2.0 的 Advisor 默认不提供”条件跳过”能力。 每个 Advisor 都会执行。如果你想”工具命中就跳过 RAG”,需要自定义 Advisor 实现。
一次真实的踩坑
实际开发中遇到过一个 bug:
用户对话流程中,Agent 突然”失忆”了——每轮对话都从零开始,不记得之前说过什么。
排查发现:ToolCallingAdvisor 排在了 MessageChatMemoryAdvisor 前面。
1 | // 错误顺序 |
后果:
- Tool Advisor 先拿到用户请求,没有历史上下文
- 工具调用参数全凭当前这一句话,经常匹配错误
- Memory Advisor 后执行,把历史拼到 prompt 里
- LLM 收到的是”历史上下文 + 错误的工具调用结果”,产生幻觉
修复方法:交换顺序。
1 | // 正确顺序 |
Advisor 顺序的通用原则
不是所有 Advisor 的顺序都像 Memory → Tool 这么明确。但有一个通用原则:
“信息提供者”在前面,”决策执行者”在后面。
| Advisor 类型 | 角色 | 典型位置 |
|---|---|---|
| MessageChatMemoryAdvisor | 信息提供者(注入历史) | 靠前 |
| QuestionAnswerAdvisor (RAG) | 信息提供者(注入文档) | 中间 |
| ToolCallingAdvisor | 决策执行者(调工具) | 靠后 |
| SafeGuardAdvisor | 过滤器(拦截不安全内容) | 最前 |
| SimpleLoggerAdvisor | 观察者(记录请求/响应) | 最前 + 最后 |
1 | ChatClient chatClient = ChatClient.builder(chatModel) |
一个容易混淆的点:Advisor 的 after 阶段
before 阶段的顺序容易理解——谁先谁后影响请求处理。
after 阶段的顺序是 反过来的:最后执行的 Advisor 在 after 阶段最先执行。
1 | before: A → B → C → LLM |
这意味着:
- ToolCallingAdvisor 在 after 阶段最先执行 → 它可以先检查 LLM 的响应是否包含工具调用请求
- MessageChatMemoryAdvisor 在 after 阶段较后执行 → 它在 Memory 中记录的是”最终处理后的响应”
如果你的 Logger 在最前面,它的 after 阶段最后执行 → 记录的是”经过所有 Advisor 处理后的最终响应”。这通常是你要的。
但如果你想记录”LLM 的原始响应”(未经任何 Advisor 处理),你需要把 Logger 放在链的最末尾——这样它的 before 最后执行(最接近 LLM),after 最先执行(拿到的是 LLM 原始响应)。
这意味着什么
Advisor 顺序不是”怎么排都行”。 Memory 在 Tool 后面 = Agent 失忆 + 工具误调。这不是理论问题,是实际会遇到的 bug。
“信息提供者在前,决策执行者在后”是通用原则。 但要理解为什么——不是背口诀,而是理解”洋葱模型”的 before/after 执行顺序。
Spring AI 2.0 的 Advisor 链是声明式的,没有”条件跳过”能力。 如果你需要”工具命中就跳过 RAG”,需要自定义 Advisor 逻辑。这可能是 Spring AI 后续版本会补的能力。
调试 Advisor 顺序问题最简单的方法:打开 DEBUG 日志,看每个 Advisor 的 before/after 阶段实际收到的 prompt。你会立刻发现 Memory 有没有被注入、Tool 有没有拿到上下文。
