Spring AI 学习篇(十)| 企业级RAG系统实战
- 一、本章核心学习目标
- 二、前置知识准备
- 三、为什么Demo级RAG不能直接用于企业?
- 四、企业级RAG系统整体架构设计
- 五、数据库设计
-
- [1. 核心表结构](#1. 核心表结构)
-
- [(1) 知识库表(tb_knowledge_base)](#(1) 知识库表(tb_knowledge_base))
- [(2) 文档表(tb_document)](#(2) 文档表(tb_document))
- [(3) 文档分块表(tb_document_chunk)](#(3) 文档分块表(tb_document_chunk))
- [(4) 权限表(tb_kb_permission)](#(4) 权限表(tb_kb_permission))
- 六、核心功能模块实现
-
- [1. 多知识库管理](#1. 多知识库管理)
-
- [(1) 知识库CRUD接口](#(1) 知识库CRUD接口)
- [2. 文档全生命周期管理](#2. 文档全生命周期管理)
-
- [(1) 异步文档处理实现](#(1) 异步文档处理实现)
- [3. 细粒度权限控制](#3. 细粒度权限控制)
-
- [(1) 检索时自动过滤权限](#(1) 检索时自动过滤权限)
- [4. 答案溯源功能](#4. 答案溯源功能)
-
- [(1) 实现思路](#(1) 实现思路)
- [(2) 代码实现](#(2) 代码实现)
- [5. 批量导入与增量更新](#5. 批量导入与增量更新)
-
- [(1) ZIP包批量导入](#(1) ZIP包批量导入)
- [(2) 增量更新](#(2) 增量更新)
- 七、性能测试与压力优化
-
- [1. 核心性能指标](#1. 核心性能指标)
- [2. 优化策略](#2. 优化策略)
- 八、企业级最佳实践
- 九、常见坑与解决方案
-
- [1. ❌ 批量导入内存溢出](#1. ❌ 批量导入内存溢出)
- [2. ❌ 权限过滤导致检索结果为空](#2. ❌ 权限过滤导致检索结果为空)
- [3. ❌ 答案溯源不准确](#3. ❌ 答案溯源不准确)
- [4. ❌ 向量数据库查询缓慢](#4. ❌ 向量数据库查询缓慢)
- 十、本章总结与下章预告
- 十一、课后练习
一、本章核心学习目标
学完本章,你将能够:
- 设计符合企业标准的多知识库RAG系统架构
- 实现文档全生命周期管理(上传→解析→切分→索引→发布→删除)
- 构建基于角色的细粒度权限控制体系
- 实现答案溯源功能,让用户清晰看到答案的来源
- 支持大批量文档导入与增量更新
- 完成RAG系统的性能测试与压力优化
- 整合前9篇所有技术,打造一个可直接上线的企业级知识库系统
二、前置知识准备
- 已经完成前9篇的学习,掌握RAG全流程技术栈
- 熟练使用Spring AI四阶段RAG流水线
- 了解Spring Security权限控制基础
- 熟悉MySQL等关系型数据库的使用
三、为什么Demo级RAG不能直接用于企业?
我们在前9篇实现的RAG系统已经具备了核心功能,但距离企业级应用还有很大差距,主要存在以下6个致命问题:
- 缺乏知识库隔离:所有文档都存在同一个集合中,无法实现部门级数据隔离
- 没有权限控制:任何人都能访问所有文档,无法保护敏感信息
- 文档管理混乱:没有分类、标签、版本管理,文档多了之后无法查找
- 没有答案溯源:用户不知道答案来自哪个文档,无法验证准确性
- 不支持批量操作:只能单个上传文档,无法处理企业级大批量文档
- 缺乏可观测性:没有监控、日志和告警,出了问题无法排查
本章我们将逐一解决这些问题,把Demo升级为真正可用于生产环境的企业级系统。
预告式提及:本章完成后,我们将结束RAG技术的学习,下一章开始进入AI Agent领域,学习如何让AI从"会回答"进化为"会执行"。
四、企业级RAG系统整体架构设计
我们采用经典的分层架构设计,确保系统的可扩展性、可维护性和安全性:
┌─────────────────────────────────────────────────────────┐
│ 接入层 (Access Layer) │
│ Web接口 移动端接口 第三方集成接口 流式输出接口 │
├─────────────────────────────────────────────────────────┤
│ 业务层 (Business Layer) │
│ 知识库管理 文档管理 RAG问答服务 权限管理 统计分析 │
├─────────────────────────────────────────────────────────┤
│ 数据层 (Data Layer) │
│ 关系型数据库(MySQL) 向量数据库(Chroma/Qdrant) 缓存 │
├─────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure Layer) │
│ 大模型服务 嵌入模型 重排序模型 文件存储 消息队列 │
└─────────────────────────────────────────────────────────┘
核心设计原则
- 数据隔离:不同知识库的数据完全隔离,互不干扰
- 权限最小化:用户只能访问自己有权限的知识库和文档
- 异步处理:所有耗时操作(文档解析、向量生成)都异步执行
- 可观测性:所有操作都有日志记录,关键指标可监控
- 可扩展性:模块化设计,方便后续添加新功能
五、数据库设计
我们使用MySQL存储元数据,向量数据库存储向量数据,两者通过文档ID关联。
1. 核心表结构
(1) 知识库表(tb_knowledge_base)
sql
CREATE TABLE tb_knowledge_base (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '知识库名称',
description VARCHAR(500) COMMENT '知识库描述',
owner_id BIGINT NOT NULL COMMENT '创建人ID',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_owner_id(owner_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库表';
(2) 文档表(tb_document)
sql
CREATE TABLE tb_document (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
kb_id BIGINT NOT NULL COMMENT '知识库ID',
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
file_type VARCHAR(20) NOT NULL COMMENT '文件类型:pdf,docx,md,txt',
file_size BIGINT NOT NULL COMMENT '文件大小(字节)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-处理中,1-已发布,2-处理失败',
chunk_count INT NOT NULL DEFAULT 0 COMMENT '分块数量',
uploader_id BIGINT NOT NULL COMMENT '上传人ID',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_kb_id(kb_id),
INDEX idx_uploader_id(uploader_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档表';
(3) 文档分块表(tb_document_chunk)
sql
CREATE TABLE tb_document_chunk (
id VARCHAR(64) PRIMARY KEY COMMENT '分块ID(与向量数据库ID一致)',
doc_id BIGINT NOT NULL COMMENT '文档ID',
kb_id BIGINT NOT NULL COMMENT '知识库ID',
chunk_number INT NOT NULL COMMENT '分块序号',
page_number INT DEFAULT 0 COMMENT '页码',
content TEXT NOT NULL COMMENT '分块内容',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_doc_id(doc_id),
INDEX idx_kb_id(kb_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档分块表';
(4) 权限表(tb_kb_permission)
sql
CREATE TABLE tb_kb_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
kb_id BIGINT NOT NULL COMMENT '知识库ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
permission TINYINT NOT NULL COMMENT '权限:1-只读,2-读写,3-管理员',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_kb_user(kb_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库权限表';
六、核心功能模块实现
1. 多知识库管理
企业级RAG必须支持多知识库,不同部门、不同业务线可以创建自己的知识库。
(1) 知识库CRUD接口
java
@RestController
@RequestMapping("/kb")
public class KnowledgeBaseController {
private final KnowledgeBaseService kbService;
@PostMapping
public Result<Long> createKb(@RequestBody CreateKbRequest request) {
Long kbId = kbService.createKnowledgeBase(request);
return Result.success(kbId);
}
@GetMapping
public Result<PageResult<KbVO>> listKb(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
PageResult<KbVO> result = kbService.listUserKb(page, size);
return Result.success(result);
}
@PutMapping("/{kbId}")
public Result<Void> updateKb(@PathVariable Long kbId,
@RequestBody UpdateKbRequest request) {
kbService.updateKnowledgeBase(kbId, request);
return Result.success();
}
@DeleteMapping("/{kbId}")
public Result<Void> deleteKb(@PathVariable Long kbId) {
kbService.deleteKnowledgeBase(kbId);
return Result.success();
}
}
2. 文档全生命周期管理
文档从上传到删除的完整流程:
用户上传 → 保存文件到本地/OSS → 异步解析 → 文本切分 → 生成向量 → 存入向量数据库 → 更新元数据 → 发布成功
(1) 异步文档处理实现
注意 :使用
@Async需在启动类上加@EnableAsync注解。
java
@Service
public class DocumentService {
private final DocumentParserService parserService;
private final TextSplitterService splitterService;
private final VectorStore vectorStore;
private final DocumentRepository docRepository;
private final DocumentChunkRepository chunkRepository;
@Async
public void processDocumentAsync(Long docId, File file, String fileName) {
try {
// 1. 更新状态为处理中
docRepository.updateStatus(docId, DocumentStatus.PROCESSING);
// 2. 解析文档
List<Document> documents = parserService.parseDocument(file, fileName);
// 3. 文本切分
List<Document> chunks = splitterService.splitDocuments(documents);
// 4. 添加元数据
for (int i = 0; i < chunks.size(); i++) {
Document chunk = chunks.get(i);
chunk.getMetadata().put("kb_id", kbId);
chunk.getMetadata().put("doc_id", docId);
chunk.getMetadata().put("chunk_number", i + 1);
chunk.getMetadata().put("page_number", getPageNumber(chunk));
}
// 5. 存入向量数据库
vectorStore.add(chunks);
// 6. 保存分块元数据
List<DocumentChunk> chunkEntities = chunks.stream()
.map(chunk -> {
DocumentChunk chunkEntity = new DocumentChunk();
chunkEntity.setId(chunk.getId());
chunkEntity.setDocId(docId);
chunkEntity.setKbId(doc.getKbId());
chunkEntity.setChunkNumber((Integer) chunk.getMetadata().get("chunk_number"));
chunkEntity.setPageNumber((Integer) chunk.getMetadata().get("page_number"));
chunkEntity.setContent(chunk.getContent());
return chunkEntity;
})
.toList();
chunkRepository.batchInsert(chunkEntities);
// 7. 更新状态为已发布
docRepository.updateStatus(docId, DocumentStatus.PUBLISHED);
docRepository.updateChunkCount(docId, chunks.size());
} catch (Exception e) {
log.error("文档处理失败,docId: {}", docId, e);
docRepository.updateStatus(docId, DocumentStatus.FAILED);
}
}
}
3. 细粒度权限控制
实现基于角色的权限控制,确保用户只能访问自己有权限的知识库。
(1) 检索时自动过滤权限
java
public List<Document> search(Long kbId, String query) {
// 检查用户是否有该知识库的访问权限
if (!permissionService.hasPermission(kbId, SecurityUtils.getCurrentUserId(), Permission.READ)) {
throw new PermissionDeniedException("您没有访问该知识库的权限");
}
// 检索时只返回该知识库的文档
return vectorStore.similaritySearch(
SearchRequest.builder().query(query)
.topK(20)
.filterExpression("kb_id == " + kbId)
.build()
);
}
4. 答案溯源功能
让用户清晰看到答案来自哪个文档的哪个部分,增强答案的可信度。
(1) 实现思路
- 每个分块都包含文档ID、分块号、页码等元数据
- 检索时获取这些元数据
- 生成回答时,在末尾添加来源引用
- 提供接口让用户可以查看原始文档片段
(2) 代码实现
java
public RagAnswerVO chat(Long kbId, String query) {
// 1. 查询重写
String rewrittenQuery = queryRewriterService.rewriteQuery(query);
// 2. 混合检索
List<Document> rawResults = vectorStore.similaritySearch(
SearchRequest.builder().query(rewrittenQuery)
.topK(20)
.filterExpression("kb_id == " + kbId)
.build()
);
// 3. 重排序
List<Document> rerankedResults = rerankingService.rerank(query, rawResults);
// 4. 上下文压缩
List<Document> compressedResults = contextCompressionService.compress(query, rerankedResults);
// 5. 构建上下文和来源列表
StringBuilder context = new StringBuilder();
List<SourceVO> sources = new ArrayList<>();
for (int i = 0; i < compressedResults.size(); i++) {
Document doc = compressedResults.get(i);
Long docId = Long.valueOf(doc.getMetadata().get("doc_id").toString());
DocumentEntity docEntity = docRepository.findById(docId).orElseThrow();
context.append("[").append(i + 1).append("] ").append(doc.getContent()).append("\n\n");
SourceVO source = new SourceVO();
source.setFileName(docEntity.getFileName());
source.setPageNumber((Integer) doc.getMetadata().get("page_number"));
source.setContent(doc.getContent());
sources.add(source);
}
// 6. 构建提示词
String prompt = """
请严格基于以下上下文回答用户的问题。
如果上下文中没有相关信息,请如实回答"抱歉,我没有找到相关信息",不要编造内容。
回答要简洁、准确、有条理。
请在回答中引用来源编号,例如[1][2]。
上下文:
{context}
用户问题:{query}
""".replace("{context}", context.toString()).replace("{query}", query);
// 7. 生成回答
String answer = chatClient.prompt()
.system("你是一个专业的知识库助手,只能基于提供的上下文回答问题。")
.user(prompt)
.call()
.content();
// 8. 返回回答和来源
RagAnswerVO answerVO = new RagAnswerVO();
answerVO.setAnswer(answer);
answerVO.setSources(sources);
return answerVO;
}
5. 批量导入与增量更新
(1) ZIP包批量导入
支持上传一个ZIP包,自动解压并导入所有文档:
java
@PostMapping("/upload/batch")
public Result<Void> batchUpload(@RequestParam("file") MultipartFile zipFile,
@RequestParam("kbId") Long kbId) throws IOException {
// 解压ZIP包
File tempDir = Files.createTempDirectory("batch_upload_").toFile();
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
String fileName = entry.getName();
if (isSupportedFileType(fileName)) {
File tempFile = new File(tempDir, UUID.randomUUID() + "_" + fileName);
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
zis.transferTo(fos);
}
// 异步处理每个文档
documentService.processDocumentAsync(docId, tempFile, fileName);
}
}
}
}
return Result.success();
}
(2) 增量更新
定时扫描指定目录,自动导入新增或更新的文档:
java
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void incrementalUpdate() {
List<KnowledgeBaseEntity> kbs = kbRepository.findAllByStatus(1);
for (KnowledgeBaseEntity kb : kbs) {
String syncDir = "/data/kb/" + kb.getId() + "/";
File dir = new File(syncDir);
if (!dir.exists()) continue;
File[] files = dir.listFiles();
if (files == null) continue;
for (File file : files) {
if (file.isFile() && isSupportedFileType(file.getName())) {
// 检查文件是否已经导入
DocumentEntity existingDoc = docRepository.findByKbIdAndFileName(kb.getId(), file.getName());
if (existingDoc == null || file.lastModified() > existingDoc.getUpdatedAt().getTime()) {
// 导入新文档或更新旧文档
documentService.uploadDocumentAsync(file, file.getName(), kb.getId());
}
}
}
}
}
七、性能测试与压力优化
1. 核心性能指标
- 响应时间:问答接口平均响应时间 < 3秒
- 并发能力:支持至少100个并发用户
- 吞吐量:每秒处理至少50个请求
- 文档处理速度:每秒处理至少10个文档分块
2. 优化策略
-
缓存优化
- 缓存常用查询的回答结果
- 缓存嵌入向量和重排序结果
- 使用Redis缓存用户权限和知识库信息
-
异步优化
- 所有耗时操作都异步执行
- 使用线程池控制并发数量
- 引入消息队列(RabbitMQ/Kafka)处理文档导入任务
-
向量数据库优化
- 使用HNSW索引,平衡速度和准确率
- 为每个知识库创建独立的集合
- 定期重建索引,提升检索性能
-
模型优化
- 使用本地模型替代商业API,降低网络延迟
- 批量生成向量,提升处理速度
- 模型缓存,避免重复加载模型
八、企业级最佳实践
-
文档预处理规范
- 统一文档格式,优先使用Markdown和Word
- 文档命名规范:分类-名称-版本.docx
- 去除文档中的页眉、页脚、水印等无关信息
-
知识库分类规范
- 按照部门、业务线、项目进行分类
- 每个知识库不要超过1000个文档
- 定期清理过时和无用的文档
-
权限管理规范
- 遵循最小权限原则
- 使用角色管理权限,不要直接给用户授权
- 定期审计权限,回收不必要的权限
-
数据备份规范
- 每天备份关系型数据库
- 每周备份向量数据库
- 每月进行一次恢复测试
九、常见坑与解决方案
1. ❌ 批量导入内存溢出
问题 :一次性导入大量文档导致内存溢出
解决方案:分批次处理,每批处理10-20个文档,控制并发数量
2. ❌ 权限过滤导致检索结果为空
问题 :用户有知识库权限,但检索不到任何结果
解决方案:检查过滤表达式是否正确,确保元数据中的kb_id是数字类型
3. ❌ 答案溯源不准确
问题 :答案引用的来源与实际内容不符
解决方案:确保分块时记录准确的页码和分块号,生成回答时正确引用来源编号
4. ❌ 向量数据库查询缓慢
问题 :数据量超过100万条后,查询速度明显下降
解决方案:升级向量数据库为分布式集群,优化索引参数,使用更快的存储设备
十、本章总结与下章预告
本章总结
- 企业级RAG系统需要解决数据隔离、权限控制、文档管理、答案溯源等核心问题
- 采用分层架构设计,确保系统的可扩展性和可维护性
- 文档全生命周期管理是企业级系统的核心,所有耗时操作都应异步执行
- 细粒度权限控制是企业级应用的必备功能,检索时自动过滤用户无权访问的内容
- 答案溯源功能增强了系统的可信度,是用户体验的关键
- 批量导入和增量更新大幅提升了系统的易用性
预告式提及:到本章为止,我们已经完整掌握了RAG技术的所有核心内容。从下一章开始,我们将进入AI Agent领域,学习如何让AI不仅能回答问题,还能调用外部工具执行操作,真正成为你的智能助手。
下章预告
下一章我们将学习Spring AI工具调用的完整实现。你将学会:
- 什么是工具调用?为什么它是AI Agent的基础?
- Spring AI
@Tool注解的使用方法 - 自定义工具的开发规范与最佳实践
- 多工具并行调用与顺序调用
- 工具调用的异常处理与安全控制
十一、课后练习
- 实现多知识库管理功能,支持创建、删除和查询知识库
- 完善文档全生命周期管理,添加文档版本管理功能
- 实现基于角色的权限控制,确保用户只能访问自己有权限的内容
- 实现答案溯源功能,在回答末尾显示来源文档和页码
- 实现ZIP包批量导入功能,测试导入100个文档的性能