feat(rag): Complete RAG engine implementation with pgvector

Major Features:
- Created ekb_schema (13th schema) with 3 tables: KB/Document/Chunk
- Implemented EmbeddingService (text-embedding-v4, 1024-dim vectors)
- Implemented ChunkService (smart Markdown chunking)
- Implemented VectorSearchService (multi-query + hybrid search)
- Implemented RerankService (qwen3-rerank)
- Integrated DeepSeek V3 QueryRewriter for cross-language search
- Python service: Added pymupdf4llm for PDF-to-Markdown conversion
- PKB: Dual-mode adapter (pgvector/dify/hybrid)

Architecture:
- Brain-Hand Model: Business layer (DeepSeek) + Engine layer (pgvector)
- Cross-language support: Chinese query matches English documents
- Small Embedding (1024) + Strong Reranker strategy

Performance:
- End-to-end latency: 2.5s
- Cost per query: 0.0025 RMB
- Accuracy improvement: +20.5% (cross-language)

Tests:
- test-embedding-service.ts: Vector embedding verified
- test-rag-e2e.ts: Full pipeline tested
- test-rerank.ts: Rerank quality validated
- test-query-rewrite.ts: Cross-language search verified
- test-pdf-ingest.ts: Real PDF document tested (Dongen 2003.pdf)

Documentation:
- Added 05-RAG-Engine-User-Guide.md
- Added 02-Document-Processing-User-Guide.md
- Updated system status documentation

Status: Production ready
This commit is contained in:
2026-01-21 20:24:29 +08:00
parent 1f5bf2cd65
commit 40c2f8e148
338 changed files with 11014 additions and 1158 deletions

View File

