# 知识库需求调整说明

## 📋 需求变更

### 原需求（PRD原文）
> "后期考虑，增加基于大规模（1000篇以内）文献的读取、识别、内容提取的工作"

**理解：** 这给人的印象是需要处理海量文献，技术难度极高。

### 实际需求（明确后）
✅ **每个用户最多创建 3个知识库**  
✅ **每个知识库最多上传 50个文件**  
✅ **主要格式：PDF、DOCX**  
✅ **单用户最大文档量：150个文件**

---

## 🎯 影响分析

### 技术难度大幅降低

| 维度 | 原理解（1000篇+） | 实际需求（150个/用户） | 影响 |
|------|-----------------|---------------------|------|
| **向量数据库** | 需要高性能集群 | Dify内置Qdrant足够 | ✅ 简化 |
| **文档处理** | 需要分布式处理 | 单机异步处理即可 | ✅ 简化 |
| **检索性能** | 需要优化索引 | 默认配置即可 | ✅ 简化 |
| **存储成本** | 需要大容量存储 | 标准对象存储 | ✅ 降低 |
| **技术难度** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ 降低 |

### Dify完全够用

**Dify的能力：**
- ✅ 单个知识库支持上万个文档片段
- ✅ 自动文档解析（PDF、Word、TXT等）
- ✅ 内置向量化（支持多种Embedding模型）
- ✅ 混合检索（关键词 + 语义检索）
- ✅ 重排序（Reranking）
- ✅ 答案溯源

**我们的需求：**
- 50个文件/知识库（远低于Dify上限）
- PDF、DOCX格式（Dify原生支持）
- **结论：Dify完全满足需求！**

---

## 💾 数据库设计

### 知识库表结构

```sql
-- 知识库表
CREATE TABLE knowledge_bases (
  id VARCHAR(50) PRIMARY KEY,
  user_id VARCHAR(50) NOT NULL,
  name VARCHAR(100) NOT NULL,
  description TEXT,
  
  -- Dify知识库ID
  dify_dataset_id VARCHAR(100) NOT NULL,
  
  -- 统计信息
  file_count INT DEFAULT 0,
  total_size_bytes BIGINT DEFAULT 0,
  
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  
  FOREIGN KEY (user_id) REFERENCES users(id),
  
  -- 限制：每个用户最多3个知识库
  CONSTRAINT check_kb_limit CHECK (
    (SELECT COUNT(*) FROM knowledge_bases WHERE user_id = NEW.user_id) <= 3
  )
);

-- 为用户ID创建索引
CREATE INDEX idx_kb_user ON knowledge_bases(user_id);

-- 文档表
CREATE TABLE documents (
  id VARCHAR(50) PRIMARY KEY,
  kb_id VARCHAR(50) NOT NULL,
  user_id VARCHAR(50) NOT NULL,
  
  filename VARCHAR(255) NOT NULL,
  file_type VARCHAR(20) NOT NULL,  -- pdf, docx
  file_size_bytes BIGINT NOT NULL,
  file_url TEXT NOT NULL,  -- 对象存储URL
  
  -- Dify文档ID
  dify_document_id VARCHAR(100) NOT NULL,
  
  -- 处理状态
  status VARCHAR(20) DEFAULT 'uploading',  -- uploading, processing, completed, failed
  progress INT DEFAULT 0,  -- 0-100
  error_message TEXT,
  
  -- 处理结果
  segments_count INT DEFAULT 0,  -- 切分的段落数
  tokens_count INT DEFAULT 0,    -- token数量
  
  uploaded_at TIMESTAMP DEFAULT NOW(),
  processed_at TIMESTAMP,
  
  FOREIGN KEY (kb_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id),
  
  -- 限制：每个知识库最多50个文件
  CONSTRAINT check_doc_limit CHECK (
    (SELECT COUNT(*) FROM documents WHERE kb_id = NEW.kb_id) <= 50
  )
);

-- 索引
CREATE INDEX idx_doc_kb ON documents(kb_id);
CREATE INDEX idx_doc_user ON documents(user_id);
CREATE INDEX idx_doc_status ON documents(status);
```

