Files
hms/docs/superpowers/specs/2026-05-26-ai-knowledge-base-v2-design.md
iven af2484e63b docs(ai): 知识库 V2 设计规格 — review 修复
修复 3 CRITICAL + 4 HIGH 问题:
- C1: 明确禁止 format! 拼接 SQL,所有 pgvector 查询参数化
- C2: 迁移 SQL 改用临时映射表精确关联,防数据重复/丢失
- C3: SSRF 防护细化(DNS rebinding + 超时 + 重定向校验)
- H1: document_count/chunk_count 改为原子 SQL 增量
- H2: embedding 部分失败:NULL embedding 写入 + embedded_count 统计
- H3: chunks 表补充 updated_at/updated_by 审计字段
- H4: 新增 validator crate 依赖说明
- M1: 空知识库边界返回空 context 不报错
- M2: 向量索引改用 HNSW(无需预热)
2026-05-26 22:18:28 +08:00

53 KiB
Raw Blame History

AI 知识库 V2 — 统一知识管理平台设计规格

日期: 2026-05-26 | 状态: 草稿 | 作者: brainstorming session 替代: 旧三层模型ai_knowledge_rules / references / guides

目录

  1. 背景与目标 — 为什么要重做、解决什么问题
  2. 架构概览 — 整体架构、核心模块、数据流
  3. 数据模型 — 3 张新表、字段定义、索引策略
  4. 文档处理管线 — 文件解析、智能切片、embedding、异步处理
  5. 混合意图路由 — 三层漏斗:关键词 → 向量 → LLM 兜底
  6. 管理后台 UI — Dify 风格三页面:知识库列表 / 文档管理 / 命中测试
  7. AI 客服集成 — RAG 注入、System Prompt、引用溯源
  8. 旧数据迁移 — 旧 3 表 → 新模型迁移策略
  9. API 端点清单 — 所有新增/变更的 API
  10. 依赖与风险 — 新增 Rust 依赖、技术风险、缓解措施

设计决策记录

# 决策 选项 选定 理由
1 服务对象 A:AI引擎 B:医护查询 C:两者兼顾 C当前优先 AI 客服) 渐进交付
2 知识范围 机构信息/就医流程/健康科普 三类全覆盖 用户确认
3 录入方式 单一/混合 混合(上传+表单+URL 灵活性
4 架构方案 A:MVP B:完整平台 C:渐进 方案 B 一次做好更省时
5 意图路由 A:LLM B:向量 C:混合 混合(关键词+向量+LLM兜底 平衡准确性和速度
6 UI 风格 A:Dify B:FastGPT C:简洁 Dify 风格 用户确认
7 数据模型 保留旧模型/新建/替换 替换旧模型 统一管理

1. 背景与目标

1.1 现状问题

当前 AI 知识库基于三层模型(ai_knowledge_rules / ai_knowledge_references / ai_knowledge_guides),存在以下核心问题:

优先级 问题 影响
P0 录入方式原始 — 仅支持手动文本输入到 textarea无文件上传、无 PDF 解析、无 URL 导入 医护人员无法高效录入200 页临床指南无法导入
P1 知识无分类体系 — 仅靠 analysis_type 一个维度6 个选项)过滤 检索效率低,不同类型知识混杂
P1 无质量审核 — 任何人可直接发布,无版本控制 医疗知识错误影响患者安全
P2 无使用效果追踪 — 不知道哪些知识被 AI 引用、embedding 质量如何 投入产出不可衡量
P2 规则层无 UIai_knowledge_rules 无 CRUD API 和管理界面 规则只能通过数据库操作
P2 引用不透明 — AI 分析结果不显示知识来源和置信度 用户无法验证 AI 推理依据

1.2 目标

核心目标: 构建 Dify 级的统一知识管理平台,作为 AI 客服的知识底座,让 AI 能准确回答机构相关问题。

阶段性交付:

  • Phase 1当前 AI 客服知识底座 — 机构信息、就医流程、健康科普
  • Phase 2未来 医护直接查询 — 医学文献检索、临床决策支持
  • Phase 3未来 AI 分析引擎增强 — 化验解读、趋势分析的知识增强

1.3 成功标准

指标 目标值
知识录入效率 从手动粘贴 200 页文档 → 上传 1 个 PDF5 分钟内自动处理完成
检索准确率 意图路由命中率 ≥ 90%(命中测试验证)
AI 回答质量 知识库覆盖范围内的问题85%+ 能基于知识库准确回答
管理效率 非技术人员可在 10 分钟内完成一个知识库的创建和文档导入

2. 架构概览

2.1 核心架构图

┌─────────────────────────────────────────────────────────────────┐
│                        管理后台 (Web)                            │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────────────┐  │
│  │ 知识库列表    │  │ 文档管理      │  │ 命中测试 + 切片预览    │  │
│  │ (卡片视图)    │  │ (表格 + 上传) │  │ (检索验证)            │  │
│  └──────┬───────┘  └──────┬───────┘  └───────────┬───────────┘  │
│         │                 │                       │              │
└─────────┼─────────────────┼───────────────────────┼──────────────┘
          │                 │                       │
          ▼                 ▼                       ▼
┌─────────────────────────────────────────────────────────────────┐
│                     API Layer (Axum)                             │
│  ┌─────────────────┐  ┌──────────────────┐  ┌────────────────┐  │
│  │ KB CRUD Handler  │  │ Document Handler  │  │ Search Handler │  │
│  │ (知识库管理)      │  │ (上传/状态/重处理) │  │ (命中测试)      │  │
│  └────────┬────────┘  └────────┬─────────┘  └───────┬────────┘  │
└───────────┼─────────────────────┼────────────────────┼───────────┘
            │                     │                    │
            ▼                     ▼                    ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Service Layer (Rust)                          │
│                                                                  │
│  ┌──────────────────┐  ┌──────────────────────────────────────┐ │
│  │ KnowledgeService  │  │ DocumentPipeline                     │ │
│  │ - CRUD 知识库      │  │ - 文件解析 (PDF/Word/Excel/URL/TXT) │ │
│  │ - CRUD 文档        │  │ - 智能切片 (按 KB 策略)              │ │
│  │ - 切片管理         │  │ - Embedding (复用 EmbeddingService)  │ │
│  └────────┬─────────┘  │ - 异步处理 (tokio::spawn)             │ │
│           │             └──────────────┬───────────────────────┘ │
│           ▼                           ▼                         │
│  ┌──────────────────────────────────────────────────────────────┐│
│  │ HybridKnowledgeRouter (实现 KnowledgeSource trait)           ││
│  │ Layer 1: 关键词粗筛 → Layer 2: 向量检索 → Layer 3: LLM 兜底 ││
│  └──────────────────────────┬───────────────────────────────────┘│
│                              │                                   │
└──────────────────────────────┼───────────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                    AI 客服集成                                    │
│  ┌──────────────┐    ┌─────────────────┐    ┌────────────────┐  │
│  │ chat_handler  │───▶│ RAG Context 注入  │───▶│ LLM Provider   │  │
│  │ (现有)        │    │ (System Prompt)   │    │ (Claude/Ollama)│  │
│  └──────────────┘    └─────────────────┘    └────────────────┘  │
│         │                                             │          │
│         ▼                                             ▼          │
│  ┌──────────────┐                          ┌────────────────┐   │
│  │ 前端 ChatPage │◀─── [ref:xxx] 引用 ◀────│ AI 回答 + 溯源  │   │
│  └──────────────┘                          └────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                    PostgreSQL (pgvector)                         │
│  ┌──────────────────┐  ┌───────────────────┐  ┌──────────────┐ │
│  │ ai_knowledge_bases│  │ ai_knowledge_docs  │  │ ai_chunks    │ │
│  │ (知识库目录)       │  │ (文档元数据)        │  │ (切片+向量)   │ │
│  └──────────────────┘  └───────────────────┘  └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘

2.2 核心模块职责

模块 位置 职责
KnowledgeService erp-ai/src/service/knowledge.rs 知识库/文档/切片 CRUD、统计更新
DocumentPipeline erp-ai/src/knowledge/pipeline.rs(新增) 文件解析 → 切片 → embedding → 存储
DocumentParser erp-ai/src/knowledge/parser.rs(新增) PDF/Word/Excel/URL/Markdown/TXT 格式解析
ChunkStrategy erp-ai/src/knowledge/chunk.rs(新增) 按 KB 配置策略执行切片
HybridKnowledgeRouter erp-ai/src/knowledge/hybrid_router.rs(新增) 三层意图路由,实现 KnowledgeSource trait
EmbeddingService erp-ai/src/service/embedding.rs(现有) 复用,无需修改
KnowledgeSearchRepo erp-ai/src/knowledge/vector_search.rs(改造) pgvector 查询,适配新表结构

2.3 关键数据流

知识录入流:

用户上传文件 → POST /ai/knowledge/bases/{id}/documents
  → 创建 document 记录 (status=pending)
  → tokio::spawn 异步处理
  → DocumentPipeline::process(document_id)
    → DocumentParser::parse(file) → 提取纯文本
    → ChunkStrategy::chunk(text, strategy) → 生成切片
    → EmbeddingService::embed_batch(chunks) → 生成向量
    → 批量写入 ai_knowledge_chunks
    → 更新 document.status = completed

知识检索流AI 客服):

