首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入Spring AI:刨析 Advisors 机制

深入Spring AI:刨析 Advisors 机制

原创
作者头像
有一只柴犬
发布2025-11-24 18:58:22
发布2025-11-24 18:58:22
120
举报
文章被收录于专栏:人工智能人工智能

1、序言

在上一篇《深入Spring AI与OpenAI集成:实现智能对话系统》中,我们有一段实现上下文记忆的代码:

代码语言:bash
复制
public Flux<String> chatWithMemoryStream(String conversationId, String message) {
    
        ChatClient.StreamResponseSpec resp = ChatClient.builder(openAiChatModel)
                // 设置历史对话的保存方式,这里我们使用内存保存
                .defaultAdvisors(new PromptChatMemoryAdvisor(chatMemory))
                .build()
                .prompt().user(message)
                .advisors(advisor ->
                        // 设置保存的历史对话ID
                        advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
                                // 设置需要保存几轮的历史对话,用于避免内存溢出,因为这里我们没做持久化
                                .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)
                ).stream();
        return resp.content();
    }

代码中,我们关注defaultAdvisors(new PromptChatMemoryAdvisor(chatMemory))。通过 PromptChatMemoryAdvisor 动态管理上下文,这就是我们今天要来具体学习的Advisor(顾问)机制。

2、什么是Advisor?

Spring AI Advisor 是连接 AI 模型与业务逻辑的核心中间件,其设计理念与 Spring AOP(切面编程)深度契合。他提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动的交互。通过利用 Advisors API,开发人员可以创建更复杂、可重用和可维护的 AI 组件。例如,我们可能会建立聊天记录、排除敏感词或为每个请求添加额外的上下文。

其大致的处理流程如下所示:

简单解释下流程:

  1. 我们将提示词发送给AI大模型;
  2. 大模型附加的Advisor,会依次执行链中每个Advisor中的before方法;
  3. 执行完所有的before方法后,交给大模型处理;
  4. 大模型处理后的响应,会依次执行链中每个Advisor的after方法;
  5. 执行完所有的after方法后,我们获得了最终的大模型响应内容,执行结束。

3、源码分析Advisor

从官方提供的关于Advisor的类图中,我们可以看到核心接口Advisor有两个实现类CallAroundAdvisor和StreamAroundAdvisor。

  • CallAroundAdvisor 和 CallAroundAdvisorChain主要用于非流式处理场景;
  • StreamAroundAdvisor 和 StreamAroundAdvisorChain主要用于流式处理场景;
  • AdvisedRequest 提供了一种在请求发送之前修改它的方式,Advisor 可以拦截 AdvisedRequest 来对请求提示语进行增强,如添加上下文、重写查询、更改参数、丰富元数据等操作
  • AdvisedResponse 表示 AI 客户端响应的数据。 在 AI 服务返回结果之后,AdvisedResponse 会封装这个结果,并提供了一系列增强方式,可以在返回给应用程序之前进行如过滤、转换、附加信息等。
  • advisorContext实际上是个Map<String, Object>,AdvisedRequest 和AdvisedResponse都携带了advisorContext,也就是通知上下文。来允许Advisor访问某个阶段建立的共享状态。

3.1、Advisor接口

Advisor是advisor机制的顶层接口,他继承了Ordered,用于方便指定Advisor链的执行顺序:

代码语言:java
复制
public interface Advisor extends Ordered {

    /**
     * Useful constant for the default Chat Memory precedence order. Ensures this order
     * has lower priority (e.g. precedences) than the Spring AI internal advisors. It
     * leaves room (1000 slots) for the user to plug in their own advisors with higher
     * priority.
     */
    int DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = Ordered.HIGHEST_PRECEDENCE + 1000;

    /**
     * Return the name of the advisor.
     * @return the advisor name.
     */
    String getName();

}

该接口的主要作用就是用来声明当前的类是Advisor增强类,并可以声明自己的advisor名称。

由 Spring AI 框架创建的 Advisor 链允许按顺序调用多个 advisor,这些 advisor 按其 getOrder() 值排序。首先执行较低的值。自动添加的最后一个 advisor 将请求发送到 LLM。

3.2、Advisor Ordered

Ordered接口提供了getOrder()方法来决定链式Advisor的执行顺序。

代码语言:java
复制
public interface Ordered {
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    int getOrder();
}

执行顺序为,Order值越低的优先级越高。advisor 链以堆栈的形式运行:

  • 链中的第一个 advisor 是第一个处理请求的人。
  • 但它也是最后一个处理响应的服务器。

