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