init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn) - RAG 引擎(混合检索 + 云端 Reranker + 引用溯源) - 文档解析(MinerU + 多格式支持) - Docker 生产部署配置 - 排除前端项目、敏感配置、模型文件
This commit is contained in:
448
docs/RAG数据流程完整分析.md
Normal file
448
docs/RAG数据流程完整分析.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# RAG 数据流程完整分析
|
||||
|
||||
> 本文档详细分析 RAG 系统从文档上传到回答生成的完整数据流程,
|
||||
> 帮助理解各模块职责和定位问题。
|
||||
|
||||
---
|
||||
|
||||
## 一、整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 用户查询 │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ api/chat_routes.py - generate() │
|
||||
│ ├── 意图分析 (intent_analyzer.py) - 问题改写 + 是否需要检索 │
|
||||
│ ├── 混合检索 (search_hybrid → engine.search_knowledge) │
|
||||
│ ├── 图片选择 (select_images) - 打分排序 │
|
||||
│ └── LLM 生成回答 │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、文档入库流程
|
||||
|
||||
### 2.1 解析层 (parsers/mineru_parser.py)
|
||||
|
||||
**输入**:PDF/Word/Excel 文件
|
||||
|
||||
**核心函数**:`parse_with_mineru()`
|
||||
|
||||
**处理流程**:
|
||||
1. MinerU 解析文档 → 输出 `content_list.json` + 图片文件
|
||||
2. 遍历 content_list,按类型处理:
|
||||
|
||||
| item_type | 处理方式 | MinerUChunk 字段 |
|
||||
|-----------|----------|------------------|
|
||||
| `text` | 文本块 | content, section_path, text_level |
|
||||
| `table` | 表格 | content, table_html, image_path(表格图片) |
|
||||
| `image` | 图片 | content(=caption), image_path, context_before/after |
|
||||
| `chart` | 图表 | content(=caption), image_path, context_before/after |
|
||||
|
||||
**图片上下文提取** (第 366-384 行):
|
||||
```python
|
||||
def get_context_for_image(image_idx: int, page_idx: int, window: int = 3) -> tuple:
|
||||
"""获取图片前后的文本上下文"""
|
||||
context_before = []
|
||||
context_after = []
|
||||
|
||||
# 查找图片前后的文本项
|
||||
for item_idx, text, item_page in text_items:
|
||||
if item_idx < image_idx and item_page >= page_idx - 1:
|
||||
context_before.append(text) # 图片之前的文本
|
||||
elif item_idx > image_idx and item_page <= page_idx + 1:
|
||||
context_after.append(text) # 图片之后的文本
|
||||
|
||||
# 只保留最近的 window 条
|
||||
return " ".join(context_before[-window:]), " ".join(context_after[:window])
|
||||
```
|
||||
|
||||
**输出**:`MinerUChunk` 列表
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MinerUChunk:
|
||||
content: str # 文本内容(图片类型通常是 caption 或默认值)
|
||||
chunk_type: str # 类型: text, table, image, chart
|
||||
page_start: int = 1 # 起始页码
|
||||
page_end: int = 1 # 结束页码
|
||||
text_level: int = 0 # 标题级别 (0=body, 1=h1, 2=h2...)
|
||||
title: str = "" # 标题文本
|
||||
section_path: str = "" # 章节路径
|
||||
bbox: Optional[List[float]] = None # 边界框
|
||||
source_file: str = "" # 源文件名
|
||||
table_html: Optional[str] = None # 表格 HTML
|
||||
image_path: Optional[str] = None # 图片路径
|
||||
images: Optional[List[Dict]] = None # 关联图片列表
|
||||
context_before: str = "" # 图片前的文本上下文
|
||||
context_after: str = "" # 图片后的文本上下文
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 入库层 (knowledge/manager.py)
|
||||
|
||||
**核心函数**:`add_file_to_kb()`
|
||||
|
||||
**处理流程**:
|
||||
|
||||
```
|
||||
MinerUChunk 列表
|
||||
│
|
||||
├── 文本块 ──────────────────────► 文本切片入库
|
||||
│ ├── 计算 embedding
|
||||
│ └── collection.add(id, embedding, document, metadata)
|
||||
│
|
||||
├── 表格块 ──────────────────────► 表格切片入库
|
||||
│ ├── 生成语义增强内容
|
||||
│ └── collection.add(...)
|
||||
│
|
||||
└── 图片块 ──────────────────────► 图片切片入库
|
||||
├── 检查 VLM 缓存(新增)
|
||||
├── 生成描述(VLM 或轻量描述)
|
||||
└── collection.add(...)
|
||||
```
|
||||
|
||||
**图片切片入库详细流程** (第 1296-1378 行):
|
||||
|
||||
```python
|
||||
# 1. 检查 VLM 缓存(优先使用)
|
||||
vlm_desc = self._get_vlm_cache(full_image_path)
|
||||
|
||||
if vlm_desc:
|
||||
# 使用 VLM 描述(语义更丰富)
|
||||
description = vlm_desc
|
||||
image_meta['has_vlm_desc'] = True
|
||||
else:
|
||||
# 生成轻量描述(包含上下文)
|
||||
description = self.generate_lightweight_image_description(...)
|
||||
|
||||
# 2. 计算 embedding
|
||||
vector = embedding_model.encode(description).tolist()
|
||||
|
||||
# 3. 存入向量库
|
||||
collection.add(
|
||||
ids=[chunk_id],
|
||||
embeddings=[vector],
|
||||
documents=[description],
|
||||
metadatas=[image_meta]
|
||||
)
|
||||
```
|
||||
|
||||
**generate_lightweight_image_description() 输出格式**:
|
||||
|
||||
```
|
||||
图表:图2.3,位于「... > 2.3发电」,第12页
|
||||
前文:受长江流域性严重枯水影响,2022 年三峡电站年度发电量为 787.90 亿千瓦时...
|
||||
后文:2.4航运 三峡船闸和葛洲坝船闸实行统一调度...
|
||||
```
|
||||
|
||||
**VLM 缓存描述格式**(更精准):
|
||||
|
||||
```
|
||||
图2.3 柱状图 主要内容描述:该柱状图展示了2003年至2022年每年的发电量(单位:亿千瓦时)。
|
||||
发电量在2003年为86.07亿千瓦时,随后逐年波动上升,至2020年达到峰值1118.02亿千瓦时...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 向量库存储结构
|
||||
|
||||
**ChromaDB 存储字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `ids` | str | 切片唯一标识,如 `三峡公报_1-15页.pdf_text_24` |
|
||||
| `embeddings` | List[float] | 768 维向量(bge-base-zh-v1.5) |
|
||||
| `documents` | str | 切片内容(用于 LLM 上下文和相似度计算) |
|
||||
| `metadatas` | dict | 元数据 |
|
||||
|
||||
**图片切片 metadata 字段**:
|
||||
|
||||
```python
|
||||
{
|
||||
'source': '三峡公报_1-15页.pdf',
|
||||
'page': 12,
|
||||
'chunk_type': 'chart', # image 或 chart
|
||||
'section': '综述 > 2.3发电',
|
||||
'caption': '图表', # 通常为默认值
|
||||
'figure_number': '2.3', # 从上下文提取的图号
|
||||
'image_path': 'ab77281e7913.jpg',
|
||||
'has_vlm_desc': True, # 是否有 VLM 描述
|
||||
'preview': '图2.3 柱状图 主要内容...' # 描述预览
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、检索流程
|
||||
|
||||
### 3.1 混合检索 (core/engine.py)
|
||||
|
||||
**核心函数**:`search_knowledge()`
|
||||
|
||||
**流程图**:
|
||||
|
||||
```
|
||||
用户查询
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. 查询缓存检查 │
|
||||
│ cache.get_query_result(query, kb_name) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│ 未命中
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. 向量检索 │
|
||||
│ query_vector = embedding_model.encode(query) │
|
||||
│ collection.query(query_embeddings=[query_vector], n_results=100) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. BM25 关键词检索(可选) │
|
||||
│ bm25_results = bm25_index.search(query, top_k=100) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. RRF 融合 │
|
||||
│ fused_results = reciprocal_rank_fusion([vector, bm25], weights) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. MMR 去重 │
|
||||
│ fused_results = _apply_mmr(query, fused_results, top_k=30) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Rerank 重排序 │
|
||||
│ rerank_results(query, fused_results, top_k=5) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 7. 返回结果 │
|
||||
│ {ids: [[...]], documents: [[...]], metadatas: [[...]], │
|
||||
│ distances: [[...]]} │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键参数**:
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `recall_k` | 100 | 召回候选数量 |
|
||||
| `top_k` | 5-20 | 最终返回数量 |
|
||||
| `VECTOR_WEIGHT` | 0.6 | 向量检索权重 |
|
||||
| `BM25_WEIGHT` | 0.4 | BM25 检索权重 |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 图片选择 (api/chat_routes.py)
|
||||
|
||||
**核心函数**:`select_images()`
|
||||
|
||||
**流程**:
|
||||
|
||||
```
|
||||
检索结果 contexts
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. 提取图表引用 │
|
||||
│ 从 top 5 文本块提取 "见图2.3"、"如表2.2" 等 │
|
||||
│ → referenced_figures = {'2.3': {来源文件}} │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. 遍历图片切片,打分 │
|
||||
│ for ctx in contexts: │
|
||||
│ if ctx['meta']['chunk_type'] in ('image', 'chart'): │
|
||||
│ score = score_image_relevance(query, meta, doc) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. 排序返回 top N │
|
||||
│ scored_images.sort(key=lambda x: x['score'], reverse=True) │
|
||||
│ return scored_images[:MAX_IMAGES] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**score_image_relevance() 打分逻辑**:
|
||||
|
||||
| 匹配项 | 加分 |
|
||||
|--------|------|
|
||||
| 图号精确匹配(查询中有"图2.3") | +10 分 |
|
||||
| 表号精确匹配 | +10 分 |
|
||||
| 关键词匹配("发电量"等) | +2 分/个 |
|
||||
| 字符重叠 | +0.2 分/字符 |
|
||||
| 章节匹配 | +1.5 分 |
|
||||
| 图片类型(chart > image) | +2 / +1 分 |
|
||||
| 向量相似度 | +2 分(最高) |
|
||||
|
||||
---
|
||||
|
||||
## 四、问题诊断
|
||||
|
||||
### 4.1 问题现象
|
||||
|
||||
用户查询"蓄水以来逐年发电量",期望返回图2.3,实际返回图2.5/表2.2。
|
||||
|
||||
### 4.2 诊断结果
|
||||
|
||||
| 排名 | 类型 | 内容 | Distance | 问题 |
|
||||
|------|------|------|----------|------|
|
||||
| 1 | text | "2.3发电"章节文本 | 0.9980 | ✅ 正确 |
|
||||
| 24 | image | 封面图片 | 0.0002 | ❌ 极低 |
|
||||
| N/A | chart | 图2.3 | 未进入 top 50 | ❌ 极低 |
|
||||
|
||||
### 4.3 根因分析
|
||||
|
||||
**问题 1:图片切片向量相似度极低**
|
||||
|
||||
- 图片的 `document` 是轻量描述格式
|
||||
- 关键词"发电量"出现在"前文"中,被大量上下文稀释
|
||||
- embedding 模型对这种格式的内容相似度计算不准确
|
||||
|
||||
**问题 2:VLM 缓存未被利用**
|
||||
|
||||
- 已有 VLM 缓存包含精准描述:"展示了2003年至2022年每年的发电量"
|
||||
- 但入库时未检查 VLM 缓存
|
||||
- 导致图片切片的语义表达不准确
|
||||
|
||||
**问题 3:文本切片覆盖图片语义**
|
||||
|
||||
- "2.3发电" 章节的文本切片包含完整描述
|
||||
- 文本切片排名靠前,但没有关联图片
|
||||
- 图片切片独立存在,无法通过文本切片找到
|
||||
|
||||
---
|
||||
|
||||
## 五、优化方案
|
||||
|
||||
### 5.1 P0:入库时使用 VLM 缓存(已实现)
|
||||
|
||||
**修改文件**:`knowledge/manager.py`
|
||||
|
||||
**方案**:
|
||||
```python
|
||||
# 优先使用 VLM 缓存
|
||||
vlm_desc = self._get_vlm_cache(full_image_path)
|
||||
|
||||
if vlm_desc:
|
||||
description = vlm_desc # 使用 VLM 描述
|
||||
image_meta['has_vlm_desc'] = True
|
||||
else:
|
||||
description = self.generate_lightweight_image_description(...) # 轻量描述
|
||||
|
||||
vector = embedding_model.encode(description).tolist()
|
||||
```
|
||||
|
||||
**验证结果**(2026-04-28):
|
||||
- 为图2.3 生成了 VLM 描述,包含关键词"发电量"、"柱状图"、"2003年至2022年每年"
|
||||
- 更新向量库后,查询"蓄水以来逐年发电量"时图2.3 排名第2(distance=0.3153)
|
||||
- 效果显著提升
|
||||
|
||||
### 5.2 P1:意图分析器优化(已实现)
|
||||
|
||||
**问题**:用户再次问相同问题时,意图分析器错误设置 `need_retrieval=False`,导致复用错误的上下文。
|
||||
|
||||
**修改文件**:`core/intent_analyzer.py`
|
||||
|
||||
**方案**:在 SYSTEM_PROMPT 中添加规则:
|
||||
- **当用户重复提问相同或相似问题时,必须设置 need_retrieval = true**
|
||||
- 原因:用户可能对之前的回答不满意,或之前的回答包含错误信息
|
||||
|
||||
### 5.3 P2:建立图文关联索引
|
||||
|
||||
**方案**:
|
||||
1. 文本切片存储时,提取其中的图表引用
|
||||
2. 在 metadata 中记录 `referenced_images: ["图2.3"]`
|
||||
3. 检索时,通过文本切片的 `referenced_images` 找到对应图片
|
||||
|
||||
### 5.4 P3:图片独立召回通道
|
||||
|
||||
**方案**:
|
||||
1. 向量检索时,对图片切片使用独立的 top_k
|
||||
2. 保证图片切片有足够的召回机会
|
||||
3. 最终融合文本和图片结果
|
||||
|
||||
---
|
||||
|
||||
## 六、验证方案
|
||||
|
||||
### 6.1 重建向量库
|
||||
|
||||
```bash
|
||||
# 方式1:删除向量库目录后同步
|
||||
rm -rf knowledge/vector_store/chroma/public_kb
|
||||
curl -X POST http://localhost:5001/sync
|
||||
|
||||
# 方式2:通过 API 重新上传文档
|
||||
```
|
||||
|
||||
### 6.2 测试检索
|
||||
|
||||
```bash
|
||||
# 测试 1:关键词查询
|
||||
curl -X POST http://localhost:5001/rag \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "蓄水以来逐年发电量"}'
|
||||
# 预期:图2.3 排名靠前
|
||||
|
||||
# 测试 2:图号查询
|
||||
curl -X POST http://localhost:5001/rag \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "图2.3 发电量"}'
|
||||
# 预期:精确返回图2.3
|
||||
|
||||
# 测试 3:语义查询
|
||||
curl -X POST http://localhost:5001/rag \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "三峡水库补水统计"}'
|
||||
# 预期:返回表2.2/图2.5
|
||||
```
|
||||
|
||||
### 6.3 检查向量库内容
|
||||
|
||||
```python
|
||||
import chromadb
|
||||
client = chromadb.PersistentClient(path='knowledge/vector_store/chroma/public_kb')
|
||||
col = client.get_collection('public_kb')
|
||||
|
||||
# 查看图片切片
|
||||
results = col.get(
|
||||
where={'chunk_type': {'$in': ['image', 'chart']}},
|
||||
include=['metadatas', 'documents'],
|
||||
limit=10
|
||||
)
|
||||
|
||||
for i, chunk_id in enumerate(results['ids']):
|
||||
meta = results['metadatas'][i]
|
||||
doc = results['documents'][i]
|
||||
print(f"[{i+1}] {meta.get('chunk_type')} | has_vlm_desc: {meta.get('has_vlm_desc')}")
|
||||
print(f" Doc: {doc[:100]}...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、关键文件索引
|
||||
|
||||
| 文件 | 职责 | 关键函数 |
|
||||
|------|------|----------|
|
||||
| `parsers/mineru_parser.py` | 文档解析 | `parse_with_mineru()`, `get_context_for_image()` |
|
||||
| `knowledge/manager.py` | 向量库管理 | `add_file_to_kb()`, `generate_lightweight_image_description()`, `_get_vlm_cache()` |
|
||||
| `core/engine.py` | 检索引擎 | `search_knowledge()`, `reciprocal_rank_fusion()`, `rerank_results()` |
|
||||
| `api/chat_routes.py` | API 路由 | `generate()`, `select_images()`, `score_image_relevance()` |
|
||||
| `core/intent_analyzer.py` | 意图分析 | `analyze()`, `IntentAnalysis` |
|
||||
| `knowledge/lazy_enhance.py` | 懒加载增强 | `lazy_vlm_description()`, `enhance_retrieved_chunks()` |
|
||||
Reference in New Issue
Block a user