在上一篇《深入Spring AI与OpenAI集成:实现智能对话系统》中,我们有一段实现上下文记忆的代码:
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(顾问)机制。
Spring AI Advisor 是连接 AI 模型与业务逻辑的核心中间件,其设计理念与 Spring AOP(切面编程)深度契合。他提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动的交互。通过利用 Advisors API,开发人员可以创建更复杂、可重用和可维护的 AI 组件。例如,我们可能会建立聊天记录、排除敏感词或为每个请求添加额外的上下文。
其大致的处理流程如下所示:

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

Advisor是advisor机制的顶层接口,他继承了Ordered,用于方便指定Advisor链的执行顺序:
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。
Ordered接口提供了getOrder()方法来决定链式Advisor的执行顺序。
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
int getOrder();
}执行顺序为,Order值越低的优先级越高。advisor 链以堆栈的形式运行:
注意:如果多个 advisor 具有相同的订单价值,则不能保证他们的执行顺序。
CallAroundAdvisor继承于Advisor接口,提供了一个环绕通知(aroundCall)方法,可以在目标方法执行 之前 和 之后 都会执行逻辑,完全控制目标方法的执行流程。
public interface CallAroundAdvisor extends Advisor {
AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
}StreamAroundAdvisor同样继承于Advisor接口,同样提供了一个环绕通知(aroundStream)方法,同样在目标方法执行前后执行逻辑,控制执行流程。
public interface StreamAroundAdvisor extends Advisor {
Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);
}StreamAroundAdvisor与CallAroundAdvisor不同的是。StreamAroundAdvisor是流式请求和流式响应,通过返回Flux<>来增强流中的数据操作。
流式和非流式响应流程:

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

熟悉Spring AOP的应该立马可以看到两个关键方法:before和after。没错这个就是我们上面介绍advisor流程图中介绍的前置和后置方法。
BaseAdvisor同时继承CallAroundAdvisor, StreamAroundAdvisor接口,同时提供了流式和非流式的链式默认实现。在默认实现的环绕通知方法中,前置和后置方法调用before和after方法用于使用者自定义。因此我们在需要时只需要定义我们的before和after即可,是不是像极了AspectJ的实现。
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交互的流程图:

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

管理多轮对话上下文,使用 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()); }}显示效果:

PromptChatMemoryAdvisor与MessageChatMemoryAdvisor都能实现类似功能。但不同的是PromptChatMemoryAdvisor会将对话历史封装到系统提示词(System Prompt)中,兼容不支持多轮上下文的模型。
从源码中可以看出,他创建了一个chatMemoryStore来存储上下文信息:
// 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响应速度更快。

通过使用 VectorStoreChatMemoryAdvisor,我们可以获得更强大的功能。
我们通过向量存储中的相似性匹配搜索消息的上下文。搜索相关文档时,我们会考虑对话 ID。在我们的示例中,我们将使用稍作改动的 SimpleVectorStore,但也可以替换为任何向量数据库。
@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();
}
}用于执行 RAG 检索,从知识库中提取相关文本并注入用户提问,提升回答准确性。使用该 Advisor,我们可以根据准备好的上下文准备一个请求信息的提示。上下文通过相似性搜索从向量存储中获取。
@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配置,在向量库中执行搜索,从而达到更精确的响应。
// 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()));该Advisor基于关键词或正则表达式过滤敏感内容,拦截非法请求。从源码中也很容易看出他的实现逻辑,其实就是字符串匹配,很好奇这里没有做成相似度的匹配,而是精确匹配。
@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);
}使用上也很简单:
@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());
}
}显示结果,可以发现第一问已经被屏蔽,询问有敏感词。 而第二问可以正常响应内容。

总的来说Spring AI Advisor 提供了一种强大而优雅的方式来扩展和定制与 AI 模型的交互行为,使得开发者能够以声明式和非侵入式的方式添加各种增强功能,从而构建更健壮、灵活和可维护的 AI 应用。当然除了内置的这些Advisor,Spring的SPI机制绝对允许我们自定义Advisor,下篇内容我们来自定义Advisor。
代码我已经上传Github,地址:https://github.com/Shamee99/spring-ai-demo。需要的可以自取。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。