Files
rag/docs/RAG数据流程完整分析.md
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

449 lines
18 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.
# 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 模型对这种格式的内容相似度计算不准确
**问题 2VLM 缓存未被利用**
- 已有 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 排名第2distance=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()` |