1. 项目概述:基于Spring AI构建RAG知识库应用
RAG(Retrieval-Augmented Generation)架构已经成为当前AI应用开发的热门范式,它通过结合检索(Retrieval)和生成(Generation)两个关键环节,有效解决了传统大语言模型(LLM)在知识更新滞后和事实性错误方面的痛点。作为一名长期从事企业级Java开发的工程师,我发现Spring AI框架的推出为Java生态的AI应用开发带来了全新的可能性。
这个项目将展示如何利用Spring Boot + Spring AI技术栈,从零构建一个完整的RAG知识库系统。与常见的Python实现方案不同,我们的方案具有以下独特优势:
- 全Java技术栈:适合已有Java技术积累的团队平滑过渡到AI应用开发
- 生产就绪架构:采用标准的Spring工程结构,易于集成到现有企业系统
- 模块化设计:各组件职责明确,支持灵活替换不同AI供应商实现
关键提示:在实际企业应用中,建议将向量数据库与业务数据库分离部署,避免AI查询负载影响核心业务系统性能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目架构与核心组件
2.1 工程结构设计解析
项目采用标准Maven多模块结构,核心设计理念是"高内聚低耦合"。以下是经过多个生产项目验证的最佳实践结构:
code复制rag-knowledge-base/
├── pom.xml
├── src/main/java/com/example/rag/
│ ├── RAGApplication.java # Spring Boot主入口
│ ├── config/ # 配置中心
│ │ ├── AIConfig.java # AI模型配置
│ │ ├── VectorStoreConfig.java # 向量数据库配置
│ │ └── EmbeddingConfig.java # 嵌入模型配置
│ ├── controller/ # API层
│ │ └── ChatController.java # 对话接口
│ ├── service/ # 业务逻辑层
│ │ ├── RagService.java # RAG核心服务
│ │ ├── DocumentService.java # 文档处理服务
│ │ └── RetrievalService.java # 检索服务
│ ├── etl/ # 数据处理流水线
│ │ ├── DocumentLoader.java # 文档加载器
│ │ ├── DocumentSplitter.java # 文档分割器
│ │ └── DocumentProcessor.java # 文档处理器
│ └── retriever/ # 检索增强模块
│ ├── CustomRetriever.java # 自定义检索器
│ └── QueryEnhancer.java # 查询增强器
└── src/main/resources/
└── application.yml # 统一配置
这种结构的优势在于:
- 配置集中管理:所有AI模型和基础设施的配置统一在config包维护
- 处理流水线清晰:etl包实现文档处理的完整pipeline
- 检索逻辑可扩展:retriever包支持自定义检索策略
2.2 关键技术选型与依赖配置
Maven核心依赖设计
在pom.xml中,我们精心设计了分层依赖策略:
xml复制<dependencies>
<!-- Spring AI 核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- OpenAI 适配器(可替换为其他厂商实现) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- 向量数据库 - PGVector实现 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- 文档解析支持多种格式 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- Spring Boot Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
经验分享:在实际项目中,建议将Spring AI相关依赖版本通过
<dependencyManagement>统一管理,避免多模块项目中出现版本冲突。
配置最佳实践
application.yml采用环境敏感的配置策略:
yaml复制spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 从环境变量读取
base-url: https://api.openai.com
chat:
options:
model: gpt-4-turbo-preview # 平衡性能与成本的选择
temperature: 0.7 # 创造性控制参数
embedding:
options:
model: text-embedding-3-small # 性价比最高的嵌入模型
vectorstore:
pgvector:
url: jdbc:postgresql://${DB_HOST:localhost}:5432/ragdb
username: ${DB_USER:postgres}
password: ${DB_PASSWORD:password}
table-name: vector_store
dimension: 1536 # 必须与嵌入模型维度匹配
document:
splitter:
max-tokens: 500 # 适合大多数知识文档的块大小
overlap: 50 # 避免信息割裂的块重叠
loader:
supported-formats: pdf,docx,txt,md # 企业常见文档格式
关键配置说明:
temperature参数控制生成结果的创造性,知识库场景建议0.5-0.7- 块大小(max-tokens)需要根据文档类型调整,技术文档建议400-600tokens
- PGVector的dimension必须与嵌入模型输出维度严格一致
3. 核心实现细节
3.1 AI配置类深度解析
AIConfig.java - AI模型配置
java复制@Configuration
@EnableConfigurationProperties(OpenAiProperties.class)
public class AIConfig {
@Bean
public ChatClient chatClient(OpenAiChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你是一个专业的知识库助手,请严格遵循以下规则:
1. 仅基于提供的上下文回答问题
2. 不确定时明确告知用户
3. 保持回答专业且简洁""")
.build();
}
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiApi openAiApi) {
return new OpenAiEmbeddingModel(openAiApi);
}
}
关键设计考量:
- 使用
@EnableConfigurationProperties实现配置外部化 defaultSystem消息精心设计,约束AI行为符合企业要求- 分离ChatModel和EmbeddingModel配置,便于独立调优
VectorStoreConfig.java - 向量存储配置
java复制@Configuration
@RequiredArgsConstructor
public class VectorStoreConfig {
private final DataSource dataSource;
private final EmbeddingModel embeddingModel;
@Bean
public VectorStore vectorStore() {
return new PgVectorStore(
new JdbcTemplate(dataSource),
embeddingModel,
PgVectorStore.PgVectorStoreConfig.builder()
.withTableName("doc_vectors") // 自定义表名
.withEmbeddingDimension(1536) // 显式指定维度
.withDistanceType(PgVectorStore.DistanceType.COSINE) // 余弦相似度
.build()
);
}
}
生产环境建议:
- 为向量表添加合适的索引:
CREATE INDEX ON doc_vectors USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); - 定期执行
VACUUM ANALYZE维护向量表性能
3.2 文档处理流水线实现
DocumentLoader.java - 多格式文档加载
java复制@Component
@RequiredArgsConstructor
public class DocumentLoader {
private final ResourcePatternResolver resourceResolver;
public List<Document> loadDocuments(String locationPattern) {
try {
Resource[] resources = resourceResolver.getResources(locationPattern);
return Arrays.stream(resources)
.parallel() // 并行处理提升吞吐量
.map(this::parseDocument)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException("文档加载失败", e);
}
}
private Document parseDocument(Resource resource) {
try {
String filename = resource.getFilename();
String extension = filename.substring(filename.lastIndexOf(".") + 1);
return switch (extension.toLowerCase()) {
case "pdf" -> new PdfDocumentReader(resource).read();
case "docx" -> new DocxDocumentReader(resource).read();
case "txt", "md" -> new TextDocumentReader(resource).read();
default -> throw new IllegalArgumentException("不支持的文档格式: " + extension);
};
} catch (Exception e) {
log.error("文档解析失败: {}", resource.getFilename(), e);
return null;
}
}
}
性能优化技巧:
- 使用
parallel()流实现多文档并行加载 - 对大型PDF文件,可考虑使用
PDFBox的逐页读取策略 - 添加文件格式白名单机制,避免恶意文件上传
DocumentSplitter.java - 智能文本分块
java复制@Component
@RequiredArgsConstructor
public class DocumentSplitter {
private final TokenCountEstimator tokenEstimator;
public List<TextSegment> splitDocument(Document document, int maxTokens, int overlap) {
String content = document.getContent();
List<String> paragraphs = splitByParagraph(content);
List<TextSegment> chunks = new ArrayList<>();
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String para : paragraphs) {
int paraTokens = tokenEstimator.estimate(para);
if (currentTokens + paraTokens > maxTokens && currentChunk.length() > 0) {
chunks.add(createSegment(currentChunk.toString(), document));
currentChunk = new StringBuilder(
chunks.isEmpty() ? "" : chunks.get(chunks.size()-1).getText()
.substring(maxTokens - overlap)
);
currentTokens = tokenEstimator.estimate(currentChunk.toString());
}
currentChunk.append(para).append("\n\n");
currentTokens += paraTokens;
}
if (currentChunk.length() > 0) {
chunks.add(createSegment(currentChunk.toString(), document));
}
return chunks;
}
private TextSegment createSegment(String text, Document document) {
return new TextSegment(text, Map.of(
"source", document.getMetadata().get("source"),
"page", document.getMetadata().get("page")
));
}
}
分块算法要点:
- 按段落边界分割,保持语义完整性
- 动态计算token数量,精确控制块大小
- 重叠区域处理避免关键信息被割裂
- 保留原始文档元数据便于溯源
4. RAG核心服务实现
4.1 检索增强生成服务
RagService.java - RAG核心逻辑
java复制@Service
@RequiredArgsConstructor
public class RagService {
private final ChatClient chatClient;
private final RetrievalService retrievalService;
private final QueryEnhancer queryEnhancer;
public String generateResponse(String query) {
// 1. 查询增强
String enhancedQuery = queryEnhancer.enhance(query);
// 2. 多路检索
List<Document> relevantDocs = retrievalService.retrieve(enhancedQuery);
// 3. 构建提示词
String prompt = buildRagPrompt(query, relevantDocs);
// 4. 生成回答
return chatClient.generate(prompt);
}
private String buildRagPrompt(String query, List<Document> docs) {
StringBuilder context = new StringBuilder();
context.append("基于以下上下文回答问题:\n\n");
for (Document doc : docs) {
context.append("--- 来源:")
.append(doc.getMetadata().get("source"))
.append(" ---\n")
.append(doc.getContent())
.append("\n\n");
}
context.append("问题:").append(query).append("\n");
context.append("回答要求:专业、准确、简洁,不超过3句话");
return context.toString();
}
}
关键优化点:
- 查询增强提升检索召回率
- 动态构建包含来源信息的提示词
- 严格约束生成格式和长度
4.2 高级检索策略
CustomRetriever.java - 混合检索实现
java复制@Component
@RequiredArgsConstructor
public class CustomRetriever {
private final VectorStore vectorStore;
private final JdbcTemplate jdbcTemplate;
public List<Document> hybridRetrieve(String query) {
// 1. 向量相似度检索
List<Document> vectorResults = vectorStore.similaritySearch(query, 5);
// 2. 关键词BM25检索
List<Document> keywordResults = keywordSearch(query);
// 3. 结果融合 (RRF算法)
return fuseResults(vectorResults, keywordResults);
}
private List<Document> keywordSearch(String query) {
String sql = """
SELECT content, metadata
FROM documents
WHERE to_tsvector('english', content) @@ to_tsquery(?)
ORDER BY ts_rank(to_tsvector('english', content), to_tsquery(?)) DESC
LIMIT 5""";
return jdbcTemplate.query(sql, ps -> {
String tsQuery = Arrays.stream(query.split(" "))
.filter(w -> w.length() > 3)
.collect(Collectors.joining(" & "));
ps.setString(1, tsQuery);
ps.setString(2, tsQuery);
}, (rs, rowNum) -> {
Document doc = new Document(rs.getString("content"));
doc.getMetadata().putAll(
(Map<String, Object>) rs.getObject("metadata")
);
return doc;
});
}
private List<Document> fuseResults(List<Document> list1, List<Document> list2) {
Map<String, Document> combined = new LinkedHashMap<>();
// 实现 Reciprocal Rank Fusion 算法
int rank = 1;
for (Document doc : list1) {
String key = doc.getMetadata().get("source").toString();
combined.compute(key, (k, v) -> {
if (v == null) {
doc.getMetadata().put("score", 1.0 / rank);
return doc;
}
v.getMetadata().put("score",
(Double)v.getMetadata().get("score") + 1.0 / rank);
return v;
});
rank++;
}
rank = 1;
for (Document doc : list2) {
String key = doc.getMetadata().get("source").toString();
combined.compute(key, (k, v) -> {
if (v == null) {
doc.getMetadata().put("score", 1.0 / rank);
return doc;
}
v.getMetadata().put("score",
(Double)v.getMetadata().get("score") + 1.0 / rank);
return v;
});
rank++;
}
return combined.values().stream()
.sorted((d1, d2) -> Double.compare(
(Double)d2.getMetadata().get("score"),
(Double)d1.getMetadata().get("score")
))
.limit(5)
.collect(Collectors.toList());
}
}
混合检索优势:
- 结合语义搜索和关键词搜索的优势
- RRF算法有效平衡两种检索结果的权重
- 支持结构化字段过滤(如时间范围、文档类型等)
5. 生产环境优化建议
5.1 性能调优策略
-
向量索引优化:
sql复制CREATE INDEX ON doc_vectors USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);- lists参数建议设置为总记录数/1000
- 定期执行
ANALYZE doc_vectors更新统计信息
-
缓存策略:
- 对频繁查询实施Redis缓存
- 考虑缓存嵌入向量计算结果
-
批量处理优化:
java复制// 批量插入向量 @Transactional public void batchInsert(List<Document> docs) { int batchSize = 100; for (int i = 0; i < docs.size(); i += batchSize) { List<Document> batch = docs.subList(i, Math.min(i + batchSize, docs.size())); vectorStore.add(batch); } }
5.2 监控与运维
-
关键指标监控:
- 检索耗时百分位值(P99/P95)
- 生成token数量分布
- 缓存命中率
-
异常处理策略:
java复制@Retryable(value = {OpenAiApiException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public String generateWithRetry(String prompt) { return chatClient.generate(prompt); } -
限流保护:
java复制@RateLimiter(name = "openaiApi", fallbackMethod = "rateLimitFallback") public String safeGenerate(String prompt) { return chatClient.generate(prompt); }
6. 典型问题排查指南
6.1 常见错误与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 检索结果不相关 | 嵌入模型不匹配 | 检查vectorstore维度与嵌入模型是否一致 |
| 生成内容不符合预期 | 提示词设计不当 | 优化system message和问题模板 |
| 处理PDF文件失败 | 文件加密或损坏 | 添加文件预处理校验逻辑 |
| 向量插入速度慢 | 未启用批量模式 | 使用addAll替代单条插入 |
| 相似度分数异常 | 距离度量方式错误 | 确认使用COSINE距离 |
6.2 调试技巧
-
检索过程调试:
java复制@Profile("dev") @Bean public VectorStore debugVectorStore() { return new InMemoryVectorStore(); // 用于快速调试 } -
提示词可视化:
java复制@Bean public ChatClient chatClientWithLogging(OpenAiChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem("...") .withRequestLogger((prompt, options) -> { log.debug("Prompt:\n{}", prompt); return prompt; }) .build(); } -
性能分析工具:
java复制@Aspect @Component public class PerformanceMonitor { @Around("execution(* com.example.rag.service..*(..))") public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); log.info("{} executed in {}ms", pjp.getSignature(), System.currentTimeMillis() - start); return result; } }
7. 项目扩展方向
7.1 多模态支持
java复制// 图像处理扩展
@Component
public class ImageProcessor {
private final OpenAiImageModel imageModel;
public String analyzeImage(Resource image) {
return imageModel.call(
new ImagePrompt(
List.of(new ImageMessage(image, "请描述图片内容")),
ModelOptionsBuilder.builder()
.withModel("gpt-4-vision-preview")
.build()
)
).getResult().getOutput().getContent();
}
}
7.2 知识图谱集成
java复制// 知识图谱检索增强
@Component
@RequiredArgsConstructor
public class KnowledgeGraphRetriever {
private final Neo4jTemplate neo4jTemplate;
public List<Document> retrieveFromKG(String query) {
String cypher = """
MATCH (n:Concept)-[r]->(m)
WHERE n.label CONTAINS $query OR m.label CONTAINS $query
RETURN n, r, m
LIMIT 5""";
return neo4jTemplate.findAll(cypher,
Map.of("query", query),
Document.class
);
}
}
7.3 微调与领域适配
java复制// 领域适配提示词工程
@Bean
public ChatClient domainSpecificClient(OpenAiChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你是XX领域的专业顾问,回答时请:
1. 使用专业术语但解释核心概念
2. 引用行业标准和最佳实践
3. 对不确定的信息明确标注""")
.withPromptTemplate("""
基于以下{context}回答{question}。
要求:{requirements}""")
.build();
}
在实际企业部署中,我们通过以下策略确保系统可靠性:
- 实施蓝绿部署策略,逐步切换AI模型版本
- 建立自动化回归测试套件,验证核心检索逻辑
- 设计降级方案,在AI服务不可用时切换至规则引擎
- 定期更新知识库,建立文档版本管理机制
