# **知识库引擎数据模型设计审查报告** 文档状态: ✅ 已确认 (v2.0 融合版) 审查日期: 2026-01-20 审查对象: \[04-数据模型设计.md\] & 架构师建议方案 适用架构: Postgres-Only (Prisma \+ pgvector \+ pg-boss) 目标读者: 开发团队、架构师 ## **1\. 核心决策摘要 (Executive Summary)** 经过对业务需求(文献、病历、多模态)和团队现状(2人开发、运维极简)的深度评估,我们做出了以下关键技术决策: ### **✅ 决策 1:采用 "Unified Schema"(统一模型)策略** * **结论**:所有类型的知识库(个人文献库、系统规则库、病历库)**共用一套数据库表结构**。 * **理由**: * **运维成本**:避免维护多套向量索引(HNSW),减少 2 人团队的数据库维护负担。 * **业务灵活性**:通过 contentType 和 JSONB 字段支持未知的新内容类型,无需频繁迁移数据库 (Migration)。 ### **✅ 决策 2:引入 "Container Table"(容器表)** * **结论**:在文档表之上,必须增加 EkbKnowledgeBase 表。 * **理由**: * **权限隔离**:明确区分 "User KB" (个人私有) 和 "System KB" (全员共享)。 * **策略配置**:允许为不同库配置不同的切片大小 (ChunkSize) 或检索策略 (TopK)。 ### **✅ 决策 3:确立 "四层数据架构"** * **结论**:采纳上传文档中的 Layer 0-4 分层设计。 * **价值**:确保系统的**高容错性** —— 即使 AI 提取结构化数据失败(Layer 4),基础的 RAG 检索(Layer 0)依然可用。 ## **2\. 最终融合版架构图 (ER Diagram)** 我们融合了原设计的“四层架构”优势和建议方案的“容器管理”优势,形成了最终的 v2.0 模型。 erDiagram %% 容器层(新增):管理权限和策略 Ekb\_KnowledgeBase { string id PK string type "USER | SYSTEM" string owner\_id "userId | moduleId" jsonb config "策略配置(chunkSize等)" } %% 文档层(四层架构):管理内容和元数据 Ekb\_Document { string id PK string kb\_id FK string content\_type "LITERATURE | CASE | ..." string extracted\_text "Layer 0: 全文" jsonb metadata "Layer 4: 标准属性(DOI/作者)" jsonb structured\_data "Layer 4: 业务属性(PICO/诊断)" } %% 切片层(检索核心):管理向量和索引 Ekb\_Chunk { string id PK string document\_id FK vector embedding "1024维向量" string content "切片文本" jsonb metadata "切片级元数据(IsAnswer)" } Ekb\_KnowledgeBase ||--|{ Ekb\_Document : "contains" Ekb\_Document ||--|{ Ekb\_Chunk : "split into" **潜在风险(需要补充):** 1. **缺失 `EkbKnowledgeBase` 表**:目前的 Schema 中 `EkbDocument` 有 `kbId` 字段,但没有定义 `EkbKnowledgeBase` 表。 * *风险*:你之前提到“用户有 3 个库,系统有业务库”。如果没有这个表,你无法管理“谁拥有这个库”、“这是系统库还是用户库”、“这个库的 RAG 策略是什么”。 2. **`Chunk` 还可以更灵活**:目前的 `EkbChunk` 只有 `sectionType`。 * *建议*:建议给 Chunk 也加上 `metadata` (JSONB)。比如做职业考试题时,你可能希望直接在 Chunk 层面标记 `{ "isAnswer": true }`,这样检索时可以加权。 场景模拟 场景 1:ASL 模块搜文献(只搜文献库,不搜病历) 场景 2:PKB 用户搜自己的知识库(只搜自己的,不搜别人的) 场景 3:AIA 智能问答(既要查系统医学库,又要查用户上传的病历) | 维度 | 我的设计 (上一版) | 你的设计 (上传版) | 🎯 建议融合方案 | | :---- | :---- | :---- | :---- | | **知识库管理** | **有 EkbKnowledgeBase 表**。 明确区分 USER vs SYSTEM 类型。 | **无**。 仅在 Document 中引用 kbId。 | **采纳我的**。 必须有这张表来管理权限和配置。 | | **文档元数据** | 单个 metadata JSONB 字段。 简单,但混杂。 | **分离设计**。 metadata (标准) \+ structuredData (业务)。 | **采纳你的**。 分离设计对医学场景更友好,利于 TS 类型定义。 | | **全文存储** | 隐含在 Chunk 中。 | **显式存储 extractedText**。 存放在 Document 表中。 | **采纳你的**。 保留全文很有必要,万一以后要换切片策略(比如 512 改为 1024),不用重新解析文件。 | | **切片属性** | metadata JSONB。 | sectionType 枚举 \+ pageNumber。 | **融合**。 保留 sectionType,但增加 metadata JSONB 以备不时之需。 | ## **3\. 详细数据模型定义 (Prisma Schema)** **开发者注意**:这是最终落地的 Schema,请直接复制到 prisma/schema.prisma 的 ekb\_schema 部分。 ### **3.1 知识库容器 (EkbKnowledgeBase)** **解决痛点**:解决了“怎么区分这是用户的库还是系统的库”的问题。 model EkbKnowledgeBase { id String @id @default(uuid()) name String description String? // 核心隔离字段 // USER: 用户创建的,ownerId \= userId,受配额限制 (如最多3个) // SYSTEM: 系统预置的,ownerId \= moduleId (如 "AIA", "IIT"),全员可读 type KbType @default(USER) ownerId String // 策略配置 (JSONB) // 允许未来针对特定库进行优化,例如: // { "chunkSize": 1024, "enableRerank": true, "embeddingModel": "v3" } config Json? @db.JsonB documents EkbDocument\[\] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index(\[ownerId\]) @@index(\[type\]) @@schema("ekb\_schema") } enum KbType { USER SYSTEM } ### **3.2 通用文档对象 (EkbDocument)** **解决痛点**:解决了“医学数据类型极其复杂”的问题。采用**双 JSONB 策略**。 model EkbDocument { id String @id @default(uuid()) // 归属关系 kbId String knowledgeBase EkbKnowledgeBase @relation(fields: \[kbId\], references: \[id\], onDelete: Cascade) userId String // 冗余存储上传者,方便快速查询 // \===== Layer 1: 基础文件信息 \===== filename String fileType String // pdf, docx, md, txt fileSizeBytes BigInt fileHash String // 用于实现“秒传”和去重 fileUrl String // OSS 路径 status DocStatus @default(PENDING) // 状态机:PENDING \-\> PROCESSING \-\> COMPLETED errorMessage String? @db.Text // \===== Layer 0: RAG 核心层 \===== // 必须保留全文,以便未来更改切片策略时,无需重新解析原始文件 extractedText String? @db.Text // \===== Layer 2: 内容增强层 \===== summary String? @db.Text tokenCount Int? pageCount Int? // \===== Layer 3: 分类标签层 \===== contentType String? // 枚举字符串:literature, case, exam, drug tags String\[\] category String? // \===== Layer 4: 结构化数据层 (核心设计) \===== // 1\. 通用元数据:所有文献都有的 (DOI, Journal, Year, Author) metadata Json? @db.JsonB // 2\. 业务结构化数据:特定类型特有的 (PICO, Diagnosis, Dosage, ExamAnswer) // 使用 JSONB \+ GIN 索引,实现 NoSQL 般的灵活性 structuredData Json? @db.JsonB chunks EkbChunk\[\] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index(\[kbId\]) @@index(\[contentType\]) // GIN 索引:支持对 metadata 和 structuredData 内部字段的高速查询 @@index(\[metadata\], type: Gin) @@index(\[structuredData\], type: Gin) @@schema("ekb\_schema") } enum DocStatus { PENDING PROCESSING COMPLETED FAILED } ### **3.3 向量切片 (EkbChunk)** **解决痛点**:解决了“如何让检索更精准”的问题。 model EkbChunk { id String @id @default(uuid()) documentId String document EkbDocument @relation(fields: \[documentId\], references: \[id\], onDelete: Cascade) // 核心内容 content String @db.Text chunkIndex Int // 向量数据 (PostgreSQL pgvector) // 注意:需要手动执行 SQL 创建 HNSW 索引 embedding Unsupported("vector(1024)")? // 溯源信息 pageNumber Int? sectionType String? // abstract, methods, results... // 扩展元数据 (新增) // 允许在切片粒度做标记。例如:考试题的答案切片标记 { "isAnswer": true } // 检索时可降低纯答案切片的权重,优先展示问题 metadata Json? @db.JsonB @@index(\[documentId\]) @@schema("ekb\_schema") } ## **4\. 关键设计问答 (Q\&A)** ### **Q1: 为什么要把 metadata 和 structuredData 分开?** * **清晰度**:metadata 存放通用的、标准化的属性(如标题、作者、时间),这是所有文档共有的;structuredData 存放业务特定的深层数据(如 PICO、诊断结果)。 * **前端适配**:前端组件可以统一渲染 metadata,而针对 structuredData 则根据 contentType 动态加载不同的展示组件。 ### **Q2: 为什么要保留 extractedText (全文)?只存 Chunk 不行吗?** * **未来兼容性**:如果我们下个月发现 chunkSize=512 效果不好,想改成 1024。如果有 extractedText,我们可以直接在数据库里重新切片 (Re-chunking);如果没有,我们就得去 OSS 重新下载文件并解析,成本巨大。 ### **Q3: 为什么 Chunk 也要加 metadata?** * **精准检索**:在“职业考试”场景中,一个题目可能被切成“题干”和“解析”两段。我们希望用户搜问题时,优先匹配“题干”。如果在 Chunk 上标记 { "type": "question" },检索算法就可以加权。 ## **5\. 业务场景演练** ### **场景 A:PKB 个人文献库** * **用户操作**:上传一篇 PDF。 * **数据流**: 1. 创建 EkbDocument (kbId=用户的个人库ID)。 2. status \= PROCESSING。 3. 后台解析 \-\> 填充 extractedText。 4. 后台切片 \-\> 插入 EkbChunk。 5. status \= COMPLETED。 ### **场景 B:ASL 智能文献筛选** * **系统操作**:批量导入 1000 篇文献,并进行 PICO 提取。 * **数据流**: 1. 创建 EkbDocument (kbId=ASL临时库ID, contentType='literature')。 2. 调用 LLM 提取 PICO。 3. 更新 structuredData \= { "pico": { "P": "...", "I": "..." } }。 4. **查询时**:SELECT \* FROM EkbDocument WHERE structuredData-\>'pico'-\>\>'I' LIKE '%Aspirin%' (利用 GIN 索引加速)。 ## **6\. 下一步行动 (Action Plan)** 1. **数据库迁移**: * 更新 prisma/schema.prisma。 * 运行 npx prisma migrate dev \--name init\_ekb\_v2。 2. **索引创建 (SQL)**: * 手动执行 HNSW 索引创建语句(Prisma 暂不支持自动生成 vector 索引)。 * 手动执行 pg\_bigm 索引创建语句。 3. **Service 层更新**: * 更新 KnowledgeBaseEngine,在入库时必须要求传入 kbId。 * 在检索时,增加对 structuredData 的 JSON 过滤支持。