文章目录
前言
最近学习了下使用 Spring AI 进行和大模型的聊天对话,其中涉及到对话记忆,向量数据库,RAG 检索增强。
全部代码已经提交到代码仓库中,具体的可以去gitte中看看。
一、正文
1.1 项目结构
项目继承于 spring-ai-demo项目,父级主要控制依赖版本,子模块用于具体的功能实现。
1.2 项目环境
本次实践会使用到向量数据库 Qdrant,需要在 Qdrant下载地址 中下载,并安装启动。
另外,对外提供了聊天接口,向量数据库新增和清空文档接口,获取文档接口等。
java 版本选择21,springboot版本是3.4.2
1.3 完整代码
1.3.1 spring-ai-demo的pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.pine</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-ai-demo</name>
<url>http://maven.apache.org</url>
<modules>
<module>dynamic-datasource-mcp-sse-server</module>
<module>dynamic-datasource-mcp-stdio-server</module>
<module>spring-ai-chat-server</module>
</modules>
<properties>
<java.version>21</java.version>
<spring-boot.version>3.4.2</spring-boot.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-ai-alibaba.version>1.0.0.1</spring-ai-alibaba.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
1.3.2 spring-ai-chat-server 的pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.pine</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-chat-server</artifactId>
<packaging>jar</packaging>
<name>spring-ai-chat-server</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
<version>1.65.1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<!-- 阿里ai的starter -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<inherited>true</inherited>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<parameters>true</parameters>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>org.feng.MainApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
1.3.3 ChatConfig
package org.feng.config;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import io.qdrant.client.QdrantClient;
import lombok.SneakyThrows;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 聊天配置
*
* @author pine
* @version v1.0
* @since 2025-08-12 20:22
*/
@Configuration
public class ChatConfig {
/**
* 配置ChatClient,注册系统指令和工具函数
*/
@Bean
@SneakyThrows
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("""
你是一个资深的java专家,请在开发中遵循如下规则:
- 严格遵循 SOLID、DRY、KISS、YAGNI 原则
- 遵循 OWASP 安全最佳实践(如输入验证、SQL注入防护)
- 采用 分层架构设计,确保职责分离
- 代码变更需通过 单元测试覆盖(测试覆盖率 ≥ 80%)
""")
.build();
}
/**
* 内存聊天记忆
*/
@Bean
public MessageWindowChatMemory chatMemory() {
return MessageWindowChatMemory.builder().maxMessages(100).build();
}
/**
* 向量数据库 qdrant
*/
@Bean
public VectorStore vectorStore(@Autowired QdrantClient qdrantClient, @Autowired DashScopeEmbeddingModel dashScopeEmbeddingModel) {
return QdrantVectorStore.builder(qdrantClient, dashScopeEmbeddingModel).build();
}
}
1.3.4 WebfluxConfig
package org.feng.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
/**
* webflux配置
*
* @author pine
* @version v1.0
* @since 2025-08-12 20:27
*/
@Configuration
public class WebfluxConfig {
@Bean
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
1.3.5 ChatController
package org.feng.controller;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.document.Document;
import org.springframework.ai.rag.Query;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.postretrieval.document.DocumentPostProcessor;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.ai.rag.retrieval.join.ConcatenationDocumentJoiner;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 聊天控制器
*
* @author pine
* @version v1.0
* @since 2025-08-12 20:28
*/
@Controller
@Slf4j
@RequestMapping("/api")
public class ChatController {
@Resource
private ChatClient chatClient;
@Resource
private ChatMemory chatMemory;
@Resource
private VectorStore vectorStore;
private final ChatClient.Builder chatBuilder;
public ChatController(ChatClient.Builder chatBuilder) {
this.chatBuilder = chatBuilder;
}
/**
* 聊天
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@SneakyThrows
@ResponseBody
public Flux<String> chat(@RequestParam(name="conversationId", required = false, defaultValue = "feng123") String conversationId, @RequestParam(name="message", defaultValue = "你好,你有什么功能") String message) {
log.info("chat(), conversationId:{},message:{}", conversationId, message);
// 查询扩展
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
// 扩展数量:3
.numberOfQueries(3)
.chatClientBuilder(chatBuilder)
// 包括源查询条件
.includeOriginal(true)
.build();
// 增加上下文的信息
ContextualQueryAugmenter contextualQueryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build();
// RAG
RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
// 使用向量数据库作为文档源
.documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
// 扩充查询条件
.queryExpander(queryExpander)
// 拼接查询到的文档
.documentJoiner(new ConcatenationDocumentJoiner())
// 取多个doc中的第一个
.documentPostProcessors(new SelectedFirstDocumentPostProcessor())
// 对生成的查询增强上下文
.queryAugmenter(contextualQueryAugmenter)
.build();
return chatClient.prompt(message)
// 日志
.advisors(new SimpleLoggerAdvisor())
// 聊天记忆(暂时使用内存,后续支持jdbc)
.advisors(MessageChatMemoryAdvisor.builder(chatMemory).conversationId(conversationId).build())
// RAG
.advisors(retrievalAugmentationAdvisor)
.stream()
.content();
}
/**
* 聊天记录
*/
@GetMapping(value = "/messages")
@SneakyThrows
@ResponseBody
public List<Message> messages(@RequestParam(name="conversationId", defaultValue = "feng123") String conversationId) {
return chatMemory.get(conversationId);
}
/**
* 清空聊天记录
*/
@GetMapping(value = "/clearMessages")
@SneakyThrows
@ResponseBody
public String clearMessages(@RequestParam(name="conversationId", defaultValue = "feng123") String conversationId) {
chatMemory.clear(conversationId);
return "success";
}
/**
* 向量数据库增加数据
*/
@ResponseBody
@SneakyThrows
@PostMapping(value = "/addDocuments")
public String addDocuments(@RequestParam(name = "documents") List<String> documents) {
List<Document> collected = documents.stream().map(Document::new).collect(Collectors.toList());
vectorStore.add(collected);
return "success";
}
@SuppressWarnings("all")
private static class SelectedFirstDocumentPostProcessor implements DocumentPostProcessor {
@Override
public List<Document> process(Query query, List<Document> documents) {
if (documents.isEmpty()) {
return Collections.emptyList();
}
return Collections.singletonList(documents.get(0));
}
}
}
1.3.6 MainApplication
package org.feng;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.web.bind.annotation.CrossOrigin;
/**
* 启动类
*
* @author pine
* @version v1.0
* @since 2025-08-12 20:23
*/
@SpringBootApplication
@CrossOrigin(
origins = "*",
allowedHeaders = "*",
exposedHeaders = {"Cache-Control", "Connection"} // 暴露必要头
)
@ConfigurationPropertiesScan
public class MainApplication {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(MainApplication.class);
springApplication.setWebApplicationType(WebApplicationType.REACTIVE);
springApplication.run(args);
}
}
1.4 完整配置
1.4.1 application.yaml
server:
port: 8181
tomcat:
uri-encoding: UTF-8
keep-alive-timeout: 30000
max-connections: 100
servlet:
encoding:
charset: UTF-8
force: true
enabled: true
compression:
enabled: false # 禁用压缩(否则流式数据可能被缓冲)
spring:
main:
allow-bean-definition-overriding: true
web-application-type: reactive
banner-mode: console
application:
name: spring-ai-chat-demo
ai:
# 配置阿里的密钥,模型
dashscope:
api-key: sk#你自己的key
chat:
options:
model: qwen-plus
# 向量数据库
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: vector-store
use-tls: false
initialize-schema: true
# 日志增强
logging:
level:
org:
springframework:
ai:
chat:
client:
advisor: DEBUG
1.5 调用效果
使用get请求,进行聊天对话。
二、附录
2.1 参考文档
- https://docs.spring.io/spring-ai/reference/api/chatclient.html
- https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
- https://docs.spring.io/spring-ai/reference/api/vectordbs/qdrant.html
- https://springdoc.cn/spring-ai/index.html
2.2 建议
https://element-plus-x.com/zh/
前端页面可以采取 element-plus-x 来实现。