用户提问 → chat_handler
  → HybridKnowledgeRouter::get_context(query)
    → Layer 1: keyword_match(query, kb.intent_keywords) → 匹配知识库列表
    → Layer 2: vector_search(query, kb_ids) → top 5, threshold 0.65
    → Layer 3: (仅 top < 0.75) llm_classify(query) → 重定向知识库 → 重新 vector_search
  → 组装 RAG context (≤4000 tokens)
  → 注入 System Prompt
  → LLM 生成回答 + [ref:xxx] 引用
  → 异步更新 chunk.hit_count

3. 数据模型

3.1 表设计概览

3 张新表替换旧 3 表(ai_knowledge_rules / ai_knowledge_references / ai_knowledge_guides

ai_knowledge_bases (1) ──< ai_knowledge_documents (N) ──< ai_knowledge_chunks (N)
     知识库目录                    文档元数据                    切片 + 向量

所有表包含标准审计字段:id(UUID v7) / tenant_id / created_at / updated_at / created_by / updated_by / deleted_atversion_lock 仅 bases 和 documents 表使用。

3.2 ai_knowledge_bases知识库目录

CREATE TABLE ai_knowledge_bases (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    name            VARCHAR(200) NOT NULL,           -- 知识库名称
    kb_type         VARCHAR(50) NOT NULL,            -- 类型枚举
    description     TEXT,                             -- 描述(帮助 AI 理解用途)
    icon            VARCHAR(50),                      -- 图标标识
    chunk_strategy  JSONB NOT NULL DEFAULT '{}',      -- 切片策略
    intent_keywords JSONB NOT NULL DEFAULT '[]',      -- 意图关键词
    embedding_model VARCHAR(100),                     -- embedding 模型(默认全局配置)
    is_enabled      BOOLEAN NOT NULL DEFAULT true,
    document_count  INT NOT NULL DEFAULT 0,           -- 缓存:文档数
    chunk_count     INT NOT NULL DEFAULT 0,           -- 缓存:切片总数
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_by      UUID,
    updated_by      UUID,
    deleted_at      TIMESTAMPTZ,
    version_lock    INT NOT NULL DEFAULT 1
);

-- 索引
CREATE INDEX idx_kb_tenant ON ai_knowledge_bases(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_kb_type   ON ai_knowledge_bases(tenant_id, kb_type) WHERE deleted_at IS NULL;
CREATE INDEX idx_kb_enabled ON ai_knowledge_bases(tenant_id, is_enabled) WHERE deleted_at IS NULL;

kb_type 枚举值:

中文名 用途
rule 规则 KDIGO 分期、危急值阈值、药物相互作用
reference 参考资料 ICD-10 映射、药品数据库、检验参考范围
clinical_guide 临床指南 诊疗规范、科室专科方案
institution_info 机构信息 科室介绍、医生排班、服务项目、价格套餐
medical_process 就医流程 预约挂号流程、体检须知、报告领取方式
health_education 健康科普 疾病知识、检查项目解读、生活方式建议
faq 常见问答 用户高频问答对

chunk_strategy 默认值(按 kb_type

// rule / reference
{ "max_tokens": 300, "overlap": 0, "separator": "\n\n", "strategy": "per_item" }

// faq
{ "max_tokens": 400, "overlap": 0, "separator": "\n\n", "strategy": "qa_pair" }

// institution_info / medical_process
{ "max_tokens": 500, "overlap": 50, "separator": "\n\n", "strategy": "paragraph" }

// health_education
{ "max_tokens": 600, "overlap": 80, "separator": "\n\n", "strategy": "paragraph" }

// clinical_guide
{ "max_tokens": 800, "overlap": 100, "separator": "\n##", "strategy": "section" }

3.3 ai_knowledge_documents文档元数据

CREATE TABLE ai_knowledge_documents (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL,
    knowledge_base_id   UUID NOT NULL REFERENCES ai_knowledge_bases(id),
    title               VARCHAR(500) NOT NULL,
    source_type         VARCHAR(20) NOT NULL,         -- upload / url / manual
    file_path           VARCHAR(500),                  -- 文件存储路径upload
    file_type           VARCHAR(20),                   -- pdf/docx/txt/md/xlsx/csv/html
    file_size           BIGINT,                         -- 字节
    source_url          TEXT,                           -- 原始 URLurl 类型)
    raw_content         TEXT,                           -- 解析后的完整纯文本(冗余存储)
    status              VARCHAR(20) NOT NULL DEFAULT 'pending',
    chunk_count         INT NOT NULL DEFAULT 0,
    error_message       TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_by          UUID,
    updated_by          UUID,
    deleted_at          TIMESTAMPTZ,
    version_lock        INT NOT NULL DEFAULT 1
);

CREATE INDEX idx_doc_kb ON ai_knowledge_documents(knowledge_base_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_doc_status ON ai_knowledge_documents(knowledge_base_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_doc_tenant ON ai_knowledge_documents(tenant_id) WHERE deleted_at IS NULL;

status 状态机:

pending → processing → completed
                    ↘ failed
  • pending: 刚创建,等待处理
  • processing: 正在解析/切片/embedding
  • completed: 处理完成,切片可用
  • failed: 处理失败,error_message 记录原因

3.4 ai_knowledge_chunks切片 + 向量)

CREATE TABLE ai_knowledge_chunks (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL,
    document_id         UUID NOT NULL REFERENCES ai_knowledge_documents(id),
    knowledge_base_id   UUID NOT NULL REFERENCES ai_knowledge_bases(id),  -- 冗余加速查询
    chunk_index         INT NOT NULL,                     -- 文档内序号 (0-based)
    content             TEXT NOT NULL,                    -- 切片原文
    embedding           vector(1536),                     -- pgvector 向量
    token_count         INT,                              -- token 数量
    metadata            JSONB DEFAULT '{}',               -- 元数据: page, section, source_line
    hit_count           INT NOT NULL DEFAULT 0,           -- 命中次数
    last_hit_at         TIMESTAMPTZ,                      -- 最近命中时间
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_by          UUID
);

-- 向量检索核心索引HNSW无需预热数据pgvector >= 0.5.0
CREATE INDEX idx_chunk_embedding ON ai_knowledge_chunks
    USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

-- 按知识库查询
CREATE INDEX idx_chunk_kb ON ai_knowledge_chunks(knowledge_base_id);
-- 按文档查询
CREATE INDEX idx_chunk_doc ON ai_knowledge_chunks(document_id);
-- 按租户查询
CREATE INDEX idx_chunk_tenant ON ai_knowledge_chunks(tenant_id);
-- 命中统计排序
CREATE INDEX idx_chunk_hits ON ai_knowledge_chunks(knowledge_base_id, hit_count DESC);

metadata 字段示例:

// PDF 文档切片
{ "page": 3, "section": "第三章 血透适应症", "source_line": 45 }

// Excel 行切片
{ "row": 12, "columns": ["套餐名", "价格", "包含项目"] }

// URL 导入切片
{ "url": "https://xxx.com/about", "selector": ".main-content" }

// Markdown 切片
{ "heading": "## 体检前注意事项", "level": 2 }

4. 文档处理管线

4.1 管线架构

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌───────────┐    ┌──────────┐
│ 接收文件  │───▶│ 格式解析  │───▶│ 智能切片  │───▶│ Embedding │───▶│ 存储      │
│ upload/   │    │ Parser   │    │ Chunker  │    │ Service   │    │ chunks   │
│ url/manual│    │          │    │          │    │ (复用)     │    │ table    │
└──────────┘    └──────────┘    └──────────┘    └───────────┘    └──────────┘
                     │
                     ▼
              ┌──────────────┐
              │ 错误处理      │
              │ failed + msg │
              └──────────────┘

整个管线在 tokio::spawn 中异步执行,前端通过轮询 GET /documents/{id} 获取状态。

4.2 格式解析器DocumentParser

每种格式对应一个解析函数,统一输出 ParsedDocument { title, text, metadata }

格式 Rust 依赖 解析策略 特殊处理
PDF pdf-extractlopdf 逐页提取文本 检测表格结构 → Markdown 表格;纯图片页标记跳过
Word (.docx) docx-rs 按段落提取,保留标题层级 标题作为 metadata.section
TXT 标准库 直接读取,按空行分段 无特殊处理
Markdown (.md) 标准库 + 自定义 按标题层级分割 #/## 作为 metadata.heading
Excel (.xlsx) calamine 逐行拼接为自然语言描述 表头 + 行数据 → "套餐A价格599元包含..."
CSV 标准库 同 Excel 策略 自动检测分隔符
URL (HTML) reqwest + scraper HTTP GET → 提取 body 正文 SSRF 防护(禁止内网地址);去广告/导航/页脚

URL 导入的正文提取逻辑:

fn extract_main_content(html: &str) -> String {
    // 1. 用 scraper 解析 DOM
    // 2. 移除 <nav>, <footer>, <header>, <aside>, <script>, <style>
    // 3. 优先提取 <article> 或 <main> 标签内容
    // 4. fallback 到 <body> 全文
    // 5. 清理多余空白
}

Excel 行转自然语言:

fn row_to_text(headers: &[String], row: &[String]) -> String {
    // 输入: headers=["套餐名","价格","包含项目"], row=["基础体检A","599","血常规,尿常规"]
    // 输出: "基础体检A价格599元包含血常规、尿常规"
}

4.3 智能切片ChunkStrategy

切片策略由知识库的 chunk_strategy JSONB 字段配置5 种内置策略:

策略 标识 适用类型 逻辑
逐条切分 per_item rule, reference 按分隔符切,每条独立
QA 对 qa_pair faq 识别 Q: / A: 标记,一对一切片
按段落 paragraph institution_info, health_education 按空行分段,保留 overlap
按章节 section clinical_guide 按标题层级切,大 overlap
按行 per_row Excel/CSV 自动选择 表头 + 每行一个切片

切片伪代码:

fn chunk_text(text: &str, strategy: &ChunkStrategy) -> Vec<Chunk> {
    match strategy.strategy.as_str() {
        "per_item" => split_by_separator(text, &strategy.separator),
        "qa_pair"  => split_by_qa_pattern(text),
        "paragraph" => split_with_overlap(text, &strategy.separator, strategy.max_tokens, strategy.overlap),
        "section"  => split_by_headings(text, strategy.max_tokens, strategy.overlap),
        "per_row"  => split_by_rows(text),  // 仅用于表格数据
        _ => split_with_overlap(text, "\n\n", strategy.max_tokens, strategy.overlap),
    }
    // 每个切片附加上下文元数据document_title + 前后文摘要
}

上下文增强: 每个切片在存储时追加文档标题作为前缀,提升向量检索准确度:

[文档: 体检套餐价目表.xlsx] 基础体检A价格599元包含血常规、尿常规、肝功能、肾功能检查...

4.4 Embedding 与存储

复用现有 EmbeddingService,批量调用:

async fn process_chunks(chunks: &[NewChunk]) -> Result<()> {
    // 分批 embedding单批失败不阻塞其他批次
    let mut embedded_count = 0;
    for batch in chunks.chunks(EMBEDDING_BATCH_SIZE) {
        match embedding_service.embed_batch(&batch_texts).await {
            Ok(embeddings) => {
                batch_insert_chunks(batch, &embeddings).await?;
                embedded_count += batch.len();
            }
            Err(e) => {
                tracing::warn!("Embedding batch failed: {}, storing {} chunks with NULL embedding", e, batch.len());
                // 切片仍然写入embedding = NULL后续可重新 embedding
                batch_insert_chunks_without_embedding(batch).await?;
            }
        }
    }

    // 更新文档状态和计数(原子增量,防并发计数不一致)
    let doc_id = chunks.first().unwrap().document_id;
    let kb_id = chunks.first().unwrap().knowledge_base_id;
    atomic_update_document_status(doc_id, "completed", chunks.len(), embedded_count).await?;
    // 使用 SQL: UPDATE ... SET document_count = document_count + 1, chunk_count = chunk_count + N
    atomic_increment_kb_counts(kb_id, 1, chunks.len()).await?;
}

错误容忍:

  • embedding API 不可用时:切片写入但 embedding = NULL,标记文档为 completed(降级为纯文本匹配)
  • 单个切片 embedding 失败:该切片以 embedding = NULL 写入,其他切片正常处理
  • 文档完成后记录 embedded_count / total_count,前端展示"42/50 切片已向量化"
  • 所有 embedding = NULL 的切片可后续通过"重新 embedding"按钮批量处理
  • 单个切片 embedding 失败:跳过该切片,不影响其他切片
  • 整体失败:文档状态设为 failed + error_message

4.5 重新处理与增量更新

操作 触发 处理逻辑
重新处理文档 用户点击"重新处理"按钮 删除该文档所有 chunks → 重新走管线
更新文档 用户上传新版本文件 同"重新处理"
删除文档 用户删除文档 软删除 document + 真删除对应 chunks
禁用知识库 管理员操作 不删除数据,检索时跳过该 KB 的 chunks
修改切片策略 修改 KB 配置 需手动触发该 KB 全量重新切片

5. 混合意图路由

5.1 三层漏斗设计

用户问题
    │
    ▼
┌─────────────────────────────────────────┐
│ Layer 1: 关键词粗筛(毫秒级)             │
│ 遍历所有启用知识库的 intent_keywords       │
│ 命中 → 锁定知识库列表 candidate_kb_ids    │
│ 未命中 → candidate_kb_ids = 全部启用 KB   │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│ Layer 2: 向量检索(百毫秒级)             │
│ 在 candidate_kb_ids 范围内              │
│ pgvector cosine similarity 检索          │
│ top_k = 5, threshold >= 0.65            │
│ 返回切片 + 相似度分数                     │
└──────────────┬──────────────────────────┘
               │
               ▼
       top_score >= 0.75?
          │          │
         YES         NO
          │          │
          ▼          ▼
     直接使用    ┌──────────────────────────────────┐
     检索结果    │ Layer 3: LLM 兜底(秒级)           │
                │ 调用 LLM 做意图分类                 │
                │ "用户意图应查询哪类知识库?"          │
                │ 根据 LLM 回答重新锁定 KB            │
                │ 再次 Layer 2 向量检索               │
                └──────────────────────────────────┘

5.2 Layer 1: 关键词粗筛

fn keyword_match(query: &str, all_kb_ids: &[KnowledgeBase]) -> Vec<Uuid> {
    let mut matched = Vec::new();
    for kb in all_kb_ids {
        if kb.intent_keywords.iter().any(|kw| query.contains(kw)) {
            matched.push(kb.id);
        }
    }
    // 无匹配则返回全部启用的 KB降级为全库搜索
    if matched.is_empty() {
        all_kb_ids.iter().map(|kb| kb.id).collect()
    } else {
        matched
    }
}

关键词配置示例:

知识库 intent_keywords
机构信息 价格, 费用, 多少钱, 套餐, 体检项目, 科室, 医生, 地址, 电话, 营业时间, 上班, 血透, 透析
就医流程 预约, 挂号, 流程, 怎么, 如何, 需要带, 准备, 注意, 报告, 取结果, 空腹
健康科普 正常吗, 什么意思, 偏高, 偏低, 指标, 血压, 血糖, 肝功能, 肾功能, 胆固醇
FAQ 能不能, 可以吗, 有没有, 是否, 支持, 医保, 报销
临床指南 指南, 规范, 诊疗, 方案, 适应症, 禁忌症

5.3 Layer 2: 向量检索

复用现有 pgvector 基础设施,改造 KnowledgeSearchRepository 适配新表:

-- 在指定知识库范围内检索(全部参数化绑定,禁止 format! 拼接)
SELECT c.id, c.content, c.metadata, c.document_id, c.knowledge_base_id,
       kb.name AS kb_name, doc.title AS doc_title,
       1 - (c.embedding <=> $1::vector) AS similarity
FROM ai_knowledge_chunks c
JOIN ai_knowledge_bases kb ON c.knowledge_base_id = kb.id
JOIN ai_knowledge_documents doc ON c.document_id = doc.id
WHERE c.knowledge_base_id = ANY($2::uuid[])
  AND c.embedding IS NOT NULL
  AND c.tenant_id = $3
ORDER BY c.embedding <=> $1::vector
LIMIT $4;

安全约束: 所有 pgvector 查询必须使用参数化绑定($N),禁止 format! 拼接任何用户输入。现有 vector_search.rs 中的 format!("AND analysis_type = '{}'", ...) 模式必须在此次重构中消除。如需按 kb_type 过滤,通过 JOIN ai_knowledge_bases 实现,不直接拼接到 WHERE 子句。

检索参数:

参数 说明
top_k 5 返回最多 5 个切片
similarity_threshold 0.65 低于此阈值的切片丢弃
max_context_tokens 4000 总注入上下文 token 上限
single_chunk_max_chars 1500 单个切片最大字符数(超长截断)

5.4 Layer 3: LLM 兜底

当 Layer 2 最高相似度 < 0.75 时触发,调用轻量 LLM 做意图分类:

async fn llm_classify_intent(query: &str, kb_types: &[(&str, &str)]) -> Result<Vec<Uuid>> {
    let prompt = format!(
        "用户问题:{}\n\n可用的知识库类型:\n{}\n\n\
         请判断用户的问题应该查询哪个知识库类型。只返回类型标识,不要解释。",
        query,
        kb_types.iter().map(|(t, desc)| format!("- {}: {}", t, desc)).collect::<Vec<_>>().join("\n")
    );
    // 调用 LLM解析返回的 kb_type
    // 返回匹配的知识库 ID 列表
}

使用轻量模型: Layer 3 优先使用 Ollama 本地模型(如 qwen3:4b避免额外的 API 开销。Ollama 不可用时降级使用配置的默认 Provider。

5.5 检索结果组装

将命中的切片组装为 RAG context注入 System Prompt

fn build_rag_context(results: &[SearchResult], tenant_name: &str) -> String {
    let mut context = format!("## 参考知识\n");
    context.push_str("以下是来自「{}」知识库的相关内容,请基于这些内容回答用户问题。\n", tenant_name);
    context.push_str("如果参考知识不足以回答,请诚实告知。\n\n");

    for r in results {
        context.push_str(&format!("[来源: {} | {} | 相似度: {:.2}]\n",
            r.kb_name, r.doc_title, r.similarity));
        context.push_str(&r.content);
        context.push_str("\n\n");
    }

    context.push_str("## 回答要求\n");
    context.push_str("- 基于以上参考知识回答,不要编造信息\n");
    context.push_str("- 引用知识时标注来源 [ref:知识库名]\n");
    context.push_str("- 涉及医疗建议时提示\"仅供参考,请遵医嘱\"\n");

    context
}

Context 截断策略: 按相似度从高到低累加,总 tokens 超过 max_context_tokens 时停止添加。

5.6 命中统计

每次检索完成后,异步更新命中切片的统计数据:

tokio::spawn(async move {
    for chunk_id in hit_chunk_ids {
        sqlx::query(
            "UPDATE ai_knowledge_chunks SET hit_count = hit_count + 1, last_hit_at = now() WHERE id = $1"
        )
        .bind(chunk_id)
        .execute(&pool)
        .await
        .ok(); // 非关键路径,忽略错误
    }
});

6. 管理后台 UI

6.1 页面结构

/health/ai-knowledge                    → 知识库列表页(主入口)
/health/ai-knowledge/:kbId              → 知识库详情页(文档管理)
/health/ai-knowledge/:kbId?tab=settings → 知识库设置
/health/ai-knowledge/:kbId?tab=test     → 命中测试

6.2 Page 1: 知识库列表页

路由: /health/ai-knowledge

布局:

  • 顶部:标题"知识库管理" + 搜索框 + "创建知识库"按钮
  • 主体:卡片网格布局(每行 3-4 张卡片)

知识库卡片内容:

  • 渐变色头部(按 kb_type 区分颜色)+ 图标 + 名称
  • 类型标签
  • 文档数 / 切片数统计
  • 启用/禁用状态指示器
  • 点击进入详情页

创建知识库弹窗:

  • 名称(必填)
  • 类型选择7 种,点击选择,单选)
  • 描述(选填)
  • 意图关键词TagInput选填
  • 切片策略(根据类型自动推荐默认值,高级选项折叠展示)
    • max_tokens 滑块
    • overlap 滑块
    • separator 输入框

6.3 Page 2: 知识库详情页

路由: /health/ai-knowledge/:kbId

布局:

  • 面包屑导航:知识库 > {名称}
  • 顶部操作栏:设置按钮 / 命中测试按钮 / 添加文档按钮
  • Tab 切换:文档列表 | 设置 | 命中测试

文档列表 Tab

内容
文档名称 图标 + 标题
来源类型 标签(上传/URL/手动)
切片数 数字
状态 已完成 / 处理中(进度) / 失败(原因)
更新时间 日期
操作 重新处理 / 编辑 / 删除

添加文档入口(三种方式):

  1. 文件上传 — 拖拽或点击上传,支持 PDF/Word/TXT/MD/Excel/CSV单文件最大 50MB
  2. URL 导入 — 输入 URL系统抓取并解析
  3. 手动录入 — 富文本编辑器Markdown适合 FAQ 和简短内容

处理进度展示: 文档状态为 processing 时,轮询 GET /documents/{id}(每 2 秒),显示进度条和当前处理阶段。

6.4 Page 3: 命中测试 + 切片预览

路由: /health/ai-knowledge/:kbId?tab=test

布局:

  • 顶部:输入框 + "测试"按钮
  • 路由路径展示:关键词命中 ["xxx"] → 锁定: 知识库名 → 向量检索 top 3
  • 结果列表:按相似度排序的切片卡片

切片卡片内容:

  • 切片序号 + 来源文档名 + 页码/行号
  • 相似度分数(颜色区分:绿 ≥ 0.85 / 黄 ≥ 0.70 / 红 < 0.70
  • 切片原文(超长可展开)
  • 编辑按钮(修改切片原文后自动重新 embedding

6.5 知识库设置 Tab

路由: /health/ai-knowledge/:kbId?tab=settings

可编辑配置:

  • 基本信息:名称、描述、图标
  • 启用/禁用开关
  • 意图关键词管理(增删改)
  • 切片策略配置(修改后需手动触发全量重新切片)
  • 删除知识库(软删除,含确认弹窗)

6.6 组件清单

组件 类型 说明
KnowledgeBaseList 页面组件 知识库卡片网格
KnowledgeBaseCard 卡片组件 单个知识库卡片
CreateKBModal 弹窗组件 创建/编辑知识库表单
DocumentList 页面组件 文档列表表格
UploadDocument 上传组件 拖拽上传 + URL 输入 + 手动录入
DocumentProgress 进度组件 处理进度轮询展示
HitTestPanel 面板组件 命中测试输入 + 结果展示
ChunkCard 卡片组件 单个切片预览卡片
TagInput 输入组件 关键词标签输入(回车添加)

所有组件使用 Ant Design 基础组件构建,遵循现有项目 UI 规范。


7. AI 客服集成

7.1 集成架构

在现有 chat_handler 的消息处理链中插入 RAG 检索步骤,改动最小化:

现有 chat_handler 流程:
  用户消息 → 构建 messages → 调用 LLM → 返回回答

新增 RAG 链路(在"构建 messages"和"调用 LLM"之间插入):
  用户消息 → 构建 messages
    → [NEW] HybridKnowledgeRouter::get_context(last_user_message)
    → [NEW] rag_context 注入到 system prompt
  → 调用 LLM → 返回回答
    → [NEW] 解析 [ref:xxx] 引用
    → [NEW] 异步更新 chunk.hit_count

7.2 KnowledgeSource trait 适配

保留现有 trait 接口,新增 HybridKnowledgeRouter 实现:

// 现有 trait保持不变
#[async_trait]
pub trait KnowledgeSource: Send + Sync {
    async fn get_context(&self, query: &KnowledgeQuery) -> Result<KnowledgeContext>;
    fn source_type(&self) -> &str;
    async fn health_check(&self) -> Result<bool>;
}

// 新实现
pub struct HybridKnowledgeRouter {
    pool: DatabaseConnection,
    embedding_service: Arc<EmbeddingService>,
    llm_provider: Arc<dyn LlmProvider>,  // Layer 3 用
    config: RouterConfig,
}

#[async_trait]
impl KnowledgeSource for HybridKnowledgeRouter {
    async fn get_context(&self, query: &KnowledgeQuery) -> Result<KnowledgeContext> {
        // Layer 0: 边界情况 — 无知识库或全部禁用时,返回空 context不报错
        let all_kbs = self.load_enabled_kbs(&query.tenant_id).await?;
        if all_kbs.is_empty() {
            return Ok(KnowledgeContext {
                source: "hybrid_router".into(),
                context_text: String::new(),
                references: vec![],
                confidence: 0.0,
            });
        }

        // Layer 1: 关键词粗筛
        let candidate_kb_ids = self.keyword_match(&query.query_text, &all_kbs).await?;

        // Layer 2: 向量检索
        let results = self.vector_search(&query.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.query_text, &results).await?
        } else {
            results
        };

        // 组装 context
        let context_text = self.build_context(&final_results, &query.tenant_id).await?;
        let references = final_results.iter().map(|r| Reference {
            source: format!("{} > {}", r.kb_name, r.doc_title),
            content: r.content.clone(),
            confidence: r.similarity,
        }).collect();

        Ok(KnowledgeContext {
            source: "hybrid_router".to_string(),
            context_text,
            references,
            confidence: final_results.first().map(|r| r.similarity).unwrap_or(0.0),
        })
    }
}

旧的 StructuredKnowledgeSourceVectorKnowledgeSource 废弃,统一由 HybridKnowledgeRouter 替代。

7.3 System Prompt 模板

AI 客服的 System Prompt 由三部分拼接:

Part 1: 角色指令(租户可自定义,存储在 ai_tenant_config
Part 2: RAG 知识上下文(由 HybridKnowledgeRouter 动态注入)
Part 3: 引用要求和安全约束(固定模板)

Part 1 — 角色指令模板:

你是「{tenant_name}」的健康管理助手。你的职责是基于知识库内容回答用户关于机构服务、就医流程、健康科普等问题。

## 行为准则
1. 优先使用参考知识回答,不要编造不存在的服务或价格
2. 引用知识时使用 [ref:来源名] 标注出处
3. 涉及医疗建议时,必须附加"仅供参考,请遵医嘱"
4. 知识库中找不到答案时,诚实告知并建议联系前台 {phone}
5. 保持友好、专业、简洁的语气

## 知识范围
- 机构信息(科室、医生、服务项目、价格)
- 就医流程(预约、检查、报告领取)
- 健康科普(常见疾病、指标解读)
- 常见问答FAQ

## 超出范围
以下问题请引导用户联系专业医护人员:
- 具体诊断和处方建议
- 药物用量调整
- 紧急医疗情况 → 拨打 120

Part 3 — 固定约束:

## 回答要求
- 基于参考知识回答,不要编造信息
- 引用知识时标注来源 [ref:知识库名]
- 涉及医疗建议时提示"仅供参考,请遵医嘱"
- 如果参考知识不足以回答,诚实告知并建议用户联系前台

7.4 前端引用渲染

AI 回答中的 [ref:xxx] 标记在前端解析为可交互的引用标签:

// 解析 AI 回答中的 [ref:xxx] 标记
function parseReferences(text: string): ParsedContent[] {
  const refRegex = /\[ref:(.+?)\]/g;
  // 将文本拆分为普通文本段和引用标记
  // 引用标记渲染为可点击的蓝色标签
  // 点击展开显示完整来源信息
}

// 引用来源折叠面板
<Collapse ghost size="small">
  <Collapse.Panel header={`📎 引用来源 (${refs.length})`} key="refs">
    {refs.map(ref => (
      <div key={ref.id}>
        <Text type="secondary">
          📚 {ref.kbName}  {ref.docTitle}  切片#{ref.chunkIndex}
        </Text>
      </div>
    ))}
  </Collapse.Panel>
</Collapse>

7.5 Agent Tool 适配

现有 search_medical_knowledge Agent Tool 改造为使用新的统一知识库:

// 改造前
pub struct SearchMedicalKnowledgeTool { /* 直接用 vector_search */ }

// 改造后
pub struct SearchMedicalKnowledgeTool {
    router: Arc<HybridKnowledgeRouter>,
}

impl AgentTool for SearchMedicalKnowledgeTool {
    async fn execute(&self, params: &Value) -> Result<Value> {
        let query = params["query"].as_str().ok_or(...)?;
        let kb_type = params["knowledge_base_type"].as_str();  // 新增可选参数

        let knowledge_query = KnowledgeQuery {
            query_text: query.to_string(),
            kb_type_filter: kb_type.map(String::from),
            ..Default::default()
        };

        let context = self.router.get_context(&knowledge_query).await?;
        // 返回格式化的检索结果
    }
}

新增可选参数 knowledge_base_typeAgent 可指定搜索特定类型知识库。


8. 旧数据迁移

8.1 迁移策略

采用一次性迁移脚本,在 SeaORM migration 中执行:

Phase 1: 创建新表ai_knowledge_bases / ai_knowledge_documents / ai_knowledge_chunks
Phase 2: 迁移旧表数据
Phase 3: 标记旧表 deprecated不删除保留备份

8.2 数据映射

ai_knowledge_rules → 新模型:

旧字段 新映射
rule_name kb.name + doc.title
analysis_type kb.kb_type = "rule"
condition_expr chunk.metadata.condition
action_text chunk.content
priority chunk.metadata.priority

每个 rule 创建为1 个知识库(按 analysis_type 分组)→ 1 个文档 → 1 个切片。

ai_knowledge_references → 新模型:

旧字段 新映射
title doc.title
analysis_type 归入对应知识库
source_name chunk.metadata.source_name
content_summary chunk.content
embedding chunk.embedding直接迁移
tags chunk.metadata.tags

ai_knowledge_guides → 新模型:

旧字段 新映射
title doc.title
analysis_type 归入对应知识库
content chunk.content
category chunk.metadata.category
embedding chunk.embedding直接迁移

8.3 迁移 SQL 示例

关键约束: 使用临时映射表确保旧数据精确关联到新知识库,避免 JOIN 模糊匹配导致数据重复或丢失。

-- Step 0: 创建临时映射表tenant_id + analysis_type → new_kb_id
CREATE TEMP TABLE kb_migration_map (
    tenant_id UUID,
    analysis_type VARCHAR(50),
    old_table VARCHAR(20),      -- 'references' / 'guides' / 'rules'
    new_kb_id UUID
);

-- Step 1: 按 tenant_id + analysis_type 创建知识库,同时记录映射
INSERT INTO ai_knowledge_bases (id, tenant_id, name, kb_type, description, chunk_strategy, intent_keywords)
SELECT
    gen_random_uuid(),
    tenant_id,
    '参考资料 - ' || analysis_type,
    'reference',
    '从旧表自动迁移',
    '{"max_tokens": 400, "overlap": 0, "separator": "\n\n", "strategy": "per_item"}'::jsonb,
    '[]'::jsonb
FROM ai_knowledge_references
WHERE deleted_at IS NULL
GROUP BY tenant_id, analysis_type
RETURNING id, tenant_id, name;  -- PostgreSQL RETURNING 支持精确关联

-- Step 1b: 填充映射表(使用 name 匹配,因为 RETURNING 不能直接填充 temp table
INSERT INTO kb_migration_map (tenant_id, analysis_type, old_table, new_kb_id)
SELECT
    r.tenant_id,
    r.analysis_type,
    'references',
    kb.id
FROM (SELECT DISTINCT tenant_id, analysis_type FROM ai_knowledge_references WHERE deleted_at IS NULL) r
JOIN ai_knowledge_bases kb ON kb.tenant_id = r.tenant_id
    AND kb.name = '参考资料 - ' || r.analysis_type
    AND kb.kb_type = 'reference';

-- Step 2: 为每个 reference 创建文档(通过映射表精确 JOIN
INSERT INTO ai_knowledge_documents (id, tenant_id, knowledge_base_id, title, source_type, raw_content, status, chunk_count)
SELECT
    r.id,
    r.tenant_id,
    m.new_kb_id,
    r.title,
    'manual',
    r.content_summary,
    'completed',
    1
FROM ai_knowledge_references r
JOIN kb_migration_map m ON m.tenant_id = r.tenant_id
    AND m.analysis_type = r.analysis_type
    AND m.old_table = 'references'
WHERE r.deleted_at IS NULL;

-- Step 3: 迁移切片 + 向量(通过映射表精确 JOIN
INSERT INTO ai_knowledge_chunks (id, tenant_id, document_id, knowledge_base_id, chunk_index, content, embedding, metadata)
SELECT
    gen_random_uuid(),
    r.tenant_id,
    r.id,
    m.new_kb_id,
    0,
    r.content_summary,
    r.embedding::vector,
    jsonb_build_object('source_name', r.source_name, 'tags', r.tags, 'migrated_from', 'ai_knowledge_references')
FROM ai_knowledge_references r
JOIN kb_migration_map m ON m.tenant_id = r.tenant_id
    AND m.analysis_type = r.analysis_type
    AND m.old_table = 'references'
WHERE r.deleted_at IS NULL AND r.embedding IS NOT NULL;

-- Step 4-6: 同理迁移 ai_knowledge_guides 和 ai_knowledge_rules
-- guides 的 kb_type='clinical_guide'rules 的 kb_type='rule'

-- Step 7: 清理临时表
DROP TABLE kb_migration_map;

8.4 旧表处理

  • 不删除旧表,添加注释 -- DEPRECATED: 迁移到 ai_knowledge_* 新模型 (2026-05-26)
  • 旧 Entity 文件标记 #[deprecated]
  • 旧 API 端点保留但返回 301 重定向到新端点(过渡期 1 个月)
  • 过渡期结束后,下个版本移除旧表和旧 API

9. API 端点清单

9.1 知识库管理

方法 路径 Handler 权限 说明
GET /api/v1/ai/knowledge/bases list_bases ai.knowledge.list 知识库列表(分页)
POST /api/v1/ai/knowledge/bases create_base ai.knowledge.manage 创建知识库
GET /api/v1/ai/knowledge/bases/{id} get_base ai.knowledge.list 知识库详情
PUT /api/v1/ai/knowledge/bases/{id} update_base ai.knowledge.manage 更新知识库配置
DELETE /api/v1/ai/knowledge/bases/{id} delete_base ai.knowledge.manage 删除知识库(软删除)

9.2 文档管理

方法 路径 Handler 权限 说明
GET /api/v1/ai/knowledge/bases/{kbId}/documents list_documents ai.knowledge.list 文档列表
POST /api/v1/ai/knowledge/bases/{kbId}/documents/upload upload_document ai.knowledge.manage 上传文件
POST /api/v1/ai/knowledge/bases/{kbId}/documents/url import_url ai.knowledge.manage URL 导入
POST /api/v1/ai/knowledge/bases/{kbId}/documents/manual create_manual ai.knowledge.manage 手动录入
GET /api/v1/ai/knowledge/bases/{kbId}/documents/{docId} get_document ai.knowledge.list 文档详情(含状态)
DELETE /api/v1/ai/knowledge/bases/{kbId}/documents/{docId} delete_document ai.knowledge.manage 删除文档
POST /api/v1/ai/knowledge/bases/{kbId}/documents/{docId}/reprocess reprocess_document ai.knowledge.manage 重新处理

9.3 切片管理

方法 路径 Handler 权限 说明
GET /api/v1/ai/knowledge/bases/{kbId}/documents/{docId}/chunks list_chunks ai.knowledge.list 切片列表
PUT /api/v1/ai/knowledge/chunks/{chunkId} update_chunk ai.knowledge.manage 编辑切片内容
POST /api/v1/ai/knowledge/chunks/{chunkId}/re-embed re_embed_chunk ai.knowledge.manage 重新 embedding

9.4 检索与测试

方法 路径 Handler 权限 说明
POST /api/v1/ai/knowledge/bases/{kbId}/test hit_test ai.knowledge.list 命中测试
POST /api/v1/ai/knowledge/search search ai.knowledge.list 统一检索AI 内部调用)

9.5 废弃端点

以下旧端点保留 1 个月过渡期,返回 301

旧路径 重定向到
/api/v1/ai/knowledge/references /api/v1/ai/knowledge/bases?kb_type=reference
/api/v1/ai/knowledge/guides /api/v1/ai/knowledge/bases?kb_type=clinical_guide

9.6 DTO 清单

CreateKnowledgeBaseReq:

pub struct CreateKnowledgeBaseReq {
    pub name: String,                              // validate(length(min=1, max=200))
    pub kb_type: String,                           // validate(custom = validate_kb_type)
    pub description: Option<String>,               // validate(length(max=2000))
    pub icon: Option<String>,                      // validate(length(max=50))
    pub intent_keywords: Option<Vec<String>>,       // validate(length(max=50))
    pub chunk_strategy: Option<ChunkStrategyConfig>, // 可选,默认按 kb_type
}

UploadDocumentReq:

  • multipart/form-data: file (必填) + title (选填)

ImportUrlReq:

pub struct ImportUrlReq {
    pub url: String,    // validate(url, custom = validate_no_ssrf)
    pub title: Option<String>,
}

SSRF 防护要求§9.6 安全约束):

  • validate_no_ssrf 必须同时验证:① 仅允许 http/https 协议 ② 禁止 localhost/127.0.0.1/10.x/172.16-31.x/192.168.x/0.0.0.0 ③ DNS 解析后检查实际 IP防 DNS rebinding④ 请求超时 10 秒 ⑤ 禁止重定向到内网地址(跟随重定向时重新校验)
  • 文件上传安全MIME 类型白名单(application/pdf, application/vnd.openxmlformats-officedocument.*, text/*+ 大小上限 50MB + 文件名 sanitize防路径穿越

CreateManualReq:

pub struct CreateManualReq {
    pub title: String,           // validate(length(min=1, max=500))
    pub content: String,         // validate(length(min=1))
}

HitTestReq:

pub struct HitTestReq {
    pub query: String,           // validate(length(min=1, max=500))
    pub top_k: Option<i32>,      // 默认 5最大 10
}

10. 依赖与风险

10.1 新增 Rust 依赖

依赖 用途 大小评估 备注
pdf-extract PDF 文本提取 纯 Rust无系统依赖
docx-rs Word 文档解析 纯 Rust
calamine Excel 文件读取 纯 Rust支持 xlsx/xls/csv
scraper HTML DOM 解析URL 导入) 已在项目间接依赖中
validator DTO 输入校验 workspace 已有,需在 erp-ai 的 Cargo.toml 中启用 derive feature

不新增的系统级依赖 — 所有解析库均为纯 Rust 实现,无需安装系统库。

10.2 技术风险

风险 概率 影响 缓解措施
PDF 扫描件无法提取文本 检测纯图片 PDF → 标记 failed + 提示用户使用 OCR 版本
Embedding API 不可用 降级为纯文本匹配embedding=NULL 的切片不参与向量检索)
大文件处理超时 设置单文件 50MB 上限;大文件按页分批处理
pgvector 索引性能下降 切片数 < 100 万用 IVFFlat超过后迁移到 HNSW
旧数据迁移向量维度不匹配 迁移脚本预检查维度;不匹配则跳过 embedding标记需重新处理
LLM 兜底延迟影响响应时间 Layer 3 仅在低置信度时触发(预估 < 20% 请求);使用本地 Ollama 模型

10.3 性能预估

指标 预估值
文档上传响应 < 200ms异步处理立即返回
PDF 解析速度 ~100 页/10 秒
Embedding 生成 ~100 chunks/5 秒OpenAI API
向量检索延迟 < 100ms< 10 万切片)
LLM 兜底延迟 1-3 秒Ollama 本地)
端到端 AI 客服响应 2-5 秒(含 RAG 检索 + LLM 生成)

10.4 存储预估

资源 预估值
单个切片存储 ~2KBcontent + metadata + 1536 维 float vector ≈ 6KB
1000 文档 ≈ 10,000 切片 ~60MB
文件存储uploads 取决于上传量,建议 10GB 起步
pgvector 索引 约切片数据的 1.5 倍