Spring Ai 1.0学习笔记

看的黑马的SpringAi教学视频,他用的是1.0.0-m6版本,很多东西都变了,但那又是个测试版,而正式版的1.0.0又有很多不同,所以自己跑官网来看看文档:聊天记忆 (Chat Memory) | Spring AI1.0.0中文文档|Spring官方文档|SpringBoot 教程|Spring中文网

我是以Deepseek模型为例子,推荐统一openai依赖:用了openai就需要加配个base_url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>

内存记忆存储

首先第一个变化就是记忆对话,先上代码:

配置类

记得去官网看看你的模型是否支持embedding嵌入:

config完整代码(包含了后面的记忆和rag配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

@Configuration
public class AiConfig {

@Bean
public ChatMemoryRepository chatMemoryRepository() {
return new InMemoryChatMemoryRepository();
}

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

@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository){
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(15)
.build();
}

@Bean
public ChatClient aiService(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}

@Bean
CommandLineRunner commandLineRunner(@Autowired VectorStore vectorStore, @Value("classpath:static/test.txt") Resource testTxt){
return args -> {
vectorStore.write(new TokenTextSplitter().transform(new TextReader(testTxt).read()));
};
}
}

其中classpath:static/test.txt为我的rag嵌入文本

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequiredArgsConstructor
public class ChatController {

private final ChatClient chatClient;

private final VectorStore vectorStore;

@RequestMapping(value = "/chat",produces = "text/html; charset=UTF-8")
public Flux<String> chat(String message,String chatId){
return chatClient.prompt()
.user(message)
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, chatId))
.stream()
.content();
}

}

这种是将对话数据存在内存的

区别下1.0.0-m6的写法:

反正我用了1.0.0-m6后使用advisor会报错:”Handler dispatch failed: java.lang.NoSuchMethodError: ‘reactor.core.publisher.Flux org.springframework.ai.chat.model.MessageAggregator.aggregateAdvisedResponse(reactor.core.publisher.Flux, java.util.function.Consumer)’”所以我就用1.0.0版本了

数据库记忆存储

首先引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>

支持的数据库:

  • PostgreSQL 数据库
  • MySQL / MariaDB
  • SQL 服务器
  • HSQLDB 数据库

修改刚刚的配置文件(把ChatMemoryRepository改成JdbcChatMemoryRepository就行):

1
2
3
4
5
6
7
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository){
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(15)
.build();
}

使用 JDBC URL 时,可以从 JDBC URL 中自动检测正确的方言JdbcChatMemoryRepositoryDialect.from(DataSource).您可以通过实现JdbcChatMemoryRepositoryDialect接口,即spring.datasource.url

使用数据库存储记忆后会自动进行初始化创建一张表,官网话说就是Schema 初始化会自动配置将自动创建SPRING_AI_CHAT_MEMORY表,使用特定于提供商的数据库 SQL 脚本。默认情况下,架构初始化仅针对嵌入式数据库(H2、HSQL、Derby 等)运行

您可以使用spring.ai.chat.memory.repository.jdbc.initialize-schema财产:

1
2
3
spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded # 仅适用于嵌入式数据库(默认)
spring.ai.chat.memory.repository.jdbc.initialize-schema=always # 总是初始化
spring.ai.chat.memory.repository.jdbc.initialize-schema=never # #不要初始化(对Flyway/ liquebase有用)

目前最佳实践是将initialize-schema设置为never,并设置platform为你的数据库语言像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
datasource:
url: jdbc:mysql://localhost:3306/h?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&allowMultiQueries=true&useSSL=true
username: root
password: 1234
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
chat:
memory:
repository:
jdbc:
platform: mysql
initialize-schema: never

然后再手动创建数据库表,参考mysql.sql:

1
2
3
4
5
6
7
8
9
10
11
-- schema-mysql.sql
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL,
content TEXT NOT NULL,
type VARCHAR(10) NOT NULL,
`timestamp` TIMESTAMP NOT NULL,
CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);

CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);

扩展方言

要添加对新数据库的支持,请实现JdbcChatMemoryRepositoryDialect接口,并提供用于选择、插入和删除消息的 SQL。然后,您可以将自定义 dialect 传递给存储库构建器。

1
2
3
4
ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder()
.jdbcTemplate(jdbcTemplate)
.dialect(new MyCustomDbDialect())
.build();

还有两种存储方式:CassandraChatMemoryRepository和Neo4jChatMemoryRepository可以去官网看看,这里就不介绍了,新手不怎么用;

检索增强生成(Rag)

首先先引入依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>1.0.0</version>
</dependency>

我这里用的智谱的模型它支持嵌入

1
2
3
4
@Bean
public VectorStore vectorStore(ZhiPuAiEmbeddingModel embeddingModel){
return SimpleVectorStore.builder(embeddingModel).build();
}
1
2
3
4
5
6
@Bean
CommandLineRunner commandLineRunner(@Autowired VectorStore vectorStore, @Value("classpath:static/test.txt") Resource testTxt){
return args -> {
vectorStore.write(new TokenTextSplitter().transform(new TextReader(testTxt).read()));
};
}

我这里把deepseek用作了对话模型,zhipu用于嵌入模型

屎山代码自用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@Override
public Flux<String> aiChat(MessageDto messageDto) {
ConversationUser conversationUser = conversationUserMapper.selectOne(new QueryWrapper<ConversationUser>().lambda()
.eq(ConversationUser::getConversationId, messageDto.getChatId()));
Long currentLoginId = UserUtils.getCurrentLoginId();
if (conversationUser == null){
conversationUserMapper.insert(new ConversationUser()
.setConversationId(messageDto.getChatId())
.setCreatedTime(LocalDateTime.now())
.setTitle(LocalDateTime.now().toString().substring(5,9)+"新对话")
.setUserId(currentLoginId));
}

if (messageDto.getModel() ==null || messageDto.getModel().isEmpty()){
messageDto.setModel("DEEPSEEK");
}


switch (messageDto.getModel()) {
case "GPT"->{
if (messageDto.getFile() == null){
return ChatClient.builder(openChatModel)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
// .advisors(QuestionAnswerAdvisor.builder(vectorStore)
// .searchRequest(SearchRequest.builder()
// .topK(5)//最多 5 个检索结果
// .similarityThreshold(0.6)//最小相似度匹配百分比
// .build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();
}
String fileContent = "";
MediaType mediaType = MediaType.valueOf(Objects.requireNonNull(messageDto.getFile().getContentType()));
String type = mediaType.getType();
boolean isImage = "image".equals(type);
String originalFilename = messageDto.getFile().getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
final String fileName = UUID.randomUUID() + "."+fileExtension;
String url = domin+aliUploadUtils.uploadFile(messageDto.getFile(), "chat", fileName, isImage);
Media media = new Media(mediaType, URI.create(url));
if (isImage){
fileContent ="<picture>"+url+"</picture>";
}else {
fileContent ="<file>"+url+"</file>";
}


OpenAiChatOptions zhiPuAiChatOptions = OpenAiChatOptions.builder()
.model("gpt-5")
.build();
UserMessage userMessage = UserMessage.builder().media(media).text(messageDto.getMessage()).build();
ChatResponse chatResponseFlux =openChatModel.call(new Prompt(List.of(userMessage), zhiPuAiChatOptions));
String text = chatResponseFlux.getResult().getOutput().getText();
String finalFileContent = fileContent;
springAiChatMemoryMapper.insert(new SpringAiChatMemory()
.setContent(messageDto.getMessage()+ finalFileContent)
.setType("USER")
.setConversationId(messageDto.getChatId())
.setTimestamp(LocalDateTime.now()));
springAiChatMemoryMapper.insert(new SpringAiChatMemory()
.setContent(text)
.setType("ASSISTANT")
.setConversationId(messageDto.getChatId())
.setTimestamp(LocalDateTime.now()));
return Flux.just(text);


}
case "DEEPSEEK" -> {
return ChatClient.builder(deepseekModel)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();


}
case "QWEN" ->{
return ChatClient.builder(qwenModel)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();
}
case "GEMINI" -> {

return ChatClient.builder(geminiModel)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();


}
case "GROK" ->{
return ChatClient.builder(gorkModel)
.defaultSystem("你叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();
}
case "GLM" ->{
if (messageDto.getFile() == null){
return ChatClient.builder(zhiPuAiChatModel)
.defaultSystem("你名字叫何平安,是一个高冷酷酷的IT高手")
.build()
.prompt()
.user(messageDto.getMessage())
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)//最多 5 个检索结果
.similarityThreshold(0.6)//最小相似度匹配百分比
.build()).build())
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
.stream()
.content();
}
String fileContent = "";
MediaType mediaType = MediaType.valueOf(Objects.requireNonNull(messageDto.getFile().getContentType()));
String type = mediaType.getType();
boolean isImage = "image".equals(type);
String originalFilename = messageDto.getFile().getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
final String fileName = UUID.randomUUID() + "."+fileExtension;
String url = domin+aliUploadUtils.uploadFile(messageDto.getFile(), "chat", fileName, isImage);
Media media = new Media(mediaType, URI.create(url));
if (isImage){
fileContent ="<picture>"+url+"</picture>";
}else {
fileContent ="<file>"+url+"</file>";
}

ZhiPuAiChatOptions zhiPuAiChatOptions = ZhiPuAiChatOptions.builder()
.model(AiModelConstant.GLM)
.build();
UserMessage userMessage = UserMessage.builder().media(media).text(messageDto.getMessage()).build();
Flux<ChatResponse> chatResponseFlux =zhiPuAiChatModel.stream(new Prompt(List.of(userMessage), zhiPuAiChatOptions));
Flux<String> map = chatResponseFlux.mapNotNull(chatResponse -> chatResponse.getResult().getOutput().getText());
String finalFileContent = fileContent;
map.doOnNext(chunk -> {}).reduce((s1, s2) -> s1 + s2).subscribe(
result -> {
// 流式结束后,
springAiChatMemoryMapper.insert(new SpringAiChatMemory()
.setContent(messageDto.getMessage()+ finalFileContent)
.setType("USER")
.setConversationId(messageDto.getChatId())
.setTimestamp(LocalDateTime.now()));
springAiChatMemoryMapper.insert(new SpringAiChatMemory()
.setContent(result)
.setType("ASSISTANT")
.setConversationId(messageDto.getChatId())
.setTimestamp(LocalDateTime.now()));
}
);
return map;

// return ChatClient.create(zhiPuAiChatModel).prompt()
// .user(u -> u.text(messageDto.getMessage())
// .media(new Media(mediaType, URI.create(url))))
//// .advisors(QuestionAnswerAdvisor.builder(vectorStore)
//// .searchRequest(SearchRequest.builder()
//// .topK(5)//最多 5 个检索结果
//// .similarityThreshold(0.6)//最小相似度匹配百分比
//// .build()).build())
// .advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
// .stream()
// .content();

// Consumer<ChatClient.PromptUserSpec> userMessageConsumer = promptUserSpec ->
// promptUserSpec.text(messageDto.getMessage())
// .media(media);
// Flux<String> content = ChatClient.create(zhiPuAiChatModel).prompt().user(userMessageConsumer)
// .advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, messageDto.getChatId()))
// .stream()
// .content();
// ChatClient.create(zhiPuAiChatModel).prompt()
// .user(promptUserSpec -> promptUserSpec.text(messageDto.getMessage()))
// .stream()

}
}
return Flux.empty();
}