Files
hms/docs/superpowers/plans/2026-05-26-ai-knowledge-base-v2-plan.md
iven dda8be9079 docs(ai): AI 知识库 V2 实施计划 — 7 Phase / 26 Tasks
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)
2026-05-26 23:00:46 +08:00

39 KiB
Raw Blame History

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 前端管理 UIDify 风格) 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_strategy
  • get_base(tenant_id, id) -> Option<KBModel> — 单条查询
  • update_base(tenant_id, user_id, id, req) — 更新,乐观锁 version_lock
  • delete_base(tenant_id, id) — 软删除,同时软删除下级 documents

所有数据库操作使用 raw SQLsea_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 JSONURL 导入,调用 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 结构:

// 知识库管理
.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.rsupload_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.rsupload_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.rssearch_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 + VectorKnowledgeSourceHybridKnowledgeRouterAnalysisServiceknowledge_source 字段类型不变(Option<Arc<dyn KnowledgeSource>>)。

  • Step 3: 旧 source 文件标记 deprecated

structured_source.rsvector_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文件上传Draggeraccept=".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.listai.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 所有新端点有权限守卫 代码审查