基于 Spring AI 构建 RAG 知识库检索系统:设计与实现
一、引言
随着大语言模型的广泛应用,检索增强生成(Retrieval-Augmented Generation,RAG)已成为解决模型幻觉、提升回答准确性的核心方案。本文将基于 Spring AI 框架,详细讲解如何构建一套完整的 RAG 知识库检索系统,涵盖向量数据库配置、知识库管理、智能检索策略等核心模块的设计与实现。
二、技术栈与核心依赖
本系统基于 Spring Boot 构建,核心依赖包括:
- Spring AI:提供向量嵌入、向量存储、Prompt 模板、文本拆分等核心能力
- PgVector:PostgreSQL 的向量扩展,作为向量数据库存储知识库向量
- JdbcTemplate:操作 PgVector 数据库
- TokenTextSplitter:基于 Token 的文本拆分工具,解决大文本处理问题
- ChatClient:Spring AI 的对话客户端,用于问题重写等 LLM 交互场景
三、核心模块设计与实现
3.1 向量数据库配置(PgVector)
向量数据库是 RAG 系统的核心存储层,负责存储知识库文本的向量表示,支撑相似性检索。本系统通过 Spring AI 的 PgVectorStore 实现向量存储的配置与初始化。
3.1.1 配置类实现
java
package org.dromara.online.rag.module.vector;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.ai.vectorstore.pgvector.autoconfigure.PgVectorStoreProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* 向量数据库配置
*
* @Author:
* @Date: 2026/06/05
*/
@Configuration
public class VectorConfig {
@Bean
@ConfigurationProperties(prefix = "spring.ai.vectorstore.pgvector")
public PgVectorStoreProperties pgVectorStoreProperties() {
return new PgVectorStoreProperties();
}
@Bean
public VectorStore initVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashscopeEmbeddingModel,
PgVectorStoreProperties pgVectorStoreProperties) {
VectorStore vectorStore = PgVectorStore.builder(jdbcTemplate, dashscopeEmbeddingModel)
.dimensions(pgVectorStoreProperties.getDimensions()) // 向量维度,与嵌入模型匹配
.distanceType(pgVectorStoreProperties.getDistanceType()) // 距离计算方式(如余弦相似度)
.indexType(pgVectorStoreProperties.getIndexType()) // 向量索引类型
.initializeSchema(pgVectorStoreProperties.isInitializeSchema()) // 自动初始化数据库表结构
.schemaName(pgVectorStoreProperties.getSchemaName()) // 数据库schema
.vectorTableName(pgVectorStoreProperties.getTableName()) // 向量表名
.maxDocumentBatchSize(pgVectorStoreProperties.getMaxDocumentBatchSize()) // 批量处理大小
.removeExistingVectorStoreTable(pgVectorStoreProperties.isRemoveExistingVectorStoreTable()) // 是否删除现有表
.build();
return vectorStore;
}
}
3.1.2 核心配置说明
- 通过
@ConfigurationProperties绑定spring.ai.vectorstore.pgvector前缀的配置,实现灵活的参数配置; - 基于
PgVectorStore.builder构建向量存储实例,核心参数包括向量维度、距离计算类型、索引类型等,需与嵌入模型(EmbeddingModel)的输出维度匹配; - 依赖
JdbcTemplate实现数据库交互,依赖EmbeddingModel实现文本到向量的转换。
3.2 RAG 核心配置属性
为了灵活控制 RAG 系统的行为,我们通过配置类绑定系统参数,支持问题重写开关、检索策略参数等动态调整。
3.2.1 配置属性类
java
package org.dromara.online.rag.module.Resume.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* RAG系统配置属性
*
* @Author:
* @Date: 2026/06/16
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.ai.rag")
public class RagProperties {
private Rewrite rewrite = new Rewrite(); // 问题重写配置
private Search search = new Search(); // 检索策略配置
@Data
public static class Rewrite {
private boolean enabled = true; // 是否开启问题重写
}
@Data
public static class Search {
private int shortQueryLength = 4; // 短问题长度阈值
private int topkShort = 20; // 短问题检索返回条数
private int topkMedium = 12; // 中等长度问题检索返回条数
private int topkLong = 8; // 长问题检索返回条数
private double minScoreShort = 0.25; // 短问题最小相似度分数
private double minScoreDefault = 0.28; // 默认最小相似度分数
}
}
3.2.2 配置文件示例(application.yml)
yaml
spring:
ai:
rag:
rewrite:
enabled: false # 关闭问题重写(可根据场景动态调整)
search:
short-query-length: 4
topk-short: 20
topk-medium: 12
topk-long: 8
min-score-short: 0.18 # 短问题相似度阈值调低,提升召回率
min-score-default: 0.28
3.3 RAG 核心业务逻辑(RagService)
RagService 是系统的核心业务层,封装了知识库添加、智能检索、问题重写等核心能力。
3.3.1 核心依赖注入与初始化
java
package org.dromara.online.rag.module.Resume.service;
import com.alibaba.nacos.common.utils.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.dromara.online.rag.module.Resume.domain.enums.KnowledgeStatus;
import org.dromara.online.rag.module.Resume.properties.RagProperties;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.*;
/**
* Rag相关操作
*
* @Author:
* @Date: 2026/06/15
*/
@Component
@Slf4j
public class RagService {
private static final int MAX_BATCH_SIZE = 10; // 向量入库批量大小
private static final String KNOWLEDGE_STATUS = "knowledge_status"; // 知识库状态元数据key
private static final String KNOWLEDGE_ID = "knowledge_id"; // 知识库ID元数据key
@jakarta.annotation.Resource
VectorService vectorService; // 向量操作服务(封装VectorStore)
RagProperties queryProperties; // RAG配置属性
PromptTemplate rewriteTemplate; // 问题重写Prompt模板
@jakarta.annotation.Resource
ChatClient.Builder chatClient; // LLM对话客户端构建器
TokenTextSplitter textSplitter; // 文本拆分工具
// 检索策略参数
private int shortQueryLength;
private int topkShort;
private double minScoreShort;
private int topkMedium;
private double minScoreDefault;
private int topkLong;
// 构造函数注入配置与Prompt模板
public RagService(RagProperties queryProperties,
@Value("classpath:prompts/rag-rewrite.st") Resource resource) {
this.queryProperties = queryProperties;
// 初始化检索策略参数
this.shortQueryLength = queryProperties.getSearch().getShortQueryLength();
this.topkShort = queryProperties.getSearch().getTopkShort();
this.topkMedium = queryProperties.getSearch().getTopkMedium();
this.topkLong = queryProperties.getSearch().getTopkLong();
this.minScoreShort = queryProperties.getSearch().getMinScoreShort();
this.minScoreDefault = queryProperties.getSearch().getMinScoreDefault();
// 加载问题重写Prompt模板
this.rewriteTemplate = new PromptTemplate(resource);
// 初始化文本拆分工具(基于Token)
this.textSplitter = TokenTextSplitter.builder().build();
}
}
3.3.2 知识库添加(文本拆分 + 批量向量化)
知识库文本通常较长,直接向量化会导致精度下降,因此需要先拆分再批量入库,同时通过元数据标记知识库状态和 ID。
java
/**
* 添加知识库
*
* @param knowledgeId 知识库唯一标识
* @param content 知识库文本内容
*/
public void addDocument(String knowledgeId, String content) {
// 参数校验
Assert.hasLength(content.trim(), "content cannot be null");
Assert.hasLength(knowledgeId.trim(), "knowledgeId cannot be null");
try {
// 1. 文本拆分:将长文本拆分为Token级别的小片段
List<Document> chunks = textSplitter.split(Document.builder().text(content).build());
log.info("知识库id = {},知识库拆分完成: {}", knowledgeId, chunks.size());
// 2. 为每个文本片段添加元数据(状态、知识库ID)
chunksAddTempMetadata(chunks, knowledgeId, KnowledgeStatus.PENDING);
// 3. 批量向量化入库(避免单次入库过多导致性能问题)
int totalChunks = chunks.size();
int batchCount = (totalChunks + MAX_BATCH_SIZE - 1) / MAX_BATCH_SIZE; // 向上取整计算批次数
log.info("开始分批向量化: 总共 {} 个chunks,分 {} 批处理,每批最多 {} 个",
totalChunks, batchCount, MAX_BATCH_SIZE);
for (int i = 0; i < batchCount; i++) {
int start = i * MAX_BATCH_SIZE;
int end = Math.min((i + 1) * MAX_BATCH_SIZE, totalChunks);
vectorService.add(chunks.subList(start, end));
}
// 4. 更新知识库状态(标记为已完成)
chunksUpdateTempMetadata(knowledgeId);
log.info("向量化完成: 总共 {} 个chunks", totalChunks);
} catch (Exception e) {
log.error("知识库id = {} 添加失败", knowledgeId);
// 异常回滚:删除已入库的该知识库片段
chunksDeleteTempMetadata(knowledgeId);
throw new RuntimeException(e);
}
}
// 为文本片段添加元数据
private void chunksAddTempMetadata(List<Document> chunks, String knowledgeId, KnowledgeStatus knowledgeStatus) {
chunks.stream()
.forEach(chunk -> {
chunk.getMetadata().put(KNOWLEDGE_STATUS, knowledgeStatus);
chunk.getMetadata().put(KNOWLEDGE_ID, knowledgeId);
});
}
// 删除指定知识库的所有文本片段
private void chunksDeleteTempMetadata(String knowledgeId) {
vectorService.deleteByKnowledgeId(knowledgeId);
}
// 更新指定知识库的状态元数据
private void chunksUpdateTempMetadata(String knowledgeId) {
vectorService.updateByKnowledgeId(knowledgeId);
}
3.3.3 智能向量检索(多候选查询 + 动态策略)
检索环节是 RAG 系统的核心,本系统实现了 "多候选查询 + 基于问题长度的动态检索策略",提升召回率和准确性。
3.3.3.1 检索上下文封装
通过 Record 封装检索上下文,包含原始问题、候选查询、检索参数等:
// 检索上下文:封装检索所需的所有信息
public record QueryContext(String originalQuestion, List<String> candidateQueries, SearchParams searchParams) {}
// 检索参数:知识库ID、返回条数、最小相似度分数
public record SearchParams(List<String> knowledgeIds, int topK, double minScore) {}
3.3.3.2 核心检索逻辑
java
/**
* 向量检索:遍历候选查询,返回首个命中的相关文档
*
* @param queryContext 检索上下文
* @return 相关文档列表
*/
public List<Document> retrieveRelevantDocs(QueryContext queryContext) {
for (String candidateQuery : queryContext.candidateQueries()) {
if (candidateQuery.isBlank()) {
continue;
}
// 调用向量服务进行相似性检索
List<Document> docs = vectorService.similaritySearch(
candidateQuery, // 检索词(候选查询)
queryContext.searchParams().knowledgeIds(), // 目标知识库ID列表
queryContext.searchParams().topK(), // 返回条数
queryContext.searchParams().minScore() // 最小相似度分数(过滤低相关度结果)
);
log.info("检索候选 query='{}',命中 {} 条", candidateQuery, docs.size());
if (CollectionUtils.isNotEmpty(docs)) {
return docs; // 只要有候选查询命中,立即返回结果
}
}
return List.of(); // 无命中返回空列表
}
3.3.3.3 动态检索策略(基于问题长度)
在构建检索上下文时,根据问题长度动态调整检索参数(topK、minScore),平衡召回率和精准度:
java
/**
* 构建检索上下文:标准化问题、生成候选查询、动态配置检索参数
*
* @param originalQuestion 原始用户问题
* @param historyQuestions 对话历史
* @param knowledgeIds 目标知识库ID列表
* @return 检索上下文
*/
public RagService.QueryContext buildQueryContext(String originalQuestion, List<Message> historyQuestions,
List<String> knowledgeIds) {
// 1. 问题标准化:去除首尾空格
String normalizeQuestion = normalizeQuestion(originalQuestion);
// 2. 问题重写:基于LLM生成更优的检索词(可选,由配置开关控制)
String rewrittenQuestion = rewriteQuestion(normalizeQuestion, historyQuestions);
// 3. 构建候选查询列表(原始问题+重写问题)
Set<String> candidates = new LinkedHashSet<>();
candidates.add(normalizeQuestion);
candidates.add(rewrittenQuestion);
// 4. 动态解析检索参数(基于问题长度)
SearchParams searchParams = resolveSearchParams(normalizeQuestion, knowledgeIds);
// 5. 封装检索上下文
return new QueryContext(normalizeQuestion, new ArrayList<>(candidates), searchParams);
}
/**
* 解析检索参数:根据问题长度选择不同的topK和minScore
*
* @param question 标准化后的问题
* @param knowledgeIds 目标知识库ID列表
* @return 检索参数
*/
private SearchParams resolveSearchParams(String question, List<String> knowledgeIds) {
// 计算问题紧凑长度(去除所有空格)
int compactLength = question.replaceAll("\\s+", "").length();
if (compactLength <= shortQueryLength) {
// 短问题:增大topK,降低minScore,提升召回率
return new SearchParams(knowledgeIds, topkShort, minScoreShort);
}
if (compactLength <= 12) {
// 中等长度问题:平衡召回率和精准度
return new SearchParams(knowledgeIds, topkMedium, minScoreDefault);
}
// 长问题:减小topK,保证精准度
return new SearchParams(knowledgeIds, topkLong, minScoreDefault);
}
// 问题标准化
private String normalizeQuestion(String originalQuestion) {
return originalQuestion == null ? "" : originalQuestion.trim();
}
3.3.4 问题重写(可选能力)
基于 LLM 将用户原始问题重写为更适合向量检索的形式,提升召回率(由配置开关控制是否开启):
java
/**
* 问题重写:基于Prompt模板调用LLM生成更优的检索词
*
* @param normalizeQuestion 标准化后的原始问题
* @param historyQuestions 对话历史
* @return 重写后的问题
*/
public String rewriteQuestion(String normalizeQuestion, List<Message> historyQuestions) {
// 未开启重写或问题为空,直接返回原问题
if (!queryProperties.getRewrite().isEnabled() || normalizeQuestion.isEmpty()) {
return normalizeQuestion;
}
try {
// 构建Prompt变量
Map<String, Object> variables = new HashMap<>();
variables.put("question", normalizeQuestion);
variables.put("history", historyQuestions);
// 渲染Prompt模板
String render = rewriteTemplate.render(variables);
// 调用LLM生成重写后的问题
ChatResponse chatResponse = chatClient.build().prompt()
.user(render)
.call()
.chatResponse();
String rewritten = chatResponse.getResult().getOutput().getText();
if (rewritten == null || rewritten.isBlank()) {
return normalizeQuestion;
}
String normalized = rewritten.trim();
log.info("Query rewrite: id = {} origin='{}', rewritten='{}', historySize={},totalToken = {}",
chatResponse.getMetadata().getId(),
normalizeQuestion,
normalized,
historyQuestions.size(),
chatResponse.getMetadata().getUsage().getTotalTokens()
);
return normalized;
} catch (Exception e) {
log.error("Query rewrite 失败,使用原问题继续检索: {}", e.getMessage());
return normalizeQuestion;
}
}
四、系统设计亮点
4.1 灵活的配置体系
通过@ConfigurationProperties绑定配置,支持问题重写开关、检索参数(topK、minScore)等动态调整,无需修改代码即可适配不同场景。
4.2 高性能的知识库入库
- 文本拆分:基于 Token 的拆分方式,适配 LLM 的上下文长度限制;
- 批量入库:避免单次入库过多导致数据库压力过大;
- 异常回滚:入库失败时自动删除已入库的片段,保证数据一致性。
4.3 智能的检索策略
- 多候选查询:原始问题 + 重写问题,提升召回率;
- 动态检索参数:根据问题长度调整 topK 和 minScore,平衡召回率与精准度;
- 相似度过滤:通过 minScore 过滤低相关度结果,减少噪声。
4.4 可扩展的架构设计
- 向量服务解耦:VectorService 封装 VectorStore 的底层操作,便于替换向量数据库(如切换为 Milvus、Chroma 等);
- Prompt 模板化:问题重写的 Prompt 通过文件加载,便于迭代优化。
五、应用场景与扩展方向
5.1 典型应用场景
- 智能问答:基于企业知识库的智能客服、内部助手;
- 文档检索:结构化 / 非结构化文档的相似性检索;
- 对话增强:结合对话历史的上下文感知检索。
5.2 扩展方向
- 多模态支持:扩展图片、PDF 等多模态文档的解析与向量化;
- 检索结果重排序:引入 Rerank 模型,对检索结果进一步排序,提升精准度;
- 缓存优化:对高频查询的检索结果进行缓存,提升响应速度;
- 知识库更新:支持知识库的增量更新、删除、版本管理;
- 监控与可观测性:增加检索命中率、响应时间等指标监控。
六、总结
本文基于 Spring AI 框架,详细讲解了 RAG 知识库检索系统的核心模块设计与实现,包括向量数据库配置、知识库管理、智能检索策略等。该系统具备配置灵活、性能高效、检索智能等特点,可快速适配企业级知识库检索场景。通过合理的文本拆分、批量处理、动态检索策略,平衡了系统的召回率与精准度,为构建高性能 RAG 系统提供了完整的解决方案。