# 知识库需求调整说明 ## 📋 需求变更 ### 原需求(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**