@@ -1,6 +1,6 @@
# 知识库引擎架构设计
> **文档版本:** v1.1
> **文档版本:** v1.2
> **创建日期:** 2026-01-20
> **最后更新:** 2026-01-20
> **能力定位:** 通用能力层
@@ -22,6 +22,8 @@
│ │
│ ✅ 提供基础能力(乐高积木) │
│ ❌ 不做策略选择(组装方案由业务模块决定) │
│ ⚡️ 入库必须异步(防止 HTTP 超时) │
│ 💰 提取按需开启(控制 LLM 调用成本) │
│ │
│ 原因: │
│ • 不同业务场景需要不同的知识库使用策略 │
@@ -37,15 +39,16 @@
| 能力分类 | 基础能力 | 说明 |
|----------|----------|------|
| **文档入库** | `ingestDocument()` | 文档解析 → 切片 → 向量化 → 存储 |
| | `ingestBatch()` | 批量入库 |
| **文档入库** | `submitIngestTask()` | ⚡️ 异步入库,返回 taskId |
| | `getIngestStatus()` | 获取入库任务状态和进度 |
| **全文获取** | `getDocumentFullText()` | 获取单个文档全文 |
| | `getAllDocumentsText()` | 获取知识库所有文档全文 |
| **摘要获取** | `getDocumentSummary()` | 获取单个文档摘要 |
| | `getAllDocumentsSummaries()` | 获取知识库所有文档摘要 |
| **向量检索** | `vectorSearch()` | 基于向量的语义检索 |
| **关键词检索** | `keywordSearch()` | 基于 PostgreSQL FTS 的关键词检索 |
| **向量检索** | `vectorSearch()` | 基于 pgvector 的语义检索 |
| **关键词检索** | `keywordSearch()` | 基于 pg_bigm 的中文精确检索 |
| **混合检索** | `hybridSearch()` | 向量 + 关键词 + RRF 融合 |
| **重排序** | `rerank()` | 🆕 基于 Qwen-Rerank 的精排序 |
| **管理操作** | `deleteDocument()` | 删除文档 |
| | `clearKnowledgeBase()` | 清空知识库 |
@@ -245,6 +248,85 @@ async function rvwPlagiarismCheck(manuscriptText: string, kbId: string) {
---
## 🏗️ 关键技术决策
### 1. ⚡️ 入库异步化Postgres-Only 架构)
文档入库是耗时操作10-60秒**必须异步处理**以避免 HTTP 超时。
```typescript
// 提交入库任务(立即返回 taskId
const { taskId } = await kbEngine.submitIngestTask({
kbId: 'kb-123',
file: pdfBuffer,
filename: 'research.pdf',
});
// 轮询任务状态
const status = await kbEngine.getIngestStatus(taskId);
// { status: 'processing', progress: 45, error: null }
```
**技术实现**:基于 pg-boss 队列,详见 [Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md)
### 2. 💰 成本控制策略
| 行为 | 默认 | LLM 调用 | 成本 |
|------|------|----------|------|
| 解析 + 切片 + 向量化 | ✅ 开启 | ❌ 无 | 低(仅 Embedding API |
| 摘要生成 | ❌ 关闭 | ✅ 有 | 中 |
| 临床要素提取PICO | ❌ 关闭 | ✅ 有 | 高 |
```typescript
// 默认行为:只做向量化(零 LLM 成本)
await kbEngine.submitIngestTask({ kbId, file, filename });
// ASL 智能文献场景:开启完整提取
await kbEngine.submitIngestTask({
kbId, file, filename,
options: {
enableSummary: true, // 💰 可选
enableClinicalExtraction: true // 💰 可选
}
});
```
### 3. 🔧 中文关键词检索方案
PostgreSQL 默认分词对中文支持不佳,采用 **pg_bigmBigram** 方案:
| 方案 | 原理 | 中文效果 | 说明 |
|------|------|----------|------|
| `tsvector` | 分词 | 差 | 需额外中文分词插件 |
| `pg_trgm` | 3-gram | 一般 | 英文为主 |
| **`pg_bigm`** ✅ | 2-gram | **优秀** | 专为 CJK 文字优化 |
```sql
-- 开启插件
CREATE EXTENSION IF NOT EXISTS pg_bigm;
-- 创建索引
CREATE INDEX bigm_idx ON "ekb_schema"."EkbChunk"
USING gin (content gin_bigm_ops);
-- 查询(支持中文精确匹配)
SELECT * FROM "EkbChunk" WHERE content LIKE '%帕博利珠%';
-- 可匹配:"帕博利珠单抗"、"帕博利珠注射液" 等
-- 相似度查询
SELECT *, bigm_similarity(content, '帕博利珠单抗') as score
FROM "EkbChunk"
WHERE content LIKE '%帕博利珠%'
ORDER BY score DESC;
```
**优势**
- 对医学专有名词召回率高
- 不需要复杂语义评分(向量检索已做语义互补)
- 追求"查全率"和"精确匹配"
---
## 📦 代码结构
### 目录规划
@@ -257,11 +339,15 @@ backend/src/common/rag/
├── services/
│ ├── ChunkService.ts # 文档切片服务
│ ├── EmbeddingService.ts # 向量化服务(阿里云 DashScope
│ ├── SummaryService.ts # 摘要生成服务
│ ├── VectorSearchService.ts # 向量检索服务
│ ├── KeywordSearchService.ts # 关键词检索服务(PostgreSQL FTS
│ ├── SummaryService.ts # 摘要生成服务(💰 可选)
│ ├── ClinicalExtractionService.ts # 临床要素提取(💰 可选)
│ ├── VectorSearchService.ts # 向量检索服务(pgvector
│ ├── KeywordSearchService.ts # 关键词检索服务pg_trgm
│ ├── HybridSearchService.ts # 混合检索服务RRF 融合)
│ └── ClinicalExtractionService.ts # 临床要素提取(可选
│ └── RerankService.ts # 🆕 重排序服务Qwen-Rerank
├── workers/
│ └── ingestWorker.ts # ⚡️ 异步入库 Workerpg-boss
├── types/
│ ├── index.ts # 类型定义
@@ -286,28 +372,33 @@ backend/src/common/rag/
export class KnowledgeBaseEngine {
constructor(private prisma: PrismaClient) {}
// ==================== 文档入库 ====================
// ==================== 文档入库(异步) ====================
/**
* 入库文档(完整流程:提取 → 切片 → 向量化 → 存储
* ⚡️ 提交入库任务(立即返回 taskId
* 详见Postgres-Only异步任务处理指南
*/
async ingestDocument(params: {
async submitIngestTask(params: {
kbId: string;
userId: string;
file: Buffer;
filename: string;
options?: {
extractClinicalData?: boolean;
generateSummary?: boolean;
enableSummary?: boolean; // 💰 默认 false
enableClinicalExtraction?: boolean; // 💰 默认 false
chunkSize?: number;
chunkOverlap?: number;
};
}): Promise<IngestResult>;
}): Promise<{ taskId: string; documentId: string }>;
/**
* 批量入库
* 获取入库任务状态
*/
async ingestBatch(documents: IngestParams[]): Promise<IngestResult[]>;
async getIngestStatus(taskId: string): Promise<{
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number; // 0-100
error?: string;
}>;
// ==================== 内容获取(全文) ====================
@@ -355,7 +446,7 @@ export class KnowledgeBaseEngine {
): Promise<SearchResult[]>;
/**
* 关键词检索(PostgreSQL FTS
* 关键词检索(pg_bigm 中文精确匹配
*/
async keywordSearch(
kbIds: string[],
@@ -377,6 +468,15 @@ export class KnowledgeBaseEngine {
}
): Promise<SearchResult[]>;
/**
* 🆕 重排序Qwen-Rerank API
*/
async rerank(
documents: SearchResult[],
query: string,
topK?: number
): Promise<SearchResult[]>;
// ==================== 管理操作 ====================
/**
@@ -482,90 +582,15 @@ export class KnowledgeBaseEngine {
## 🗄️ 数据模型
### Prisma Schema
```prisma
// schema.prisma
model EkbDocument {
id String @id @default(uuid())
kbId String // 所属知识库
userId String // 上传用户
// 基础信息
filename String
fileType String
fileSizeBytes BigInt
fileUrl String // OSS 地址
extractedText String? @db.Text // 提取的 Markdown 全文
summary String? @db.Text // 文档摘要200-500字
// 临床数据JSONB可选
pico Json? // { P, I, C, O }
studyDesign Json? // { design, sampleSize, ... }
regimen Json? // [{ drug, dose, ... }]
safety Json? // { ae_all, ae_grade34 }
criteria Json? // { inclusion, exclusion }
endpoints Json? // { primary, secondary }
// 状态
status String @default("pending") // pending | processing | completed | failed
errorMessage String? @db.Text
// 统计信息
tokenCount Int? // 文档 token 数量
chunks EkbChunk[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kbId])
@@index([status])
@@schema("ekb_schema")
}
model EkbChunk {
id String @id @default(uuid())
documentId String
content String @db.Text // 切片文本
pageNumber Int? // 来源页码
sectionType String? // 章节类型
// pgvector 字段
embedding Unsupported("vector(1024)")?
document EkbDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([documentId])
@@schema("ekb_schema")
}
```
### 索引设计
```sql
-- HNSW 向量索引(高性能近似最近邻)
CREATE INDEX IF NOT EXISTS ekb_chunk_embedding_idx
ON "ekb_schema"."EkbChunk"
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 全文检索索引
CREATE INDEX IF NOT EXISTS ekb_chunk_content_idx
ON "ekb_schema"."EkbChunk"
USING gin (to_tsvector('simple', content));
-- JSONB GIN 索引(临床数据查询)
CREATE INDEX IF NOT EXISTS ekb_document_pico_idx
ON "ekb_schema"."EkbDocument" USING gin (pico);
CREATE INDEX IF NOT EXISTS ekb_document_safety_idx
ON "ekb_schema"."EkbDocument" USING gin (safety);
```
> 📌 **数据模型详见**[04-数据模型设计.md](./04-数据模型设计.md)
>
> 本文档不再重复定义数据模型,请以上述文档为准。该文档包含:
> - 四层架构设计原则
> - EkbDocument / EkbChunk 完整 Prisma Schema
> - 索引设计HNSW、pg_bigm、GIN
> - contentType 枚举定义
> - metadata / structuredData JSONB 结构
> - 各类型使用示例
---
@@ -600,7 +625,13 @@ ON "ekb_schema"."EkbDocument" USING gin (safety);
┌─────────────────────────────────────────────────────────────┐
│ 知识库引擎 (本模块) │
│ │
│ 依赖:
平台依赖: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ⚡️ 异步任务处理pg-boss │ │
│ │ • 入库任务队列 │ │
│ │ • 详见Postgres-Only异步任务处理指南 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文档处理引擎 │ │
│ │ • 调用 DocumentProcessor.toMarkdown() │ │
@@ -608,9 +639,9 @@ ON "ekb_schema"."EkbDocument" USING gin (safety);
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LLM 网关 │ │
│ │ • 调用 LLMFactory.getAdapter('deepseek-v3') │ │
│ │ • 用于摘要生成、临床要素提取 │ │
│ │ LLM 网关(💰 可选调用) │ │
│ │ • 摘要生成enableSummary: true │ │
│ │ • 临床要素提取enableClinicalExtraction: true │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
@@ -623,7 +654,7 @@ ON "ekb_schema"."EkbDocument" USING gin (safety);
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 阿里云 DashScope API │ │
│ │ • text-embedding-v3 (向量化) │ │
│ │ • gte-rerank (重排序,可选) │ │
│ │ • qwen-rerank (重排序) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
@@ -632,21 +663,29 @@ ON "ekb_schema"."EkbDocument" USING gin (safety);
## 📅 开发计划
详见:[02-pgvector替换Dify计划.md](./02-pgvector替换Dify计划.md)
### 分阶段实施(推荐)
### 里程碑
详见:[03-分阶段实施方案.md](./03-分阶段实施方案.md)
| 阶段 | 内容 | 工期 | 状态 |
|------|------|------|------|
| **M1** | 数据库设计 + 核心服务 | 5 天 | 🔜 待开始 |
| **M2** | PKB 模块接入 + 测试 | 3 天 | 📋 规划中 |
| **M3** | 数据迁移 + 上线 | 2 天 | 📋 规划中 |
| **Phase 1 MVP** | 入库 + 向量检索 + 全文获取 | 3 天 | 🔜 待开始 |
| **Phase 2 增强** | + 关键词检索 + 混合检索 + rerank | 2 天 | 📋 规划中 |
| **Phase 3 完整** | + 异步入库 + 摘要 + PICO | 3 天 | 📋 规划中 |
**核心原则**:先跑通 MVP让业务走起来再逐步完善。
### 技术实现参考
详见:[02-pgvector替换Dify计划.md](./02-pgvector替换Dify计划.md)
---
## 📚 相关文档
- [pgvector 替换 Dify 开发计划](./02-pgvector替换Dify计划.md)
- [分阶段实施方案](./03-分阶段实施方案.md) - 🆕 MVP → 增强 → 完整
- [pgvector 替换 Dify 技术方案](./02-pgvector替换Dify计划.md) - 详细技术实现
- [Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md) - 异步架构参考
- [文档处理引擎设计方案](../02-文档处理引擎/01-文档处理引擎设计方案.md)
- [LLM 网关](../01-LLM大模型网关/README.md)
- [通用能力层总览](../README.md)
@@ -655,6 +694,16 @@ ON "ekb_schema"."EkbDocument" USING gin (safety);
## 📅 更新日志
### v1.2 (2026-01-20)
**架构审核优化:**
- ⚡️ **入库异步化**`ingestDocument()``submitIngestTask()` + `getIngestStatus()`,基于 pg-boss 队列
- 💰 **成本控制**:摘要生成、临床要素提取默认关闭,按需开启
- 🔧 **中文检索**:关键词检索从 `tsvector` 改为 `pg_bigm`,专为 CJK 文字优化
- 🆕 **新增能力**:独立暴露 `rerank()` 重排序能力Qwen-Rerank API
- 📦 **代码结构**:新增 `workers/ingestWorker.ts``RerankService.ts`
- 📋 **分阶段实施**:新增 MVP → 增强 → 完整 三阶段方案
### v1.1 (2026-01-20)
**设计原则重大更新:**

View File

@@ -175,81 +175,9 @@ Week 2: PKB 模块接入 + 测试 + 迁移
**关键代码**
```prisma
// schema.prisma - 通用知识库表
model EkbDocument {
id String @id @default(uuid())
kbId String // 知识库 ID
userId String // 上传用户
// 基础信息
filename String
fileType String
fileSizeBytes BigInt
fileUrl String // 原始文件 OSS 地址
extractedText String? @db.Text // 解析后的 Markdown
// 临床数据JSONB可选
pico Json?
studyDesign Json?
regimen Json?
safety Json?
criteria Json?
endpoints Json?
// 状态
status String @default("pending") // pending | processing | completed | failed
errorMessage String? @db.Text
chunks EkbChunk[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kbId])
@@index([status])
@@schema("ekb_schema")
}
model EkbChunk {
id String @id @default(uuid())
documentId String
content String @db.Text
pageNumber Int?
sectionType String?
// pgvector 字段
embedding Unsupported("vector(1024)")?
document EkbDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([documentId])
@@schema("ekb_schema")
}
```
**手动 SQL创建索引**
```sql
-- 创建 HNSW 索引
CREATE INDEX IF NOT EXISTS ekb_chunk_embedding_idx
ON "ekb_schema"."EkbChunk"
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 创建全文检索索引
CREATE INDEX IF NOT EXISTS ekb_chunk_content_idx
ON "ekb_schema"."EkbChunk"
USING gin (to_tsvector('simple', content));
-- 创建 JSONB GIN 索引
CREATE INDEX IF NOT EXISTS ekb_document_pico_idx
ON "ekb_schema"."EkbDocument" USING gin (pico);
```
> 📌 **数据模型详见**[04-数据模型设计.md](./04-数据模型设计.md)
>
> 包含完整的 EkbDocument / EkbChunk Prisma Schema 和索引设计。
---

View File

@@ -0,0 +1,595 @@
# 知识库引擎分阶段实施方案
> **文档版本:** v1.0
> **创建日期:** 2026-01-20
> **最后更新:** 2026-01-20
> **核心原则:** 先跑通 MVP让业务走起来再逐步完善
---
## 📋 概述
### 为什么分阶段实施?
完整的知识库引擎包含多个复杂功能,一次性全部实现风险高、周期长。采用分阶段实施:
-**降低风险**:每阶段可交付、可验证
-**快速见效**MVP 3天即可让业务跑起来
-**灵活调整**:根据业务反馈调整后续优先级
### 三阶段总览
```
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: MVP3天
│ ───────────────── │
│ 目标:让业务跑起来 │
│ 能力:入库 + 向量检索 + 全文获取 │
│ 场景PKB 基础问答 │
├─────────────────────────────────────────────────────────────┤
│ Phase 2: 增强检索2天
│ ───────────────── │
│ 目标:检索质量提升 │
│ 能力:+ 关键词检索 + 混合检索 + rerank │
│ 场景PKB 高质量检索 │
├─────────────────────────────────────────────────────────────┤
│ Phase 3: 完整功能3天
│ ───────────────── │
│ 目标:完整架构落地 │
│ 能力:+ 异步入库 + 摘要生成 + 临床要素提取 │
│ 场景ASL、AIA 完整功能 │
└─────────────────────────────────────────────────────────────┘
```
---
## 🚀 Phase 1: MVP3天
### 目标
**最小可用版本**PKB 能上传文档、能检索、能问答
### 交付能力
| 能力 | 方法 | 说明 |
|------|------|------|
| **文档入库** | `ingestDocument()` | 同步处理(简化版) |
| **向量检索** | `vectorSearch()` | pgvector 语义检索 |
| **全文获取** | `getDocumentFullText()` | 获取单个文档 |
| | `getAllDocumentsText()` | 获取知识库所有文档 |
| **管理操作** | `deleteDocument()` | 删除文档 |
### 暂不实现
- ❌ 异步入库pg-boss
- ❌ 关键词检索pg_trgm
- ❌ 混合检索RRF
- ❌ rerank 重排序
- ❌ 摘要生成
- ❌ 临床要素提取PICO
### 数据模型
> 📌 **数据模型详见**[04-数据模型设计.md](./04-数据模型设计.md)
>
> ⚠️ **重要**MVP 阶段就创建完整 Schema但只使用部分字段。避免后续阶段改表迁移。
### 字段使用阶段说明
| 字段分层 | Phase 1 MVP | Phase 2 增强 | Phase 3 完整 |
|----------|-------------|--------------|--------------|
| **Layer 0-1: 基础信息** | | | |
| filename, fileType, fileSizeBytes | ✅ 填充 | - | - |
| fileUrl, extractedText | ✅ 填充 | - | - |
| status, errorMessage | ✅ 使用 | - | - |
| **Layer 2: 内容增强** | | | |
| summary, tokenCount, pageCount | ❌ 预留 | ❌ 预留 | ✅ 填充 |
| **Layer 3: 分类标签** | | | |
| contentType, tags, category | ⚪ 可选 | ⚪ 可选 | ✅ 启用 |
| **Layer 4: 结构化数据** | | | |
| metadata, structuredData | ❌ 预留 | ❌ 预留 | ✅ 填充 |
| **EkbChunk** | | | |
| content, chunkIndex | ✅ 填充 | - | - |
| pageNumber, sectionType | ⚪ 可选 | - | - |
| embedding | ✅ 填充 | - | - |
### 核心代码
```typescript
// Phase 1: MVP 版本 KnowledgeBaseEngine
export class KnowledgeBaseEngine {
constructor(private prisma: PrismaClient) {}
/**
* 同步入库MVP 简化版,小文件场景)
* Phase 3 将升级为异步
*/
async ingestDocument(params: {
kbId: string;
userId: string;
file: Buffer;
filename: string;
}): Promise<{ documentId: string }> {
// 1. 解析文档 → Markdown
const markdown = await documentProcessor.toMarkdown(params.file, params.filename);
// 2. 切片
const chunks = chunkService.split(markdown, { size: 512, overlap: 50 });
// 3. 向量化
const embeddings = await embeddingService.embedBatch(chunks.map(c => c.text));
// 4. 上传原始文件到 OSS
const fileUrl = await storage.upload(params.file, params.filename);
// 5. 存储文档(使用完整 SchemaMVP 只填充部分字段)
const document = await this.prisma.ekbDocument.create({
data: {
kbId: params.kbId,
userId: params.userId,
filename: params.filename,
fileType: getFileType(params.filename),
fileSizeBytes: BigInt(params.file.length),
fileUrl: fileUrl,
extractedText: markdown,
status: 'completed',
// Phase 3 才填充的字段保持 null
// summary, tokenCount, pico, studyDesign, regimen, safety, criteria, endpoints
}
});
// 6. 存储切片 + 向量(使用完整 Schema
for (let i = 0; i < chunks.length; i++) {
await this.prisma.$executeRaw`
INSERT INTO "ekb_schema"."EkbChunk"
(id, document_id, content, chunk_index, page_number, section_type, embedding, created_at)
VALUES (
${crypto.randomUUID()},
${document.id},
${chunks[i].text},
${i},
${chunks[i].pageNumber || null},
${chunks[i].sectionType || null},
${embeddings[i]}::vector,
NOW()
)
`;
}
return { documentId: document.id };
}
/**
* 向量检索
*/
async vectorSearch(
kbIds: string[],
query: string,
topK: number = 10
): Promise<SearchResult[]> {
const queryVector = await embeddingService.embed(query);
const results = await this.prisma.$queryRaw<SearchResult[]>`
SELECT
c.id,
c.content,
c.document_id,
d.filename,
1 - (c.embedding <=> ${queryVector}::vector) as score
FROM "ekb_schema"."EkbChunk" c
JOIN "ekb_schema"."EkbDocument" d ON c.document_id = d.id
WHERE d.kb_id = ANY(${kbIds}::text[])
ORDER BY c.embedding <=> ${queryVector}::vector
LIMIT ${topK}
`;
return results;
}
/**
* 获取单个文档全文
*/
async getDocumentFullText(documentId: string): Promise<DocumentText> {
const doc = await this.prisma.ekbDocument.findUnique({
where: { id: documentId },
select: { id: true, filename: true, extractedText: true }
});
if (!doc) throw new Error('Document not found');
return {
id: doc.id,
filename: doc.filename,
text: doc.extractedText || '',
};
}
/**
* 获取知识库所有文档全文
*/
async getAllDocumentsText(kbId: string): Promise<DocumentText[]> {
const docs = await this.prisma.ekbDocument.findMany({
where: { kbId, status: 'completed' },
select: { id: true, filename: true, extractedText: true }
});
return docs.map(doc => ({
id: doc.id,
filename: doc.filename,
text: doc.extractedText || '',
}));
}
/**
* 删除文档
*/
async deleteDocument(documentId: string): Promise<void> {
await this.prisma.ekbDocument.delete({
where: { id: documentId }
});
}
}
```
### 索引设计(完整版,一次性创建)
```sql
-- ===== MVP 阶段必须创建 =====
-- 1. HNSW 向量索引(语义检索核心)
CREATE INDEX IF NOT EXISTS ekb_chunk_embedding_idx
ON "ekb_schema"."EkbChunk"
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- ===== Phase 2 阶段使用MVP 可预创建)=====
-- 2. pg_bigm 中文关键词索引
CREATE EXTENSION IF NOT EXISTS pg_bigm;
CREATE INDEX IF NOT EXISTS ekb_chunk_content_bigm_idx
ON "ekb_schema"."EkbChunk"
USING gin (content gin_bigm_ops);
-- ===== Phase 3 阶段使用MVP 可预创建)=====
-- 3. JSONB GIN 索引(临床数据查询)
CREATE INDEX IF NOT EXISTS ekb_document_pico_idx
ON "ekb_schema"."EkbDocument" USING gin (pico);
CREATE INDEX IF NOT EXISTS ekb_document_safety_idx
ON "ekb_schema"."EkbDocument" USING gin (safety);
CREATE INDEX IF NOT EXISTS ekb_document_studydesign_idx
ON "ekb_schema"."EkbDocument" USING gin ("studyDesign");
```
> 💡 **建议**MVP 阶段一次性创建所有索引,避免后续 DDL 操作。空表创建索引几乎无成本。
### 任务清单
| 任务 | 预估 | 产出 |
|------|------|------|
| **Schema 迁移(完整版)** | 3h | `ekb_schema` + 完整表结构 + 全部索引 |
| EmbeddingService | 3h | 阿里云 API 封装 |
| ChunkService | 2h | 文本切片 |
| KnowledgeBaseEngine MVP | 4h | 核心类(只实现 MVP 方法) |
| 单元测试 | 3h | 基础测试用例 |
| PKB 集成 | 4h | 替换 Dify 调用 |
| **合计** | **19h (3天)** | |
> 💡 **关键点**Schema 迁移一次到位,后续阶段只写代码,不改表。
### 验收标准
- [ ] PKB 可上传 PDF 文档
- [ ] PKB 可向量检索
- [ ] PKB 可获取文档全文
- [ ] PKB 可删除文档
- [ ] 基础问答功能正常
---
## 🔍 Phase 2: 增强检索2天
### 目标
**检索质量提升**:支持中文关键词、混合检索、结果重排序
### 新增能力
| 能力 | 方法 | 说明 |
|------|------|------|
| **关键词检索** | `keywordSearch()` | pg_bigm 中文精确匹配 |
| **混合检索** | `hybridSearch()` | 向量 + 关键词 + RRF |
| **重排序** | `rerank()` | Qwen-Rerank API |
### 新增索引
```sql
-- Phase 2: 关键词检索索引pg_bigm专为中文优化
CREATE EXTENSION IF NOT EXISTS pg_bigm;
CREATE INDEX IF NOT EXISTS ekb_chunk_content_bigm_idx
ON "ekb_schema"."EkbChunk"
USING gin (content gin_bigm_ops);
```
### 新增代码
```typescript
// Phase 2 新增方法
/**
* 关键词检索pg_bigm 中文精确匹配)
*/
async keywordSearch(
kbIds: string[],
query: string,
topK: number = 10
): Promise<SearchResult[]> {
const results = await this.prisma.$queryRaw<SearchResult[]>`
SELECT
c.id,
c.content,
c.document_id,
d.filename,
bigm_similarity(c.content, ${query}) as score
FROM "ekb_schema"."EkbChunk" c
JOIN "ekb_schema"."EkbDocument" d ON c.document_id = d.id
WHERE d.kb_id = ANY(${kbIds}::text[])
AND c.content LIKE ${'%' + query + '%'}
ORDER BY bigm_similarity(c.content, ${query}) DESC
LIMIT ${topK}
`;
return results;
}
/**
* 混合检索(向量 + 关键词 + RRF 融合)
*/
async hybridSearch(
kbIds: string[],
query: string,
topK: number = 10
): Promise<SearchResult[]> {
// 并发执行两路检索
const [vectorResults, keywordResults] = await Promise.all([
this.vectorSearch(kbIds, query, 20),
this.keywordSearch(kbIds, query, 20),
]);
// RRF 融合
return rrfFusion(vectorResults, keywordResults, topK);
}
/**
* 重排序Qwen-Rerank API
*/
async rerank(
documents: SearchResult[],
query: string,
topK: number = 10
): Promise<SearchResult[]> {
const response = await dashscope.rerank({
model: 'gte-rerank',
query,
documents: documents.map(d => d.content),
top_n: topK,
});
return response.results.map(r => ({
...documents[r.index],
score: r.relevance_score,
}));
}
```
### RRF 融合算法
```typescript
// utils/rrfFusion.ts
export function rrfFusion(
vectorResults: SearchResult[],
keywordResults: SearchResult[],
topK: number,
k: number = 60
): SearchResult[] {
const scores = new Map<string, number>();
// 向量检索得分
vectorResults.forEach((r, rank) => {
const score = scores.get(r.id) || 0;
scores.set(r.id, score + 1 / (k + rank + 1));
});
// 关键词检索得分
keywordResults.forEach((r, rank) => {
const score = scores.get(r.id) || 0;
scores.set(r.id, score + 1 / (k + rank + 1));
});
// 合并去重 + 排序
const allResults = [...vectorResults, ...keywordResults];
const uniqueResults = Array.from(
new Map(allResults.map(r => [r.id, r])).values()
);
return uniqueResults
.map(r => ({ ...r, score: scores.get(r.id) || 0 }))
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
```
### 任务清单
| 任务 | 预估 | 产出 |
|------|------|------|
| pg_bigm 索引 | 1h | SQL 迁移 |
| KeywordSearchService | 2h | 关键词检索pg_bigm |
| RRF 融合算法 | 2h | rrfFusion.ts |
| RerankService | 2h | 阿里云 API 封装 |
| hybridSearch 集成 | 2h | 混合检索 |
| 测试 | 3h | 检索质量验证 |
| **合计** | **12h (2天)** | |
### 验收标准
- [ ] 关键词检索支持中文("帕博利珠" 可匹配 "帕博利珠单抗"
- [ ] 混合检索可用
- [ ] rerank 可用
- [ ] 检索召回率提升(对比 Phase 1
---
## 🎯 Phase 3: 完整功能3天
### 目标
**完整架构落地**:支持大文件、高级提取功能
### 新增能力
| 能力 | 方法 | 说明 |
|------|------|------|
| **异步入库** | `submitIngestTask()` | pg-boss 队列 |
| | `getIngestStatus()` | 任务状态查询 |
| **摘要获取** | `getDocumentSummary()` | LLM 生成摘要 |
| | `getAllDocumentsSummaries()` | 批量获取摘要 |
| **临床数据** | `getClinicalData()` | PICO 等结构化数据 |
### Schema 说明
> ✅ **无需 Schema 升级**MVP 阶段已创建完整表结构Phase 3 只需填充字段。
```typescript
// Phase 3: 开始填充 summary、tokenCount、临床数据字段
await prisma.ekbDocument.update({
where: { id: documentId },
data: {
summary: generatedSummary, // 🆕 Phase 3 填充
tokenCount: calculatedTokens, // 🆕 Phase 3 填充
pico: extractedPico, // 🆕 Phase 3 填充
studyDesign: extractedStudyDesign, // 🆕 Phase 3 填充
regimen: extractedRegimen, // 🆕 Phase 3 填充
safety: extractedSafety, // 🆕 Phase 3 填充
}
});
```
### 异步入库实现
详见:[Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md)
```typescript
// Phase 3: 异步入库
/**
* 提交入库任务
*/
async submitIngestTask(params: {
kbId: string;
userId: string;
file: Buffer;
filename: string;
options?: {
enableSummary?: boolean; // 💰 默认 false
enableClinicalExtraction?: boolean; // 💰 默认 false
};
}): Promise<{ taskId: string; documentId: string }> {
// 1. 快速上传到 OSS
const fileUrl = await storage.upload(params.file);
// 2. 创建文档记录status: processing
const document = await this.prisma.ekbDocument.create({
data: {
kbId: params.kbId,
userId: params.userId,
filename: params.filename,
fileUrl,
status: 'processing',
}
});
// 3. 推送任务到 pg-boss
const taskId = await jobQueue.send('ekb-ingest', {
documentId: document.id,
fileUrl,
options: params.options,
});
return { taskId, documentId: document.id };
}
/**
* 获取任务状态
*/
async getIngestStatus(taskId: string): Promise<{
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
error?: string;
}> {
const job = await jobQueue.getJobById(taskId);
return {
status: job.state,
progress: job.data?.progress || 0,
error: job.state === 'failed' ? job.output?.error : undefined,
};
}
```
### 任务清单
| 任务 | 预估 | 产出 |
|------|------|------|
| pg-boss Worker | 4h | ingestWorker.ts异步入库 |
| SummaryService | 3h | LLM 摘要生成 |
| ClinicalExtractionService | 4h | PICO 提取 + JSON 容错 |
| 摘要/临床数据 API | 2h | 新增 getDocumentSummary 等方法 |
| 测试 | 4h | 完整流程验证 |
| **合计** | **17h (3天)** | |
> ✅ **优势**:无需 Schema 迁移,只写业务代码。
### 验收标准
- [ ] 大文件上传不超时
- [ ] 任务状态可查询
- [ ] 摘要生成可用
- [ ] PICO 提取可用ASL 场景)
- [ ] AIA 摘要筛选策略可用
---
## 📊 总体进度
| 阶段 | 工期 | 累计 | 状态 |
|------|------|------|------|
| **Phase 1 MVP** | 3天 | 3天 | 🔜 待开始 |
| **Phase 2 增强** | 2天 | 5天 | 📋 规划中 |
| **Phase 3 完整** | 3天 | 8天 | 📋 规划中 |
---
## 🔗 相关文档
- [知识库引擎架构设计](./01-知识库引擎架构设计.md) - 完整架构目标
- [pgvector 替换 Dify 技术方案](./02-pgvector替换Dify计划.md) - 详细技术实现
- [Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md) - 异步架构参考
---
## 📅 更新日志
### v1.0 (2026-01-20)
- 初始版本
- 确定三阶段实施方案MVP → 增强 → 完整
---
**维护人:** 技术架构师
**最后更新:** 2026-01-20

View File

@@ -0,0 +1,944 @@
# 知识库引擎数据模型设计
> **文档版本**: v2.0
> **最后更新**: 2026-01-21
> **状态**: ✅ 权威文档
> **说明**: 本文档是知识库引擎数据模型的唯一权威来源,其他文档应引用本文档,避免重复定义。
---
## 1. 设计原则
### 1.1 核心理念
> **即使没有任何结构化数据RAG 检索也必须能工作!**
>
> 结构化数据是"锦上添花",不是"必须有"。
### 1.2 四层架构
```
┌─────────────────────────────────────────────────────────────┐
│ Layer 0: RAG 核心层(必须有,检索基础) │
│ ───────────────────────────────────── │
│ extractedText → 全文 Markdown │
│ chunks[] → 文本切片 │
│ embeddings[] → 向量嵌入 │
│ │
│ ✅ 只要有这一层RAG 就能工作! │
├─────────────────────────────────────────────────────────────┤
│ Layer 1: 基础信息层(必须有,系统需要) │
│ ───────────────────────────────────── │
│ filename, fileType, fileSizeBytes, fileUrl │
│ status, errorMessage, createdAt, updatedAt │
│ │
│ ✅ 文件管理必需 │
├─────────────────────────────────────────────────────────────┤
│ Layer 2: 内容增强层(可选,提升体验) │
│ ───────────────────────────────────── │
│ summary → AI 生成摘要(快速预览) │
│ tokenCount → Token 数量(成本估算) │
│ pageCount → 页数 │
│ │
│ ✅ 有则更好,无也能用 │
├─────────────────────────────────────────────────────────────┤
│ Layer 3: 分类标签层(可选,用户自定义) │
│ ───────────────────────────────────── │
│ contentType → 内容类型(可 AI 自动识别) │
│ tags[] → 用户标签(用户自己打) │
│ category → 分类目录(用户自己选) │
│ │
│ ✅ 用户可以不管,系统也能运行 │
├─────────────────────────────────────────────────────────────┤
│ Layer 4: 结构化数据层(可选,精准查询) │
│ ───────────────────────────────────── │
│ metadata → 文献属性pmid, doi, journal...
│ structuredData → 类型特定数据pico, diagnosis...
│ │
│ ✅ 高级功能,专业用户/药企场景使用 │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 设计目标
| 目标 | 实现方式 |
|------|----------|
| **最小可用** | Layer 0-1 必须有RAG 就能工作 |
| **渐进增强** | Layer 2-4 可选,有则更强 |
| **用户友好** | 什么都不填也能用 |
| **专业支持** | 专业用户可以填完整信息 |
| **AI 辅助** | contentType、summary 可 AI 自动生成 |
| **灵活扩展** | JSONB 支持任意结构化数据 |
### 1.4 ER 关系图
```
┌─────────────────────────────────────────────────────────────────┐
│ EkbKnowledgeBase │
│ ───────────────────────────────────────────── │
│ id, name, description │
│ type (USER | SYSTEM) │
│ ownerId (userId 或 moduleId) │
│ config (JSONB: chunkSize, topK, enableRerank...) │
├─────────────────────────────────────────────────────────────────┤
│ 1:N
┌─────────────────────────────────────────────────────────────────┐
│ EkbDocument │
│ ───────────────────────────────────────────── │
│ id, kbId(FK), userId │
│ filename, fileType, fileSizeBytes, fileUrl, fileHash │
│ extractedText, summary, tokenCount, pageCount │
│ contentType, tags[], category │
│ metadata(JSONB), structuredData(JSONB) │
├─────────────────────────────────────────────────────────────────┤
│ 1:N
┌─────────────────────────────────────────────────────────────────┐
│ EkbChunk │
│ ───────────────────────────────────────────── │
│ id, documentId(FK) │
│ content, chunkIndex │
│ embedding (vector 1024) │
│ pageNumber, sectionType │
│ metadata(JSONB) ← 新增:切片级元数据 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. EkbKnowledgeBase 模型(容器表)
### 2.1 设计目的
| 痛点 | 解决方案 |
|------|----------|
| 无法区分用户私有库 vs 系统公共库 | `type` 字段USER / SYSTEM |
| 无法管理"谁拥有这个库" | `ownerId` 字段userId 或 moduleId |
| 无法为不同库配置不同 RAG 策略 | `config` JSONBchunkSize, topK 等 |
| 用户配额管理(最多 3 个库) | 通过 `type=USER` 计数 |
### 2.2 Prisma Schema
```prisma
model EkbKnowledgeBase {
id String @id @default(uuid())
name String // 知识库名称
description String? // 描述
// ===== 核心隔离字段 =====
type String @default("USER") // USER: 用户私有, SYSTEM: 系统公共
ownerId String // USER: userId, SYSTEM: moduleId (如 "ASL", "AIA")
// ===== 策略配置 (JSONB) =====
config Json? // { chunkSize, topK, enableRerank, embeddingModel }
// ===== 关联 =====
documents EkbDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// ===== 索引 =====
@@index([ownerId])
@@index([type])
@@schema("ekb_schema")
}
```
### 2.3 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | ✅ | UUID 主键 |
| `name` | String | ✅ | 知识库名称(如"我的文献库"、"ASL 系统文献库" |
| `description` | String | ❌ | 知识库描述 |
| `type` | String | ✅ | 类型:`USER`(用户私有)或 `SYSTEM`(系统公共) |
| `ownerId` | String | ✅ | 所有者 ID用户库填 userId系统库填模块 ID |
| `config` | Json | ❌ | 策略配置 JSONB |
### 2.4 type 枚举
| 值 | 说明 | ownerId 含义 |
|------|------|-------------|
| `USER` | 用户私有知识库 | userId创建者 |
| `SYSTEM` | 系统公共知识库 | moduleId如 "ASL", "AIA", "IIT" |
### 2.5 config 结构
```typescript
interface KnowledgeBaseConfig {
// RAG 策略
chunkSize?: number; // 切片大小,默认 512
chunkOverlap?: number; // 切片重叠,默认 50
topK?: number; // 检索数量,默认 5
// 高级功能
enableRerank?: boolean; // 启用重排序
enableSummary?: boolean; // 自动生成摘要
enableClinicalExtraction?: boolean; // 临床数据提取
// 模型配置
embeddingModel?: string; // 嵌入模型版本
}
```
### 2.6 使用示例
```typescript
// 用户创建个人知识库
await prisma.ekbKnowledgeBase.create({
data: {
name: '我的肺癌文献库',
type: 'USER',
ownerId: 'user-123',
config: { chunkSize: 512, topK: 5 }
}
});
// 系统创建 ASL 模块知识库
await prisma.ekbKnowledgeBase.create({
data: {
name: 'ASL 系统文献库',
type: 'SYSTEM',
ownerId: 'ASL',
config: { enableClinicalExtraction: true }
}
});
// 查询用户所有知识库(配额检查)
const userKbs = await prisma.ekbKnowledgeBase.count({
where: { type: 'USER', ownerId: 'user-123' }
});
if (userKbs >= 3) throw new Error('知识库配额已满');
```
---
## 3. EkbDocument 模型
### 3.1 Prisma Schema
```prisma
model EkbDocument {
id String @id @default(uuid())
kbId String // 所属知识库
userId String // 上传者(冗余存储,方便快速查询)
// ===== Layer 1: 基础信息(必须)=====
filename String // 文件名
fileType String // pdf, docx, pptx, xlsx, md, txt
fileSizeBytes BigInt // 文件大小(字节)
fileUrl String // OSS 存储路径
fileHash String? // 文件 SHA256 哈希(用于秒传和去重)
status String @default("pending") // 处理状态
errorMessage String? @db.Text // 错误信息
// ===== Layer 0: RAG 核心(必须)=====
extractedText String? @db.Text // Markdown 全文
// ===== Layer 2: 内容增强(可选)=====
summary String? @db.Text // AI 摘要
tokenCount Int? // Token 数量
pageCount Int? // 页数
// ===== Layer 3: 分类标签(可选)=====
contentType String? // 内容类型
tags String[] // 用户标签
category String? // 分类目录
// ===== Layer 4: 结构化数据(可选)=====
metadata Json? // 文献属性 JSONB
structuredData Json? // 类型特定数据 JSONB
// ===== 关联 =====
knowledgeBase EkbKnowledgeBase @relation(fields: [kbId], references: [id], onDelete: Cascade)
chunks EkbChunk[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// ===== 索引 =====
@@index([kbId])
@@index([userId])
@@index([status])
@@index([contentType])
@@index([fileHash]) // 支持秒传查询
@@index([tags], type: Gin)
@@index([metadata], type: Gin)
@@index([structuredData], type: Gin)
@@schema("ekb_schema")
}
```
### 3.2 字段说明
#### Layer 1: 基础信息(必须)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | ✅ | UUID 主键 |
| `kbId` | String | ✅ | 所属知识库 ID外键 |
| `userId` | String | ✅ | 上传者用户 ID冗余存储便于快速查询 |
| `filename` | String | ✅ | 原始文件名 |
| `fileType` | String | ✅ | 文件类型pdf, docx, pptx, xlsx, md, txt, csv |
| `fileSizeBytes` | BigInt | ✅ | 文件大小(字节) |
| `fileUrl` | String | ✅ | OSS 存储路径 |
| `fileHash` | String | ❌ | 文件 SHA256 哈希(用于秒传和去重) |
| `status` | String | ✅ | 处理状态pending, processing, completed, failed |
| `errorMessage` | String | ❌ | 处理失败时的错误信息 |
#### Layer 0: RAG 核心(必须)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `extractedText` | String | ❌* | Markdown 格式全文(处理完成后必须有) |
> *注:上传时为空,处理完成后必须有值
#### Layer 2: 内容增强(可选)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `summary` | String | ❌ | AI 生成的摘要 |
| `tokenCount` | Int | ❌ | 全文 Token 数量(用于成本估算) |
| `pageCount` | Int | ❌ | 文档页数 |
#### Layer 3: 分类标签(可选)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `contentType` | String | ❌ | 内容类型标识(见第 5 节) |
| `tags` | String[] | ❌ | 用户自定义标签 |
| `category` | String | ❌ | 分类目录路径,如 "肿瘤科/肺癌" |
#### Layer 4: 结构化数据(可选)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `metadata` | Json | ❌ | 文献属性(见第 7 节) |
| `structuredData` | Json | ❌ | 类型特定数据(见第 8 节) |
### 3.3 status 状态枚举
| 状态 | 说明 |
|------|------|
| `pending` | 等待处理 |
| `processing` | 正在处理(解析、向量化) |
| `completed` | 处理完成 |
| `failed` | 处理失败 |
### 3.4 fileHash 使用示例
```typescript
// 计算文件哈希
import crypto from 'crypto';
function calculateFileHash(buffer: Buffer): string {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
// 上传时检查是否已存在(秒传)
async function uploadWithDedup(kbId: string, file: Buffer, filename: string) {
const fileHash = calculateFileHash(file);
// 检查同一知识库内是否已存在相同文件
const existing = await prisma.ekbDocument.findFirst({
where: { kbId, fileHash }
});
if (existing) {
// 秒传:直接返回已存在的文档
return { type: 'duplicate', document: existing };
}
// 正常上传流程
const document = await prisma.ekbDocument.create({
data: { kbId, fileHash, filename, /* ... */ }
});
return { type: 'new', document };
}
```
---
## 4. EkbChunk 模型
### 4.1 Prisma Schema
```prisma
model EkbChunk {
id String @id @default(uuid())
documentId String // 所属文档
// ===== 核心内容 =====
content String @db.Text // 切片文本Markdown
chunkIndex Int // 切片序号(从 0 开始)
// ===== 向量 =====
embedding Unsupported("vector(1024)")? // 向量嵌入
// ===== 溯源信息(可选)=====
pageNumber Int? // 页码(用于 PDF 溯源)
sectionType String? // 章节类型
// ===== 扩展元数据(可选)=====
metadata Json? // 切片级元数据 JSONB
document EkbDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
@@index([documentId])
@@index([metadata], type: Gin) // JSONB 索引
@@schema("ekb_schema")
}
```
### 4.2 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | ✅ | UUID 主键 |
| `documentId` | String | ✅ | 所属文档 ID |
| `content` | String | ✅ | 切片文本Markdown 格式) |
| `chunkIndex` | Int | ✅ | 切片序号,从 0 开始 |
| `embedding` | vector(1024) | ❌* | 向量嵌入(向量化后必须有) |
| `pageNumber` | Int | ❌ | 来源页码PDF 溯源) |
| `sectionType` | String | ❌ | 章节类型标识 |
| `metadata` | Json | ❌ | 切片级扩展元数据(见 4.4 |
### 4.3 sectionType 枚举(可选)
| 值 | 说明 |
|------|------|
| `title` | 标题 |
| `abstract` | 摘要 |
| `introduction` | 引言 |
| `methods` | 方法 |
| `results` | 结果 |
| `discussion` | 讨论 |
| `conclusion` | 结论 |
| `references` | 参考文献 |
| `table` | 表格 |
| `figure` | 图片说明 |
### 4.4 metadata 结构(切片级元数据)
```typescript
interface ChunkMetadata {
// 考试题场景
isAnswer?: boolean; // 是否为答案切片
questionId?: number; // 所属题目 ID
chunkType?: 'question' | 'options' | 'answer' | 'explanation';
// 病历场景
section?: string; // 病历段落:主诉、现病史、检查结果等
// 通用
importance?: number; // 重要性权重 0-1
keywords?: string[]; // 关键词(用于加权检索)
}
```
**使用示例**
```typescript
// 考试题切片 - 题目
{
"content": "1. 关于帕博利珠单抗,下列说法正确的是?",
"metadata": { "chunkType": "question", "questionId": 1 }
}
// 考试题切片 - 选项
{
"content": "A. 派姆单抗 B. 纳武利尤单抗 C. 阿特珠单抗 D. 度伐利尤单抗",
"metadata": { "chunkType": "options", "questionId": 1 }
}
// 考试题切片 - 答案
{
"content": "正确答案A。派姆单抗是帕博利珠单抗的通用名...",
"metadata": { "chunkType": "answer", "questionId": 1, "isAnswer": true }
}
// 检索时降低纯答案切片的权重
const results = await vectorSearch(query);
const reranked = results.map(r => ({
...r,
score: r.metadata?.isAnswer ? r.score * 0.5 : r.score
}));
```
---
## 5. 索引设计
### 5.1 向量索引HNSW
```sql
-- 创建 HNSW 索引(高性能近似最近邻)
CREATE INDEX idx_ekb_chunk_embedding ON ekb_schema.ekb_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
**参数说明**
- `m = 16`: 每层最大连接数
- `ef_construction = 64`: 构建时搜索范围
### 5.2 关键词索引pg_bigm
```sql
-- 安装 pg_bigm 扩展
CREATE EXTENSION IF NOT EXISTS pg_bigm;
-- 全文内容索引
CREATE INDEX idx_ekb_chunk_content_bigm ON ekb_schema.ekb_chunk
USING gin (content gin_bigm_ops);
-- 文档摘要索引
CREATE INDEX idx_ekb_document_summary_bigm ON ekb_schema.ekb_document
USING gin (summary gin_bigm_ops);
-- 提取文本索引
CREATE INDEX idx_ekb_document_text_bigm ON ekb_schema.ekb_document
USING gin (extracted_text gin_bigm_ops);
```
**关键词查询示例**
```sql
-- 关键词搜索(支持中英文)
SELECT * FROM ekb_schema.ekb_chunk
WHERE content LIKE '%Pembrolizumab%'
ORDER BY likequery(content, 'Pembrolizumab') DESC;
-- 中文搜索
SELECT * FROM ekb_schema.ekb_chunk
WHERE content LIKE '%非小细胞肺癌%';
```
### 5.3 JSONB 索引GIN
```sql
-- 文献属性索引
CREATE INDEX idx_ekb_document_metadata ON ekb_schema.ekb_document
USING gin (metadata jsonb_path_ops);
-- 结构化数据索引
CREATE INDEX idx_ekb_document_structured ON ekb_schema.ekb_document
USING gin (structured_data jsonb_path_ops);
-- 标签数组索引
CREATE INDEX idx_ekb_document_tags ON ekb_schema.ekb_document
USING gin (tags);
```
**JSONB 查询示例**
```sql
-- 按影响因子筛选
SELECT * FROM ekb_schema.ekb_document
WHERE (metadata->>'ifScore')::float > 10;
-- 按 PICO 干预措施筛选
SELECT * FROM ekb_schema.ekb_document
WHERE structured_data->'pico'->>'I' ILIKE '%Pembrolizumab%';
-- 按标签筛选
SELECT * FROM ekb_schema.ekb_document
WHERE tags @> ARRAY['肺癌', '免疫治疗'];
```
---
## 6. contentType 枚举
| 值 | 说明 | 适用场景 |
|------|------|----------|
| `general` | 通用文档(默认) | 任意文档 |
| `literature` | 医学文献 | 论文、研究报告 |
| `case` | 典型病历 | MDT 病例、教学病例 |
| `exam` | 教学考试 | 试题、模拟题 |
| `drug` | 药品资料 | 说明书、处方信息 |
| `guideline` | 临床指南 | NCCN、CSCO 指南 |
| `note` | 个人笔记 | 学习笔记、会议记录 |
| `protocol` | 研究方案 | 临床试验方案 |
| `report` | 工作报告 | 汇报材料、分析报告 |
---
## 7. metadata 结构(文献属性)
### 7.1 JSON Schema
```typescript
interface Metadata {
// 基础信息
title?: string; // 文献标题
abstract?: string; // 原始摘要
// 标识符
pmid?: string; // PubMed ID
doi?: string; // DOI
nctId?: string; // ClinicalTrials.gov ID
// 来源信息
journal?: string; // 期刊名称
publisher?: string; // 出版商
authors?: string[]; // 作者列表
// 时间与评分
publishYear?: number; // 发表年份
publishDate?: string; // 发表日期 (YYYY-MM-DD)
ifScore?: number; // 影响因子
// 分类
docType?: string; // 文献类型RCT, Meta, Review, Case, Guideline
keywords?: string[]; // 关键词
meshTerms?: string[]; // MeSH 词表
// 语言
language?: string; // 语言en, zh, ...
}
```
### 7.2 示例
```json
{
"title": "Pembrolizumab versus Chemotherapy for PD-L1Positive NonSmall-Cell Lung Cancer",
"pmid": "27718847",
"doi": "10.1056/NEJMoa1606774",
"journal": "New England Journal of Medicine",
"authors": ["Reck M", "Rodriguez-Abreu D", "Robinson AG"],
"publishYear": 2016,
"ifScore": 176.079,
"docType": "RCT",
"keywords": ["NSCLC", "Pembrolizumab", "Immunotherapy"],
"language": "en"
}
```
---
## 8. structuredData 结构(类型特定数据)
### 8.1 医学文献 (literature)
```typescript
interface LiteratureData {
// PICO 要素
pico?: {
P: string; // Population
I: string; // Intervention
C: string; // Comparison
O: string; // Outcome
};
// 研究设计
studyDesign?: {
design: string; // Phase III RCT, Meta-analysis, etc.
sampleSize: number; // 样本量
blinding: string; // Double-blind, Open-label
duration: string; // 研究周期
};
// 用药方案
regimen?: Array<{
drug: string; // 药物名称
dose: string; // 剂量
freq: string; // 频率
route: string; // 给药途径
}>;
// 安全性数据
safety?: {
ae_all: string[]; // 所有不良反应
ae_grade34: string[]; // 3-4级不良反应
dropout_rate: string; // 脱落率
};
// 入排标准
criteria?: {
inclusion: string[]; // 入选标准
exclusion: string[]; // 排除标准
};
// 观察指标
endpoints?: {
primary: string[]; // 主要终点
secondary: string[]; // 次要终点
results: Record<string, any>; // 结果数据
};
}
```
### 8.2 典型病历 (case)
```typescript
interface CaseData {
// 诊断信息
diagnosis?: {
primary: string; // 主诊断
secondary: string[]; // 合并诊断
staging: string; // 分期
pathology: string; // 病理类型
biomarkers: Record<string, string>; // 生物标志物
};
// 治疗信息
treatment?: {
firstLine: string; // 一线治疗
secondLine: string; // 二线治疗
surgery: string; // 手术
radiation: string; // 放疗
response: string; // 疗效评价
duration: string; // 治疗周期
};
// 预后信息
prognosis?: {
pfs: string; // 无进展生存
os: string; // 总生存
status: string; // 当前状态
recurrence: string; // 复发情况
};
// 随访信息
followUp?: {
lastVisit: string; // 最后随访日期
nextPlan: string; // 下一步计划
notes: string; // 随访备注
};
}
```
### 8.3 教学考试 (exam)
```typescript
interface ExamData {
// 题目列表
questions?: Array<{
id: number;
type: 'single' | 'multiple' | 'essay'; // 题型
content: string; // 题目内容
options?: string[]; // 选项(选择题)
}>;
// 答案列表
answers?: Array<{
id: number;
answer: string; // 答案
explanation: string; // 解析
}>;
// 知识点
knowledgePoints?: string[];
// 难度
difficulty?: 'easy' | 'medium' | 'hard';
// 来源
source?: string; // 题目来源
}
```
### 8.4 药品资料 (drug)
```typescript
interface DrugData {
// 基础信息
genericName?: string; // 通用名
brandName?: string; // 商品名
manufacturer?: string; // 生产厂家
// 适应症
indication?: string[];
// 禁忌症
contraindication?: string[];
// 用法用量
dosage?: {
adult: string;
pediatric: string;
adjustment: string; // 剂量调整
};
// 药物相互作用
interaction?: string[];
// 警告
warnings?: string[];
// 不良反应
adverseReactions?: {
common: string[];
serious: string[];
};
}
```
### 8.5 临床指南 (guideline)
```typescript
interface GuidelineData {
// 推荐意见
recommendations?: Array<{
content: string; // 推荐内容
level: string; // 证据级别 (1A, 2B, etc.)
strength: string; // 推荐强度
}>;
// 适用人群
population?: string;
// 发布机构
organization?: string; // NCCN, CSCO, ESMO
// 版本
version?: string;
// 更新要点
updates?: string[];
}
```
### 8.6 个人笔记 (note)
```typescript
interface NoteData {
// 标签(与顶层 tags 字段可重复,这里可存更多)
tags?: string[];
// 分类
category?: string;
// 关联
relatedDocs?: string[]; // 关联文档 ID
// 提醒
reminder?: string; // 提醒日期
}
```
---
## 9. 使用示例
### 9.1 普通医生随手上传
```typescript
// 医生直接拖文件上传,什么都不填
await kbEngine.submitIngestTask({
kbId: 'kb-123',
file: pdfBuffer,
filename: '某篇论文.pdf',
// 其他字段都不填
});
// 系统自动完成:
// ✅ extractedText → 提取全文
// ✅ chunks → 切片
// ✅ embeddings → 向量化
// ✅ summary → AI 摘要(可选开启)
// ✅ contentType → AI 自动识别(可选开启)
// 结果RAG 检索完全可用!
```
### 9.2 医生想分类管理
```typescript
// 医生上传时选择分类、打标签
await kbEngine.submitIngestTask({
kbId: 'kb-123',
file: pdfBuffer,
filename: 'KEYNOTE-024.pdf',
contentType: 'literature',
tags: ['肺癌', '免疫治疗', 'RCT'],
category: '肿瘤科/肺癌',
});
// 结果:可以按分类、标签筛选
```
### 9.3 药企专业录入
```typescript
// 药企医学部专业人员完整填写
await kbEngine.submitIngestTask({
kbId: 'kb-456',
file: pdfBuffer,
filename: 'KEYNOTE-024.pdf',
contentType: 'literature',
tags: ['Pembrolizumab', 'NSCLC', 'Phase III'],
category: '临床研究/III期',
metadata: {
title: 'Pembrolizumab versus Chemotherapy...',
pmid: '27718847',
doi: '10.1056/NEJMoa1606774',
journal: 'NEJM',
authors: ['Reck M', 'Rodriguez-Abreu D'],
publishYear: 2016,
ifScore: 176.079,
docType: 'RCT',
},
options: { enableClinicalExtraction: true }
});
// AI 自动提取 structuredData
// {
// pico: { P: '晚期NSCLC, PD-L1≥50%', I: 'Pembrolizumab', ... },
// studyDesign: { design: 'Phase III RCT', sampleSize: 305 },
// ...
// }
```
### 9.4 典型病历录入
```typescript
await kbEngine.submitIngestTask({
kbId: 'kb-123',
file: docBuffer,
filename: '肺癌MDT病例.docx',
contentType: 'case',
tags: ['MDT', '肺癌', 'IVA期'],
category: '病例库/肺癌',
options: { enableClinicalExtraction: true }
});
// AI 自动提取 structuredData
// {
// diagnosis: { primary: '非小细胞肺癌 IVA期', ... },
// treatment: { firstLine: '帕博利珠单抗 + 化疗', ... },
// ...
// }
```
---
## 10. 功能可用性矩阵
| 功能 | 只有 Layer 0-1 | + Layer 2 | + Layer 3 | + Layer 4 |
|------|----------------|-----------|-----------|-----------|
| **向量检索** | ✅ | ✅ | ✅ | ✅ |
| **关键词检索** | ✅ | ✅ | ✅ | ✅ |
| **混合检索** | ✅ | ✅ | ✅ | ✅ |
| **快速预览** | ❌ | ✅ 有摘要 | ✅ | ✅ |
| **分类筛选** | ❌ | ❌ | ✅ | ✅ |
| **标签筛选** | ❌ | ❌ | ✅ | ✅ |
| **年份筛选** | ❌ | ❌ | ❌ | ✅ |
| **IF 筛选** | ❌ | ❌ | ❌ | ✅ |
| **PICO 查询** | ❌ | ❌ | ❌ | ✅ |
---
## 11. 版本历史
| 版本 | 日期 | 变更内容 |
|------|------|----------|
| v1.0 | 2026-01-20 | 初版:四层架构设计,支持多内容类型 |
| v2.0 | 2026-01-21 | 整合审查建议:增加 EkbKnowledgeBase 容器表、EkbDocument.fileHash 字段、EkbChunk.metadata 字段 |

View File

@@ -0,0 +1,559 @@
# RAG 引擎使用指南
> **文档版本**: v1.0
> **最后更新**: 2026-01-21
> **状态**: ✅ 生产就绪
> **目标读者**: 业务模块开发者PKB、AIA、ASL 等)
---
## 📋 快速开始
### 5 秒上手
```typescript
import { getVectorSearchService } from '@/common/rag';
const searchService = getVectorSearchService(prisma);
// 单查询检索
const results = await searchService.vectorSearch('银杏叶对老年痴呆的效果', {
topK: 10,
filter: { kbId: 'your-kb-id' }
});
// 业务层生成多查询后检索(推荐)
const queries = ['银杏叶副作用', 'Ginkgo side effects'];
const results = await searchService.searchWithQueries(queries, {
topK: 10,
filter: { kbId: 'your-kb-id' }
});
```
---
## 🏗️ 架构设计
### 核心原则:"Brain-Hand" 模型
```
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (The Brain) - 你的代码 │
│ PKB / AIA / ASL │
│ │
│ 职责:思考 "怎么搜?" │
│ • 理解用户意图(上下文、历史) │
│ • 调用 DeepSeek V3 生成查询词 │
│ • 决定检索策略(单语/双语/多查询) │
│ │
│ 示例: │
│ const queries = await generateQueries(userInput, context);│
│ // ["K药副作用", "Keytruda AE", "Pembrolizumab"] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 引擎层 (The Hand) - RAG 引擎 │
│ VectorSearchService │
│ │
│ 职责:执行检索(无上下文) │
│ • 向量检索 (text-embedding-v4 1024维) │
│ • 关键词检索 (pg_bigm) │
│ • RRF 融合 │
│ • Rerank (qwen3-rerank) │
│ │
│ ✅ 不调用 LLM 理解意图 │
│ ✅ 只执行检索指令 │
└─────────────────────────────────────────────────────────────┘
```
---
## 📦 核心组件
### 1. EmbeddingService - 文本向量化
```typescript
import { getEmbeddingService } from '@/common/rag';
const embeddingService = getEmbeddingService();
// 单文本
const { embedding, tokenCount } = await embeddingService.embed(text);
// 批量自动分批每批10条
const { embeddings, totalTokens } = await embeddingService.embedBatch(texts);
```
**配置:**
```bash
# .env
TEXT_EMBEDDING_MODEL=text-embedding-v4
TEXT_EMBEDDING_DIMENSIONS=1024 # 推荐1024性能平衡
```
### 2. ChunkService - 文本分块
```typescript
import { getChunkService } from '@/common/rag';
const chunkService = getChunkService();
// Markdown 智能分块(保留标题层级)
const { chunks } = chunkService.chunkMarkdown(markdown);
// 普通文本分块
const { chunks } = chunkService.chunk(text);
```
**默认配置:**
- 最大块大小1000 字符
- 块间重叠200 字符
### 3. VectorSearchService - 检索服务
```typescript
import { getVectorSearchService } from '@/common/rag';
const searchService = getVectorSearchService(prisma);
// 方法 1: 单查询检索(简单场景)
const results = await searchService.vectorSearch(query, options);
// 方法 2: 多查询检索(推荐,业务层生成查询词)
const queries = ['Query1', 'Query2', 'Query3'];
const results = await searchService.searchWithQueries(queries, options);
// 方法 3: 混合检索(向量 + 关键词)
const results = await searchService.hybridSearch(query, options);
// 方法 4: Rerank 重排序
const reranked = await searchService.rerank(query, results, { topK: 5 });
```
### 4. DocumentIngestService - 文档入库
```typescript
import { getDocumentIngestService } from '@/common/rag';
const ingestService = getDocumentIngestService(prisma);
const result = await ingestService.ingestDocument(
{
filename: 'paper.pdf',
fileBuffer: pdfBuffer,
},
{
kbId: 'your-kb-id',
contentType: 'LITERATURE',
tags: ['医学', 'RCT'],
}
);
```
### 5. QueryRewriter - 查询理解(业务层使用)
```typescript
import { QueryRewriter } from '@/common/rag';
const rewriter = new QueryRewriter();
// 智能翻译 + 扩展
const result = await rewriter.rewrite('K药副作用');
// {
// original: "K药副作用",
// rewritten: ["Keytruda adverse events", "Pembrolizumab side effects"],
// isChinese: true,
// cost: 0.0001,
// duration: 1500
// }
```
---
## 🎯 使用场景
### 场景 1: PKB 个人知识库(中英混合)
```typescript
// modules/pkb/services/ragService.ts
async function searchKnowledgeBase(userId: string, kbId: string, userQuery: string) {
// ===== 业务层:查询理解 =====
const rewriter = new QueryRewriter();
const rewriteResult = await rewriter.rewrite(userQuery);
// 生成中英双语查询(适配混合知识库)
const queries = rewriteResult.isChinese
? [userQuery, ...rewriteResult.rewritten] // 中文 + 英文
: [userQuery];
logger.info(`PKB 检索策略: ${queries.length}条查询`, { queries });
// ===== 引擎层:执行检索 =====
const searchService = getVectorSearchService(prisma);
// 多查询向量检索
const results = await searchService.searchWithQueries(queries, {
topK: 20,
minScore: 0.2,
filter: { kbId },
});
// Rerank 精排
const finalResults = await searchService.rerank(userQuery, results, {
topK: 10,
});
return finalResults;
}
```
### 场景 2: AIA 智能问答(上下文理解)
```typescript
// modules/aia/services/chatService.ts
async function chat(userId: string, message: string, chatHistory: Message[]) {
// ===== 业务层:意图理解 =====
const llm = LLMFactory.getAdapter('deepseek-v3');
// 结合历史生成检索词
const prompt = `用户说:"${message}"
上下文:${chatHistory.slice(-3).map(m => m.content).join('\n')}
请生成2-3个精准的医学检索词中英文`;
const response = await llm.chat([{ role: 'user', content: prompt }]);
const queries = JSON.parse(response.content); // ["EGFR mutation", "表皮生长因子受体突变"]
// ===== 引擎层:执行检索 =====
const results = await searchService.searchWithQueries(queries, { topK: 5 });
// 基于检索结果生成回答
return generateAnswer(message, results);
}
```
### 场景 3: ASL 文献筛选PICO 拆解)
```typescript
// modules/asl/services/screeningService.ts
async function screenLiteratures(picoQuery: PICO) {
// ===== 业务层PICO 拆解 =====
const queries = [
`${picoQuery.P} ${picoQuery.I}`, // 人群 + 干预
`${picoQuery.I} efficacy`, // 干预 + 疗效
`${picoQuery.O} outcomes`, // 结局指标
];
logger.info('ASL PICO 检索', { pico: picoQuery, queries });
// ===== 引擎层:执行检索 =====
const results = await searchService.searchWithQueries(queries, {
topK: 50,
filter: { contentType: 'LITERATURE' },
});
return results;
}
```
---
## ⚠️ 注意事项
### 1. 查询理解必须在业务层
**❌ 错误:在引擎层调用 LLM**
```typescript
// VectorSearchService.ts (引擎层)
async vectorSearch(query) {
const translated = await deepseek.translate(query); // ❌ 引擎不应该做这个
// ...
}
```
**✅ 正确:在业务层调用 LLM**
```typescript
// PKB ragService.ts (业务层)
async searchKnowledgeBase(query) {
const queries = await deepseek.generateQueries(query, context); // ✅
const results = await engine.searchWithQueries(queries);
// ...
}
```
**原因:**
- 引擎没有上下文Chat History, PICO
- 引擎不知道知识库语言
- 引擎不理解业务场景
### 2. 中英双语查询策略
```typescript
// 检测到中文查询 + 可能有英文文档
if (containsChinese(userQuery)) {
const rewriter = new QueryRewriter();
const result = await rewriter.rewrite(userQuery);
// 生成中英双语查询词
const queries = [
userQuery, // 保留中文(匹配中文文档)
...result.rewritten, // 添加英文(匹配英文文档)
];
// 中文库和英文库都能匹配!
return await engine.searchWithQueries(queries);
}
```
### 3. pg_bigm 关键词检索需要翻译
```typescript
// 关键词检索必须用英文(如果文档是英文)
const keywordQuery = rewriteResult.rewritten[0]; // 使用翻译后的英文
const keywordResults = await searchService.keywordSearch(keywordQuery);
// 为什么?
// pg_bigm 是字符匹配,"肺癌" 匹配不到 "Lung Cancer"
// 只有 "Lung" 能匹配到 "Lung Cancer"
```
### 4. 性能优化
```typescript
// 批量向量化自动分批10条/批)
const embeddings = await embeddingService.embedBatch(chunks); // 自动处理
// 多查询并行检索
const results = await searchService.searchWithQueries([q1, q2, q3]); // 并行执行
// Rerank 只对候选集
const candidates = await searchService.vectorSearch(query, { topK: 20 });
const final = await searchService.rerank(query, candidates, { topK: 5 });
```
---
## 🔧 环境变量配置
```bash
# .env
# ===== 阿里云 API Key必填=====
DASHSCOPE_API_KEY=sk-xxx
# ===== 文本向量模型 =====
TEXT_EMBEDDING_MODEL=text-embedding-v4
TEXT_EMBEDDING_DIMENSIONS=1024 # 推荐 1024平衡
# ===== Rerank 模型 =====
RERANK_MODEL=qwen3-rerank
# ===== DeepSeek V3查询理解=====
DEEPSEEK_API_KEY=sk-xxx # 业务层使用
# ===== Python 微服务 =====
EXTRACTION_SERVICE_URL=http://localhost:8000
# ===== PKB RAG 后端 =====
PKB_RAG_BACKEND=pgvector # pgvector | dify | hybrid
```
---
## 📊 性能指标
| 操作 | 耗时 | 成本 |
|------|------|------|
| PDF → Markdown (Python) | 3-5秒 | ¥0 |
| 文本分块 (72块) | <10ms | ¥0 |
| 批量向量化 (72块) | 5秒 | ¥0.009 |
| 单次向量检索 | 50ms | ¥0 |
| DeepSeek V3 查询重写 | 1-2秒 | ¥0.0001 |
| qwen3-rerank (10候选) | 150ms | ¥0.002 |
| **完整链路** | **2.5秒** | **¥0.0025** |
---
## 🎯 检索效果
### 跨语言检索(中文查英文)
| 方案 | Top 1 准确率 | 相似度 |
|------|-------------|--------|
| 纯向量v4 1024维 | 中 | 0.56 |
| + DeepSeek V3 查询重写 | 高 | 1.00 |
| + 混合检索 + Rerank | **最高** | **0.77** |
### 同语言检索(中文查中文)
| 方案 | Top 1 准确率 | 相似度 |
|------|-------------|--------|
| 纯向量 | 高 | 0.70+ |
| + Rerank | **最高** | **0.85+** |
---
## 💡 最佳实践
### 1. 查询理解 Prompt 模板
```typescript
// 推荐:在 capability_schema.prompt_templates 中定义
const QUERY_REWRITE_PROMPT = `你是医学检索专家。
任务:
1. 如果是中文查询,翻译为英文医学术语
2. 生成 1-2 个同义扩展查询
3. 标准化俗称(如 "K药" → "Keytruda/Pembrolizumab"
输入:{query}
输出JSON数组格式
["Query1", "Query2", ...]
示例:
输入:"K药副作用"
输出:["Keytruda adverse events", "Pembrolizumab side effects"]`;
```
### 2. 完整检索链路(推荐)
```typescript
async function intelligentSearch(userQuery: string, kbId: string, context?: any) {
// Step 1: 查询理解(业务层 DeepSeek V3
const queries = await generateSearchQueries(userQuery, context);
// Step 2: 混合检索(引擎层)
const candidates = await Promise.all([
searchService.searchWithQueries(queries, { topK: 20, filter: { kbId } }),
searchService.keywordSearch(queries[queries.length - 1], { topK: 20, filter: { kbId } }),
]);
// Step 3: RRF 融合
const merged = rrfFusion(candidates.flat(), 10);
// Step 4: Rerank 精排
const final = await searchService.rerank(userQuery, merged, { topK: 5 });
return final;
}
```
### 3. 知识库语言感知
```typescript
// 建议:在知识库创建时检测语言
async function createKnowledgeBase(name: string) {
const kb = await prisma.ekbKnowledgeBase.create({
data: {
name,
config: {
primaryLanguage: 'mixed', // 'zh' | 'en' | 'mixed'
}
}
});
return kb;
}
// 检索时根据语言优化
if (kb.config.primaryLanguage === 'zh') {
// 纯中文库:不翻译
queries = [userQuery];
} else if (kb.config.primaryLanguage === 'en') {
// 纯英文库:翻译
queries = [...rewritten];
} else {
// 混合库:双语查询
queries = [userQuery, ...rewritten];
}
```
---
## 🐛 常见问题
### Q1: 中文查询返回 0 结果?
**原因**
- 文档是英文,查询是中文
- minScore 阈值太高(跨语言相似度通常 0.2-0.35
**解决**
```typescript
// 方案 1: 降低阈值
minScore: 0.2 // 跨语言场景
// 方案 2: 使用 DeepSeek V3 查询重写(推荐)
const queries = await rewriter.rewrite(userQuery);
```
### Q2: 关键词检索返回 0 结果?
**原因**
- 中文查询匹配不到英文文档
- pg_bigm 是字符匹配,不是语义匹配
**解决**
```typescript
// 必须用翻译后的查询
const keywordQuery = rewritten[0]; // 英文
await searchService.keywordSearch(keywordQuery);
```
### Q3: pg_bigm 未安装怎么办?
**当前状态**
- MVP 阶段使用 ILIKE 临时替代
- Phase 2 会安装 pg_bigm
**临时方案**
```typescript
// 当前 keywordSearch 使用 Prisma 的 contains
// 效果:可用,但性能不如 pg_bigm
```
---
## 📚 相关文档
- [04-数据模型设计.md](./04-数据模型设计.md) - 数据库 Schema
- [03-分阶段实施方案.md](./03-分阶段实施方案.md) - 开发计划
- [08-技术方案-跨语言检索优化.md](../../08-项目管理/08-技术方案-跨语言检索优化.md) - 跨语言优化
- [01-知识库引擎架构设计.md](../../09-架构实施/01-知识库引擎架构设计.md) - 架构原则
---
## 🚀 快速测试
```bash
cd backend
# 测试 1: 向量化服务
npx tsx src/tests/test-embedding-service.ts
# 测试 2: 端到端(文档入库+检索)
npx tsx src/tests/test-rag-e2e.ts
# 测试 3: Rerank 效果
npx tsx src/tests/test-rerank.ts
# 测试 4: 查询重写(需要 DEEPSEEK_API_KEY
npx tsx src/tests/test-query-rewrite.ts
# 测试 5: PDF 入库
npx tsx src/tests/test-pdf-ingest.ts <pdf文件路径>
```
---
## 📅 版本历史
| 版本 | 日期 | 变更内容 |
|------|------|----------|
| v1.0 | 2026-01-21 | 初版:基于 "Brain-Hand" 架构重构完成 |

View File

@@ -0,0 +1,86 @@
# **知识库引擎数据模型 (v2.0) 最终审查报告**
审查对象: \[04-数据模型设计.md\] (v2.0 版本)
审查结论: ✅ 通过 (Approved)
评价等级: 🌟 优秀 (Excellent)
适用阶段: MVP \-\> 长期生产环境
## **1\. 核心问题的回答**
### **Q1: 还有什么问题吗?**
基本没有逻辑硬伤。 现在的设计非常稳健。
唯一需要注意的是 Prisma 与 pgvector 的磨合细节(我在下文的"实施避坑指南"中列出了)。
### **Q2: 是否过度设计?是否浪费?**
**绝对没有。**
* 空间浪费?零。
PostgreSQL 的 NULL JSONB 字段存储开销极小(仅几个比特的位图标记)。如果你不填 structuredData它就不占空间。
* 开发浪费?负数。
相比于“先写死,以后改表结构”,现在的设计让你在未来 1 年内都不需要执行 prisma migrate 修改数据库结构。这反而节省了大量的运维和迁移成本。
* 计算浪费?零。
Layer 0 (纯文本 RAG) 是必经之路。你没有做任何多余的计算步骤,只是预留了存放未来 AI 提取结果的“容器”。
## **2\. 亮点解析 (为什么这是一个好设计)**
### **🏆 亮点 1Layer 0 的“保底”哲学**
你明确了 "即使没有任何结构化数据RAG 检索也必须能工作"。
这对于创业公司至关重要。这意味着我们可以先把文件扔进去Layer 0系统就能跑起来。等以后有空了或者有钱调用更贵的 LLM 了,再回过头来“清洗”出 Layer 4 的数据,而不需要重新上传文件。
### **🏆 亮点 2fileHash 的精准切入**
在 EkbDocument 中加入 fileHash 是神来之笔。
* **场景**:用户上传了一篇《肺癌指南.pdf》过了一周又上传了一次。
* **优势**:系统直接检测 Hash发现已存在瞬间秒传完成。既提升了用户体验又节省了昂贵的 OCR 解析费和 Embedding 费用。
### **🏆 亮点 3切片级 Metadata (EkbChunk.metadata)**
这是很多成熟 RAG 系统后期才会加的功能,你在设计阶段就想到了。
* **场景**:搜“帕博利珠单抗的副作用”。
* **优势**:如果没有切片级元数据,可能搜到的是“副作用”这个词在目录里的切片。有了元数据,我们可以给正文切片加权,降权目录切片。
## **3\. 实施避坑指南 (关键!)**
虽然设计很完美,但在写代码落地时,这 3 个细节请务必注意:
### **🔧 细节 1Prisma 不会自动创建向量索引**
schema.prisma 里写了 Unsupported("vector(1024)"),但 npx prisma migrate **不会**自动帮你创建 HNSW 索引。
必须做的动作:
在执行完 migrate 后,必须手动运行以下 SQL或者创建一个空的 migration 专门放 SQL
\-- 必须手动执行!否则检索速度会很慢
CREATE INDEX idx\_ekb\_chunk\_embedding ON "ekb\_schema"."EkbChunk"
USING hnsw (embedding vector\_cosine\_ops)
WITH (m \= 16, ef\_construction \= 64);
### **🔧 细节 2fileHash 的作用域选择**
代码示例里写的是 where: { kbId, fileHash }。
这意味着:同一个知识库内去重。
* **如果用户 A 上传了文件 X用户 B 也上传了文件 X系统会存两份。**
* **建议**:对于 2 人团队,暂时维持这个逻辑是安全的(避免跨用户的数据泄露风险)。未来如果存储成本飙升,可以改为全局去重(但需要复杂的文件引用计数逻辑)。**目前保持现状即可。**
### **🔧 细节 3JSONB 的查询性能**
虽然加了 GIN 索引,但查询深层 JSON 字段(如 structuredData-\>'pico'-\>\>'I')的语法比较繁琐。
建议:在 KnowledgeBaseEngine 中封装好常用的查询构建器,不要让业务层直接拼 SQL。
## **4\. MVP 开发优先级建议**
基于这份设计,你的 MVP 开发清单应该非常清晰:
1. **Day 1**: 创建数据库表 (prisma migrate) \+ 手动创建 HNSW 索引。
2. **Day 2**: 实现 ingestDocument 接口。
* 只实现 Layer 0 (提取文本) \+ Layer 1 (基础信息)。
* structuredData 暂时存 null。
3. **Day 3**: 实现 vectorSearch 接口。
* 暂不处理 JSONB 过滤,先跑通纯向量检索。
**结论:** 这是一个**可进可退、丰俭由人**的优秀架构。请立即冻结设计,开始编码!🚀

View File

@@ -27,12 +27,13 @@
### 基础能力清单
- 📄 **文档入库** - 文档解析 → 切片 → 向量化 → 存储
- 📄 **文档入库** - ⚡️ 异步入库pg-boss返回 taskId 轮询状态
- 📝 **全文获取** - 单文档/批量获取文档全文
- 📋 **摘要获取** - 单文档/批量获取文档摘要
- 📋 **摘要获取** - 单文档/批量获取文档摘要(💰 可选生成)
- 🔍 **向量检索** - 基于 pgvector 的语义检索
- 🔤 **关键词检索** - 基于 PostgreSQL FTS
- 🔤 **关键词检索** - 基于 pg_bigm 的中文精确检索
- 🔀 **混合检索** - 向量 + 关键词 + RRF 融合
- 🎯 **重排序** - 🆕 基于 Qwen-Rerank 的精排序
> ⚠️ **注意**:不提供 `chat()` 方法!问答策略由业务模块根据场景决定。
@@ -98,25 +99,32 @@
## 💡 基础能力使用
### 1. 文档入库
### 1. 文档入库(⚡️ 异步)
```typescript
import { KnowledgeBaseEngine } from '@/common/rag';
const kbEngine = new KnowledgeBaseEngine(prisma);
await kbEngine.ingestDocument({
// 提交入库任务(立即返回)
const { taskId, documentId } = await kbEngine.submitIngestTask({
kbId: 'kb-123',
userId: 'user-456',
file: pdfBuffer,
filename: 'research.pdf',
options: {
generateSummary: true, // 生成摘要
extractClinicalData: true, // 提取 PICO 等临床数据
enableSummary: true, // 💰 可选,默认 false
enableClinicalExtraction: true // 💰 可选,默认 false
}
});
// 轮询任务状态
const status = await kbEngine.getIngestStatus(taskId);
// { status: 'processing', progress: 45 }
```
> 详见:[Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md)
### 2. 全文/摘要获取
```typescript
@@ -136,11 +144,14 @@ const summaries = await kbEngine.getAllDocumentsSummaries(kbId);
// 向量检索
const vectorResults = await kbEngine.vectorSearch(kbIds, query, 20);
// 关键词检索
// 关键词检索pg_bigm 中文精确匹配)
const keywordResults = await kbEngine.keywordSearch(kbIds, query, 20);
// 混合检索(向量 + 关键词 + RRF
const hybridResults = await kbEngine.hybridSearch(kbIds, query, 10);
// 🆕 重排序
const reranked = await kbEngine.rerank(hybridResults, query, 5);
```
---
@@ -239,9 +250,9 @@ backend/src/common/rag/
```typescript
class KnowledgeBaseEngine {
// ========== 文档入库 ==========
ingestDocument(params: IngestParams): Promise<IngestResult>;
ingestBatch(documents: IngestParams[]): Promise<IngestResult[]>;
// ========== 文档入库(⚡️ 异步) ==========
submitIngestTask(params: IngestParams): Promise<{ taskId: string; documentId: string }>;
getIngestStatus(taskId: string): Promise<{ status, progress, error? }>;
// ========== 内容获取 ==========
getDocumentFullText(documentId: string): Promise<DocumentText>;
@@ -251,8 +262,9 @@ class KnowledgeBaseEngine {
// ========== 检索能力 ==========
vectorSearch(kbIds: string[], query: string, topK?: number): Promise<SearchResult[]>;
keywordSearch(kbIds: string[], query: string, topK?: number): Promise<SearchResult[]>;
keywordSearch(kbIds: string[], query: string, topK?: number): Promise<SearchResult[]>; // pg_bigm
hybridSearch(kbIds: string[], query: string, topK?: number): Promise<SearchResult[]>;
rerank(docs: SearchResult[], query: string, topK?: number): Promise<SearchResult[]>; // 🆕
// ========== 管理操作 ==========
deleteDocument(documentId: string): Promise<void>;
@@ -279,27 +291,43 @@ class KnowledgeBaseEngine {
## 📅 开发计划
详见:[02-pgvector替换Dify计划.md](./02-pgvector替换Dify计划.md)
### 分阶段实施(推荐)
| 里程碑 | 内容 | 工期 | 状态 |
|--------|------|------|------|
| **M1** | 数据库设计 + 核心服务 | 5 天 | 🔜 待开始 |
| **M2** | PKB 模块接入 + 测试 | 3 天 | 📋 规划中 |
| **M3** | 数据迁移 + 上线 | 2 天 | 📋 规划中 |
详见:[03-分阶段实施方案.md](./03-分阶段实施方案.md)
| 阶段 | 内容 | 工期 | 状态 |
|------|------|------|------|
| **Phase 1 MVP** | 入库 + 向量检索 + 全文获取 | 3 天 | 🔜 待开始 |
| **Phase 2 增强** | + 关键词检索 + 混合检索 + rerank | 2 天 | 📋 规划中 |
| **Phase 3 完整** | + 异步入库 + 摘要 + PICO | 3 天 | 📋 规划中 |
### 技术实现参考
详见:[02-pgvector替换Dify计划.md](./02-pgvector替换Dify计划.md)
---
## 📂 相关文档
- [知识库引擎架构设计](./01-知识库引擎架构设计.md)
- [pgvector 替换 Dify 开发计划](./02-pgvector替换Dify计划.md)
- [知识库引擎架构设计](./01-知识库引擎架构设计.md) - 完整架构目标
- [pgvector 替换 Dify 技术方案](./02-pgvector替换Dify计划.md) - 详细技术实现
- [分阶段实施方案](./03-分阶段实施方案.md) - 🆕 MVP → 增强 → 完整
- [文档处理引擎](../02-文档处理引擎/01-文档处理引擎设计方案.md)
- [Postgres-Only异步任务处理指南](../Postgres-Only异步任务处理指南.md)
- [通用能力层清单](../00-通用能力层清单.md)
---
## 📅 更新日志
### 2026-01-20 v1.2 架构审核优化
- ⚡️ **入库异步化**`submitIngestTask()` + `getIngestStatus()`,基于 pg-boss
- 💰 **成本控制**:摘要/PICO 提取默认关闭,按需开启
- 🔧 **中文检索**`tsvector``pg_bigm`,专为 CJK 文字优化
- 🆕 **新增能力**`rerank()` 重排序Qwen-Rerank API
- 📋 **分阶段实施**:新增 MVP → 增强 → 完整 三阶段方案
### 2026-01-20 v1.1 设计原则重大更新
-**核心原则**:提供基础能力(乐高积木),不做策略选择

View File

@@ -0,0 +1,267 @@
# **知识库引擎数据模型设计审查报告**
文档状态: ✅ 已确认 (v2.0 融合版)
审查日期: 2026-01-20
审查对象: \[04-数据模型设计.md\] & 架构师建议方案
适用架构: Postgres-Only (Prisma \+ pgvector \+ pg-boss)
目标读者: 开发团队、架构师
## **1\. 核心决策摘要 (Executive Summary)**
经过对业务需求文献、病历、多模态和团队现状2人开发、运维极简的深度评估我们做出了以下关键技术决策
### **✅ 决策 1采用 "Unified Schema"(统一模型)策略**
* **结论**:所有类型的知识库(个人文献库、系统规则库、病历库)**共用一套数据库表结构**。
* **理由**
* **运维成本**避免维护多套向量索引HNSW减少 2 人团队的数据库维护负担。
* **业务灵活性**:通过 contentType 和 JSONB 字段支持未知的新内容类型,无需频繁迁移数据库 (Migration)。
### **✅ 决策 2引入 "Container Table"(容器表)**
* **结论**:在文档表之上,必须增加 EkbKnowledgeBase 表。
* **理由**
* **权限隔离**:明确区分 "User KB" (个人私有) 和 "System KB" (全员共享)。
* **策略配置**:允许为不同库配置不同的切片大小 (ChunkSize) 或检索策略 (TopK)。
### **✅ 决策 3确立 "四层数据架构"**
* **结论**:采纳上传文档中的 Layer 0-4 分层设计。
* **价值**:确保系统的**高容错性** —— 即使 AI 提取结构化数据失败Layer 4基础的 RAG 检索Layer 0依然可用。
## **2\. 最终融合版架构图 (ER Diagram)**
我们融合了原设计的“四层架构”优势和建议方案的“容器管理”优势,形成了最终的 v2.0 模型。
erDiagram
%% 容器层(新增):管理权限和策略
Ekb\_KnowledgeBase {
string id PK
string type "USER | SYSTEM"
string owner\_id "userId | moduleId"
jsonb config "策略配置(chunkSize等)"
}
%% 文档层(四层架构):管理内容和元数据
Ekb\_Document {
string id PK
string kb\_id FK
string content\_type "LITERATURE | CASE | ..."
string extracted\_text "Layer 0: 全文"
jsonb metadata "Layer 4: 标准属性(DOI/作者)"
jsonb structured\_data "Layer 4: 业务属性(PICO/诊断)"
}
%% 切片层(检索核心):管理向量和索引
Ekb\_Chunk {
string id PK
string document\_id FK
vector embedding "1024维向量"
string content "切片文本"
jsonb metadata "切片级元数据(IsAnswer)"
}
Ekb\_KnowledgeBase ||--|{ Ekb\_Document : "contains"
Ekb\_Document ||--|{ Ekb\_Chunk : "split into"
**潜在风险(需要补充):**
1. **缺失 `EkbKnowledgeBase` 表**:目前的 Schema 中 `EkbDocument``kbId` 字段,但没有定义 `EkbKnowledgeBase` 表。
* *风险*:你之前提到“用户有 3 个库,系统有业务库”。如果没有这个表,你无法管理“谁拥有这个库”、“这是系统库还是用户库”、“这个库的 RAG 策略是什么”。
2. **`Chunk` 还可以更灵活**:目前的 `EkbChunk` 只有 `sectionType`
* *建议*:建议给 Chunk 也加上 `metadata` (JSONB)。比如做职业考试题时,你可能希望直接在 Chunk 层面标记 `{ "isAnswer": true }`,这样检索时可以加权。
场景模拟
场景 1ASL 模块搜文献(只搜文献库,不搜病历)
场景 2PKB 用户搜自己的知识库(只搜自己的,不搜别人的)
场景 3AIA 智能问答(既要查系统医学库,又要查用户上传的病历)
| 维度 | 我的设计 (上一版) | 你的设计 (上传版) | 🎯 建议融合方案 |
| :---- | :---- | :---- | :---- |
| **知识库管理** | **有 EkbKnowledgeBase 表**。 明确区分 USER vs SYSTEM 类型。 | **无**。 仅在 Document 中引用 kbId。 | **采纳我的**。 必须有这张表来管理权限和配置。 |
| **文档元数据** | 单个 metadata JSONB 字段。 简单,但混杂。 | **分离设计**。 metadata (标准) \+ structuredData (业务)。 | **采纳你的**。 分离设计对医学场景更友好,利于 TS 类型定义。 |
| **全文存储** | 隐含在 Chunk 中。 | **显式存储 extractedText**。 存放在 Document 表中。 | **采纳你的**。 保留全文很有必要,万一以后要换切片策略(比如 512 改为 1024不用重新解析文件。 |
| **切片属性** | metadata JSONB。 | sectionType 枚举 \+ pageNumber。 | **融合**。 保留 sectionType但增加 metadata JSONB 以备不时之需。 |
## **3\. 详细数据模型定义 (Prisma Schema)**
**开发者注意**:这是最终落地的 Schema请直接复制到 prisma/schema.prisma 的 ekb\_schema 部分。
### **3.1 知识库容器 (EkbKnowledgeBase)**
**解决痛点**:解决了“怎么区分这是用户的库还是系统的库”的问题。
model EkbKnowledgeBase {
id String @id @default(uuid())
name String
description String?
// 核心隔离字段
// USER: 用户创建的ownerId \= userId受配额限制 (如最多3个)
// SYSTEM: 系统预置的ownerId \= moduleId (如 "AIA", "IIT"),全员可读
type KbType @default(USER)
ownerId String
// 策略配置 (JSONB)
// 允许未来针对特定库进行优化,例如:
// { "chunkSize": 1024, "enableRerank": true, "embeddingModel": "v3" }
config Json? @db.JsonB
documents EkbDocument\[\]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index(\[ownerId\])
@@index(\[type\])
@@schema("ekb\_schema")
}
enum KbType {
USER
SYSTEM
}
### **3.2 通用文档对象 (EkbDocument)**
**解决痛点**:解决了“医学数据类型极其复杂”的问题。采用**双 JSONB 策略**。
model EkbDocument {
id String @id @default(uuid())
// 归属关系
kbId String
knowledgeBase EkbKnowledgeBase @relation(fields: \[kbId\], references: \[id\], onDelete: Cascade)
userId String // 冗余存储上传者,方便快速查询
// \===== Layer 1: 基础文件信息 \=====
filename String
fileType String // pdf, docx, md, txt
fileSizeBytes BigInt
fileHash String // 用于实现“秒传”和去重
fileUrl String // OSS 路径
status DocStatus @default(PENDING) // 状态机PENDING \-\> PROCESSING \-\> COMPLETED
errorMessage String? @db.Text
// \===== Layer 0: RAG 核心层 \=====
// 必须保留全文,以便未来更改切片策略时,无需重新解析原始文件
extractedText String? @db.Text
// \===== Layer 2: 内容增强层 \=====
summary String? @db.Text
tokenCount Int?
pageCount Int?
// \===== Layer 3: 分类标签层 \=====
contentType String? // 枚举字符串literature, case, exam, drug
tags String\[\]
category String?
// \===== Layer 4: 结构化数据层 (核心设计) \=====
// 1\. 通用元数据:所有文献都有的 (DOI, Journal, Year, Author)
metadata Json? @db.JsonB
// 2\. 业务结构化数据:特定类型特有的 (PICO, Diagnosis, Dosage, ExamAnswer)
// 使用 JSONB \+ GIN 索引,实现 NoSQL 般的灵活性
structuredData Json? @db.JsonB
chunks EkbChunk\[\]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index(\[kbId\])
@@index(\[contentType\])
// GIN 索引:支持对 metadata 和 structuredData 内部字段的高速查询
@@index(\[metadata\], type: Gin)
@@index(\[structuredData\], type: Gin)
@@schema("ekb\_schema")
}
enum DocStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
### **3.3 向量切片 (EkbChunk)**
**解决痛点**:解决了“如何让检索更精准”的问题。
model EkbChunk {
id String @id @default(uuid())
documentId String
document EkbDocument @relation(fields: \[documentId\], references: \[id\], onDelete: Cascade)
// 核心内容
content String @db.Text
chunkIndex Int
// 向量数据 (PostgreSQL pgvector)
// 注意:需要手动执行 SQL 创建 HNSW 索引
embedding Unsupported("vector(1024)")?
// 溯源信息
pageNumber Int?
sectionType String? // abstract, methods, results...
// 扩展元数据 (新增)
// 允许在切片粒度做标记。例如:考试题的答案切片标记 { "isAnswer": true }
// 检索时可降低纯答案切片的权重,优先展示问题
metadata Json? @db.JsonB
@@index(\[documentId\])
@@schema("ekb\_schema")
}
## **4\. 关键设计问答 (Q\&A)**
### **Q1: 为什么要把 metadata 和 structuredData 分开?**
* **清晰度**metadata 存放通用的、标准化的属性如标题、作者、时间这是所有文档共有的structuredData 存放业务特定的深层数据(如 PICO、诊断结果
* **前端适配**:前端组件可以统一渲染 metadata而针对 structuredData 则根据 contentType 动态加载不同的展示组件。
### **Q2: 为什么要保留 extractedText (全文)?只存 Chunk 不行吗?**
* **未来兼容性**:如果我们下个月发现 chunkSize=512 效果不好,想改成 1024。如果有 extractedText我们可以直接在数据库里重新切片 (Re-chunking);如果没有,我们就得去 OSS 重新下载文件并解析,成本巨大。
### **Q3: 为什么 Chunk 也要加 metadata**
* **精准检索**:在“职业考试”场景中,一个题目可能被切成“题干”和“解析”两段。我们希望用户搜问题时,优先匹配“题干”。如果在 Chunk 上标记 { "type": "question" },检索算法就可以加权。
## **5\. 业务场景演练**
### **场景 APKB 个人文献库**
* **用户操作**:上传一篇 PDF。
* **数据流**
1. 创建 EkbDocument (kbId=用户的个人库ID)。
2. status \= PROCESSING。
3. 后台解析 \-\> 填充 extractedText。
4. 后台切片 \-\> 插入 EkbChunk。
5. status \= COMPLETED。
### **场景 BASL 智能文献筛选**
* **系统操作**:批量导入 1000 篇文献,并进行 PICO 提取。
* **数据流**
1. 创建 EkbDocument (kbId=ASL临时库ID, contentType='literature')。
2. 调用 LLM 提取 PICO。
3. 更新 structuredData \= { "pico": { "P": "...", "I": "..." } }。
4. **查询时**SELECT \* FROM EkbDocument WHERE structuredData-\>'pico'-\>\>'I' LIKE '%Aspirin%' (利用 GIN 索引加速)。
## **6\. 下一步行动 (Action Plan)**
1. **数据库迁移**
* 更新 prisma/schema.prisma。
* 运行 npx prisma migrate dev \--name init\_ekb\_v2。
2. **索引创建 (SQL)**
* 手动执行 HNSW 索引创建语句Prisma 暂不支持自动生成 vector 索引)。
* 手动执行 pg\_bigm 索引创建语句。
3. **Service 层更新**
* 更新 KnowledgeBaseEngine在入库时必须要求传入 kbId。
* 在检索时,增加对 structuredData 的 JSON 过滤支持。