### 用户配额表

```sql
-- 用户配额表（可选，用于更灵活的配额管理）
CREATE TABLE user_quotas (
  user_id VARCHAR(50) PRIMARY KEY,
  
  -- 知识库配额
  kb_quota INT DEFAULT 3,           -- 允许创建的知识库数量
  kb_used INT DEFAULT 0,             -- 已使用
  
  -- 文件配额（每个知识库）
  files_per_kb_quota INT DEFAULT 50,
  
  -- 存储配额（字节）
  storage_quota_bytes BIGINT DEFAULT 1073741824,  -- 1GB
  storage_used_bytes BIGINT DEFAULT 0,
  
  updated_at TIMESTAMP DEFAULT NOW(),
  
  FOREIGN KEY (user_id) REFERENCES users(id)
);
```

---

## 🔧 实现方案

### 1. 知识库创建（带限制检查）

```typescript
// backend/src/services/knowledge-base.service.ts

export class KnowledgeBaseService {
  
  /**
   * 创建知识库
   */
  async createKnowledgeBase(userId: string, data: CreateKBDto) {
    // 1. 检查用户知识库数量限制
    const userKbCount = await db.knowledge_bases.count({
      where: { user_id: userId }
    });
    
    if (userKbCount >= 3) {
      throw new Error('已达到知识库数量上限（3个）');
    }
    
    // 2. 在Dify中创建知识库
    const difyDataset = await difyClient.createDataset({
      name: data.name,
      description: data.description,
      indexing_technique: 'high_quality',  // 高质量索引
      permission: 'only_me'
    });
    
    // 3. 在数据库中保存记录
    const kb = await db.knowledge_bases.create({
      id: generateId('kb'),
      user_id: userId,
      name: data.name,
      description: data.description,
      dify_dataset_id: difyDataset.id,
      file_count: 0,
      total_size_bytes: 0
    });
    
    return kb;
  }
  
  /**
   * 上传文档
   */
  async uploadDocument(userId: string, kbId: string, file: File) {
    // 1. 检查知识库是否存在且属于该用户
    const kb = await db.knowledge_bases.findOne({
      where: { id: kbId, user_id: userId }
    });
    
    if (!kb) {
      throw new Error('知识库不存在或无权限');
    }
    
    // 2. 检查文件数量限制
    const docCount = await db.documents.count({
      where: { kb_id: kbId }
    });
    
    if (docCount >= 50) {
      throw new Error('知识库文件数量已达上限（50个）');
    }
    
    // 3. 检查文件格式
    const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
    if (!allowedTypes.includes(file.mimetype)) {
      throw new Error('仅支持PDF和DOCX格式');
    }
    
    // 4. 检查文件大小（单个文件最大50MB）
    const maxSize = 50 * 1024 * 1024; // 50MB
    if (file.size > maxSize) {
      throw new Error('文件大小超过限制（最大50MB）');
    }
    
    // 5. 上传到对象存储
    const fileUrl = await objectStorage.upload(file);
    
    // 6. 创建文档记录
    const doc = await db.documents.create({
      id: generateId('doc'),
      kb_id: kbId,
      user_id: userId,
      filename: file.originalname,
      file_type: path.extname(file.originalname).slice(1),
      file_size_bytes: file.size,
      file_url: fileUrl,
      status: 'uploading',
      dify_document_id: ''  // 稍后更新
    });
    
    // 7. 异步提交到Dify处理
    this.processDocumentAsync(doc.id, kb.dify_dataset_id, fileUrl);
    
    return doc;
  }
  
  /**
   * 异步处理文档
   */
  private async processDocumentAsync(docId: string, difyDatasetId: string, fileUrl: string) {
    try {
      // 1. 更新状态为处理中
      await db.documents.update({
        where: { id: docId },
        data: { status: 'processing' }
      });
      
      // 2. 提交到Dify处理
      const difyDoc = await difyClient.uploadDocument({
        dataset_id: difyDatasetId,
        file_url: fileUrl,
        indexing_technique: 'high_quality',
        process_rule: {
          mode: 'automatic',
          rules: {
            pre_processing_rules: [
              { id: 'remove_extra_spaces', enabled: true },
              { id: 'remove_urls_emails', enabled: false }  // 保留医学文献中的引用
            ],
            segmentation: {
              separator: '\n\n',
              max_tokens: 500  // 每段最大500 tokens
            }
          }
        }
      });
      
      // 3. 保存Dify文档ID
      await db.documents.update({
        where: { id: docId },
        data: { dify_document_id: difyDoc.id }
      });
      
      // 4. 轮询处理状态
      let status = 'processing';
      while (status === 'processing') {
        await sleep(2000); // 2秒后重试
        
        const result = await difyClient.getDocumentStatus({
          dataset_id: difyDatasetId,
          document_id: difyDoc.id
        });
        
        status = result.indexing_status;
        
        // 更新进度
        await db.documents.update({
          where: { id: docId },
          data: {
            progress: Math.round((result.completed_segments / result.total_segments) * 100)
          }
        });
      }
      
      // 5. 处理完成
      if (status === 'completed') {
        const result = await difyClient.getDocumentStatus({
          dataset_id: difyDatasetId,
          document_id: difyDoc.id
        });
        
        await db.documents.update({
          where: { id: docId },
          data: {
            status: 'completed',
            progress: 100,
            segments_count: result.total_segments,
            tokens_count: result.tokens,
            processed_at: new Date()
          }
        });
        
        // 更新知识库统计
        await this.updateKbStats(docId);
        
      } else {
        // 处理失败
        await db.documents.update({
          where: { id: docId },
          data: {
            status: 'failed',
            error_message: result.error || '文档处理失败'
          }
        });
      }
      
    } catch (error) {
      // 异常处理
      await db.documents.update({
        where: { id: docId },
        data: {
          status: 'failed',
          error_message: error.message
        }
      });
    }
  }
  
  /**
   * 检索知识库
   */
  async queryKnowledgeBase(userId: string, kbId: string, query: string) {
    // 1. 验证权限
    const kb = await db.knowledge_bases.findOne({
      where: { id: kbId, user_id: userId }
    });
    
    if (!kb) {
      throw new Error('知识库不存在或无权限');
    }
    
    // 2. 调用Dify检索
    const results = await difyClient.queryKnowledgeBase({
      dataset_id: kb.dify_dataset_id,
      query,
      retrieval_model: {
        search_method: 'hybrid_search',  // 混合检索
        reranking_enable: true,          // 启用重排序
        top_k: 5,                        // 返回前5个结果
        score_threshold: 0.5             // 相似度阈值
      }
    });
    
    // 3. 格式化返回结果
    return results.records.map(record => ({
      content: record.segment.content,
      score: record.score,
      document: {
        id: record.segment.document.id,
        name: record.segment.document.name,
        position: record.segment.position  // 在文档中的位置
      }
    }));
  }
  
  /**
   * 获取用户的知识库列表
   */
  async getUserKnowledgeBases(userId: string) {
    const kbs = await db.knowledge_bases.findMany({
      where: { user_id: userId },
      include: {
        _count: {
          select: { documents: true }
        }
      }
    });
    
    return kbs.map(kb => ({
      id: kb.id,
      name: kb.name,
      description: kb.description,
      file_count: kb.file_count,
      total_size_mb: (kb.total_size_bytes / 1024 / 1024).toFixed(2),
      created_at: kb.created_at,
      quota: {
        used: kb.file_count,
        limit: 50
      }
    }));
  }
  
  /**
   * 删除文档
   */
  async deleteDocument(userId: string, docId: string) {
    // 1. 验证权限
    const doc = await db.documents.findOne({
      where: { id: docId, user_id: userId },
      include: { knowledge_base: true }
    });
    
    if (!doc) {
      throw new Error('文档不存在或无权限');
    }
    
    // 2. 从Dify删除
    await difyClient.deleteDocument({
      dataset_id: doc.knowledge_base.dify_dataset_id,
      document_id: doc.dify_document_id
    });
    
    // 3. 从对象存储删除
    await objectStorage.delete(doc.file_url);
    
    // 4. 从数据库删除
    await db.documents.delete({ where: { id: docId } });
    
    // 5. 更新知识库统计
    await this.updateKbStats(doc.kb_id);
  }
  
  /**
   * 更新知识库统计信息
   */
  private async updateKbStats(kbId: string) {
    const stats = await db.documents.aggregate({
      where: { kb_id: kbId, status: 'completed' },
      _count: true,
      _sum: { file_size_bytes: true }
    });
    
    await db.knowledge_bases.update({
      where: { id: kbId },
      data: {
        file_count: stats._count,
        total_size_bytes: stats._sum.file_size_bytes || 0
      }
    });
  }
}
```

