SpringAI特性
一、SpringAI 顾问(Advisors)
Spring AI 使用 Advisors机制来增强 AI 的能力,可以理解为一系列可插拔的拦截器,在调用 AI 前和调用 AI 后可以执行一些额外的操作,比如:
- 前置增强:调用 AI 前改写一下 Prompt 提示词、检查一下提示词是否安全
- 后置增强:调用 AI 后记录一下日志、处理一下返回的结果
解释上图的执行流程:
- Spring AI 框架从用户的 Prompt 创建一个 AdvisedRequest,同时创建一个空的 AdvisorContext 对象,用于传递信息。
- 链中的每个 advisor 处理这个请求,可能会对其进行修改。或者,它也可以选择不调用下一个实体来阻止请求继续传递,这时该 advisor 负责填充响应内容。
- 由框架提供的最终 advisor 将请求发送给聊天模型 ChatModel。
- 聊天模型的响应随后通过 advisor 链传回,并被转换为 AdvisedResponse。后者包含了共享的 AdvisorContext 实例。
- 每个 advisor 都可以处理或修改这个响应。
- 最终的 AdvisedResponse 通过提取 ChatCompletion 返回给客户端。
自定义实现拦截器:
- CallAroundAdvisor:用于处理同步请求和响应(非流式)
- StreamAroundAdvisor:用于处理流式请求和响应
由于源码中实现类很多、所以我们可以自定义拦截器
package com.MrSun.mrsun_agent.advisor;import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.advisor.api.*;
import org.springframework.ai.chat.model.MessageAggregator;
import reactor.core.publisher.Flux;/*** 自定义日志 Advisor* 打印 info 级别日志、只输出单次用户提示词和 AI 回复的文本*/
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return 0;}/**** @param request* @return*/private AdvisedRequest before(AdvisedRequest request) {// 日志输出log.info("AI Request: {}", request.userText());return request;}private void observeAfter(AdvisedResponse advisedResponse) {log.info("AI Response: {}", advisedResponse.response().getResult().getOutput().getText());}public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {// 先执行before 处理请求,然后在进行处理响应advisedRequest = this.before(advisedRequest);AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);this.observeAfter(advisedResponse);return advisedResponse;}public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {advisedRequest = this.before(advisedRequest);Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);// 消息聚合器 等到一起在进行响应return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::observeAfter);}
}
1、Chat Memory Advisor
前面我们提到了,想要实现对话记忆功能,可以使用 Spring AI 的 ChatMemoryAdvisor,它主要有几种内置的实现方式:
- MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中
- PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
- VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话
1)MessageChatMemoryAdvisor 将对话历史作为一系列独立的消息添加到提示中,保留原始对话的完整结构,包括每条消息的角色标识(用户、助手、系统)。
[{"role": "user", "content": "你好"},{"role": "assistant", "content": "你好!有什么我能帮助你的吗?"},{"role": "user", "content": "讲个笑话"}
]
2)PromptChatMemoryAdvisor 将对话历史添加到提示词的系统文本部分,因此可能会失去原始的消息边界。
以下是之前的对话历史:
用户: 你好
助手: 你好!有什么我能帮助你的吗?
用户: 讲个笑话
现在请继续回答用户的问题。
一般情况下,更建议使用 MessageChatMemoryAdvisor。更符合大多数现代 LLM 的对话模型设计,能更好地保持上下文连贯性。
2、Chat Memory
上述 ChatMemoryAdvisor 都依赖 [Chat Memory]进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。其实就是基本的增删改查
Spring AI 内置了几种 Chat Memory,可以将对话保存到不同的数据源中,比如:
- InMemoryChatMemory:内存存储
- CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
- Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
- JdbcChatMemory:在 JDBC 中没有过期时间限制的持久化存储
实现对话记忆
1)首先初始化 ChatClient 对象。使用 Spring 的构造器注入方式来注入阿里大模型 dashscopeChatModel 对象,并使用该对象来初始化 ChatClient。初始化时指定默认的系统 Prompt 和基于内存的对话记忆 Advisor。代码如下:
@Component
@Slf4j
public class LoveApp {private final ChatClient chatClient;private static final String SYSTEM_PROMPT = "扮演深耕恋爱心理领域的专家。开场向用户表明身份,告知用户可倾诉恋爱难题。" +"围绕单身、恋爱、已婚三种状态提问:单身状态询问社交圈拓展及追求心仪对象的困扰;" +"恋爱状态询问沟通、习惯差异引发的矛盾;已婚状态询问家庭责任与亲属关系处理的问题。" +"引导用户详述事情经过、对方反应及自身想法,以便给出专属解决方案。";public LoveApp(ChatModel dashscopeChatModel) {// 初始化基于内存的对话记忆ChatMemory chatMemory = new InMemoryChatMemory();chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(SYSTEM_PROMPT).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)).build();}
}
2)编写对话方法。调用 chatClient 对象,传入用户 Prompt,并且给 advisor 指定对话 id 和对话记忆大小。代码如下:
public String doChat(String message, String chatId) {ChatResponse response = chatClient.prompt().user(message).advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)).call().chatResponse();String content = response.getResult().getOutput().getText();log.info("content: {}", content);return content;
}
3)编写单元测试,测试多轮对话:
@Testvoid testChat() {//生成一个绘画IDString chatId= UUID.randomUUID().toString();//第一轮String message="你好,我是南瓜";String answer= loveApp.doChat(message,chatId);//第二轮message="我想让另一半(松鼠)更喜欢我";answer= loveApp.doChat(message,chatId);Assertions.assertNotNull(answer);//第三轮message="我的另一半是谁了。我忘记了,你帮我回忆一下";answer= loveApp.doChat(message,chatId);Assertions.assertNotNull(answer);}
效果如下:
这样就可以实现多轮对话功能。
3、Re-Reading Advisor
实现一个 Re-Reading(重读)Advisor,又称 Re2。该技术通过让模型重新阅读问题来提高推理能力,有文献 来印证它的效果。
注意:
虽然该技术可提高大语言模型的推理能力,不过成本会加倍!
Re2 的实现原理很简单,改写用户 Prompt 为下列格式,也就是让 AI 重复阅读用户的输入:
/*** 自定义 Re2 Advisor* 可提高大型语言模型的推理能力*/
public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {private AdvisedRequest before(AdvisedRequest advisedRequest) {Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());advisedUserParams.put("re2_input_query", advisedRequest.userText());return AdvisedRequest.from(advisedRequest).userText("""{re2_input_query}Read the question again: {re2_input_query}""").userParams(advisedUserParams).build();}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {return chain.nextAroundCall(this.before(advisedRequest));}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {return chain.nextAroundStream(this.before(advisedRequest));}@Overridepublic int getOrder() {return 0;}@Overridepublic String getName() {return this.getClass().getSimpleName();}
}
以上就是SpringAI的一些特性。