JeecgBoot RAG 知识库引擎
JeecgBoot RAG 知识库引擎
学习目标
通过本章学习,你将能够:
- 理解 JeecgBoot 的 RAG 全流程:文档摄入 → 分段 → 向量化 → 检索
- 掌握可配置分段策略的实现方案
- 了解多知识库 QueryRouter 的路由机制
- 掌握 pgvector 向量存储的缓存与元数据过滤设计
前置知识
本章涉及 RAG 管线的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 JeecgBoot 的具体实现。
项目实践
文档摄入流水线
JeecgBoot 支持三种文档来源:
| 来源类型 | 实现方式 |
|---|---|
| 文件上传 | 本地文件路径 → Apache Tika 解析 |
| 网页抓取 | URL → Jsoup/WebPageParser 提取正文 |
| 文本录入 | 直接输入文本 → 跳过解析 |
多格式解析:
文件上传 ↓TikaDocumentParser(Apache Tika 3.2.3)├── tika-parser-pdf-module → PDF├── tika-parser-html-module → HTML├── tika-parser-text-module → 纯文本└── langchain4j-document-parser-apache-poi → Word/PPT/Excel ↓Document(langchain4j 统一文档模型)可配置分段策略
这是 JeecgBoot RAG 系统的核心特色之一。分段参数不在代码中硬编码,而是存储在知识库配置中:
Knowledge 配置项├── enableSegment # 是否开启默认分段(字符串:"enableSegment")├── segmentStrategy # auto(自动)| custom(自定义)├── maxSegment # 分段长度,默认 1000 字符├── overlap # 重叠率,0-90%├── separator # 分隔符:\n | \n\n | 。 | ! | ? | . | ! | ? | custom├── customSeparator # 自定义分隔符└── textRules # 文本预处理规则 ├── cleanSpaces # 替换连续空格/换行/制表符 └── removeUrlsEmails # 删除 URL 和邮箱地址分段实现:
// 伪代码:CustomDocumentSplitterDocumentSplitter createSplitter(knowledgeConfig) { int chunkSize = config.maxSegment ?? 1000; int overlap = config.overlap * chunkSize / 100; String separator = resolveSeparator(config.separator, config.customSeparator);
return DocumentSplitters.recursive(chunkSize, overlap, separator);}分隔符解析:
| separator 值 | 实际分隔符 |
|---|---|
\n | 换行符 |
\n\n | 双换行(段落边界) |
。 | 中文句号 |
! | 中文叹号 |
? | 中文问号 |
. | 英文句号 |
custom | 用户自定义的 customSeparator |
设计价值:不同文档类型适合不同的分段策略。技术文档按换行分段更准确,而文学性文本按句号分段能保持语义完整性。用户可根据知识库内容选择最优策略。
向量化与存储
向量存储架构:
TextSegment[](分段结果) ↓EmbeddingModel(可配置的嵌入模型) ↓Embedding[] ↓PgVectorEmbeddingStore(PostgreSQL + pgvector) ├── 向量数据 → pgvector 列 └── 元数据 → metadata JSONB 列 ├── knowledgeId # 知识库 ID(隔离不同知识库) ├── username # 创建用户(租户隔离) ├── docId # 文档 ID(溯源) ├── docName # 文档名称 └── createTime # 创建时间向量存储缓存:
// 内存中的向量存储缓存private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE;
EmbeddingStore getStore(knowledgeId) { return EMBED_STORE_CACHE.computeIfAbsent(knowledgeId, id -> PgVectorEmbeddingStore.builder() .host(embedStoreConfigBean.getHost()) .database(embedStoreConfigBean.getDatabase()) .table("knowledge_" + id) // 每个知识库独立的表 .dimension(embedModel.getDimension()) .build() );}设计亮点:每个知识库对应 pgvector 中的独立表(knowledge_{id}),实现了数据物理隔离。缓存避免重复创建连接。
多知识库 QueryRouter 路由
当 AI 应用绑定了多个知识库时,通过 DefaultQueryRouter 统一路由:
// 伪代码:getQueryRouterQueryRouter getQueryRouter(List<String> knowIds, int topNumber, double similarity) { List<ContentRetriever> retrievers = new ArrayList<>();
for (String knowId : knowIds) { EmbeddingStore store = getStore(knowId); EmbeddingModel embedModel = getEmbeddingModel(knowId);
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embedModel) .maxResults(topNumber) .minScore(similarity) .build();
retrievers.add(retriever); }
return new DefaultQueryRouter(retrievers);}检索参数来源:AI 应用界面可独立配置 topNumber(返回文档数)和 similarity(相似度阈值),这些参数会覆盖知识库的默认值。
知识库文档生命周期
构建流程:
- 上传文档 → 状态设为
building - Tika 解析 → 分段 → 向量化入库
- 成功 → 状态设为
complete - 失败 → 状态设为
failed,记录错误信息
问题与规避
| 陷阱 | 表现 | 对策 |
|---|---|---|
| 分段过长导致向量匹配不精确 | chunkSize=1000 但实际分段远超 | 设置 maxSegment 上限,超出强制截断 |
| Markdown 图片链接被当作分段边界 |  中包含换行 | 分段前先提取 Markdown 图片链接,避免在图片语法中间分割 |
| pgvector 表名冲突 | 知识库 ID 包含特殊字符 | 表名使用 knowledge_ 前缀 + 数字 ID |
| 嵌入模型切换导致已有向量失效 | 不同模型维度不同 | 切换嵌入模型时标记已有文档为”需重建” |
| 文档更新后旧向量未删除 | 修改文档内容但只追加新向量 | 删除旧 docId 对应的向量后再插入新向量 |
设计取舍
物理隔离 vs 逻辑隔离
JeecgBoot 选择:每个知识库独立的 pgvector 表(物理隔离)。
| 方案 | 优势 | 代价 |
|---|---|---|
| 物理隔离(独立表) | 数据隔离彻底,可按知识库删除/重建 | 表数量多时管理复杂 |
| 逻辑隔离(metadata 过滤) | 单表管理简单 | 删除知识库数据需要扫描全表 |
在多租户 SaaS 场景中,物理隔离更符合数据安全要求。
同步向量化 vs 异步向量化
JeecgBoot 选择:同步向量化(用户上传后等待构建完成)。
| 方案 | 体验 | 复杂度 |
|---|---|---|
| 同步 | 用户知道进度,但大文件等待时间长 | 低 |
| 异步 | 用户可继续操作,但需要状态轮询 | 高(需要任务队列、状态通知) |
对于企业级文档(通常几 MB 以内),同步方案已够用。
参考来源
- langchain4j RAG 文档
- pgvector 官方文档
- JeecgBoot 源码 v3.9.2,
EmbeddingHandler.java、CustomDocumentSplitter.java