# 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` 表: ```rust // 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` 的迁移列表中添加: ```rust Box::new(m20260526_000163_create_ai_knowledge_bases::Migration), ``` - [ ] **Step 3: 运行迁移验证** ```bash cd crates/erp-server && cargo check ``` Expected: 编译通过,无错误 - [ ] **Step 4: 提交** ```bash 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** ```rust Box::new(m20260526_000164_create_ai_knowledge_documents::Migration), Box::new(m20260526_000165_create_ai_knowledge_chunks::Migration), ``` - [ ] **Step 4: cargo check 验证** - [ ] **Step 5: 提交** ```bash 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 添加新依赖** ```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: 提交** ```bash 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]`): ```rust #[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, #[validate(length(max = 50))] pub icon: Option, pub intent_keywords: Option>, pub chunk_strategy: Option, } #[derive(Debug, Serialize, Deserialize, Validate)] pub struct UpdateKnowledgeBaseReq { #[validate(length(min = 1, max = 200))] pub name: Option, #[validate(length(max = 2000))] pub description: Option, pub icon: Option, pub intent_keywords: Option>, pub chunk_strategy: Option, pub is_enabled: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct ListKBQuery { pub kb_type: Option, pub page: Option, pub page_size: Option, } 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` — 查询知识库列表,支持 kb_type 过滤 - `create_base(tenant_id, user_id, req) -> Uuid` — 创建知识库,kb_type 对应默认 chunk_strategy - `get_base(tenant_id, id) -> Option` — 单条查询 - `update_base(tenant_id, user_id, id, req)` — 更新,乐观锁 version_lock - `delete_base(tenant_id, id)` — 软删除,同时软删除下级 documents 所有数据库操作使用 raw SQL(`sea_orm::Statement`),因为 chunks 表含 vector 类型。 - [ ] **Step 3: cargo check 验证** - [ ] **Step 4: 提交** ```bash git commit -m "feat(ai): 知识库 V2 Service — 知识库 CRUD 方法" ``` ### Task 5: KnowledgeService 重写 — 文档 CRUD + 文件上传 **Files:** - Modify: `crates/erp-ai/src/service/knowledge.rs` - [ ] **Step 1: 定义文档 DTO** ```rust #[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, } #[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` — 按 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` — 含处理状态 - `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` — 分页查询 - `update_chunk(tenant_id, chunk_id, content) — 更新切片内容 - `re_embed_chunk(tenant_id, chunk_id) — 重新 embedding 单个切片 - [ ] **Step 4: cargo check 验证** - [ ] **Step 5: 提交** ```bash 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` — DELETE - `reprocess_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 结构: ```rust // 知识库管理 .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` 字段不变,构造函数适配新方法签名。 - [ ] **Step 4: cargo check 验证** - [ ] **Step 5: 提交** ```bash 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** ```rust async fn upload_document( State(state): State>, user: AuthUser, Path(kb_id): Path, 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 + 手动测试上传** ```bash 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: 提交** ```bash 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** ```rust // parser.rs #[derive(Debug, Clone)] pub struct ParsedDocument { pub title: String, pub text: String, // 提取的纯文本 pub total_pages: Option, pub metadata: serde_json::Value, // { format, pages, ... } } pub struct DocumentParser; impl DocumentParser { pub async fn parse_file(file_path: &str, file_type: &str) -> AiResult { 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 { /* ... */ } } ``` - [ ] **Step 2: 实现 PDF 解析** ```rust async fn parse_pdf(path: &str) -> AiResult { 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 模块** ```rust pub mod parser; ``` - [ ] **Step 5: cargo check 验证** - [ ] **Step 6: 提交** ```bash 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 结构** ```rust #[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 种切片策略** ```rust pub fn chunk_text(text: &str, doc_title: &str, config: &ChunkStrategyConfig) -> Vec { 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: 提交** ```bash git commit -m "feat(ai): 智能切片器 — 5 种策略" ``` ### Task 10: 文档处理管线(Pipeline) **Files:** - Create: `crates/erp-ai/src/knowledge/pipeline.rs` - [ ] **Step 1: 定义 Pipeline 结构和主流程** ```rust pub struct DocumentPipeline { db: DatabaseConnection, embedding: Arc, } impl DocumentPipeline { pub fn new(db: DatabaseConnection, embedding: Arc) -> 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 // 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 核心逻辑** ```rust 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` 中: ```rust 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: 提交** ```bash 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: 提交** ```bash git commit -m "feat(ai): 命中测试 API + 向量检索适配新表" ``` ### Task 12: 管线端到端测试 **Files:** - Create: `crates/erp-ai/tests/knowledge_pipeline_test.rs` - [ ] **Step 1: 写端到端测试** ```rust #[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: 运行测试** ```bash cargo test --package erp-ai knowledge_pipeline -- --nocapture ``` - [ ] **Step 3: 提交** ```bash 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** ```rust pub struct HybridKnowledgeRouter { db: DatabaseConnection, embedding: Arc, llm_provider: Arc, } #[async_trait::async_trait] impl KnowledgeSource for HybridKnowledgeRouter { async fn get_context(&self, query: &KnowledgeQuery) -> AiResult { 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: 提交** ```bash 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** ```rust pub struct KnowledgeQuery { pub tenant_id: Uuid, pub analysis_type: Option, // 保留兼容 pub patient_context: Option, pub query_text: Option, pub kb_type_filter: Option, // 新增:按知识库类型过滤 } ``` - [ ] **Step 2: 在 analysis.rs 中使用 HybridKnowledgeRouter** 替换 `StructuredKnowledgeSource` + `VectorKnowledgeSource` 为 `HybridKnowledgeRouter`。`AnalysisService` 的 `knowledge_source` 字段类型不变(`Option>`)。 - [ ] **Step 3: 旧 source 文件标记 deprecated** 在 `structured_source.rs` 和 `vector_source.rs` 顶部添加 `#[deprecated(note = "Use HybridKnowledgeRouter instead")]` - [ ] **Step 4: cargo check** - [ ] **Step 5: 提交** ```bash 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 使用新路由** ```rust pub struct SearchMedicalKnowledgeTool { router: Arc, } impl AgentTool for SearchMedicalKnowledgeTool { async fn execute(&self, params: &Value) -> AiResult { 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: 提交** ```bash 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 接口** ```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; 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; } 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: 提交** ```bash 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: 提交** ```bash 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: 提交** ```bash 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 路由** ```tsx } /> } /> ``` - [ ] **Step 4: pnpm build 验证** - [ ] **Step 5: 提交** ```bash 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>` 字段。 - [ ] **Step 3: cargo check** - [ ] **Step 4: 提交** ```bash 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: 提交** ```bash 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: 提交** ```bash 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: 提交** ```bash 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: 提交** ```bash 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: 提交** ```bash 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: 提交 + 推送** ```bash 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 | 所有新端点有权限守卫 | 代码审查 |