前言

本文章对Spring AI的Advisor类进行源码讲解,截至目前,官方文档已更新至1.0.0-M6版本,官方的release版本也有望于今年发布。本文章基于M6版本的源码进行本人学习时的笔记记录。

认识和了解Spring AI的Advisors机制非常重要且是首要的,他是Spring AI在Prompt工程里非常关键的增强实现技术
Advisors和AOP的思想类似,其功能是面向切面(提示词)对应用程序(请求/响应体)进行增强;在请求的流程中,Advisor以链式结构维持执行顺序,各个Advisor都有可能对该次请求响应进行处理。

 介绍

        本章将包含以下类的讲解,以及官方提供的类实现继承树图如下[1.0.0-M6] (右下角被挡住的类名为VectorStoreChatMemoryAdvisor)

Advisor工作流程

        官方提供的Advisors工作流程图如下

 

可以看出,Advisor机制的核心参与者是AroundAdvisor,每个Advisor会在请求流抵达时,对请求进行处理并转发给链中的下一个Advisor,在抵达了原子性组件即Chat Model(就是封装了LLM通信响应能力的类)并得到响应后,将会沿着链路原路返回(通过递归调用实现),最终返回一个AdvisedResponse(增强后的响应)交给客户端。 

Advisor实现类

        接着我们来看RequestResponseAdvisor 【自1.0.0-M6版本起正式弃用】 Advisor及其所有实现类

Advisor

在M5版本之后,RequestResponseAdvisor正式弃用(废除),而是抽成了更为泛用的Advisor,在此之上又有不同的实现。

Advisor实现自Ordered接口,便于自定义编排Advisor的执行优先级。

在Advisor类中,设置了一个默认的优先级值(默认聊天记忆类Advisor优先级)为最小整数值(默认最后执行);

从Advisor又实现了两个接口,分别为CallAroundAdvisor和StreamAroundAdvisor,其在AroundAdvisor链上返回的响应类型为AdvisedResponse和Flux<AdvisedResponse>

 CallAroundAdvisor & StreamAroundAdvisor

         Advisor在AdvisorChain上返回类型的约束接口,其只包含aroundCall/aroundStream方法及返回类型的声明(AdvisedResponse和Flux<AdvisedResponse>)。

 BaseAdvisor

         官方定义了一个扩展接口也就是BaseAdvisor,提供了aroundCall和aroundStream的默认实现,也就是Advisor的执行顺序实现(包括aroundStream的线程调度Scheduler),使你只需要关注before和after的处理逻辑的实现即可。

AbstractChatMemoryAdvisor<T> 

        嵌入对话历史记录的Advisor,此类AroundAdvisor一般在before环节对提示词进行增强。此处的泛型T是成员变量chatMemoryStore的类型,看完后文可以发现其支持VectorStore和chatMemory接口,或者你自己实现的话,可以添加其他类作为历史记录的仓储服务。

成员变量

  1. CHAT_MEMORY_CONVERSATION_ID_KEY

    对话记录的id,用于advisor与chatMemoryStore交互查询历史记录

  2. CHAT_MEMORY_RETRIEVE_SIZE_KEY

    历史记录的最大条数,限制此整型数值可以一定程度上限制上下文的token数

  3. chatMemoryStore

    Advisor与历史记录的交互组件,advisor通过与该组件交互获得历史记录列表

  4. DEFAULT_CHAT_MEMORY_RESPONSE_SIZE

    默认的对话历史检索大小

  5. protectFromMemoryBlocking [默认值为true]判断是否启用线程模型保护的布尔变量,在使用Reactor编程模型时,当该Advisor中涉及阻塞IO时(如JDBC),会将后续操作切换到弹性调度器线程池执行,防止阻塞操作影响响应式流的非阻塞线程

    protected Flux<AdvisedResponse> doNextWithProtectFromBlockingBefore(...) {
    return this.protectFromBlocking 
        ? Mono.just(advisedRequest)
            .publishOn(Schedulers.boundedElastic()) // 切换线程池
            .map(beforeAdvise)
            .flatMapMany(request -> chain.nextAroundStream(request)) 
        : chain.nextAroundStream(beforeAdvise.apply(advisedRequest)); // 直接执行
    }
    

方法

  1. getChatMemoryStore : 对继承类提供的chatMemory的getter

  2. doGetConversationId :从上下文中获取会话ID

  3. doGetChatMemoryRetrieveSize : 从上下文中获取检索到的历史记录的大小

 继承类

        聊天历史的Advisor中,Spring AI官方提供了以下三种实现(截至1.0.0M6),区别在于chatMemoryStore的类型不同,以及Advisor与chatMemoryStore的交互方式不同

VectorStoreChatMemoryAdvisor

成员变量

        VectorStore:用于存储和检索对话记忆的向量数据库

        systemTextAdivse:包含{longtermmemory}占位符的系统提示模板

        messageType:标识处理信息的类型(用户、助手、工具)

