Spring AI 1.x 系列【37】RAG 知识库平台案例:知识库管理

文章目录

  • [1. 功能概述](#1. 功能概述)
    • [1.1 知识库管理](#1.1 知识库管理)
    • [1.2 核心能力](#1.2 核心能力)
    • [1.3 整体流程](#1.3 整体流程)
  • [2. 从零实现知识库管理](#2. 从零实现知识库管理)
    • [2.1 创建知识库模型](#2.1 创建知识库模型)
    • [2.2 实现 KnowledgeBaseService](#2.2 实现 KnowledgeBaseService)
    • [2.3 实现 VectorStoreManager](#2.3 实现 VectorStoreManager)
    • [2.4 实现 DocumentProcessingService](#2.4 实现 DocumentProcessingService)
    • [2.5 实现 KnowledgeBaseController](#2.5 实现 KnowledgeBaseController)
    • [2.6 前端页面实现](#2.6 前端页面实现)
    • [2.7 调用示例](#2.7 调用示例)
  • [3. 架构小结](#3. 架构小结)

1. 功能概述

1.1 知识库管理

Demo 演示了一种多知识库隔离 + 文档向量化 的管理思路:用户可以创建多个独立的知识库,每个知识库绑定不同的 Embedding 模型供应商,上传文档后自动完成解析、分块、向量化全流程,最终在对话时按知识库名称检索对应的向量数据。

这套思路的通用价值在于:通过元数据knowledge_name实现同一向量存储中的多租户隔离,无需为每个知识库单独部署向量数据库。无论是文件型、手动录入型、网页采集型还是数据库型知识库,核心的「上传 → 解析 → 分块 → 向量化 → 检索」流水线保持不变。

1.2 核心能力

多知识库管理 :创建、删除、启用/禁用知识库,每个知识库独立配置 Embedding 供应商。

文档自动处理 :上传 PDF / DOCX / HTML / MD / TXT 等格式,自动解析并分块。

可配置的分块策略:支持按长度切分,提供块大小、重叠量、最小字符数等参数调节。

其他功能

  • 向量化存储 :分块后自动向量化并写入向量数据库,元数据注入 knowledge_name 实现隔离
  • 分块可视化:详情页展示每个文档的分块预览和元数据,方便调试检索效果
  • 命中测试:提供检索测试入口,验证知识库的召回质量

1.3 整体流程


2. 从零实现知识库管理

以下从数据模型开始,逐步实现完整的知识库管理模块。

2.1 创建知识库模型

KnowledgeBase 是知识库的核心 DTO,存储名称、类型、绑定的嵌入模型、文档列表等所有状态。

java 复制代码
public class KnowledgeBase {

    private String name;                      // 唯一标识
    private String description;               // 描述
    private String knowledgeType;             // file / manual / web / database
    private String embeddingModelName;        // 绑定的 Embedding 供应商,如 ZhiPu
    private int documentCount;                // 文档数量
    private boolean enabled;                  // 是否启用
    private final List<DocumentInfo> documents = new ArrayList<>();  // 文档列表
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    // 文档信息
    public static class DocumentInfo {
        private final String name;            // 文件名
        private final long size;              // 字节大小
        private final int chunks;             // 分块数量
        private final String uploadTime;
        private final List<ChunkDetail> chunkDetails;  // 分块详情
    }

    // 分块详情
    public static class ChunkDetail {
        private final int index;              // 分块序号
        private final String preview;         // 内容预览(前 200 字符)
        private final String metadataJson;    // 元数据 JSON
    }

    // 添加上传文档
    public void addDocument(String name, long size, int chunks) {
        documents.add(new DocumentInfo(name, size, chunks, List.of()));
        this.documentCount = documents.size();
        this.updateTime = LocalDateTime.now();
    }

    // 更新最后一个文档的分块详情(处理完成后回填)
    public void updateLastDocumentChunks(int chunks, List<ChunkDetail> chunkDetails) {
        if (!documents.isEmpty()) {
            var last = documents.get(documents.size() - 1);
            documents.set(documents.size() - 1,
                new DocumentInfo(last.getName(), last.getSize(), chunks, chunkDetails));
        }
    }
}

2.2 实现 KnowledgeBaseService

知识库数据存储在当前 Demo 中使用 ConcurrentHashMap(内存),生产环境可替换为 MySQL / Redis

java 复制代码
@Service
public class KnowledgeBaseService {

    private final Map<String, KnowledgeBase> store = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        // 预置 4 个示例知识库
        create("技术文档库", "包含 Spring AI、Milvus、RAG 相关技术文档", "file", "ZhiPu");
        create("常见问题库", "用户常见问题与解答", "manual", "DashScope");
        create("外部教程库", "采集外部技术博客和教程", "web", "ZhiPu");
        create("数据表格库", "结构化数据与统计表格", "database", "DashScope");
    }

    public List<KnowledgeBase> list(String name) {
        List<KnowledgeBase> all = new ArrayList<>(store.values());
        if (name != null && !name.trim().isEmpty()) {
            String keyword = name.trim().toLowerCase();
            all = all.stream()
                    .filter(kb -> kb.getName().toLowerCase().contains(keyword))
                    .collect(Collectors.toList());
        }
        all.sort(Comparator.comparing(KnowledgeBase::getUpdateTime).reversed());
        return all;
    }

    public KnowledgeBase get(String name) {
        return store.get(name);
    }

    public KnowledgeBase create(String name, String description,
                                 String knowledgeType, String embeddingModelName) {
        KnowledgeBase kb = new KnowledgeBase(name, description, knowledgeType);
        kb.setEmbeddingModelName(embeddingModelName);
        store.put(name, kb);
        return kb;
    }

    public void delete(String name) {
        store.remove(name);
    }

    public void enable(String name) {
        KnowledgeBase kb = store.get(name);
        if (kb != null) { kb.setEnabled(true); kb.setUpdateTime(LocalDateTime.now()); }
    }

    public void disable(String name) {
        KnowledgeBase kb = store.get(name);
        if (kb != null) { kb.setEnabled(false); kb.setUpdateTime(LocalDateTime.now()); }
    }
}

2.3 实现 VectorStoreManager

VectorStoreManager 是向量存储的统一入口。它按 Embedding 供应商缓存 SimpleVectorStore 实例,不同供应商使用各自的嵌入模型,同一个供应商下的多个知识库共享一个向量存储,通过元数据 knowledge_name 实现隔离。

java 复制代码
@Component
public class VectorStoreManager {

    private final KnowledgeBaseService knowledgeBaseService;
    private final ModelFactory modelFactory;
    private final Map<String, VectorStore> storeCache = new ConcurrentHashMap<>();

    public VectorStoreManager(KnowledgeBaseService knowledgeBaseService,
                               ModelFactory modelFactory) {
        this.knowledgeBaseService = knowledgeBaseService;
        this.modelFactory = modelFactory;
    }

    /**
     * 根据知识库名称获取对应的 VectorStore。
     * 查找该知识库绑定的 Embedding 供应商,按供应商缓存实例。
     */
    public VectorStore getVectorStore(String knowledgeName) {
        KnowledgeBase kb = knowledgeBaseService.get(knowledgeName);
        String provider = kb != null ? kb.getEmbeddingModelName() : "ZhiPu";

        return storeCache.computeIfAbsent(provider, key -> {
            EmbeddingModel embeddingModel = modelFactory.getEmbeddingModel(key);
            return SimpleVectorStore.builder(embeddingModel).build();
        });
    }
}

2.4 实现 DocumentProcessingService

这是 RAG 流水线的核心------上传、解析、分块、向量化写入全流程。

java 复制代码
@Service
public class DocumentProcessingService {

    private final VectorStoreManager vectorStoreManager;

    // 文件临时缓存:knowledgeName → bytes + fileName
    private final Map<String, byte[]> fileCache = new ConcurrentHashMap<>();
    private final Map<String, String> fileNameCache = new ConcurrentHashMap<>();

    /** 缓存上传文件,等待用户配置分块参数后处理 */
    public void cacheFile(String knowledgeName, String fileName, byte[] bytes) {
        fileCache.put(knowledgeName, bytes);
        fileNameCache.put(knowledgeName, fileName);
    }

    /** 按用户选择的分块参数处理缓存文件 */
    public ProcessResult processCached(String knowledgeName, int chunkSize, int chunkOverlap)
            throws IOException {
        byte[] bytes = fileCache.get(knowledgeName);
        String fileName = fileNameCache.get(knowledgeName);
        if (bytes == null) throw new IOException("没有待处理的文件,请先上传");
        fileCache.remove(knowledgeName);
        fileNameCache.remove(knowledgeName);
        return processBytes(bytes, fileName, knowledgeName, chunkSize, chunkOverlap);
    }

    /** 核心处理流程 */
    public ProcessResult processBytes(byte[] bytes, String fileName, String knowledgeName,
                                       int chunkSize, int chunkOverlap) throws IOException {
        // 1. 解析文档 ------ Apache Tika 自动识别格式并提取文本
        TikaDocumentReader reader = new TikaDocumentReader(
                new InputStreamResource(new ByteArrayInputStream(bytes)));
        List<Document> documents = reader.get();

        // 2. 注入元数据
        for (Document doc : documents) {
            doc.getMetadata().put("knowledge_name", knowledgeName);
            doc.getMetadata().put("file_name", fileName);
        }

        // 3. 文本分块 ------ TokenTextSplitter 按 Token 数切分
        TokenTextSplitter splitter = new TokenTextSplitter(
                chunkSize, chunkOverlap, 5, 10000, true,
                List.of('.', '?', '!', '\n'));
        List<Document> chunks = splitter.apply(documents);

        // 4. 收集分块预览
        List<ProcessResult.ChunkInfo> chunkInfos = new ArrayList<>();
        for (int i = 0; i < chunks.size(); i++) {
            Document chunk = chunks.get(i);
            chunk.getMetadata().put("chunk_index", i);
            chunk.getMetadata().put("chunk_total", chunks.size());
            String preview = chunk.getText() != null ? chunk.getText() : "";
            if (preview.length() > 200) preview = preview.substring(0, 200) + "...";
            chunkInfos.add(new ProcessResult.ChunkInfo(i, preview,
                    new LinkedHashMap<>(chunk.getMetadata())));
        }

        // 5. 写入向量存储(按 knowledgeName 获取对应的 VectorStore)
        vectorStoreManager.getVectorStore(knowledgeName).add(chunks);

        return new ProcessResult(fileName, bytes.length, chunks.size(), chunkInfos);
    }

    public static class ProcessResult {
        private final String fileName;
        private final long fileSize;
        private final int chunkCount;
        private final List<ChunkInfo> chunks;

        public static class ChunkInfo {
            private final int index;
            private final String preview;
            private final Map<String, Object> metadata;
        }
    }
}

2.5 实现 KnowledgeBaseController

Controller 同时提供页面路由和 REST API。页面使用 Thymeleaf 渲染,API 返回 JSON

java 复制代码
@Controller
public class KnowledgeBaseController {

    private final KnowledgeBaseService knowledgeBaseService;
    private final EmbeddingModelService embeddingModelService;
    private final DocumentProcessingService documentProcessingService;

    // ==================== 页面路由 ====================

    @GetMapping("/knowledge")
    public String list(@RequestParam(defaultValue = "1") int pageNo,
                       @RequestParam(defaultValue = "10") int pageSize,
                       @RequestParam(defaultValue = "") String name, Model model) {
        List<KnowledgeBase> all = knowledgeBaseService.list(name);

        // 手动分页
        int total = all.size();
        int totalPages = Math.max((int) Math.ceil((double) total / pageSize), 1);
        int from = (pageNo - 1) * pageSize;
        int to = Math.min(from + pageSize, total);
        List<KnowledgeBase> page = from < total ? all.subList(from, to) : List.of();

        model.addAttribute("knowledgeBases", page);
        model.addAttribute("totalPages", totalPages);
        model.addAttribute("totalElements", total);
        model.addAttribute("currentPage", pageNo);
        model.addAttribute("searchName", name);
        model.addAttribute("embeddingModels", embeddingModelService.listEnabled());
        return "knowledge/list";
    }

    @GetMapping("/knowledge/{name}")
    public String detail(@PathVariable String name, Model model) {
        KnowledgeBase kb = knowledgeBaseService.get(name);
        if (kb == null) return "redirect:/knowledge";
        model.addAttribute("kb", kb);
        model.addAttribute("tab", "docs");
        return "knowledge/detail";
    }

    // ==================== REST API ====================

    @PostMapping("/admin/knowledge")
    @ResponseBody
    public ResponseEntity<KnowledgeBase> create(@RequestBody Map<String, String> body) {
        String name = body.get("name");
        String description = body.get("description");
        String knowledgeType = body.get("knowledgeType");
        String embeddingModelName = body.get("embeddingModelName");
        if (name == null || name.trim().isEmpty()) return ResponseEntity.badRequest().build();
        return ResponseEntity.ok(
                knowledgeBaseService.create(name.trim(), description, knowledgeType, embeddingModelName));
    }

    @DeleteMapping("/admin/knowledge")
    @ResponseBody
    public ResponseEntity<Map<String, String>> delete(@RequestParam String name) {
        knowledgeBaseService.delete(name);
        return ResponseEntity.ok(Map.of("code", "ok"));
    }

    @PostMapping("/admin/knowledge/enable")
    @ResponseBody
    public ResponseEntity<Map<String, String>> enable(@RequestParam String name) {
        knowledgeBaseService.enable(name);
        return ResponseEntity.ok(Map.of("code", "ok"));
    }

    @PostMapping("/admin/knowledge/{name}/upload")
    @ResponseBody
    public ResponseEntity<Map<String, Object>> uploadDocument(
            @PathVariable String name, @RequestParam("file") MultipartFile file) {
        KnowledgeBase kb = knowledgeBaseService.get(name);
        if (kb == null) return ResponseEntity.badRequest().body(Map.of("message", "知识库不存在"));

        String fileName = file.getOriginalFilename();
        long fileSize = file.getSize();
        documentProcessingService.cacheFile(name, fileName, file.getBytes());

        kb.addDocument(fileName, fileSize, 0);
        return ResponseEntity.ok(Map.of("code", "ok", "fileName", fileName,
                "fileSize", String.valueOf(fileSize)));
    }

    @PostMapping("/admin/knowledge/{name}/process")
    @ResponseBody
    public ResponseEntity<Map<String, Object>> processDocument(
            @PathVariable String name, @RequestBody Map<String, Object> config) {
        KnowledgeBase kb = knowledgeBaseService.get(name);
        if (kb == null) return ResponseEntity.badRequest().body(Map.of("message", "知识库不存在"));

        int chunkSize = config.get("chunkSize") != null
                ? ((Number) config.get("chunkSize")).intValue() : 800;
        DocumentProcessingService.ProcessResult result =
                documentProcessingService.processCached(name, chunkSize, 50);

        // 将分块详情回填到 KnowledgeBase
        List<KnowledgeBase.ChunkDetail> details = new ArrayList<>();
        for (var ci : result.getChunks()) {
            String metaJson = new ObjectMapper().writeValueAsString(ci.getMetadata());
            details.add(new KnowledgeBase.ChunkDetail(ci.getIndex(), ci.getPreview(), metaJson));
        }
        kb.updateLastDocumentChunks(result.getChunkCount(), details);
        return ResponseEntity.ok(Map.of("code", "ok", "chunkCount",
                String.valueOf(result.getChunkCount())));
    }
}

2.6 前端页面实现

列表页(templates/knowledge/list.html):

  • 卡片网格展示所有知识库,每张卡片显示名称、类型标签、文档数、嵌入模型、启用状态
  • 搜索栏 + 分页
  • 创建按钮弹出模态框:填写名称、类型、嵌入模型、描述
  • 每个卡片提供启用/禁用、删除操作

详情页(templates/knowledge/detail.html):

  • 三个标签页:文档列表 / 命中测试 / 设置
  • 文档列表:已有文档的表格展示(名称、大小、分块数、上传时间),支持查看切片详情和元数据
  • 上传文档流程:拖拽/点击上传 → 选择切片方式 → 配置分块参数 → 确认并开始处理
  • 命中测试:提供检索测试入口(UI 占位,核心逻辑在 ChatService 中实现)

前端交互(static/js/knowledge.js):

javascript 复制代码
// 核心 API 调用封装
async function apiCall(url, options = {}) {
    const response = await fetch(url, {
        headers: { 'Content-Type': 'application/json' },
        ...options
    });
    if (!response.ok) {
        const data = await response.json();
        throw new Error(data.message || '请求失败');
    }
    return response.json();
}

// 创建知识库
async function createKnowledgeBase() {
    const name = document.getElementById('kbName').value.trim();
    const knowledgeType = document.getElementById('kbType').value;
    const embeddingModelName = document.getElementById('kbEmbeddingModel')?.value || '';
    const description = document.getElementById('kbDescription').value.trim();

    await apiCall('/admin/knowledge', {
        method: 'POST',
        body: JSON.stringify({ name, description, knowledgeType, embeddingModelName })
    });
    location.reload();
}

// 上传文档 + 处理流程(detail.html 内联脚本)
// 1. 拖拽/选择文件 → POST /admin/knowledge/{name}/upload (FormData)
// 2. 选择切片方式 + 配置参数
// 3. POST /admin/knowledge/{name}/process → 返回 chunkCount
// 4. 刷新页面展示文档列表和分块详情

2.7 调用示例

java 复制代码
// 创建知识库
KnowledgeBase kb = knowledgeBaseService.create(
    "技术文档库", "Spring AI 相关文档", "file", "ZhiPu");

// 上传并处理文档
byte[] fileBytes = Files.readAllBytes(Path.of("docs/spring-ai.pdf"));
documentProcessingService.cacheFile("技术文档库", "spring-ai.pdf", fileBytes);
ProcessResult result = documentProcessingService.processCached("技术文档库", 800, 50);
// → 输出: 共 42 个分块已写入向量库

// 获取该知识库的 VectorStore(用于检索)
VectorStore store = vectorStoreManager.getVectorStore("技术文档库");
List<Document> hits = store.similaritySearch(
    SearchRequest.builder().query("什么是 RAG")
        .topK(5).similarityThreshold(0.7)
        .filterExpression("knowledge_name == \"技术文档库\"")
        .build());

3. 架构小结

复制代码
┌──────────────────────────────────────────────────────┐
│  Controller                                           │
│  GET /knowledge          → 列表页                     │
│  GET /knowledge/{name}   → 详情页                     │
│  POST /admin/knowledge   → 创建                       │
│  POST .../upload         → 上传文件(缓存 bytes)      │
│  POST .../process        → 分块 + 向量化              │
└──────────┬───────────────────────────────────────────┘
           │
    ┌──────┴──────┐
    │             ▼
    │  KnowledgeBaseService (ConcurrentHashMap)
    │   ├─ store: Map<name, KnowledgeBase>
    │   └─ KnowledgeBase.documents: List<DocumentInfo>
    │
    ▼
DocumentProcessingService
  ├─ 1. cacheFile()       → 暂存 bytes + fileName
  ├─ 2. processCached()   → 取出缓存,调用 processBytes()
  └─ 3. processBytes()
       ├─ TikaDocumentReader   → 解析 PDF/DOCX/HTML/MD
       ├─ TokenTextSplitter    → 按 Token 数切分
       └─ VectorStore.add()    → 向量化 + 写入存储
            │
            ▼
VectorStoreManager
  └─ storeCache: Map<provider, SimpleVectorStore>
       └─ 按 Embedding 供应商缓存,元数据 knowledge_name 隔离

核心设计要点:

  • 两阶段上传:先上传缓存文件,用户配置分块参数后再触发处理,避免上传大文件后自动处理不符合预期
  • 按供应商缓存向量存储 :同一供应商下的多个知识库共享 SimpleVectorStore,通过 knowledge_name 元数据隔离,简单高效
  • 分块详情回溯 :处理后把每个 chunk 的预览和元数据回填到 KnowledgeBase,前端可直接展示,无需反查向量库
  • 可切换存储后端VectorStoreManager 只依赖 VectorStore 接口,将 SimpleVectorStore 替换为 Milvus 等远程向量库零侵入
相关推荐
大C聊AI4 分钟前
通用大模型纷纷收费,垂直场景AI工具的价值正在被重估
大数据·人工智能·机器学习·办公效率·ai 工具·智标领航·ai 辅助办公
苏州邦恩精密8 分钟前
2026江苏GOM三维扫描仪定制厂家找哪家?企业数字化转型视角
人工智能·机器学习·3d·自动化·制造
python-码博士9 分钟前
PyTorch 从零实现 Flow Matching:训练、采样、画图一条龙
人工智能·pytorch·python
砍光二叉树12 分钟前
一文打通 AI 认知:LLM、Agent、MCP、Skill 完整体系
人工智能·llm·agent·skill·mcp
努力写A题的小菜鸡21 分钟前
PyTorch 图像预处理 transforms 与 TensorBoard 可视化 (自己学习记录)
人工智能·pytorch·学习
测试仪器廖生1359025638524 分钟前
罗德与施瓦茨 FSP13频谱分析仪FSP30
网络·人工智能·算法
未来和明天25 分钟前
领嵌iLeadE-588边缘计算盒子16路AI视频分析、4路AHD、4路千兆网接口
人工智能·边缘计算
上海锝秉工控29 分钟前
省线型增量编码器:用“减法思维“重构工业控制的未来
网络·人工智能·重构
蓝星空200029 分钟前
怎么使用 Image 2 高效生成商业级 AI 图像(GPT-Image-2 全流程实操教程)
人工智能·gpt·ai作画
沉下去,苦磨练!31 分钟前
张量的形状操作以及拼接
人工智能