Phase 0: 数据库迁移 + Entity (3 Tasks) Phase 1: 后端 Service + Handler (4 Tasks) Phase 2: 文档处理管线 (5 Tasks) Phase 3: 混合意图路由 (3 Tasks) Phase 4: 前端管理 UI (4 Tasks) Phase 5: AI 客服集成 + 数据迁移 (4 Tasks) Phase 6: 测试 + 验收 (3 Tasks)
39 KiB
AI 知识库 V2 实施计划
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 替换旧三层知识模型,构建 Dify 风格统一知识管理平台,作为 AI 客服的知识底座。
Architecture: 3 表新模型(knowledge_bases → documents → chunks),文档处理管线(上传/URL/手动 → 解析 → 切片 → embedding),混合意图路由(关键词 → 向量 → LLM 兜底),RAG 注入 AI 客服对话。
Tech Stack: Rust/Axum/SeaORM/pgvector(后端),React/Ant Design(前端),PostgreSQL + pgvector(存储)
Design Spec: docs/superpowers/specs/2026-05-26-ai-knowledge-base-v2-design.md
Phase 分解
| Phase | 名称 | Tasks | 预估 | 依赖 |
|---|---|---|---|---|
| Phase 0 | 数据库迁移 + Entity | 3 | 1-2h | 无 |
| Phase 1 | 后端 Service + Handler(知识库/文档 CRUD) | 4 | 3-4h | Phase 0 |
| Phase 2 | 文档处理管线(解析 + 切片 + embedding) | 5 | 4-5h | Phase 0 |
| Phase 3 | 混合意图路由 + 向量检索 | 3 | 2-3h | Phase 1 + 2 |
| Phase 4 | 前端管理 UI(Dify 风格) | 4 | 3-4h | Phase 1 |
| Phase 5 | AI 客服集成 + 旧数据迁移 | 4 | 2-3h | Phase 3 + 4 |
| Phase 6 | 测试 + 验收 | 3 | 2h | Phase 5 |
总计: 26 Tasks / ~15-20h
文件结构清单
新增文件(后端)
crates/erp-ai/src/
├── entity/
│ ├── ai_knowledge_bases.rs # 新 Entity
│ ├── ai_knowledge_documents.rs # 新 Entity
│ └── ai_knowledge_chunks.rs # 新 Entity
├── knowledge/
│ ├── mod.rs # 改造:扩展 KnowledgeQuery
│ ├── pipeline.rs # 新增:文档处理管线
│ ├── parser.rs # 新增:格式解析器
│ ├── chunker.rs # 新增:智能切片
│ ├── hybrid_router.rs # 新增:混合意图路由
│ ├── vector_search.rs # 改造:适配新表
│ ├── structured_source.rs # 保留:过渡期兼容
│ └── vector_source.rs # 废弃:标记 deprecated
├── service/
│ └── knowledge.rs # 重写:新 CRUD
├── handler/
│ └── knowledge_handler.rs # 重写:新 API handler
└── module.rs # 改造:新路由注册
crates/erp-server/migration/src/
├── m20260526_000163_create_ai_knowledge_bases.rs
├── m20260526_000164_create_ai_knowledge_documents.rs
├── m20260526_000165_create_ai_knowledge_chunks.rs
└── m20260526_000166_migrate_old_knowledge_data.rs
新增文件(前端)
apps/web/src/
├── api/ai/
│ └── knowledge.ts # 重写:新 API client
├── pages/health/
│ ├── KnowledgeBase/
│ │ ├── index.tsx # 知识库列表页
│ │ ├── KnowledgeBaseDetail.tsx # 知识库详情页(文档管理)
│ │ ├── CreateKBModal.tsx # 创建知识库弹窗
│ │ ├── UploadDocument.tsx # 文档上传组件
│ │ ├── HitTestPanel.tsx # 命中测试面板
│ │ └── ChunkCard.tsx # 切片预览卡片
│ └── AiKnowledgePage.tsx # 废弃:被 KnowledgeBase/ 替代
修改文件
crates/erp-ai/Cargo.toml # 新增依赖
crates/erp-ai/src/state.rs # 改造:新 service 注入
crates/erp-ai/src/entity/mod.rs # 新增 Entity 注册
crates/erp-ai/src/service/chat_handler.rs # 改造:RAG 注入
crates/erp-ai/src/agent/tools/search_medical_knowledge.rs # 改造:新检索
apps/web/src/App.tsx # 改造:新路由
详细 Tasks
Chunk 1: Phase 0 — 数据库迁移 + Entity
Task 1: 创建 ai_knowledge_bases 迁移
Files:
-
Create:
crates/erp-server/migration/src/m20260526_000163_create_ai_knowledge_bases.rs -
Modify:
crates/erp-server/migration/src/lib.rs -
Step 1: 创建迁移文件
参考现有迁移 m20260505_000121_create_ai_knowledge_references.rs 的模式,创建 ai_knowledge_bases 表:
// m20260526_000163_create_ai_knowledge_bases.rs
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str { "m20260526_000163_create_ai_knowledge_bases" }
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
Table::create()
.table(Alias::new("ai_knowledge_bases"))
.if_not_exists()
// 标准字段
.col(ColumnDef::new(Alias::new("id")).uuid().not_null().primary_key().default(Expr::cust("gen_random_uuid()")))
.col(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
// 业务字段
.col(ColumnDef::new(Alias::new("name")).string_len(200).not_null())
.col(ColumnDef::new(Alias::new("kb_type")).string_len(50).not_null())
.col(ColumnDef::new(Alias::new("description")).text())
.col(ColumnDef::new(Alias::new("icon")).string_len(50))
.col(ColumnDef::new(Alias::new("chunk_strategy")).json_binary().not_null().default(Expr::cust("'{}'")))
.col(ColumnDef::new(Alias::new("intent_keywords")).json_binary().not_null().default(Expr::cust("'[]'")))
.col(ColumnDef::new(Alias::new("embedding_model")).string_len(100))
.col(ColumnDef::new(Alias::new("is_enabled")).boolean().not_null().default(true))
.col(ColumnDef::new(Alias::new("document_count")).integer().not_null().default(0))
.col(ColumnDef::new(Alias::new("chunk_count")).integer().not_null().default(0))
// 审计字段
.col(ColumnDef::new(Alias::new("created_at")).timestamp_with_time_zone().not_null().default(Expr::cust("now()")))
.col(ColumnDef::new(Alias::new("updated_at")).timestamp_with_time_zone().not_null().default(Expr::cust("now()")))
.col(ColumnDef::new(Alias::new("created_by")).uuid())
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
.col(ColumnDef::new(Alias::new("version_lock")).integer().not_null().default(1))
.to_owned(),
).await?;
// 索引
manager.create_index(Index::create().name("idx_kb_tenant").table(Alias::new("ai_knowledge_bases")).col(Alias::new("tenant_id")).to_owned()).await?;
manager.create_index(Index::create().name("idx_kb_type").table(Alias::new("ai_knowledge_bases")).col(Alias::new("tenant_id")).col(Alias::new("kb_type")).to_owned()).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Alias::new("ai_knowledge_bases")).to_owned()).await
}
}
- Step 2: 注册迁移到 lib.rs
在 crates/erp-server/migration/src/lib.rs 的迁移列表中添加:
Box::new(m20260526_000163_create_ai_knowledge_bases::Migration),
- Step 3: 运行迁移验证
cd crates/erp-server && cargo check
Expected: 编译通过,无错误
- Step 4: 提交
git add crates/erp-server/migration/src/m20260526_000163_create_ai_knowledge_bases.rs crates/erp-server/migration/src/lib.rs
git commit -m "feat(db): 创建 ai_knowledge_bases 表迁移"
Task 2: 创建 ai_knowledge_documents + ai_knowledge_chunks 迁移
Files:
-
Create:
crates/erp-server/migration/src/m20260526_000164_create_ai_knowledge_documents.rs -
Create:
crates/erp-server/migration/src/m20260526_000165_create_ai_knowledge_chunks.rs -
Modify:
crates/erp-server/migration/src/lib.rs -
Step 1: 创建 documents 迁移
参照设计规格 §3.3,创建 ai_knowledge_documents 表,包含:id, tenant_id, knowledge_base_id (FK), title, source_type, file_path, file_type, file_size, source_url, raw_content, status, chunk_count, error_message, 审计字段。
FK 引用 ai_knowledge_bases(id)。索引:idx_doc_kb, idx_doc_status, idx_doc_tenant。
- Step 2: 创建 chunks 迁移
参照设计规格 §3.4,创建 ai_knowledge_chunks 表。注意:
-
embedding列使用Expr::cust("vector(1536)")创建 -
FK 引用
ai_knowledge_documents(id)和ai_knowledge_bases(id) -
HNSW 索引:
CREATE INDEX idx_chunk_embedding ON ai_knowledge_chunks USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)— 通过manager.get_connection().execute_unprepared()执行 -
普通 B-tree 索引:idx_chunk_kb, idx_chunk_doc, idx_chunk_tenant, idx_chunk_hits
-
包含 updated_at, updated_by(设计规格 review 修复后新增)
-
Step 3: 注册两个迁移到 lib.rs
Box::new(m20260526_000164_create_ai_knowledge_documents::Migration),
Box::new(m20260526_000165_create_ai_knowledge_chunks::Migration),
-
Step 4: cargo check 验证
-
Step 5: 提交
git commit -m "feat(db): 创建 ai_knowledge_documents + ai_knowledge_chunks 表迁移"
Task 3: 创建 Entity 文件 + Cargo.toml 依赖
Files:
-
Create:
crates/erp-ai/src/entity/ai_knowledge_bases.rs -
Create:
crates/erp-ai/src/entity/ai_knowledge_documents.rs -
Create:
crates/erp-ai/src/entity/ai_knowledge_chunks.rs -
Modify:
crates/erp-ai/src/entity/mod.rs -
Modify:
crates/erp-ai/Cargo.toml -
Step 1: 在 Cargo.toml 添加新依赖
pdf-extract = "0.7"
docx-rs = "0.4"
calamine = "0.25"
validator = { workspace = true, features = ["derive"] }
- Step 2: 创建 3 个 Entity 文件
参照现有 ai_knowledge_references.rs 模式。关键点:
-
embedding字段用#[sea_orm(ignore)]标记(SeaORM 不支持 vector 类型) -
chunks 的
hit_count/last_hit_at是统计字段 -
所有 Entity 包含完整审计字段
-
Step 3: 在 entity/mod.rs 注册新 Entity
-
Step 4: cargo check 验证
-
Step 5: 提交
git commit -m "feat(ai): 新增知识库 V2 Entity + Cargo 依赖"
Chunk 2: Phase 1 — 后端 Service + Handler
Task 4: KnowledgeService 重写 — 知识库 CRUD
Files:
-
Modify:
crates/erp-ai/src/service/knowledge.rs -
Step 1: 定义新 DTO 结构体
在 knowledge.rs 顶部添加(旧的 DTO 保留但标记 #[deprecated]):
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct CreateKnowledgeBaseReq {
#[validate(length(min = 1, max = 200))]
pub name: String,
#[validate(custom(function = "validate_kb_type"))]
pub kb_type: String,
#[validate(length(max = 2000))]
pub description: Option<String>,
#[validate(length(max = 50))]
pub icon: Option<String>,
pub intent_keywords: Option<Vec<String>>,
pub chunk_strategy: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct UpdateKnowledgeBaseReq {
#[validate(length(min = 1, max = 200))]
pub name: Option<String>,
#[validate(length(max = 2000))]
pub description: Option<String>,
pub icon: Option<String>,
pub intent_keywords: Option<Vec<String>>,
pub chunk_strategy: Option<serde_json::Value>,
pub is_enabled: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListKBQuery {
pub kb_type: Option<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
fn validate_kb_type(kb_type: &str) -> Result<(), validator::ValidationError> {
const VALID_TYPES: &[&str] = &["rule", "reference", "clinical_guide", "institution_info", "medical_process", "health_education", "faq"];
if VALID_TYPES.contains(&kb_type) { Ok(()) }
else { Err(validator::ValidationError::new("invalid_kb_type")) }
}
- Step 2: 实现 KnowledgeService 新方法
在 KnowledgeService impl 中添加:
list_bases(tenant_id, query) -> Vec<KBModel>— 查询知识库列表,支持 kb_type 过滤create_base(tenant_id, user_id, req) -> Uuid— 创建知识库,kb_type 对应默认 chunk_strategyget_base(tenant_id, id) -> Option<KBModel>— 单条查询update_base(tenant_id, user_id, id, req)— 更新,乐观锁 version_lockdelete_base(tenant_id, id)— 软删除,同时软删除下级 documents
所有数据库操作使用 raw SQL(sea_orm::Statement),因为 chunks 表含 vector 类型。
-
Step 3: cargo check 验证
-
Step 4: 提交
git commit -m "feat(ai): 知识库 V2 Service — 知识库 CRUD 方法"
Task 5: KnowledgeService 重写 — 文档 CRUD + 文件上传
Files:
-
Modify:
crates/erp-ai/src/service/knowledge.rs -
Step 1: 定义文档 DTO
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct ImportUrlReq {
#[validate(url)]
#[validate(custom(function = "validate_no_ssrf"))]
pub url: String,
#[validate(length(max = 500))]
pub title: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct CreateManualDocReq {
#[validate(length(min = 1, max = 500))]
pub title: String,
#[validate(length(min = 1))]
pub content: String,
}
SSRF 校验参考现有项目中的 validate_no_ssrf 实现(erp-health 中的 URL 验证模式)。
-
Step 2: 实现文档 CRUD 方法
-
list_documents(tenant_id, kb_id, query) -> Vec<DocModel>— 按 kb_id 过滤 -
upload_document(tenant_id, kb_id, user_id, file) -> Uuid— 保存文件到 uploads/,创建 document 记录 (status=pending),spawn 异步处理 -
import_url(tenant_id, kb_id, user_id, req) -> Uuid— 创建 document 记录 (source_type=url),spawn 异步处理 -
create_manual(tenant_id, kb_id, user_id, req) -> Uuid— 创建 document 记录 (source_type=manual),spawn 异步处理 -
get_document(tenant_id, doc_id) -> Option<DocModel>— 含处理状态 -
delete_document(tenant_id, doc_id)— 软删除 document + 删除对应 chunks -
reprocess_document(tenant_id, doc_id)— 删除旧 chunks → 重新走管线 -
Step 3: 实现切片查询方法
-
list_chunks(tenant_id, doc_id, page, page_size) -> Vec<ChunkModel>— 分页查询 -
`update_chunk(tenant_id, chunk_id, content) — 更新切片内容
-
`re_embed_chunk(tenant_id, chunk_id) — 重新 embedding 单个切片
-
Step 4: cargo check 验证
-
Step 5: 提交
git commit -m "feat(ai): 知识库 V2 Service — 文档/切片 CRUD + 文件上传"
Task 6: Handler 重写 + 路由注册
Files:
-
Modify:
crates/erp-ai/src/handler/knowledge_handler.rs -
Modify:
crates/erp-ai/src/module.rs -
Modify:
crates/erp-ai/src/state.rs -
Step 1: 重写 knowledge_handler.rs
新的 handler 函数(旧的标记 #[deprecated]):
知识库管理(5 个):
list_bases— GET,分页列表create_base— POST,调用req.validate()get_base— GET,单条详情update_base— PUT,调用req.validate()delete_base— DELETE,软删除
文档管理(7 个):
list_documents— GET,按 kb_id 过滤upload_document— POST multipart,文件上传import_url— POST JSON,URL 导入,调用req.validate()create_manual— POST JSON,手动录入,调用req.validate()get_document— GET,详情含状态delete_document— DELETEreprocess_document— POST,触发重新处理
切片管理(3 个):
list_chunks— GET,分页update_chunk— PUT,编辑内容re_embed_chunk— POST,重新 embedding
检索(2 个):
hit_test— POST,命中测试search— POST,统一检索(内部调用)
所有 handler 使用 require_permission 守卫。multipart 上传使用 axum::extract::Multipart。
- Step 2: 更新 module.rs 路由注册
替换旧的知识库路由为新的 RESTful 结构:
// 知识库管理
.kb_routes = Router::new()
.route("/", get(list_bases).post(create_base))
.route("/{id}", get(get_base).put(update_base).delete(delete_base))
// 文档管理
.route("/{kb_id}/documents", get(list_documents))
.route("/{kb_id}/documents/upload", post(upload_document))
.route("/{kb_id}/documents/url", post(import_url))
.route("/{kb_id}/documents/manual", post(create_manual))
.route("/{kb_id}/documents/{doc_id}", get(get_document).delete(delete_document))
.route("/{kb_id}/documents/{doc_id}/reprocess", post(reprocess_document))
// 切片管理
.route("/{kb_id}/documents/{doc_id}/chunks", get(list_chunks))
.route("/chunks/{chunk_id}", put(update_chunk))
.route("/chunks/{chunk_id}/re-embed", post(re_embed_chunk))
// 检索
.route("/{kb_id}/test", post(hit_test))
.route("/search", post(search))
旧路由保留 1 个月(重定向到新路由)。
- Step 3: 更新 state.rs
确保 AiState 中的 knowledge: Arc<KnowledgeService> 字段不变,构造函数适配新方法签名。
-
Step 4: cargo check 验证
-
Step 5: 提交
git commit -m "feat(ai): 知识库 V2 Handler + 路由注册"
Task 7: 文件上传端点 + 静态文件服务
Files:
-
Modify:
crates/erp-ai/src/handler/knowledge_handler.rs(upload_document 实现) -
Modify:
crates/erp-server/src/main.rs(静态文件服务) -
Step 1: 实现 upload_document handler
async fn upload_document(
State(state): State<Arc<AiState>>,
user: AuthUser,
Path(kb_id): Path<Uuid>,
mut multipart: Multipart,
) -> impl IntoResponse {
// 1. require_permission
// 2. 从 multipart 取 field "file"
// 3. 验证 MIME 类型(白名单)+ 文件大小(≤50MB)+ sanitize 文件名
// 4. 保存到 uploads/knowledge/{tenant_id}/{uuid}.{ext}
// 5. 创建 document 记录 (status=pending)
// 6. spawn 异步处理管线
// 7. 返回 202 + document id
}
- Step 2: 确保 uploads 目录有静态文件服务
在 erp-server/src/main.rs 中确认 /uploads 路由已注册(应已存在)。
- Step 3: cargo check + 手动测试上传
curl -X POST http://localhost:3000/api/v1/ai/knowledge/bases/{kb_id}/documents/upload \
-H "Authorization: Bearer {token}" \
-F "file=@test.pdf"
Expected: 202 返回 document id
- Step 4: 提交
git commit -m "feat(ai): 知识库文件上传 + 静态文件服务"
Chunk 3: Phase 2 — 文档处理管线
Task 8: 格式解析器(DocumentParser)
Files:
-
Create:
crates/erp-ai/src/knowledge/parser.rs -
Modify:
crates/erp-ai/src/knowledge/mod.rs -
Step 1: 定义 ParsedDocument 结构体和 trait
// parser.rs
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub title: String,
pub text: String, // 提取的纯文本
pub total_pages: Option<usize>,
pub metadata: serde_json::Value, // { format, pages, ... }
}
pub struct DocumentParser;
impl DocumentParser {
pub async fn parse_file(file_path: &str, file_type: &str) -> AiResult<ParsedDocument> {
match file_type {
"pdf" => Self::parse_pdf(file_path).await,
"docx" => Self::parse_docx(file_path).await,
"xlsx" | "csv" => Self::parse_spreadsheet(file_path, file_type).await,
"txt" => Self::parse_text(file_path).await,
"md" => Self::parse_markdown(file_path).await,
_ => Err(AiError::KnowledgeError(format!("Unsupported file type: {}", file_type))),
}
}
pub async fn parse_url(url: &str) -> AiResult<ParsedDocument> { /* ... */ }
}
- Step 2: 实现 PDF 解析
async fn parse_pdf(path: &str) -> AiResult<ParsedDocument> {
let bytes = tokio::fs::read(path).await.map_err(|e| AiError::KnowledgeError(e.to_string()))?;
let text = pdf_extract::extract_text_from_mem(&bytes)
.map_err(|e| AiError::KnowledgeError(format!("PDF parse failed: {}", e)))?;
// 检测纯图片 PDF:提取文本长度 vs 页数比例
Ok(ParsedDocument { title: Path::new(path).file_stem().unwrap().to_string_lossy().into(), text, total_pages: None, metadata: json!({"format": "pdf"}) })
}
- Step 3: 实现 Word / TXT / Markdown / Excel / URL 解析
每个格式一个私有方法,参考设计规格 §4.2:
-
parse_docx— 使用docx-rs提取段落文本 -
parse_text— 标准tokio::fs::read_to_string -
parse_markdown— 同 text,额外提取标题层级到 metadata -
parse_spreadsheet— 使用calamine,表头+行数据 → 自然语言描述 -
parse_url— 使用reqwest+scraper,提取正文(去广告/导航) -
Step 4: 在 mod.rs 中注册 parser 模块
pub mod parser;
-
Step 5: cargo check 验证
-
Step 6: 提交
git commit -m "feat(ai): 文档解析器 — PDF/Word/Excel/TXT/MD/URL"
Task 9: 智能切片器(Chunker)
Files:
-
Create:
crates/erp-ai/src/knowledge/chunker.rs -
Step 1: 定义 ChunkStrategy 和 Chunk 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkStrategyConfig {
pub max_tokens: usize,
pub overlap: usize,
pub separator: String,
pub strategy: String, // per_item / qa_pair / paragraph / section / per_row
}
#[derive(Debug, Clone)]
pub struct RawChunk {
pub index: usize,
pub content: String,
pub token_count: usize,
pub metadata: serde_json::Value,
}
- Step 2: 实现 5 种切片策略
pub fn chunk_text(text: &str, doc_title: &str, config: &ChunkStrategyConfig) -> Vec<RawChunk> {
let prefixed_text = format!("[文档: {}] {}", doc_title, text);
match config.strategy.as_str() {
"per_item" => split_by_separator(&prefixed_text, &config.separator, config.max_tokens),
"qa_pair" => split_by_qa_pattern(&prefixed_text, config.max_tokens),
"paragraph" => split_with_overlap(&prefixed_text, &config.separator, config.max_tokens, config.overlap),
"section" => split_by_headings(&prefixed_text, config.max_tokens, config.overlap),
"per_row" => split_by_rows(&prefixed_text, config.max_tokens),
_ => split_with_overlap(&prefixed_text, "\n\n", config.max_tokens, config.overlap),
}
}
每个策略函数实现具体逻辑。token_count 使用简单的 text.split_whitespace().count() / 0.75 估算。
-
Step 3: 在 mod.rs 注册
-
Step 4: cargo check
-
Step 5: 提交
git commit -m "feat(ai): 智能切片器 — 5 种策略"
Task 10: 文档处理管线(Pipeline)
Files:
-
Create:
crates/erp-ai/src/knowledge/pipeline.rs -
Step 1: 定义 Pipeline 结构和主流程
pub struct DocumentPipeline {
db: DatabaseConnection,
embedding: Arc<EmbeddingService>,
}
impl DocumentPipeline {
pub fn new(db: DatabaseConnection, embedding: Arc<EmbeddingService>) -> Self { Self { db, embedding } }
pub async fn process(&self, doc_id: Uuid) {
// 1. 读取 document 记录,获取 file_path/source_url/raw_content
// 2. 更新 status = processing
// 3. 解析文档 → ParsedDocument
// 4. 读取 KB 的 chunk_strategy
// 5. 切片 → Vec<RawChunk>
// 6. 批量 embedding
// 7. 批量写入 ai_knowledge_chunks
// 8. 原子更新 document.status + kb 计数
// 任一步骤失败 → status=failed + error_message
if let Err(e) = self.process_inner(doc_id).await {
tracing::error!("Pipeline failed for doc {}: {}", doc_id, e);
self.mark_failed(doc_id, &e.to_string()).await;
}
}
}
- Step 2: 实现 process_inner 核心逻辑
async fn process_inner(&self, doc_id: Uuid) -> AiResult<()> {
// Step 1: 读取 document
let doc = self.get_document(doc_id).await?;
self.update_status(doc_id, "processing").await?;
// Step 2: 解析
let parsed = match doc.source_type.as_str() {
"upload" => DocumentParser::parse_file(&doc.file_path, &doc.file_type).await?,
"url" => DocumentParser::parse_url(&doc.source_url).await?,
"manual" => ParsedDocument { title: doc.title, text: doc.raw_content, total_pages: None, metadata: json!({}) },
_ => return Err(AiError::KnowledgeError("Unknown source type".into())),
};
// Step 3: 切片
let kb = self.get_kb(doc.knowledge_base_id).await?;
let strategy: ChunkStrategyConfig = serde_json::from_value(kb.chunk_strategy)?;
let chunks = chunker::chunk_text(&parsed.text, &doc.title, &strategy);
// Step 4: 批量 embedding + 写入
let mut embedded_count = 0;
for batch in chunks.chunks(20) {
let texts: Vec<&str> = batch.iter().map(|c| c.content.as_str()).collect();
match self.embedding.embed_batch(&texts).await {
Ok(embeddings) => {
self.batch_insert_chunks(doc_id, doc.knowledge_base_id, doc.tenant_id, batch, &embeddings).await?;
embedded_count += batch.len();
}
Err(e) => {
tracing::warn!("Embedding batch failed: {}, storing without embedding", e);
self.batch_insert_chunks_no_embedding(doc_id, doc.knowledge_base_id, doc.tenant_id, batch).await?;
}
}
}
// Step 5: 更新状态和计数(原子增量)
self.complete_document(doc_id, chunks.len(), embedded_count).await?;
self.increment_kb_counts(doc.knowledge_base_id, 1, chunks.len()).await?;
Ok(())
}
- Step 3: 在 spawn 中调用 Pipeline
在 knowledge.rs 的 upload_document / import_url / create_manual 中:
let pipeline = DocumentPipeline::new(self.db.clone(), self.embedding.clone());
tokio::spawn(async move {
pipeline.process(doc_id).await;
});
-
Step 4: cargo check
-
Step 5: 提交
git commit -m "feat(ai): 文档处理管线 — 异步解析+切片+embedding"
Task 11: 命中测试 API
Files:
-
Modify:
crates/erp-ai/src/knowledge/vector_search.rs -
Modify:
crates/erp-ai/src/handler/knowledge_handler.rs -
Step 1: 改造 vector_search.rs 适配新表
新增 search_chunks 方法,查询 ai_knowledge_chunks JOIN ai_knowledge_bases JOIN ai_knowledge_documents。所有参数使用参数化绑定,消除旧 format! SQL 拼接。
- Step 2: 实现 hit_test handler
接收 query + kb_id,调用 EmbeddingService::embed + search_chunks,返回 top_k 切片 + 相似度 + 路由路径说明。
-
Step 3: cargo check + 手动测试
-
Step 4: 提交
git commit -m "feat(ai): 命中测试 API + 向量检索适配新表"
Task 12: 管线端到端测试
Files:
-
Create:
crates/erp-ai/tests/knowledge_pipeline_test.rs -
Step 1: 写端到端测试
#[tokio::test]
async fn test_full_pipeline() {
// 1. 创建知识库 (kb_type=institution_info)
// 2. 创建测试 TXT 文件
// 3. 调用 upload_document
// 4. 等待处理完成(轮询 status)
// 5. 验证 document.status = completed
// 6. 验证 chunks 数量 > 0
// 7. 验证 hit_test 能检索到切片
}
- Step 2: 运行测试
cargo test --package erp-ai knowledge_pipeline -- --nocapture
- Step 3: 提交
git commit -m "test(ai): 知识库管线端到端测试"
Chunk 4: Phase 3 — 混合意图路由
Task 13: HybridKnowledgeRouter 实现
Files:
-
Create:
crates/erp-ai/src/knowledge/hybrid_router.rs -
Step 1: 定义 HybridKnowledgeRouter
pub struct HybridKnowledgeRouter {
db: DatabaseConnection,
embedding: Arc<EmbeddingService>,
llm_provider: Arc<dyn LlmProvider>,
}
#[async_trait::async_trait]
impl KnowledgeSource for HybridKnowledgeRouter {
async fn get_context(&self, query: &KnowledgeQuery) -> AiResult<KnowledgeContext> {
let query_text = query.query_text.as_deref().unwrap_or("");
// Layer 0: 无知识库时返回空 context
let all_kbs = self.load_enabled_kbs(&query.tenant_id).await?;
if all_kbs.is_empty() {
return Ok(KnowledgeContext::empty());
}
// Layer 1: 关键词粗筛
let candidate_kb_ids = Self::keyword_match(query_text, &all_kbs);
// Layer 2: 向量检索
let results = self.vector_search(query_text, &candidate_kb_ids).await?;
// Layer 3: LLM 兜底(仅 top < 0.75 时触发)
let final_results = if results.first().map(|r| r.similarity).unwrap_or(0.0) < 0.75 {
self.llm_fallback(query_text, &results, &all_kbs).await?
} else {
results
};
self.build_context_and_track_hits(&final_results, &query.tenant_id).await
}
}
- Step 2: 实现 keyword_match
遍历所有 KB 的 intent_keywords,匹配 query 中的关键词。无匹配则返回全部 KB ID。
- Step 3: 实现 vector_search
调用改造后的 vector_search.rs 的 search_chunks 方法,参数化绑定。
- Step 4: 实现 llm_fallback
调用轻量 LLM(优先 Ollama qwen3:4b)做意图分类,返回匹配的 KB ID 列表,重新调用 vector_search。
- Step 5: 实现 build_context_and_track_hits
组装 RAG context + 异步更新 chunk.hit_count。
-
Step 6: 在 mod.rs 注册
-
Step 7: cargo check
-
Step 8: 提交
git commit -m "feat(ai): 混合意图路由 — 关键词+向量+LLM兜底"
Task 14: KnowledgeQuery 扩展 + 旧 source 兼容
Files:
-
Modify:
crates/erp-ai/src/knowledge/mod.rs -
Modify:
crates/erp-ai/src/service/analysis.rs -
Step 1: 扩展 KnowledgeQuery
pub struct KnowledgeQuery {
pub tenant_id: Uuid,
pub analysis_type: Option<String>, // 保留兼容
pub patient_context: Option<PatientSummary>,
pub query_text: Option<String>,
pub kb_type_filter: Option<String>, // 新增:按知识库类型过滤
}
- Step 2: 在 analysis.rs 中使用 HybridKnowledgeRouter
替换 StructuredKnowledgeSource + VectorKnowledgeSource 为 HybridKnowledgeRouter。AnalysisService 的 knowledge_source 字段类型不变(Option<Arc<dyn KnowledgeSource>>)。
- Step 3: 旧 source 文件标记 deprecated
在 structured_source.rs 和 vector_source.rs 顶部添加 #[deprecated(note = "Use HybridKnowledgeRouter instead")]
-
Step 4: cargo check
-
Step 5: 提交
git commit -m "refactor(ai): 知识源切换为 HybridKnowledgeRouter"
Task 15: Agent Tool 适配
Files:
-
Modify:
crates/erp-ai/src/agent/tools/search_medical_knowledge.rs -
Step 1: 改造 tool 使用新路由
pub struct SearchMedicalKnowledgeTool {
router: Arc<HybridKnowledgeRouter>,
}
impl AgentTool for SearchMedicalKnowledgeTool {
async fn execute(&self, params: &Value) -> AiResult<Value> {
let query = params["query"].as_str().ok_or(...)?;
let kb_type = params["knowledge_base_type"].as_str(); // 新增可选参数
let knowledge_query = KnowledgeQuery {
query_text: Some(query.to_string()),
kb_type_filter: kb_type.map(String::from),
..Default::default()
};
let context = self.router.get_context(&knowledge_query).await?;
// 格式化返回结果
}
}
-
Step 2: 更新 sandbox.rs 注册
-
Step 3: cargo check
-
Step 4: 提交
git commit -m "refactor(ai): Agent Tool 适配新知识库路由"
Chunk 5: Phase 4 — 前端管理 UI
Task 16: 前端 API client 重写
Files:
-
Modify:
apps/web/src/api/ai/knowledge.ts -
Step 1: 定义新 TypeScript 接口
export interface KnowledgeBase {
id: string; name: string; kb_type: string;
description?: string; icon?: string;
chunk_strategy: ChunkStrategyConfig; intent_keywords: string[];
is_enabled: boolean; document_count: number; chunk_count: number;
created_at: string; updated_at: string;
}
export interface KnowledgeDocument {
id: string; knowledge_base_id: string; title: string;
source_type: 'upload' | 'url' | 'manual';
file_type?: string; file_size?: number; source_url?: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
chunk_count: number; error_message?: string;
created_at: string; updated_at: string;
}
export interface KnowledgeChunk {
id: string; document_id: string; knowledge_base_id: string;
chunk_index: number; content: string; token_count?: number;
metadata: Record<string, unknown>; hit_count: number; last_hit_at?: string;
}
export interface SearchResult {
chunk_id: string; content: string; similarity: number;
kb_name: string; doc_title: string;
metadata: Record<string, unknown>;
}
export interface ChunkStrategyConfig {
max_tokens: number; overlap: number; separator: string; strategy: string;
}
- Step 2: 实现 knowledgeApi 对象
覆盖所有新端点:listBases, createBase, getBase, updateBase, deleteBase, listDocuments, uploadDocument, importUrl, createManual, getDocument, deleteDocument, reprocessDocument, listChunks, updateChunk, reEmbedChunk, hitTest。
- Step 3: 提交
git commit -m "feat(web): 知识库 V2 API client 重写"
Task 17: 知识库列表页 + 创建弹窗
Files:
-
Create:
apps/web/src/pages/health/KnowledgeBase/index.tsx -
Create:
apps/web/src/pages/health/KnowledgeBase/CreateKBModal.tsx -
Step 1: 创建 KnowledgeBaseList 页面
参照设计规格 §6.2:卡片网格布局。KB_TYPE_CONFIG 映射表(7 种类型对应图标+渐变色)。每张卡片显示:渐变色头部 + 图标 + 名称 + 类型标签 + 文档/切片数 + 状态。点击导航到详情页。
- Step 2: 创建 CreateKBModal
名称输入 + 类型选择(卡片式单选 7 种)+ 描述 + 意图关键词 TagInput + 切片策略折叠配置。
- Step 3: 提交
git commit -m "feat(web): 知识库列表页 + 创建弹窗"
Task 18: 知识库详情页 + 文档上传
Files:
-
Create:
apps/web/src/pages/health/KnowledgeBase/KnowledgeBaseDetail.tsx -
Create:
apps/web/src/pages/health/KnowledgeBase/UploadDocument.tsx -
Step 1: 创建 KnowledgeBaseDetail
面包屑 + 操作栏 + Tab 切换(文档列表|设置|命中测试)。文档列表:Ant Design Table + 处理进度轮询。设置 Tab:编辑 KB 配置。
- Step 2: 创建 UploadDocument
三种上传入口 Tabs:文件上传(Dragger,accept=".pdf,.docx,.txt,.md,.xlsx,.csv",50MB)+ URL 导入 + 手动录入(TextArea)。
- Step 3: 提交
git commit -m "feat(web): 知识库详情页 + 文档上传"
Task 19: 命中测试面板 + 路由注册
Files:
-
Create:
apps/web/src/pages/health/KnowledgeBase/HitTestPanel.tsx -
Create:
apps/web/src/pages/health/KnowledgeBase/ChunkCard.tsx -
Modify:
apps/web/src/App.tsx -
Step 1: 创建 HitTestPanel
输入框 + 测试按钮 + 路由路径展示 + 按相似度排序的 ChunkCard 列表。
- Step 2: 创建 ChunkCard
切片序号 + 来源 + 相似度标签(绿/黄/红)+ 可展开原文 + 编辑按钮。
- Step 3: 更新 App.tsx 路由
<Route path="/health/ai-knowledge" element={<KnowledgeBaseList />} />
<Route path="/health/ai-knowledge/:kbId" element={<KnowledgeBaseDetail />} />
-
Step 4: pnpm build 验证
-
Step 5: 提交
git commit -m "feat(web): 命中测试 + 切片预览 + 路由注册"
Chunk 6: Phase 5 — AI 客服集成 + 旧数据迁移
Task 20: chat_handler RAG 注入
Files:
-
Modify:
crates/erp-ai/src/service/chat_handler.rs -
Modify:
crates/erp-ai/src/state.rs -
Step 1: 在 chat_handler 中注入 RAG
消息发送前调用 knowledge_router.get_context(),非空时追加到 system prompt。失败时 warn 日志但不阻断对话。
- Step 2: AiState 注入 HybridKnowledgeRouter
新增 knowledge_router: Option<Arc<HybridKnowledgeRouter>> 字段。
-
Step 3: cargo check
-
Step 4: 提交
git commit -m "feat(ai): chat_handler RAG 注入 — AI 客服知识增强"
Task 21: 前端引用渲染
Files:
-
Modify:
apps/web/src/pages/health/ChatPage.tsx -
Step 1: 实现 parseReferences 函数
正则解析 [ref:xxx] 标记,拆分为 text/ref 段。ref 段渲染为蓝色可点击 Tag,下方 Collapse 显示来源详情。
- Step 2: 提交
git commit -m "feat(web): AI 回答引用溯源渲染"
Task 22: 旧数据迁移脚本
Files:
-
Create:
crates/erp-server/migration/src/m20260526_000166_migrate_old_knowledge_data.rs -
Modify:
crates/erp-server/migration/src/lib.rs -
Step 1: 实现迁移(临时映射表 + 事务)
参照设计规格 §8.3:临时映射表精确关联 → 按 analysis_type 创建 KB → 迁移 references/guides/rules → 清理。
-
Step 2: 注册 + cargo check + 手动运行验证
-
Step 3: 提交
git commit -m "feat(db): 旧知识库数据迁移到新模型"
Task 23: 权限 + 菜单更新
Files:
-
Create:
crates/erp-server/migration/src/m20260526_000167_update_knowledge_menu.rs(如需要) -
Step 1: 确认权限码复用
ai.knowledge.list 和 ai.knowledge.manage 已覆盖,无需新增。确认菜单路径指向 /health/ai-knowledge。
- Step 2: 提交
git commit -m "chore(db): 知识库菜单路由更新"
Chunk 7: Phase 6 — 测试 + 验收
Task 24: 后端单元/集成测试
Files:
-
Create:
crates/erp-ai/tests/knowledge_v2_test.rs -
Step 1: KB CRUD 测试 — create → get → update → list → delete 完整流程
-
Step 2: 文档处理测试 — TXT + URL 两种源的处理验证
-
Step 3: 命中测试 — 上传后检索验证切片 + 相似度
-
Step 4: 混合路由测试 — 关键词命中 + 向量检索路径
-
Step 5: 运行
cargo test --workspace -
Step 6: 提交
git commit -m "test(ai): 知识库 V2 单元/集成测试"
Task 25: 前端 E2E 测试
Files:
-
Create:
apps/web/e2e/knowledge-base.spec.ts -
Step 1: 编写 Playwright E2E — 登录 → 创建 KB → 上传 → 命中测试 → 验证结果
-
Step 2: 运行
pnpm exec playwright test knowledge-base -
Step 3: 提交
git commit -m "test(web): 知识库 V2 E2E 测试"
Task 26: 端到端验收 + wiki 更新
Files:
-
Modify:
wiki/index.md -
Step 1: 手动验收 — 创建KB → 上传PDF/Word/Excel → URL导入 → 手动录入 → 命中测试 → AI客服对话验证引用
-
Step 2: 更新 wiki 关键数字 — 表数+3, 路由数, 测试数
-
Step 3: 旧文件清理 — 旧 AiKnowledgePage.tsx 标记 deprecated
-
Step 4: 最终验证 —
cargo check+cargo test+pnpm build -
Step 5: 提交 + 推送
git push
验收标准
| # | 标准 | 验证方式 |
|---|---|---|
| 1 | 创建知识库 + 上传 PDF/Word/Excel,处理成功 | 手动测试 |
| 2 | URL 导入能抓取并切片 | 手动测试 |
| 3 | 命中测试返回正确切片 + 相似度 | 手动测试 |
| 4 | AI 客服引用知识库,显示 [ref:xxx] | 手动测试 |
| 5 | 旧数据成功迁移 | DB 查询 |
| 6 | cargo check 0 警告 | CI |
| 7 | cargo test 全部通过 | CI |
| 8 | pnpm build 通过 | CI |
| 9 | 所有新端点有权限守卫 | 代码审查 |