方法

  1. aroundCall:接收一个AdvisedRequest以及CallAroundAdvisorChain,执行自身的before方法后,调用链上下一个advisor;

  2. ⭐ before:接收AdvisedRequest,将请求中的SystemText拿出来后,添加上历史记忆的提示词,然后从向量库内检索相关的记忆,并将记忆注入系统参数(前面的占位符);最后保存当前用户信息到向量库内;

  3. ⭐ observeAfter:链路返回到当前Advisor时的函数,也就是将Aide回复保存到向量库当中。

总地来看,和官方给出的流程图一样,AroundAdvisor在整个过程中共执行两次,分别是在用户请求之前[before],插入历史记录到Prompt中;第二次是在响应回到该Advisor时[after],将助手回复的内容保存到向量库中。

MessageChatMemoryAdvisor

了解过向量库的Advisor后,其他的ChatMemoryAdvisor逻辑基本一致。

方法

  • ⭐ before:该Advisor中,不是通过构建提示词的方式嵌入历史记录,而是利用AdvisedRequest的请求参数直接添加消息对象列表messages。
private AdvisedRequest before(AdvisedRequest request) {
  //从上下文获取会话ID
        String conversationId = this.doGetConversationId(request.adviseContext());
  //从上下文获取历史记录限制数量
        int chatMemoryRetrieveSize = this.doGetChatMemoryRetrieveSize(request.adviseContext());
  //从ChatMemory(可以理解为消息记录仓储)获取当前所有消息记录
        List<Message> memoryMessages = ((ChatMemory)this.getChatMemoryStore()).get(conversationId, chatMemoryRetrieveSize);
  //增强后的消息列表->增强
        List<Message> advisedMessages = new ArrayList(request.messages());
        advisedMessages.addAll(memoryMessages);
  //通过AdvisedRequest自带的属性messages附加消息对象的列表
        AdvisedRequest advisedRequest = AdvisedRequest.from(request).messages(advisedMessages).build();
  //将当前请求的用户消息记录到ChatMemory内,并通过ChatMemory存储。
        UserMessage userMessage = new UserMessage(request.userText(), request.media());
        ((ChatMemory)this.getChatMemoryStore()).add(this.doGetConversationId(request.adviseContext()), userMessage);
        return advisedRequest;
    }
  • ⭐ observeAfter:和向量库的记忆实现逻辑都一样,只是将生成的助手消息以对象的形式传给ChatMemory,交由ChatMemory去实现存储。但需要注意的是,不是所有的LLM模型都支持直接操作message对象的记忆、解析和读取。
PromptChatMemoryAdvisor

        顾名思义,是通过将ChatMemory内嵌到提示词(Prompt)中实现记忆的类,实际上就是和VectorStore的嵌入方式是一样的:将历史记录转换成字符串,作为SystemText嵌入请求中,这样的好处就是不论LLM模型是否支持Request附带messages参数,都能够支持嵌入聊天记录了,但是具体在token消耗方面有怎样的区别,还有待考究。

方法

  1. ⭐ before:也是通过chatMemoryStore的接口方法去获取会话id对应的聊天记录,然后序列化成字符串。

    String memory = memoryMessages != null ? (String)memoryMessages.stream().filter((m) -> {
            return m.getMessageType() == MessageType.USER || m.getMessageType() == MessageType.ASSISTANT;
        }).map((m) -> {
            String var10000 = String.valueOf(m.getMessageType());
            return var10000 + ":" + m.getText();
        }).collect(Collectors.joining(System.lineSeparator())) : "";
    

    注意,在Spring AI提供的官方实现中,prompt仅对USER和ASSISTANT消息类型进行处理(如上代码)。

  2. observeAfter:和MessageChatMemoryAdvisor的实现一模一样,不过多赘述

 SafeGuardAdvisor

官方提供的敏感词过滤的Advisor示例,它更像是一个拦截器,因为如果敏感词监测不通过,会直接不继续执行下一步的Advisor,而是直接返回Response

成员变量

  1. failureResponse:由于敏感监测不通过而拒绝回答时的失败响应

  2. sensitiveWords:一个String的List,表示敏感词

  3. DEFAULTFAILURERESPONSE:默认的拒绝回答提示词

  4. DEFAULT_ORDER:默认的执行顺序(0),在官方的实现类里是最高优先级别的存在

方法

  1. ⭐ aroundCall:和对话历史的增强不同,此处如果敏感词监测不通过,会直接返回失败请求,并不继续执行Advisor链上的下一内容,也就不会传递给chatModel了(如果在其之前还有Advisor,还是会触发上一个Advisor的after方法的,因此如果需要做敏感词触发的日志记录,执行顺序要比该Advisor早);

该Advisor不需要before和after方法,仅仅只对request进行一个过滤的作用。此处实现的敏感词检测还是比较简易的,如果需要自己实现更复杂的检测逻辑,可以参照SafeGuardAdvisor的实现逻辑,自行实现。

 QuestionAnswerAdvisor

实现RAG的Advisor,根据问题在向量库中进行检索并对提示词进行增强