---

## 📊 成本影响

### 存储成本（降低）

**原估算（1000篇文献）：**
- 假设每篇10MB
- 总存储：10GB+
- 成本：¥200+/月

**实际需求（150个文件/用户）：**
```
单用户存储：
- 150个文件 × 平均5MB = 750MB
- 1000个用户 = 750GB

月度成本（阿里云OSS）：
- 存储：750GB × ¥0.12/GB = ¥90/月
- 流量：假设100GB/月 × ¥0.5/GB = ¥50/月
- 总计：¥140/月
```

**成本节省：约¥60/月**

### 处理性能（提升）

**小规模知识库的优势：**
- ✅ 检索速度更快（< 1s）
- ✅ 向量化处理更快（单文件 < 30s）
- ✅ 无需复杂的性能优化

---

## 🎯 技术选型确认

### Dify完全满足需求 ✅

| 功能 | 需求 | Dify能力 | 结论 |
|------|------|---------|------|
| 知识库数量 | 3个/用户 | 无限制 | ✅ 满足 |
| 文件数量 | 50个/知识库 | 上千个 | ✅ 满足 |
| 文件格式 | PDF、DOCX | 支持20+格式 | ✅ 满足 |
| 向量化 | 自动 | 内置支持 | ✅ 满足 |
| 检索 | 混合检索 | 支持 | ✅ 满足 |
| 答案溯源 | 需要 | 支持 | ✅ 满足 |

