知识库模块
约 763 字大约 3 分钟
布欧-Lewyon
2026-05-15
首页 › Spring AI › 实战项目:AI 智能客服系统
知识库模块负责文档的上传、解析、向量化和检索,是 RAG 系统的数据基础。
配置
app:
knowledge:
upload-path: /data/knowledge/ # 上传文件存储路径
chunk-size: 500 # 切分大小 (Token)
chunk-overlap: 50 # 重叠 Token 数
max-file-size: 10MB # 单文件最大大小
allowed-formats: pdf,txt,md,docx # 支持的文件格式实体
@Entity
@Table(name = "knowledge_docs")
public class KnowledgeDoc {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String filename;
@Column(nullable = false)
private String fileType; // pdf, txt, md
private String category; // 分类(faq, product, policy)
private Long fileSize;
private Integer chunkCount; // 切块数
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false)
private String status; // processing, ready, failed
private String errorMessage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}知识库服务
@Service
public class KnowledgeService {
private final KnowledgeDocRepository repository;
private final VectorStore vectorStore;
private final TokenTextSplitter splitter;
private final Path uploadDir;
public KnowledgeService(KnowledgeDocRepository repository,
VectorStore vectorStore,
@Value("${app.knowledge.upload-path}") String uploadPath) {
this.repository = repository;
this.vectorStore = vectorStore;
this.splitter = TokenTextSplitter.builder()
.withChunkSize(500)
.withChunkOverlap(50)
.build();
this.uploadDir = Paths.get(uploadPath);
}
// 上传并处理文档
public KnowledgeDoc uploadAndProcess(MultipartFile file, String category) {
// 1. 保存文件
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path filePath = uploadDir.resolve(filename);
try {
Files.createDirectories(uploadDir);
file.transferTo(filePath.toFile());
} catch (IOException e) {
throw new RuntimeException("Failed to save file", e);
}
// 2. 创建文档记录
KnowledgeDoc doc = new KnowledgeDoc();
doc.setFilename(file.getOriginalFilename());
doc.setFileType(getExtension(file.getOriginalFilename()));
doc.setCategory(category);
doc.setFileSize(file.getSize());
doc.setStatus("processing");
doc.setCreatedAt(LocalDateTime.now());
repository.save(doc);
// 3. 异步处理(向量化)
processAsync(doc, filePath);
return doc;
}
@Async
public void processAsync(KnowledgeDoc doc, Path filePath) {
try {
// 3.1 解析文档
TikaDocumentReader reader = new TikaDocumentReader(
new FileSystemResource(filePath.toFile()));
List<Document> documents = reader.get();
// 3.2 添加元数据
documents.forEach(d -> {
d.getMetadata().put("docId", doc.getId().toString());
d.getMetadata().put("category", doc.getCategory());
d.getMetadata().put("source", doc.getFilename());
});
// 3.3 切分
List<Document> chunks = splitter.apply(documents);
// 3.4 向量化并存入
vectorStore.add(chunks);
// 3.5 更新状态
doc.setStatus("ready");
doc.setChunkCount(chunks.size());
} catch (Exception e) {
log.error("Failed to process document: {}", doc.getFilename(), e);
doc.setStatus("failed");
doc.setErrorMessage(e.getMessage());
}
repository.save(doc);
}
// 重新索引
public void reindex(Long docId) {
KnowledgeDoc doc = repository.findById(docId)
.orElseThrow(() -> new RuntimeException("Document not found"));
Path filePath = findFile(doc);
doc.setStatus("processing");
repository.save(doc);
processAsync(doc, filePath);
}
// 删除文档和对应向量
public void delete(Long docId) {
KnowledgeDoc doc = repository.findById(docId)
.orElseThrow(() -> new RuntimeException("Document not found"));
// 删除向量(通过 metadata.docId 过滤)
vectorStore.delete(List.of(docId.toString()));
repository.delete(doc);
}
// RAG 检索
public List<Document> search(String query, int topK, String category) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(0.5)
.build();
if (category != null && !category.isEmpty()) {
request.setFilterExpression("category == '" + category + "'");
}
return vectorStore.similaritySearch(request);
}
@GetMapping("/api/admin/knowledge")
public List<KnowledgeDoc> listDocs() {
return repository.findAllByOrderByCreatedAtDesc();
}
private String getExtension(String filename) {
return filename.substring(filename.lastIndexOf('.') + 1);
}
private Path findFile(KnowledgeDoc doc) {
// 根据数据库记录查找实际文件
try {
return Files.list(uploadDir)
.filter(p -> p.getFileName().toString().contains(doc.getFilename()))
.findFirst()
.orElseThrow(() -> new RuntimeException("File not found"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}知识库管理 API
@RestController
@RequestMapping("/api/admin/knowledge")
public class KnowledgeController {
private final KnowledgeService knowledgeService;
@PostMapping("/upload")
public KnowledgeDoc upload(
@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "faq") String category) {
return knowledgeService.uploadAndProcess(file, category);
}
@PostMapping("/{id}/reindex")
public void reindex(@PathVariable Long id) {
knowledgeService.reindex(id);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
knowledgeService.delete(id);
}
@GetMapping
public List<KnowledgeDoc> list() {
return knowledgeService.listDocs();
}
}小结
- 知识库流程:上传文件 → 保存到磁盘 → Tika 解析 → TokenTextSplitter 切分 → 向量化 → 存入 PGVector。
- 异步处理:
@Async避免文件上传接口阻塞。 - 状态管理:processing → ready / failed,支持重新索引。
- 检索支持按 category 过滤,提升相关度。
- 删除文档时同步清理向量。
上一节:项目架构与准备 下一节:对话与 Tool 模块