成员变量

  1. RETRIEVED_DOCUMENTS :检索到的文档内容

  2. FILTER_EXPRESSION : 过滤表达式参数的存储键

  3. vectorStore:向量存储的接口,用于执行相似性搜索

  4. userTextAdvise:用户的提示词模板

  5. searchRequest:相似性搜索的配置参数,包含返回结果数、相似度阈值等

方法

  1. before:会先从上下文中获取用户的消息,以及过滤的表达式(该存储键似乎并没有暴露出来,可能是AI模型提供的),然后根据传入的searchRequest配置以及过滤表达式,在向量数据库中执行搜索,并存储至两个地方:第一个是上下文,以文档类形式存储(包含文档的元数据,方便后续链路处理);另一个是解析成字符串后,拼接到用户的纯文本提示词内,以questionanswercontext为键存储在用户的对话参数里。
    private AdvisedRequest before(AdvisedRequest request) {
      //获取上下文
        HashMap<String, Object> context = new HashMap(request.adviseContext());
        String var10000 = request.userText();
        String advisedUserText = var10000 + System.lineSeparator() + this.userTextAdvise;
        String query = (new PromptTemplate(request.userText(), request.userParams())).render();
      //构建查询配置
        SearchRequest searchRequestToUse = SearchRequest.from(this.searchRequest).query(query).filterExpression(this.doGetFilterExpression(context)).build();
        List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);
      //嵌入到上下文的检索文档内
        context.put("qa_retrieved_documents", documents);
        String documentContext = (String)documents.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator()));
        Map<String, Object> advisedUserParams = new HashMap(request.userParams());
      //嵌入到用户的对话参数内
        advisedUserParams.put("question_answer_context", documentContext);
        AdvisedRequest advisedRequest = AdvisedRequest.from(request).userText(advisedUserText).userParams(advisedUserParams).adviseContext(context).build();
        return advisedRequest;
    }
  1. after:在该Advisor的after方法里,会将文档类嵌入到一个响应体里进行返回,作用也有可能是为了方便展示参考的文档来源。
private AdvisedResponse after(AdvisedResponse advisedResponse) {
    // 1. 基于原始响应创建构建器
    ChatResponse.Builder chatResponseBuilder = ChatResponse.builder()
        .from(advisedResponse.response()); // 继承原始响应内容

    // 2. 注入检索文档到元数据
    chatResponseBuilder.metadata(
        "qa_retrieved_documents", // 元数据键
        advisedResponse.adviseContext().get("qa_retrieved_documents") // 从上下文获取文档列表
    );

    // 3. 构建增强后的响应
    return new AdvisedResponse(
        chatResponseBuilder.build(), // 包含文档元数据的新响应
        advisedResponse.adviseContext() // 保留原始上下文
    );
}
SimpleLoggerAdvisor

简易的Logger实现类示例,分别在before和after里用logger添加log信息

需要注意的就是官方给的示例基于Slf4j日志门面实现,如果你有别的框架,可以自己实现logger增强类,直接实现BaseAdvisor接口并实现before和after方法即可

 能看到这说明你真的对Spring AI真的很感兴趣,那么接下来就是M6版本的最后一个官方提供的Advisor了(Ailbaba M5.1有基于灵积和百炼平台的其他实现,如果想要学习,可以参考)

 RetrievalAugmentationAdvisor

这是Spring AI官方给出的较为复杂的RAG实现的增强类,比起QuestionAnswerAdvisor,他的功能更加地复杂,而非只是简单地在向量数据库内进行检索

成员变量

  1. DOCUMENT_CONTEXT:在上下文中的存储键

  2. queryTransformers:查询转换器链式处理

  3. queryExpander:查询扩展(生成子查询)

  4. documentRetriever:核心的文档检索组件(必要的)

  5. documentJoiner:多查询结果合并策略

  6. queryAugmenter:原始查询增强(注入文档上下文)

  7. taskExecutor:并行任务执行器

  8. scheduler:任务调度测类

从成员变量上来看,就能看出这个Advisor的实现类非常地复杂且功能强大了,其流程大约为:

其各个环节主要的功能大概如下

  1. 原始查询构建:组合用户输入与历史消息

  2. 链式查询转换(如拼写校正、语义改写)

  3. 查询扩展生成多版本查询(如关键词变体)

  4. 并行异步检索CompletableFuture.supplyAsync

  5. 文档合并策略(默认拼接)

  6. 上下文注入与查询增强

在并行检索中,还支持并发控制机制:

  • 线程池配置

  • 核心线程4个,最大16个(应对突发流量)

  • 上下文传播装饰器(保证线程间上下文传递)

可以看出这里面又涉及到了很多Spring AI提供的RAG相关的组件类,可以说这就是一个很丰富的官方对于RAG的Advisor实现案例了,它将很方便地支持RAG的深度优化即查询重写+多路召回+结果融合等需求。这个类还是一个实验性类,等稳定下来后,我们再来对RAG做一个完整的学习。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