注意:如果多个 advisor 具有相同的订单价值,则不能保证他们的执行顺序。

3.3、CallAroundAdvisor & StreamAroundAdvisor

CallAroundAdvisor继承于Advisor接口,提供了一个环绕通知(aroundCall)方法,可以在目标方法执行 之前 和 之后 都会执行逻辑,完全控制目标方法的执行流程。

代码语言:java
复制
public interface CallAroundAdvisor extends Advisor {
    AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
}

StreamAroundAdvisor同样继承于Advisor接口,同样提供了一个环绕通知(aroundStream)方法,同样在目标方法执行前后执行逻辑,控制执行流程。

代码语言:java
复制
public interface StreamAroundAdvisor extends Advisor {
    Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);
}

StreamAroundAdvisor与CallAroundAdvisor不同的是。StreamAroundAdvisor是流式请求和流式响应,通过返回Flux<>来增强流中的数据操作。

流式和非流式响应流程:

3.4、BaseAdvisor

接下来一起重点关注BaseAdvisor接口,说这个是接口,实际上约等于抽象类。借助于interface的default方式,让接口的能力逐步趋近于抽象类,同时不被抽象类的单继承所限制。因此不要感觉诧异。

先来看下BaseAdvisor的结构体:

熟悉Spring AOP的应该立马可以看到两个关键方法:before和after。没错这个就是我们上面介绍advisor流程图中介绍的前置和后置方法。

BaseAdvisor同时继承CallAroundAdvisor, StreamAroundAdvisor接口,同时提供了流式和非流式的链式默认实现。在默认实现的环绕通知方法中,前置和后置方法调用before和after方法用于使用者自定义。因此我们在需要时只需要定义我们的before和after即可,是不是像极了AspectJ的实现。

代码语言:java
复制
public interface BaseAdvisor extends CallAroundAdvisor, StreamAroundAdvisor {

    Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();

    /**
     * 非流式场景处理的默认实现
     */
    @Override
    default AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
       Assert.notNull(advisedRequest, "advisedRequest cannot be null");
       Assert.notNull(chain, "chain cannot be null");

       // 1. 通过before方法,对advisedRequest进行处理,并返回处理后的AdvisedRequest
       AdvisedRequest processedAdvisedRequest = before(advisedRequest);

       // 2. 调用chain的nextAroundCall方法,传入处理后的AdvisedRequest,并返回处理后的AdvisedResponse
       AdvisedResponse advisedResponse = chain.nextAroundCall(processedAdvisedRequest);

       // 3. 通过after方法,对advisedResponse进行处理,并返回处理后的AdvisedResponse
       return after(advisedResponse);
    }

    /**
     * 流式场景处理的默认实现
     */
    @Override
    default Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
       Assert.notNull(advisedRequest, "advisedRequest cannot be null");
       Assert.notNull(chain, "chain cannot be null");
       Assert.notNull(getScheduler(), "scheduler cannot be null");

       // 1. 通过before方法,对advisedRequest进行处理,并返回处理后的AdvisedRequest
       Flux<AdvisedResponse> advisedResponses = Mono.just(advisedRequest)
          .publishOn(getScheduler())
          // 通过map,对流中的每个 request 数据块进行转换
          .map(this::before)
          // 将流中的每个元素转换成一个Publisher(如Flux或Mono)
          .flatMapMany(chain::nextAroundStream);

       // 2. 通过map,对流中的每个 response 数据块进行转换
       return advisedResponses.map(ar -> {
          if (onFinishReason().test(ar)) {
             // 3. 通过after方法,对advisedResponse进行处理,并返回处理后的AdvisedResponse
             ar = after(ar);
          }
          return ar;
       }).onErrorResume(error -> Flux.error(new IllegalStateException("Stream processing failed", error)));
    }

    ...

    @Override
    default String getName() {
       return this.getClass().getSimpleName();
    }

    /**
     * Logic to be executed before the rest of the advisor chain is called.
     */
    AdvisedRequest before(AdvisedRequest request);

    /**
     * Logic to be executed after the rest of the advisor chain is called.
     */
    AdvisedResponse after(AdvisedResponse advisedResponse);

    /**
     * Scheduler used for processing the advisor logic when streaming.
     */
    default Scheduler getScheduler() {
       return DEFAULT_SCHEDULER;
    }
}

