Files
AIclinicalresearch/知识库需求调整说明.md
AI Clinical Dev Team 9acbb0ae2b feat: complete Dify platform deployment (Day 18)
## Dify 閮ㄧ讲瀹屾垚 鉁?
### 瀹屾垚鐨勫伐浣?1. Docker 闀滃儚鍔犻€熷櫒閰嶇疆
   - 閰嶇疆 5 涓浗鍐呴暅鍍忔簮
   - 澶у箙鎻愬崌涓嬭浇閫熷害鍜屾垚鍔熺巼

2. Dify 闀滃儚鎷夊彇 (鍏?11 涓湇鍔?
   - langgenius/dify-api:1.9.1
   - langgenius/dify-web:1.9.1
   - postgres, redis, weaviate, nginx 绛?   - 鎬诲ぇ灏忕害 2GB锛岃€楁椂绾?15 鍒嗛挓

3. Dify 鏈嶅姟鍚姩
   - 鉁?nginx (80/443)
   - 鉁?api, worker, worker_beat
   - 鉁?web (3000)
   - 鉁?db (PostgreSQL), redis
   - 鉁?weaviate (鍚戦噺鏁版嵁搴?
   - 鉁?sandbox, plugin_daemon, ssrf_proxy

4. Dify 鍒濆鍖栭厤缃?   - 鍒涘缓绠$悊鍛樿处鍙?   - 鍒涘缓搴旂敤: AI Clinical Research
   - 鑾峰彇 API Key: app-VZRn0vMXdmltEJkvatHVGv5j

5. 鍚庣鐜閰嶇疆
   - DIFY_API_URL=http://localhost/v1
   - DIFY_API_KEY 宸查厤缃?
### 鏂囨。鏇存柊
- 鏂板: docs/05-姣忔棩杩涘害/Day18-Dify閮ㄧ讲瀹屾垚.md
- 鏇存柊: docs/04-寮€鍙戣鍒?寮€鍙戦噷绋嬬.md (Day 18 鏍囪涓哄畬鎴?

### 涓嬩竴姝?Day 19-24: 鐭ヨ瘑搴撶郴缁熷紑鍙?- Dify 瀹㈡埛绔皝瑁?- 鐭ヨ瘑搴撶鐞?CRUD
- 鏂囨。涓婁紶涓庡鐞?- @鐭ヨ瘑搴撻泦鎴?- RAG 闂瓟楠岃瘉

---
Progress: 閲岀▼纰?1 (MVP) 85% -> 鐭ヨ瘑搴撶郴缁熷紑鍙戜腑
2025-10-11 08:58:41 +08:00

15 KiB
Raw Blame History

知识库需求调整说明

📋 需求变更

原需求PRD原文

"后期考虑增加基于大规模1000篇以内文献的读取、识别、内容提取的工作"

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

实际需求(明确后)

每个用户最多创建 3个知识库
每个知识库最多上传 50个文件
主要格式PDF、DOCX
单用户最大文档量150个文件


🎯 影响分析

技术难度大幅降低

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

Dify完全够用

Dify的能力

  • 单个知识库支持上万个文档片段
  • 自动文档解析PDF、Word、TXT等
  • 内置向量化支持多种Embedding模型
  • 混合检索(关键词 + 语义检索)
  • 重排序Reranking
  • 答案溯源

我们的需求:

  • 50个文件/知识库远低于Dify上限
  • PDF、DOCX格式Dify原生支持
  • 结论Dify完全满足需求

💾 数据库设计

知识库表结构

-- 知识库表
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);

用户配额表

-- 用户配额表(可选,用于更灵活的配额管理)
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. 知识库创建(带限制检查)

// 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

// 知识库管理
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