Knowledgebase 知识库模块设计与实现
这篇笔记记录我在 interview-guide 项目中对 Knowledgebase 模块的实现。目标是把“文档上传、向量化、RAG 查询、会话关联”打通成一个可持续迭代的知识服务能力。
模块能力概览
- 文档管理:支持上传、下载、删除、分类、关键字检索与统计。
- 向量化能力:基于
pgvector存储向量,使用异步任务进行切片与入库。 - RAG 问答:支持非流式与流式(SSE)多知识库检索问答。
- 会话协同:删除知识库时自动清理关联会话引用,降低数据不一致风险。
状态转换
图1:KnowledgeBase 主状态机
图2:分片上传知识库流程
关键接口设计
GET /api/knowledgebase/list 获取知识库列表(状态过滤 + 排序)
调用链:
Result.success(listService.listKnowledgeBases(status, sortBy));
knowledgeBaseRepository.findByVectorStatusOrderByUploadedAtDesc(vectorStatus);
knowledgeBaseRepository.findAllByOrderByUploadedAtDesc();
entities = sortEntities(entities, sortBy);
GET /api/knowledgebase/{id} 获取知识库详情
调用链:
listService.getKnowledgeBase(id);
knowledgeBaseRepository.findById(id);
DELETE /api/knowledgebase/{id} 删除知识库
核心流程:
deleteService.deleteKnowledgeBase(id);
knowledgeBaseRepository.findById(id);
sessionRepository.findByKnowledgeBaseIds(List.of(id));
vectorService.deleteByKnowledgeBaseId(id);
storageService.deleteKnowledgeBase(kb.getStorageKey());
knowledgeBaseRepository.deleteById(id);
说明:
- 先清理 RAG 会话关联,再删除向量与对象存储文件,最后删除数据库记录。
- 向量删除和对象存储删除失败仅
warn,不阻断主删除流程。
POST /api/knowledgebase/query 非流式问答(多知识库)
限流:
- GLOBAL/IP 各 10
调用链:
queryService.queryKnowledgeBase(request);
answerQuestion(...);
countService.updateQuestionCounts(...);
vectorService.similaritySearch(...);
处理要点:
- 入参
knowledgeBaseIds与question必填。 - 无命中返回固定文案“未检索到信息”。
- 有命中则拼接 context,构建提示词后调用默认 ChatClient 生成答案。
- 返回
QueryResponse(answer, primaryKbId, kbNamesStr)。
POST /api/knowledgebase/query/stream 流式问答(SSE,多知识库)
限流:
- GLOBAL/IP 各 5
调用链:
queryService.answerQuestionStream(kbIds, question);
countService.updateQuestionCounts(...);
vectorService.similaritySearch(...);
chatClient.prompt().stream().content();
normalizeStreamOutput(...);
处理要点:
- 返回类型为
Flux<String>(text/event-stream)。 - 空输入或无检索命中时返回固定降级文本流。
- 流内异常与外层异常都做统一降级输出。
GET /api/knowledgebase/categories 获取所有分类名
调用链:
listService.getAllCategories();
返回:
Result<List<String>>
GET /api/knowledgebase/category/{category} 按分类获取知识库列表
调用链:
listService.listByCategory(category);
返回:
Result<List<KnowledgeBaseListItemDTO>>
GET /api/knowledgebase/uncategorized 获取未分类知识库列表
调用链:
listService.listByCategory(category);
说明:
- 当前实现沿用分类查询路径,通过特定分类值区分未分类集合。
PUT /api/knowledgebase/{id}/category 更新知识库分类
调用链:
listService.updateCategory(id, body.get("category"));
处理要点:
- 先按
id查记录,不存在抛业务异常。 - 存在则更新
category字段并保存。
POST /api/knowledgebase/upload 上传知识库文件(multipart)
参数:
file(必填)name(可选)category(可选)
限流:
- GLOBAL/IP 各 3
调用链:
uploadService.uploadKnowledgeBase(file, name, category);
findByFileHash(fileHash);
处理流程:
- 校验文件非空、大小(最大 50MB)。
- 按 MIME + 扩展名白名单校验类型(PDF/DOCX/DOC/TXT/MD)。
- 计算
SHA-256并做去重。 - 解析正文,空文本直接失败。
- 上传原文件到 RustFS(S3 兼容),生成
fileKey/fileUrl。 - 落库
KnowledgeBaseEntity,初始向量状态PENDING。 - 投递异步向量化任务到 Redis Stream(
knowledgebase:vectorize:stream)。 - 返回
knowledgeBase + storage + duplicate=false。
GET /api/knowledgebase/{id}/download 下载原始知识库文件
调用链:
listService.getEntityForDownload(id);
listService.downloadFile(id);
返回:
ResponseEntity<byte[]>(包含Content-Disposition与Content-Type)
GET /api/knowledgebase/search?keyword=... 关键字搜索知识库
调用链:
listService.search(keyword);
GET /api/knowledgebase/stats 获取知识库统计信息
调用链:
listService.getStatistics();
返回:
KnowledgeBaseStatsDTO
POST /api/knowledgebase/{id}/revectorize 手动重新向量化
限流:
- GLOBAL/IP 各 2
调用链:
uploadService.revectorize(id);
处理流程:
- 按
id查知识库,不存在抛异常。 - 从对象存储下载原文件并重新解析文本。
- 解析失败或空文本直接失败。
- 更新向量状态为
PENDING。 - 投递向量化任务到 Redis Stream。
- 立即返回成功,前端轮询状态。
异步向量化处理流程(核心实现)
// 1) 删除旧向量
deleteByKnowledgeBaseId(knowledgeBaseId);
// 2) 文本切片(默认无重叠)
List<Document> chunks = textSplitter.apply(List.of(new Document(content)));
// 3) 写入 metadata(kb_id)
chunks.forEach(chunk -> chunk.getMetadata().put("kb_id", knowledgeBaseId.toString()));
// 4) 分批向量化写入(DashScope batch <= 10)
for (int i = 0; i < batchCount; i++) {
int start = i * MAX_BATCH_SIZE;
int end = Math.min(start + MAX_BATCH_SIZE, totalChunks);
List<Document> batch = chunks.subList(start, end);
vectorStore.add(batch);
}
小结
Knowledgebase 模块的核心是把“文件资产管理”和“检索增强问答”打通。对我来说,真正有价值的不只是上传成功,而是文档能够稳定进入向量化链路,并最终在问答场景里提供可复用、可追踪的知识支持。