看完这个之后,我们再来细品官方提供的Advisor和Chat Model交互的流程图:

  1. Spring AI 框架从用户的 Prompt 创建一个 AdvisedRequest 以及一个空的 AdvisorContext 对象。
  2. 链中的每个 advisor 都会处理请求,并可能对其进行修改。或者,它也可以选择通过不调用下一个实体来阻止请求。在后一种情况下,advisor负责填写回复。
  3. 框架提供的最终advisor程序将请求发送到 Chat Model。
  4. 然后,聊天模型的响应通过 advisor 链传回并转换为 AdvisedResponse。包括共享的 AdvisorContext 实例。
  5. 每个advisor都可以处理或修改响应。
  6. 通过提取 ChatCompletion 将最终的 AdvisedResponse 返回给客户端。

4、内置的Advisor类型

目前Spring AI已提供了多种开箱即用的Advisor,应对一些常见的场景。

4.1、MessageChatMemoryAdvisor

管理多轮对话上下文,使用 MessageChatMemoryAdvisor,我们可以通过 messages 属性提供聊天客户端调用的聊天历史记录。我们可以将所有消息保存在 ChatMemory 实现中,并控制历史记录的大小。

注:并非所有 AI 模型都支持此方法。@RestController @RequestMapping("/api/advisor") public class MessageChatMemoryAdvisorController {@Autowired private OpenAiChatModel openAiChatModel; @GetMapping("/message_memory") public void message_memory() { // 定义消息历史记录保存advisor MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(new InMemoryChatMemory()); ChatClient chatClient = ChatClient.builder(openAiChatModel) .defaultAdvisors(memoryAdvisor) .build(); ChatClient.CallResponseSpec response = chatClient.prompt() .user("推荐适合新手的相机") .advisors(advisor -> advisor.param("conversation_id", "user_123") .param("retrieve_size", 5) // 加载最近 5 轮对话 ).call(); System.out.println("-----推荐适合新手的相机:" + response.content()); ChatClient.CallResponseSpec response2 = chatClient.prompt() .user("按照价格排序一下") .advisors(advisor -> advisor.param("conversation_id", "user_123") .param("retrieve_size", 5) // 加载最近 5 轮对话 ).call(); System.out.println("-----按照价格排序一下:" + response2.content()); }}显示效果:

4.2、PromptChatMemoryAdvisor

PromptChatMemoryAdvisor与MessageChatMemoryAdvisor都能实现类似功能。但不同的是PromptChatMemoryAdvisor会将对话历史封装到系统提示词(System Prompt)中,兼容不支持多轮上下文的模型。

从源码中可以看出,他创建了一个chatMemoryStore来存储上下文信息:

代码语言:java
复制
// 1. Advise system parameters.
List<Message> memoryMessages = this.getChatMemoryStore()
    .get(this.doGetConversationId(request.adviseContext()),
          this.doGetChatMemoryRetrieveSize(request.adviseContext()));

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

Map<String, Object> advisedSystemParams = new HashMap<>(request.systemParams());
advisedSystemParams.put("memory", memory);

我们将上面示例代码的MessageChatMemoryAdvisor替换成PromptChatMemoryAdvisor,同样能实现类似的效果。但是经过测试发现,PromptChatMemoryAdvisor响应速度更快。

4.3、VectorStoreChatMemoryAdvisor

通过使用 VectorStoreChatMemoryAdvisor,我们可以获得更强大的功能。

我们通过向量存储中的相似性匹配搜索消息的上下文。搜索相关文档时,我们会考虑对话 ID。在我们的示例中,我们将使用稍作改动的 SimpleVectorStore,但也可以替换为任何向量数据库。

代码语言:java
复制
@RestController
@RequestMapping("/api/advisor")
public class VectorStoreChatMemoryAdvisorController {

    @Autowired
    private OpenAiChatModel openAiChatModel;
    @Autowired
    private VectorStore vectorStore;

    @GetMapping("/vector_memory")
    public void vector_memory() {
        // 定义消息历史记录保存advisor
        VectorStoreChatMemoryAdvisor memoryAdvisor = VectorStoreChatMemoryAdvisor.builder(vectorStore).build();
        ChatClient chatClient = ChatClient.builder(openAiChatModel)
                .defaultAdvisors(memoryAdvisor)
                .build();

        ChatClient.CallResponseSpec response = chatClient.prompt()
                .user("上次推荐的相机型号是什么?")
                .advisors(advisor ->
                        advisor.param("conversation_id", "user_123")
                                .param("retrieve_size", 5) // 加载最近 5 轮对话
                ).call();

        System.out.println("-----上次推荐的相机型号是什么?" + response.content());

    }
}

// 定义简单的向量库
@Configuration
public class VectorConfig {

    @Bean
    public SimpleVectorStore vectorStore(EmbeddingModel embeddingModel) {
        return SimpleVectorStore.builder(embeddingModel).build();
    }
}

4.4、QuestionAnswerAdvisor

用于执行 RAG 检索,从知识库中提取相关文本并注入用户提问,提升回答准确性。使用该 Advisor,我们可以根据准备好的上下文准备一个请求信息的提示。上下文通过相似性搜索从向量存储中获取。

代码语言:java
复制
@RestController
@RequestMapping("/api/advisor")
public class QuestionAnswerAdvisorController {

    @Autowired
    private OpenAiChatModel openAiChatModel;
    @Autowired
    private VectorStore vectorStore;

    @GetMapping("/question_ans")
    public void question_ans() {
        Document document = new Document("你的年龄在10-20岁左右");
        List<Document> documents = new TokenTextSplitter().apply(List.of(document));
        vectorStore.add(documents);

        QuestionAnswerAdvisor advisor = new QuestionAnswerAdvisor(vectorStore);
        ChatClient chatClient = ChatClient.builder(openAiChatModel)
                .defaultAdvisors(advisor)
                .build();

        ChatClient.CallResponseSpec response = chatClient.prompt()
                .user("你几岁了").call();

        System.out.println("-----你几岁了:" + response.content());
    }
}

运行效果:

从源码看实现原理其实也很简单,在前置before中从上下文中获取用户的消息和过滤表达式,然后根据传入的searchRequest配置,在向量库中执行搜索,从而达到更精确的响应。

代码语言:java
复制
// 2. Search for similar documents in the vector store.
String query = new PromptTemplate(request.userText(), request.userParams()).render();
var searchRequestToUse = SearchRequest.from(this.searchRequest)
    .query(query)
    .filterExpression(doGetFilterExpression(context))
    .build();

List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);

// 3. Create the context from the documents.
context.put(RETRIEVED_DOCUMENTS, documents);

String documentContext = documents.stream()
    .map(Document::getText)
    .collect(Collectors.joining(System.lineSeparator()));

4.5、SafeGuardAdvisor

该Advisor基于关键词或正则表达式过滤敏感内容,拦截非法请求。从源码中也很容易看出他的实现逻辑,其实就是字符串匹配,很好奇这里没有做成相似度的匹配,而是精确匹配。

代码语言:java
复制
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

    // 这里会对指定的词进行过滤
    if (!CollectionUtils.isEmpty(this.sensitiveWords)
          && this.sensitiveWords.stream().anyMatch(w -> advisedRequest.userText().contains(w))) {

       return createFailureResponse(advisedRequest);
    }

    return chain.nextAroundCall(advisedRequest);
}

