Spring AI 1.0.0 M1版本新特性介绍
- 前言
- 一、在1.0.0 M1版本中,主要有以下新特性:
- 1.ChatModel
- 2.ChatClient
- 3.多模态的支持
- 4.模型评估
- RequestResponseAdvisor接口
- MessageChatMemoryAdvisor
- PromptChatMemoryAdvisor
- QuestionAnswerAdvisor
- 动态过滤表达式
- VectorStoreChatMemoryAdvisor
- 总结
前言
Spring AI 1.0.0-M1这个版本相对前一个版本增加了很多新特性,比如多模态的支持、模型结果评估机制、Fluent API等等,但其中我觉得最有意思的是引入了Spring AOP中的Advisor机制。
众所周知,Advisor是Spring AOP中的概念,一个Advisor表示一个切面,由Advice和PointCut组成,Advice表示切面逻辑,也就是增强逻辑,PointCut表示切点,也就是切哪些方法。
一、在1.0.0 M1版本中,主要有以下新特性:
1.ChatModel
以前的ChatClient变为了ChatModel,ChatModel表示某个模型,具体看配置使用了哪个模型,比如实现类OpenAiChatModel表示OpenAi旗下的大模型,比如gpt-4o
2.ChatClient
但ChatClient并没有废弃,而是变成了封装在ChatModel之上的客户端,相当于ChatClient底层使用的是ChatModel,ChatClient支持fluent API,ChatModel则不支持,比如:
@GetMapping("/clientChat")
public String clientChat() {
return this.chatClient.prompt()
.user("你是谁")
.call()
.content();
}
不过,我们需要自己定义ChatClient的Bean:
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.build();
}
3.多模态的支持
通过Spring AI可以让大模型来理解图片内容
@GetMapping("/multimodalChat")
public String multimodalChat() {
ClassPathResource imageData = new ClassPathResource("/test.jpg");
var userMessage = new UserMessage("图片里有什么?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
return chatModel.call(userMessage);
}
4.模型评估
对于模型生成的结果,让可以让模型针对问题和答案评估是否相关:
@GetMapping("/evaluation")
public EvaluationResponse evaluation() {
String userText = "apikey是什么";
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.user(userText)
.call()
.chatResponse();
// 评估器
var relevancyEvaluator = new RelevancyEvaluator(ChatClient.builder(chatModel));
// 评估请求
EvaluationRequest evaluationRequest = new EvaluationRequest(userText,
(List<Content>) response.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS), response);
// 评估结果
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
return evaluationResponse;
}
评估结果为:
{
"pass": true,
"score": 1.0,
"feedback": "",
"metadata": {}
}
RequestResponseAdvisor接口
很明显,这个接口表示对请求和响应进行增强,传入一个请求或响应,返回增强后的请求或响应,有点类似于Spring MVC中的拦截器,联系到Spring AI的核心功能就是 请求大模型接口
以及 处理大模型响应结果 ,所以RequestResponseAdvisor接口的作用就是增强对于大模型的请求和响应,但具体增强逻辑是什么由具体的实现类来定义。
MessageChatMemoryAdvisor
广义的意思:
是一种Advisor,也是用来增强问答请求和响应的,而其中另外一个概念就是ChatMemory,默认实现为InMemoryChatMemory,它可以用来按conversationId进行历史对话记录的存储。因此MessageChatMemoryAdvisor的作用就是将原始请求和向量添加到ChatMemory中。
源码讲解:
第一个实现类:MessageChatMemoryAdvisor ,类名中的 ChatMemory 也是Spring AI中的一个机制,也是1.0.0版本新增的一个特性,来源于LangChain4j,作用是用来保存与大模型的,聊天上下文,或者说聊天记录,或者说会话记录,或者说问答记录,都是一个意思,ChatMemory
是一个接口,默认实现类为InMemoryChatMemory ,表示聊天记录都存储在内存中(InMemory),当然你可以自定义一个实现类把聊天记录存在数据库中,InMemoryChatMemory,的实现其实很简单:
理解为就是一个Map,key为会话ID,value为会话对应的历史消息列表,注意这里的消息既包含了我们发送给大模型的UserMessage(比如“你是谁?”),也包含了大模型返回给我们的AssistantMessage(比如“我是gpt-4o…”),都是Message。
回到 MessageChatMemoryAdvisor ,它增强的是某一次请求和对应的响应,它的第一个作用就是把当前请求所发送的UserMessage添加到ChatMemory中,重点是第二个作用,由于大模型本身是无状态的,也就是大模型本身并不会保存我们和大模型之间的聊天记录,所以,我们如果想让大模型知道我们之前聊了什么,我们就需要在请求大模型时将历史聊天记录跟着当前请求一起发送给大模型,这样大模型才能知道前面聊了什么,而具体实现方式就是发送一个List给大模型,这个List中既包含了历史消息,也包含了最新消息,这样大模型就能知道前面聊天的内容了。
所以,当Spring AI接收到一个请求时,这个请求一开始只会携带一条UserMessage,比如“请换一个风格”,之后 MessageChatMemoryAdvisor 会将该请求对应的UserMessage添加到ChatMemory中,也就是Map中当前会话ID对应的List中,然后生成一个新的请求,将List设置到新请求中,因此这个新的请求中就不止一条UserMessage了,而是一个List,然后Spring AI会将这个新的请求发送给大模型,此时大模型也就知道了历史聊天记录,就能知道“请换一个风格”到底是什么意思了。
当大模型处理完请求后,会向Spring AI返回一个响应,在Spring AI中就是一条AssistantMessage,那么MessageChatMemoryAdvisor 需要对响应做什么增强逻辑呢?很简单,只需要将AssistantMessage添加到ChatMemory即可。
PromptChatMemoryAdvisor
也是用来记录历史对话记录的,和MessageChatMemoryAdvisor的不同点在于,MessageChatMemoryAdvisor是把每个历史请求和响应封装为Message增强到请求中,而PromptChatMemoryAdvisor是把所有请求和响应也会存到ChatMemory中,但是会把所有内容合并一条Message增强到请求中。
PromptChatMemoryAdvisor ,其实它的作用和MessageChatMemoryAdvisor类似,也会利用ChatMemory来保存聊天记录,它和MessageChatMemoryAdvisor的区别在于,最终发送给大模型的请求中不是一个List了,而是会额外设置了一个系统提示词SystemMessage
,内容为:
Use the conversation memory from the MEMORY section to provide accurate answers
----------------------------
MEMORY:
{memory}
----------------------------
{memory}相当于一个变量,发送给大模型之前会进行占位符的填充,那填充的是啥呢?没错啦,就是历史聊天记录,源码中会把List用
System.lineSeparator()
进行连接,相当于把历史聊天记录转为一个字符串,然后放到系统提示词中随着请求一起发送给大模型,这样大模型也能知道之前聊天的内容,从而准确的理解当前问题,和MessageChatMemoryAdvisor一样,PromptChatMemoryAdvisor也会将大模型的响应AssistantMessage保存到ChatMemory中。
看到这,是否Get到了Advisor在Spring AI中的作用了呢,看到源码时还是觉得挺惊艳的,Spring还是那么的优雅,切面机制永远这么强大。
不过需要注意的是,以上MessageChatMemoryAdvisor和PromptChatMemoryAdvisor虽然都叫做Advisor,但是它们都不是通过动态代理来实现的,而是这么实现的:
针对原始的请求和响应,依次利用RequestResponseAdvisor进行增强,有没有感觉和BeanPostProcessor的实现机制很像呢。
QuestionAnswerAdvisor
广义: Advisor是Spring AOP中的概率,这里也应用在了Spring
AI中,QuestionAnswerAdvisor的作用是对问题请求进行增强,增强逻辑为:
- 根据原始问题进行相似度搜索,得到匹配知识点
- 拼接RAG提示词模板
后续Spring
AI会根据增强后的请求进行提示词模版的变量填充,得到请求最终的提示词,并将请求发送给大模型,得到大模型的返回结果,QuestionAnswerAdvisor也会对返回结果进行增强,会把匹配的知识点放入ChatResponse的metadata中。
向量数据库存储的是 AI 模型不知道的数据,当用户问题被发送到 AI 模型时,QuestionAnswerAdvisor 会在向量数据库中查询与用户问题相关的文档。
来自向量数据库的响应被附加到用户消息 Prompt 中,为 AI 模型生成响应提供上下文。
假设您已将数据加载到中 VectorStore,则可以通过向 ChatClient 提供 QuestionAnswerAdvisor 实例来执行检索增强生成 (RAG ) 。
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.user(userText)
.call()
.chatResponse();
在此示例中,SearchRequest.defaults() 将对 Vector 向量数据库中的所有文档执行相似性搜索。为了限制要搜索的文档类型,SearchRequest 采用了可移植到任意向量数据库中的类似 SQL 筛选表达式。
动态过滤表达式
SearchRequest 使用 FILTER_EXPRESSION Advisor 上下文参数在运行时更新过滤表达式:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.build();
// Update filter expression at runtime
String content = chatClient.prompt()
.user("Please answer my question qjc")
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Spring'"))
.call()
.content();
该 FILTER_EXPRESSION 参数允许您根据提供的表达式动态过滤搜索结果。
VectorStoreChatMemoryAdvisor
这个就更加强大了,它既会进行RAG,也会把存储历史对话,只不过会把对话记录封装为Document存到向量数据库中。
VectorStoreChatMemoryAdvisor :构造函数VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize)允许您指定要从中检索聊天历史记录的 VectorStore、唯一的对话 ID、要检索的聊天历史记录的大小(以令牌大小为单位)。
这个没有具体分析一下,我先提供一个案例:回头展开细说这个模块,因为我看到官方M2 M3也出来了,慢慢研究
@Service
public class SupportAssistant {
private final ChatClient chatClient;
public SupportAssistant(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("""
xxxxx
""")
.defaultAdvisors(
new PromptChatMemoryAdvisor(chatMemory),
// new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORY
new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()),
new LoggingAdvisor()) // RAG
.build();
}
public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient.prompt()
.user(userMessageContent)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.stream().content();
}
}
总结
这是个里程碑版本,后续M2 M3都已经出来了,目前文档看不到。