像理解 Servlet Filter 一样理解 Spring AI 的 Advisor 链
Spring AI 源码解读 · 进阶篇 | 一文搞懂 Advisor 拦截器的设计哲学、执行顺序与自定义实践
先问一个问题
你在用 Spring AI 写代码的时候,有没有想过这些问题:
- 给 AI 对话加上记忆(ChatMemory),历史消息是怎么悄悄塞进去的?
- 想在请求发出去前做日志记录、限流、敏感词过滤,该往哪插代码?
- 多个"拦截器"一起工作时,谁先谁后?顺序能自己控制吗?
如果你用过 Spring MVC 的 Filter 或 Spring Boot 的 Interceptor,那你其实已经懂了 80%——Spring AI 的 Advisor 本质上就是同一套思路。
但有个关键区别,很多人没搞明白,导致写出来的 Advisor 要么不生效,要么顺序错乱。
今天这篇文章,我把 Advisor 的设计、源码、内置实现、自定义方法,一次性讲透。
📖 本文是「Spring AI系列」源码篇第 5 篇。建议按顺序阅读效果更佳,当然直接看也完全没问题。 🔗 Spring AI系列 | 相关完整代码关注公众号【AI日撰】获取
一张图看懂 Advisor 在干什么
┌─────────────────────────────────────────────────────────────────┐
│ 你的代码调用 chatClient.prompt() │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Advisor 拦截器链(责任链模式) │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │RateLimit │ ← 限流:太频繁?直接拦掉 │ │
│ │ │Advisor │ 不调 next() = 请求终止 │ │
│ │ └──────┬──────┘ │ │
│ │ ↓ chain.nextCall() │ │
│ │ ┌─────────────┐ │ │
│ │ │ContentFilter│ ← 过滤:有敏感词?抛异常 │ │
│ │ │Advisor │ │ │
│ │ └──────┬──────┘ │ │
│ │ ↓ chain.nextCall() │ │
│ │ ┌─────────────┐ │ │
│ │ │ChatMemory │ ← 记忆:偷偷把历史对话塞进请求 │ │
│ │ │Advisor │ │ │
│ │ └──────┬──────┘ │ │
│ │ ↓ chain.nextCall() │ │
│ │ ┌─────────────┐ │ │
│ │ │Logger │ ← 日志:记一下发了什么 │ │
│ │ │Advisor │ │ │
│ │ └──────┬──────┘ │ │
│ │ ↓ chain.nextCall() │ │
│ │ ═════════════════ │ │
│ │ 发送到 LLM(大模型) │ │
│ │ ═════════════════ │ │
│ │ ↑ 响应沿原路返回 │ │
│ │ Logger → ChatMemory → ContentFilter → RateLimit │ │
│ │ (每个 Advisor 还有机会处理/修改响应) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 你拿到 ChatResponse │
└─────────────────────────────────────────────────────────────────┘
核心机制就一句话:每个 Advisor 都是一个「中间人」,请求进来它先处理,然后决定是否放行(调用 chain.next()),响应回来时它还能再处理一遍。
这和 Servlet Filter 的 doFilter(request, response, chain) 是同一个设计模式——责任链模式(Chain of Responsibility)。
二、Advisor 接口体系:从顶向下看
2.1 家族谱系
Spring AI 1.1.3 中,所有 Advisor 相关的接口都在这个包下:
org.springframework.ai.chat.client.advisor.api
继承关系一目了然:
Advisor (顶层标记接口)
│
├── CallAdvisor ← 非流式场景(最常用)
│ ├── MessageChatMemoryAdvisor (内置:记忆管理)
│ ├── SimpleLoggerAdvisor (内置:日志)
│ └── 你的 CustomAdvisor (自己写的)
│
└── StreamAdvisor ← 流式场景(SSE/WebSocket)
├── MessageChatMemoryAdvisor (同时实现了两个)
└── 你的 CustomStreamAdvisor
2.2 核心接口拆解
CallAdvisor — 非流式拦截器
public interface CallAdvisor extends Advisor {
// ★ 核心方法:处理请求,返回响应
ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain);
}
// 责任链本身
public interface CallAdvisorChain {
// ★ 调用链中的下一个 Advisor(最后一个会发送到 LLM)
ChatClientResponse nextCall(ChatClientRequest request);
}
三个关键点:
| 要点 | 说明 |
|---|---|
adviseCall | 同时接收 request 和 chain,你既能改请求,也能决定是否继续 |
chain.nextCall() | 调用它 = 放行给下一个 Advisor;不调用 = 终止请求 |
| 返回值 | 你甚至可以替换整个响应(比如缓存命中时直接返回) |
StreamAdvisor — 流式拦截器
public interface StreamAdvisor extends Advisor {
// 流式场景:处理的是 Flux<ChatResponse>
ChatClientResponse adviseStream(ChatClientRequest request, StreamAdvisorChain chain);
}
💡 什么时候用哪个?
- 用户发一条消息,等完整回复 →
CallAdvisor- 用户发消息,AI 一个字一个字往外蹦 →
StreamAdvisor
请求 & 响应对象
// 请求 —— Advisor 能读到和改的 everything
public interface ChatClientRequest {
Prompt prompt(); // Prompt 对象(包含所有消息)
String userText(); // 用户输入文本(快捷方法)
String systemText(); // 系统提示词
ChatOptions options(); // 温度/topP 等参数
Map<String, Object> context(); // ★ 共享上下文(Advisor 之间传数据用)
}
// 响应 —— Advisor 能读和改的结果
public interface ChatClientResponse {
ChatResponse chatResponse(); // 完整的 ChatResponse
ChatResponse getResult(); // 同上(别名方法)
}
⚡
context()是个隐藏神器:不同 Advisor 之间可以通过它共享数据。 比如RateLimitAdvisor可以把「剩余配额」塞进去,后面的LoggerAdvisor就能拿出来记到日志里。
三、执行顺序:一个容易踩的坑
3.1 Order 规则
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; // ≈ -21亿
int LOWEST_PRECEDENCE = Integer.MAX_VALUE; // ≈ +21亿
int getOrder();
}
规则:值 越小 → 优先级 越高 → 越 先执行
这跟直觉是反的,记住一句口诀:
Order 越小越靠前,就像排队报数,1 号在最前面。
3.2 执行顺序图解
假设有三个 Advisor:
new RateLimitAdvisor() // order = HIGHEST_PRECEDENCE (-21亿)
new ContentFilterAdvisor() // order = HIGHEST_PRECEDENCE + 50
new SimpleLoggerAdvisor() // order = LOWEST_PRECEDENCE - 100
请求阶段(从外到内):
RateLimitAdvisor.adviseCall()
↓ nextCall()
ContentFilterAdvisor.adviseCall()
↓ nextCall()
SimpleLoggerAdvisor.adviseCall()
↓ nextCall()
发送到 LLM ← 最后一个 Advisor 调用 next 时真正发出
响应阶段(从内到外)—— 注意是反序的!
LLM 返回结果
↑
SimpleLoggerAdvisor 处理响应(最先拿到)
↑
ContentFilterAdvisor 处理响应
↑
RateLimitAdvisor 处理响应(最后拿到)
🎯 洋葱模型(Onion Model / Russian Doll Model): 请求像剥洋葱一样一层层进入,响应再一层层裹回来。 第一个处理请求的 Advisor,最后一个处理响应。
四、内置 Advisor 深度解析
4.1 SimpleLoggerAdvisor — 最简单的入门示例
如果你想写自己的 Advisor,这是最好的参考模板:
public class SimpleLoggerAdvisor implements CallAdvisor {
private static final Logger log = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);
@Override
public String getName() {
return "SimpleLoggerAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100; // 较低优先级,接近 LLM
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
// 【前置处理】记录请求
log.info("=== AI 请求开始 ===");
log.info("用户消息: {}", request.prompt().getUserMessage().getText());
// ★ 放行:调用链中的下一个
ChatClientResponse response = chain.nextCall(request);
// 【后置处理】记录响应
log.info("=== AI 响应结束 ===");
log.info("AI回复: {}", response.chatResponse().getResult().getOutput().getText());
return response;
}
}
结构非常清晰,三段式:
adviseCall(request, chain) {
① 前置处理(可以读取/修改 request)
② chain.nextCall(request) ← 必须调用才能往下走
③ 后置处理(可以读取/修改 response)
return response;
}
4.2 MessageChatMemoryAdvisor — 让 AI 有记忆的核心
这个 Advisor 是 Spring AI 最重要的内置实现之一。它的职责很简单却很关键:
在请求发出去前,把历史对话塞进去;在响应回来后,把新对话存起来。
public class MessageChatMemoryAdvisor implements CallAdvisor {
private final ChatMemory chatMemory;
public MessageChatMemoryAdvisor(ChatMemory chatMemory) {
this.chatMemory = chatMemory;
}
@Override
public String getName() {
return "MessageChatMemoryAdvisor";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 100; // 高优先级,最先注入记忆
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
// ① 从 context 取会话 ID
String sessionId = (String) request.context()
.getOrDefault("sessionId", "default");
ConversationId conversationId = new ConversationId(sessionId);
// ② 取出历史消息(这就是"记忆")
List<Message> historyMessages = this.chatMemory.get(conversationId);
// ③ 将历史消息注入到当前请求中
// (实际源码中这里会修改 prompt 的 message 列表)
// ④ 放行,带着"记忆"去请求 LLM
ChatClientResponse response = chain.nextCall(request);
// ⑤ 把本次对话保存到记忆中(用户消息 + AI 回复都存)
String userText = request.prompt().getUserMessage().getText();
String assistantText = response.chatResponse()
.getResult().getOutput().getText();
this.chatMemory.add(conversationId, new UserMessage(userText));
this.chatMemory.add(conversationId, new AssistantMessage(assistantText));
return response;
}
}
为什么它的 Order 这么高(优先级高)?
因为其他 Advisor 可能需要基于「带记忆的完整上下文」来工作。如果记忆注入得太晚,前面的 Advisor 看到的就是一个没有历史的"失忆"请求。
五、动手写两个实用的自定义 Advisor
5.1 限流 Advisor — 保护你的钱包 💰
调用大模型 API 是要花钱的。如果你的接口被刷了……后果可想而知。
import com.google.common.util.concurrent.RateLimiter;
public class RateLimitAdvisor implements CallAdvisor {
private final RateLimiter rateLimiter;
/**
* @param permitsPerSecond 每秒允许的最大请求数
*/
public RateLimitAdvisor(double permitsPerSecond) {
this.rateLimiter = RateLimiter.create(permitsPerSecond);
}
@Override
public String getName() {
return "RateLimitAdvisor";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE; // 最高优先级,第一道关卡
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
// 尝试获取令牌(非阻塞)
if (!this.rateLimiter.tryAcquire()) {
throw new RuntimeException("⚠️ 请求过于频繁,请稍后再试");
}
// 拿到令牌,放行
return chain.nextCall(request);
}
}
使用方式:
// 全局生效:每秒最多 10 次请求
ChatClient client = builder
.defaultAdvisors(new RateLimitAdvisor(10.0))
.build();
// 单次请求生效
client.prompt()
.user("你好")
.advisors(new RateLimitAdvisor(5.0)) // 这个请求用更严格的限制
.call()
.content();
5.2 内容安全过滤 Advisor — 别让 AI 说不该说的
public class ContentFilterAdvisor implements CallAdvisor {
private final List<String> bannedWords;
public ContentFilterAdvisor(List<String> bannedWords) {
this.bannedWords = bannedWords;
}
@Override
public String getName() {
return "ContentFilterAdvisor";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 50; // 仅次于限流
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
String userText = request.prompt().getUserMessage().getText();
// 敏感词检查
for (String word : this.bannedWords) {
if (userText.contains(word)) {
throw new SecurityException("🚫 输入内容包含敏感词: " + word);
}
}
// 也可以对 AI 的响应做后置过滤(留给读者思考练习 😄)
return chain.nextCall(request);
}
}
🤔 思考题:上面的 ContentFilterAdvisor 只检查了用户输入。 如果你想同时过滤 AI 的回复(比如不让 AI 输出某些内容),应该怎么改? 提示:在
chain.nextCall()之后加逻辑即可。
六、组装使用:从配置到 Controller
6.1 全局注册(所有请求都生效)
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
// ① 限流(第一关)
.defaultAdvisors(new RateLimitAdvisor(10.0))
// ② 内容过滤(第二关)
.defaultAdvisors(new ContentFilterAdvisor(List.of("密码", "破解", "漏洞")))
// ③ 日志记录(最后一关)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
6.2 在 Controller 中灵活组合
@RestController
@RequestMapping("/ai")
public class AiController {
private final ChatClient chatClient;
public AiController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/** 最简单的对话 */
@GetMapping("/chat")
public Map<String, String> chat(@RequestParam String msg) {
String reply = chatClient.prompt().user(msg).call().content();
return Map.of("reply", reply);
}
/** 带安全过滤的对话 */
@GetMapping("/safe-chat")
public Map<String, Object> safeChat(@RequestParam String msg) {
try {
String reply = chatClient.prompt()
.user(msg)
.advisors(new ContentFilterAdvisor(List.of("攻击", "入侵")))
.call()
.content();
return Map.of("reply", reply, "status", "ok");
} catch (SecurityException e) {
return Map.of("error", e.getMessage(), "status", "blocked");
}
}
/** 组合多个 Advisor */
@GetMapping("/full-featured")
public Map<String, Object> fullFeatured(@RequestParam String msg) {
try {
String reply = chatClient.prompt()
.user(msg)
.advisors(
new RateLimitAdvisor(5.0),
new ContentFilterAdvisor(List.of("违规")),
new SimpleLoggerAdvisor()
)
.call()
.content();
return Map.of("reply", reply, "status", "ok");
} catch (Exception e) {
return Map.of("error", e.getMessage(), "status", "failed");
}
}
}
6.3 完整请求生命周期
chatClient.prompt().user("你好").advisors(...).call()
│
▼
┌─ RateLimitAdvisor ──→ tryAcquire()? ──No──→ 💥 异常: 请求频繁
│ │Yes
│ ▼
├─ ContentFilterAdvisor → 包含敏感词? ──Yes──→ 💥 异常: 内容违规
│ │No
│ ▼
├─ ChatMemoryAdvisor ───→ 注入历史对话
│ │
│ ▼
├─ LoggerAdvisor ───────→ 记录请求日志
│ │
│ ▼
╔══════════════════╗
║ 发送到 LLM ║ ← 这里才真正调用大模型 API
╚══════════════════╝
│
▼ (响应原路返回)
├─ LoggerAdvisor ───────→ 记录响应日志 ✅
├─ ChatMemoryAdvisor ───→ 保存本次对话到记忆 ✅
├─ ContentFilterAdvisor → 可选:过滤 AI 回复内容
└─ RateLimitAdvisor ────→ 无操作 ✅
│
▼
你拿到 ChatResponse 🎉
七、总结与对比
7.1 一句话回顾各组件
| 组件 | 一句话理解 | 典型用途 |
|---|---|---|
Advisor | 请求/响应的中间拦截器 | 万能扩展点 |
CallAdvisor | 非流式场景的拦截器 | 大多数情况用这个 |
CallAdvisorChain | 负责串联下一个 Advisor | 框架自动管理 |
ChatClientRequest | 请求封装体 | 读 userText/options/context |
ChatClientResponse | 响应封装体 | 读/改 chatResponse |
Ordered.getOrder() | 控制执行顺序 | 值越小越先执行 |
7.2 Advisor vs 你熟悉的其他"拦截器"
| 特性 | Servlet Filter | Spring Interceptor | Spring AI Advisor |
|---|---|---|---|
| 设计模式 | 责任链 | 责任链 | 责任链(一脉相承) |
| 触发时机 | HTTP 请求 | Handler 方法 | AI 对话请求 |
| 核心方法 | doFilter() | preHandle/postHandle | adviseCall() |
| 中断请求 | 不调用 chain.doFilter() | return false | 不调用 chain.next() |
| 执行顺序 | @Order 或 web.xml | Ordered 接口 | Ordered 接口 |
| 共享数据 | ServletRequest.setAttribute | ModelMap / request | context() Map |
看到没?原理一模一样。如果你之前写过 Filter 或 Interceptor,Advisor 对你来说就是换个 API 名字的事。
📚 系列导航
本篇文章属于「Spring AI 系列」源码篇,建议按顺序阅读:
| 篇 | 主题 | 关键字 |
|---|---|---|
| 入门:环境搭建与第一个 AI 对话 | Quick Start | |
| 一次对话的完整生命线 | prompt→call→response | |
| Message 体系:User/System/Assistant | 消息格式 | |
| RAG 基础:检索增强生成入门 | VectorStore | |
| ★ 5 | Advisor 拦截器链 | 责任链 / 自定义扩展 |
| → 6 | Tool Calling 工具调用(下一篇) | Function Callback |
| → 7 | ChatMemory 记忆管理深度剖析 | 滑动窗口 |
👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。
如果这篇 Advisor 解析对你有帮助,欢迎 点赞 + 收藏,评论区说说你在项目中给 AI 加了哪些"中间件"? 我们下期见!🚀
Spring AI 版本:1.1.3 | 代码已验证可运行 | 完整项目代码请查看公众号【AI日撰】