像理解 Servlet Filter 一样理解 Spring AI 的 Advisor 链

0 阅读9分钟

像理解 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 FilterSpring InterceptorSpring AI Advisor
设计模式责任链责任链责任链(一脉相承)
触发时机HTTP 请求Handler 方法AI 对话请求
核心方法doFilter()preHandle/postHandleadviseCall()
中断请求不调用 chain.doFilter()return false不调用 chain.next()
执行顺序@Order 或 web.xmlOrdered 接口Ordered 接口
共享数据ServletRequest.setAttributeModelMap / requestcontext() Map

看到没?原理一模一样。如果你之前写过 Filter 或 Interceptor,Advisor 对你来说就是换个 API 名字的事。


📚 系列导航

本篇文章属于「Spring AI 系列」源码篇,建议按顺序阅读:

主题关键字
1入门:环境搭建与第一个 AI 对话Quick Start
2一次对话的完整生命线prompt→call→response
3Message 体系:User/System/Assistant消息格式
4RAG 基础:检索增强生成入门VectorStore
★ 5Advisor 拦截器链责任链 / 自定义扩展
→ 6Tool Calling 工具调用(下一篇)Function Callback
→ 7ChatMemory 记忆管理深度剖析滑动窗口

👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。

如果这篇 Advisor 解析对你有帮助,欢迎 点赞 + 收藏,评论区说说你在项目中给 AI 加了哪些"中间件"? 我们下期见!🚀


Spring AI 版本:1.1.3 | 代码已验证可运行 | 完整项目代码请查看公众号【AI日撰】