使用上也很简单:

代码语言:java
复制
@RestController
@RequestMapping("/api/advisor")
public class SafeGuardAdvisorController {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    @GetMapping("/safe_guard")
    public void safe_guard() {
        SafeGuardAdvisor advisor = new SafeGuardAdvisor(List.of("JJ", "GG"));
        ChatClient chatClient = ChatClient.builder(openAiChatModel)
                .defaultAdvisors(advisor)
                .build();

        ChatClient.CallResponseSpec response = chatClient.prompt()
                .user("网络用词JJ是什么意思?").call();

        System.out.println("网络用词JJ是什么意思?" + response.content());

        ChatClient.CallResponseSpec response2 = chatClient.prompt()
                .user("网络用词NBA是什么意思?").call();

        System.out.println(response2.content());
    }
}

显示结果,可以发现第一问已经被屏蔽,询问有敏感词。 而第二问可以正常响应内容。

5、小结

总的来说Spring AI Advisor 提供了一种强大而优雅的方式来扩展和定制与 AI 模型的交互行为,使得开发者能够以声明式和非侵入式的方式添加各种增强功能,从而构建更健壮、灵活和可维护的 AI 应用。当然除了内置的这些Advisor,Spring的SPI机制绝对允许我们自定义Advisor,下篇内容我们来自定义Advisor。

代码我已经上传Github,地址:https://github.com/Shamee99/spring-ai-demo。需要的可以自取。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、序言
  • 2、什么是Advisor?
  • 3、源码分析Advisor
    • 3.1、Advisor接口
    • 3.2、Advisor Ordered
    • 3.3、CallAroundAdvisor & StreamAroundAdvisor
    • 3.4、BaseAdvisor
  • 4、内置的Advisor类型
    • 4.1、MessageChatMemoryAdvisor
    • 4.2、PromptChatMemoryAdvisor
    • 4.3、VectorStoreChatMemoryAdvisor
    • 4.4、QuestionAnswerAdvisor
    • 4.5、SafeGuardAdvisor
  • 5、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档