使用SpringAI进行RAG
本文属于我的AI应用学习笔记的一部分,更多内容请见:我的专栏
🤖检索增强生成 (英语:
Retrieval-augmented generation,RAG) 是赋予生成式人工智能模型信息检索能力的技术。检索增强生成优化大型语言模型(LLM) 的交互方式,让模型根据指定的一组文件回应用户的查询,并使用这些信息增强模型从自身庞大的静态训练数据中提取的信息。检索增强生成技术促使大型语言模型能够使用特定领域或更新后的信息。[1]应用案例,包括让聊天机器人访问公司内部资料,或来自权威来源的事实信息。(来自维基百科)
注意:使用SpringAI进行开发需要JDK17+,这里使用的是JDK21。
SpringAI是一个主流的Java大模型开发框架,可以简化我们的AI应用开发,本文将从文本向量嵌入与检索开始,逐步整合一个可用的RAG系统。
项目地址:ai-chat-demo
阅读本文前应该有的知识基础
-
Java基础
-
Java Web基础
-
数据库基础
-
包管理 (Maven、gradle等)
-
Spring基础、SSM整合、SpringBoot等
-
docker、容器化基础
-
Elasticsearch基础
一、准备向量数据库
你可以选择任何你想用的向量数据库,这里使用的是
Elasticseach 8.15.5和对应的Java Client,如果你已经有了向量储存方案,请跳过本章。
安装ElasticSearch
安装
ElasticSearch有多种方式,推荐使用docker,我这里为了方便起见,在windows上使用docker-compose+dockerhub部署Elasticsearch和可视化界面工具Kibana,大家也可以安装到自己的Linux虚拟机和服务器上。😎如果你已经安装了Elasticsearch,请跳过这一步。
1. 拉取镜像
- 打开你的dockerHub,搜索
elasticsearch和kibana的镜像