**结论：无需考虑其他RAG方案，Dify完全够用！**

---

## 📝 API设计

### RESTful API

```typescript
// 知识库管理
GET    /api/knowledge-bases              // 获取用户的知识库列表
POST   /api/knowledge-bases              // 创建知识库
GET    /api/knowledge-bases/:id          // 获取知识库详情
PUT    /api/knowledge-bases/:id          // 更新知识库
DELETE /api/knowledge-bases/:id          // 删除知识库

// 文档管理
GET    /api/knowledge-bases/:kbId/documents           // 获取文档列表
POST   /api/knowledge-bases/:kbId/documents           // 上传文档
GET    /api/knowledge-bases/:kbId/documents/:docId    // 获取文档详情
DELETE /api/knowledge-bases/:kbId/documents/:docId    // 删除文档

// 检索
POST   /api/knowledge-bases/:kbId/query   // 检索知识库

// 配额查询
GET    /api/users/me/quotas               // 获取用户配额信息
```

---

## ✅ 总结

### 需求明确后的影响

| 方面 | 原理解 | 实际需求 | 影响 |
|------|--------|---------|------|
| **技术难度** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大幅降低 |
| **开发成本** | 需自建RAG | 用Dify即可 | 节省30天+ |
| **运营成本** | ¥200+/月 | ¥140/月 | 节省30% |
| **性能要求** | 需要优化 | 默认配置 | 简化 |
| **风险** | 高 | 低 | 降低 |

### 最终方案

**✅ 使用Dify处理知识库，完全满足需求！**
- 每用户3个知识库
- 每知识库50个文件
- PDF、DOCX格式
- 自动向量化、检索、答案溯源

**无需考虑其他RAG方案！**

---

**文档版本：v1.0**  
**更新时间：2025-10-10**




