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

1225 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | **规则层无 UI**`ai_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_at``version_lock` 仅 bases 和 documents 表使用。
### 3.2 ai_knowledge_bases知识库目录
```sql
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**
```json
// 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文档元数据
```sql
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切片 + 向量)
```sql
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 字段示例:**
```json
// 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-extract``lopdf` | 逐页提取文本 | 检测表格结构 → 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 导入的正文提取逻辑:**
```rust
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 行转自然语言:**
```rust
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 自动选择 | 表头 + 每行一个切片 |
**切片伪代码:**
```rust
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`,批量调用:
```rust
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: 关键词粗筛
```rust
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` 适配新表:
```sql
-- 在指定知识库范围内检索(全部参数化绑定,禁止 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 做意图分类:
```rust
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
```rust
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 命中统计
每次检索完成后,异步更新命中切片的统计数据:
```rust
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` 实现:
```rust
// 现有 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),
})
}
}
```
**旧的 `StructuredKnowledgeSource` 和 `VectorKnowledgeSource` 废弃**,统一由 `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]` 标记在前端解析为可交互的引用标签:
```tsx
// 解析 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 改造为使用新的统一知识库:
```rust
// 改造前
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_type`Agent 可指定搜索特定类型知识库。
---
## 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 模糊匹配导致数据重复或丢失。
```sql
-- 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:**
```rust
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:**
```rust
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:**
```rust
pub struct CreateManualReq {
pub title: String, // validate(length(min=1, max=500))
pub content: String, // validate(length(min=1))
}
```
**HitTestReq:**
```rust
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 倍 |