这里使用的版本是8.15.5,推荐使用8.6+的版本
验证安装,打开powershell或cmd,输入:
arduino
docker image ls -a
如果看到如下输出,说明镜像拉取成功:
REPOSITORY TAG IMAGE ID CREATED SIZE
elasticsearch 8.15.5 e983a5cfa418 6 months ago 1.27GB
kibana 8.15.5 e76560fb8141 6 months ago 1.14GB
当然,你也可以在docker-hub中直接查看。
2. 使用docker-compose运行容器
编写docker-compose.yml
yml
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.15.5
container_name: elasticsearch
environment:
- node.name=es-node
- cluster.name=es-cluster
- discovery.type=single-node
- bootstrap.memory_lock=true
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- ES_JAVA_OPTS=-Xms1g -Xmx1g
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./es-data:/usr/share/elasticsearch/data
ports:
- 9200:9200
- 9300:9300
kibana:
image: kibana:8.15.5
container_name: kibana
depends_on:
- elasticsearch
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- 5601:5601
volumes:
es-data:
其中的参数可以自行调整,包括设置内存选项和端口号。
注意:数据文件会储存在docker-compose.yml目录下的es-data中。
执行docker-compose.yml
执行命令:
bash
cd ${你的docker-compose.yml所在的绝对目录}
docker-compose up -d
如果输出类似于:
erlang
Creating network "docker_default" ...
Creating volume "docker_es-data" ...
Creating elasticsearch ...
Creating kibana ...
说明安装成功了
验证容器运行情况
执行命令:
docker ps
这个命令会列出所有运行中的容器,输出类似于:
bash
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
15ee1a203ec9 kibana:8.15.5 "/bin/tini -- /usr/l..." About an hour ago Up 22 seconds 0.0.0.0:5601->5601/tcp kibana
cc9990df199d elasticsearch:8.15.5 "/bin/tini -- /usr/l..." About an hour ago Up 23 seconds 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp elasticsearch
如果STATUS为Up,说明容器正在运行,也可以在docker-hub中查看容器运行情况。
二、整合Elasticsearch和SpringAI
准备工作
1.引入依赖
我们首先需要引入Maven依赖,完整依赖可以在文章开头的代码仓库中获取:
xml
<!--SpringAI-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!--SpringAI OpenAI组件-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!--SpringAI ES整合-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!--ES客户端-->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<!--这里选择和你的实际ES版本匹配的版本-->
<version>${es.version}</version>
</dependency>
<!--SpringAI 矢量数据库-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!--SpringAI RAG-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
<version>${spring-ai.version}</version>
</dependency>
依赖版本参考:
xml
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<druid.version>1.2.25</druid.version>
<mybatis.version>3.0.4</mybatis.version>
<mysql.version>8.4.0</mysql.version>
<es.version>8.15.5</es.version>
<hutool.version>5.8.38</hutool.version>
<swagger.version>2.2.31</swagger.version>
<springboot.version>3.5.0</springboot.version>
</properties>
2.调整配置项
application.yml配置文件:
yml
spring:
ai:
vectorstore:
elasticsearch:
initialize-schema: true
index-name: your_index_name
dimensions: 1536
similarity: cosine
elasticsearch:
uris: http://192.168.200.132:9200 # 修改为你的ES地址,如果是集群,用逗号分隔
username: elastic # 如果你的ES有安全认证就加上,没有的话和密码一起注掉
password: your_password # 对应的密码
创建ES客户端实例:
java
@Configuration
public class ElasticsearchConfig {
@Value("${spring.elasticsearch.uris}")
private String esUris;
@Bean
public ElasticsearchClient elasticsearchClient() throws URISyntaxException {
URI uri = new URI(esUris);
// 提取主机和端口
String host = uri.getHost();
int port = uri.getPort() == -1 ? 9200 : uri.getPort(); // 默认端口为9200
// TODO 如果在设置里打开了认证,需要配置认证信息
// 1. 创建 RestClient(无认证、无 SSL)
RestClient restClient = org.elasticsearch.client.RestClient.builder(
// 这里换成自己的ES服务器地址,如果是本地部署,直接localhost即可
new HttpHost(host, port)).build();
// 2. 使用 Jackson 映射器创建 Transport 层
RestClientTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// 3. 创建 Elasticsearch Java 客户端
return new ElasticsearchClient(transport);
}
@Bean
public RestClient restClient() {
return RestClient.builder(new HttpHost(
// 这里换成自己的ES服务器地址,如果是本地部署,直接localhost即可
"192.168.200.132",
9200,
"http"))
.build();
}
}
指定Embedding模型,从数据库中取:
java
/**
* 动态加载数据库中的文本嵌入模型配置
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class AiEmbeddingConfig {
private final AiConfigMapper aiConfigMapper;
private static final Integer id = 2;
/**
* 注册一个 EmbeddingModel Bean,用于 SpringAI VectorStore
*/
@Bean
@Lazy // 延迟加载,避免启动时数据库尚未准备好
public EmbeddingModel embeddingModel() throws IllegalStateException {
// 从数据库读取 id=2 的配置(文本嵌入模型)
AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
if (config == null) {
log.error("未找到ID={}的嵌入模型配置", id);
}
if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 0) {
log.error("ID={}的嵌入模型嵌入模型已禁用", id);
}
// 根据 apiDomain 判定使用哪家服务
String domain = null;
String apiKey = null;
String modelName = null;
if (config != null) {
domain = config.getApiDomain();
apiKey = config.getApiKey();
modelName = config.getModelId();
}
// 构造 EmbeddingOptions
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
.model(modelName)
.dimensions(1536)
//.user("模型对用户的称呼")
.build();
OpenAiApi openAiApi = OpenAiApi.builder()
// 填入自己的API KEY
.apiKey(apiKey)
// 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
// 注意:这里与langchain4j的配置不同,不需要在后面加/v1
.completionsPath("/chat/completions")
.embeddingsPath("/embeddings")
.baseUrl(domain)
.build();
return new OpenAiEmbeddingModel(
openAiApi,
MetadataMode.NONE,
options);
}
}
对应数据库实体类,如需要完整代码,请移步至我的代码仓库:
java
/**
* AI配置信息表
* TableName ai_config
*/
@Data
public class AiConfig implements Serializable {
/**
* 主键,自增ID
*/
private Integer id;
/**
* 显示名称
*/
private String displayName;
/**
* API域名
*/
private String apiDomain;
/**
* 模型名称
*/
private String modelName;
/**
* 模型类型:0-大模型,1-文本向量,2-视觉模型
*/
private Integer modelType;
/**
* 模型ID
*/
private String modelId;
/**
* API密钥
*/
private String apiKey;
/**
* 上下文最大消息数
*/
private Integer maxContextMsgs;
/**
* 相似度TopP
*/
private Double similarityTopP;
/**
* 随机度temperature
*/
private Double temperature;
/**
* 相似度TopK
*/
private Double similarityTopK;
/**
* 是否为默认模型(是0/否1)
*/
private Integer isDefault;
/**
* 标签
*/
private String caseTags;
/**
* 简介
*/
private String caseBrief;
/**
* 备注
*/
private String caseRemark;
/**
* 是否启用
* */
private Integer isEnabled;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
以上配置文件生效后,SpringAI会自动配置VectorStore,直接在应用中注入就能用于向量的嵌入和查询,这种方式很方便但是灵活性不足,如果想要增强灵活性,可以使用手动配置,在测试时给大家演示。
单元测试
1.编写向量嵌入与查找的单元测试
在下面的测试方法中,ragStoreWithSpringAI()负责将文档转为向量插入Elasticsearch,而ragRetrieveWithSpringAI()方法负责查询关键字,并按照相关度排序返回,这是RAG能够正常工作的基础。
java
@Slf4j
@SpringBootTest
public class AiApplicationTests {
@Autowired
private AiConfigMapper aiConfigMapper;
@Autowired
private RestClient restClient;
@Autowired
private ElasticsearchClient esClient;
@Autowired
private VectorStore vectorStore;
private static final Integer id = 2;
@Test
public void ragStoreWithSpringAI() throws IOException {
/// 向量嵌入阶段
List<Document> documents = List.of(
new Document("怪物猎人荒野在2025年10月31日的在线玩家峰值为15万"),
new Document("怪物猎人世界在2025年10月31日的在线玩家峰值为12万"),
new Document("怪物猎人崛起在2025年10月31日的在线玩家峰值为7万"));
// 从数据库读取 id=2 的配置(文本嵌入模型)
AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
if (config == null) {
log.error("未找到ID={}的嵌入模型配置", id);
}
if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 1) {
log.error("ID={}的嵌入模型嵌入模型已禁用", id);
}
// 根据 apiDomain 判定使用哪家服务
String domain = null;
String apiKey = null;
String modelName = null;
if (config != null) {
domain = config.getApiDomain();
apiKey = config.getApiKey();
modelName = config.getModelId();
}
// 构造 EmbeddingOptions
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
.model(modelName)
.dimensions(1536)
//.user("模型对用户的称呼")
.build();
OpenAiApi openAiApi = OpenAiApi.builder()
// 填入自己的API KEY
.apiKey(apiKey)
// 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
// 注意:这里与langchain4j的配置不同,不需要在后面加/v1
.baseUrl(domain)
.completionsPath("/chat/completions")
.embeddingsPath("/embeddings")
.build();
OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(
openAiApi,
MetadataMode.NONE,
options);
ElasticsearchIndexUtils.createIndex("test_vector", esClient);
ElasticsearchVectorStoreOptions esOptions = new ElasticsearchVectorStoreOptions();
esOptions.setDimensions(1536);
esOptions.setIndexName("test_vector");
esOptions.setSimilarity(SimilarityFunction.cosine);
// Add the documents to Elasticsearch
ElasticsearchVectorStore elasticsearchVectorStore = ElasticsearchVectorStore.builder(restClient, openAiEmbeddingModel)
.options(esOptions).build();
elasticsearchVectorStore.add(documents);
}
@Test
public void ragRetrieveWithSpringAI() throws IOException {
/// 文档查询阶段
// 从数据库读取 id=2 的配置(文本嵌入模型)
AiConfig config = aiConfigMapper.selectByPrimaryKey(id);
if (config == null) {
log.error("未找到ID={}的嵌入模型配置", id);
}
if (config != null && config.getIsEnabled() != null && config.getIsEnabled() == 1) {
log.error("ID={}的嵌入模型嵌入模型已禁用", id);
}
// 根据 apiDomain 判定使用哪家服务
String domain = null;
String apiKey = null;
String modelName = null;
if (config != null) {
domain = config.getApiDomain();
apiKey = config.getApiKey();
modelName = config.getModelId();
}
// 构造 EmbeddingOptions
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
.model(modelName)
.dimensions(1536)
//.user("模型对用户的称呼")
.build();
OpenAiApi openAiApi = OpenAiApi.builder()
// 填入自己的API KEY
.apiKey(apiKey)
// 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
// 注意:这里与langchain4j的配置不同,不需要在后面加/v1
.baseUrl(domain)
.completionsPath("/chat/completions")
.embeddingsPath("/embeddings")
.build();
OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(
openAiApi,
MetadataMode.NONE,
options);
ElasticsearchIndexUtils.createIndex("test_vector", esClient);
ElasticsearchVectorStoreOptions esOptions = new ElasticsearchVectorStoreOptions();
esOptions.setDimensions(1536);
esOptions.setIndexName("test_vector");
esOptions.setSimilarity(SimilarityFunction.cosine);
// Add the documents to Elasticsearch
ElasticsearchVectorStore elasticsearchVectorStore = ElasticsearchVectorStore.builder(restClient, openAiEmbeddingModel)
.options(esOptions).build();
List<Document> mh = elasticsearchVectorStore.doSimilaritySearch(
SearchRequest.builder().query("怪物猎人荒野").build());
log.info("相似度搜索结果:{}", mh);
}
}
工具类:
java
@Slf4j
public class ElasticsearchIndexUtils {
public static void createIndex(String indexName, ElasticsearchClient esClient) throws IOException {
// 1.判断是否有索引
ExistsRequest existsRequest = new ExistsRequest.Builder()
.index(indexName)
.build();
boolean value = esClient.indices().exists(existsRequest).value();
if (!value) {
log.info("索引不存在,创建索引:{}", indexName);
CreateIndexRequest createIndexRequest = CreateIndexRequest.of(builder ->
builder.index(indexName)
);
esClient.indices().create(createIndexRequest);
}
}
}
以上的测试方法采用手动配置,指定了索引为test_vector,如果想用自动配置的方法,直接使用下面的方法,SpringAI会自动配置,在配置文件中编写配置项即可:
java
@Slf4j
@SpringBootTest
public class AiApplicationTests {
@Autowired
private VectorStore vectorStore;
@Test
public void ragStoreWithSpringAI() throws IOException {
List<Document> documents = List.of(
new Document(
"怪物猎人荒野在2025年10月31日的在线玩家峰值为15万",
Map.of("title", "荒野")),
new Document("怪物猎人世界在2025年10月31日的在线玩家峰值为12万",
Map.of("title", "世界")),
new Document("怪物猎人崛起在2025年10月31日的在线玩家峰值为7万",
Map.of("title", "崛起")));
vectorStore.add(documents);
}
@Test
public void ragRetrieveWithSpringAI() throws IOException {
List<Document> mh = vectorStore.similaritySearch(
SearchRequest.builder().query("怪物猎人荒野").build());
log.info("相似度搜索结果:{}", mh);
}
}
上面的文档列表中,Map.of()传入一个参数Map,作为插入文档的metadata,可以在后续的RAG中做检索和筛选时使用。
2.编写带有RAG功能对话流的单元测试
SpringAI的官方提供多种RAG的实现方式,这里先介绍入门的Naive RAG,Advanced RAG等内容将在未来更新。
测试带有RAG的对话回复:
java
@Slf4j
@SpringBootTest
public class AiApplicationTests {
@Autowired
private AiConfigMapper aiConfigMapper;
@Autowired
private ElasticsearchClient esClient;
@Autowired
private VectorStore vectorStore;
@Autowired
private MybatisChatMemory mybatisChatMemory;
private static final Integer id = 2;
@Test
void SpringAIRAGChat() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AiConfig aiConfig = aiConfigMapper.selectByPrimaryKey(1);
OpenAiApi openAiApi = OpenAiApi.builder()
// 填入自己的API KEY
.apiKey(aiConfig.getApiKey())
// 填入自己的API域名,如果是百炼,即为https://dashscope.aliyuncs.com/compatible-mode
// 注意:这里与langchain4j的配置不同,不需要在后面加/v1
.baseUrl(aiConfig.getApiDomain())
.completionsPath("/chat/completions")
.build();
// 模型选项
OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
// 模型生成的最大 tokens 数
.maxTokens(aiConfig.getMaxContextMsgs())
// 模型生成的 tokens 的概率质量范围,取值范围 0.0-1.0 越大的概率质量范围越大
.topP(aiConfig.getSimilarityTopP())
// 模型生成的 tokens 的随机度,取值范围 0.0-1.0 越大的随机度越大
.temperature(aiConfig.getTemperature())
// 模型名称
.model(aiConfig.getModelId())
// 打开流式对话token计数配置,默认为false
.streamUsage(true)
.build();
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(chatOptions)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(mybatisChatMemory).build())
.build();
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
// 相似度阈值,0.5表示只有当检索结果的相似度分数 ≥ 0.50 时,才返回上层
.similarityThreshold(0.50)
// 注入的向量存储
.vectorStore(vectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
// 使用参数,允许查找的结果为空
.allowEmptyContext(true)
.build())
.build();
// 返回反应式对话流
String userMsg = "我们刚才谈论了任何包含数据库的问题吗?"+
"怪物猎人荒野在2025年10月31日的在线玩家峰值为多少?`";
Flux<ChatResponse> chatResponseFlux = chatClient.prompt()
.user(userMsg)
// 这里还可以整合对话记忆,这里的65是我之前测试时的一个会话
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "65"))
// 这里可以指定Metadata元数据匹配筛选
.advisors(a -> a.param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "title == '荒野'"))
.advisors(retrievalAugmentationAdvisor)
.stream()
.chatResponse();
chatResponseFlux
.subscribe(
token -> {
// 获取当前输出内容片段
String text = "";
if (token.getResult() != null) {
text = token.getResult().getOutput().getText();
if (StrUtil.isNotBlank(text)) {
log.info("当前段数据:{}", text);
}
}
},
// 反应式流在报错时会直接中断
e -> {
log.error("ai对话 流式输出报错:{}", e.getMessage());
}, // 错误处理
() -> {// 流结束
log.info("\n回答完毕!");
});
// 等待响应完成,最多等待20秒
boolean await = latch.await(20, TimeUnit.SECONDS);
if (!await) {
log.error("等待响应超时");
}
}
}
输出:
bash
当前段数据:我们
当前段数据:刚才
当前段数据:谈论
当前段数据:了
当前段数据:任何包含数据库的问题
当前段数据:吗?
是
当前段数据:的,你问
当前段数据:过:"pg数据库
当前段数据:好还是Mysql
当前段数据:好?"
怪物
当前段数据:猎人荒野
当前段数据:在202
当前段数据:5年10
当前段数据:月31日
当前段数据:的在线玩家峰值
当前段数据:为15万
当前段数据:。
🎉到这里我们可以看出,模型同时集成了对话记忆和RAG功能,我们的整合成功了,下一步就是将这个功能整合到我们的应用中。在下一篇文章中,我将整合一个知识库管理后台,并改造我们的应用,使其基于我们投喂的文档,支持RAG的问答。