# RAG 数据流程详解 > 本文档详细梳理 RAG 系统从文档解析到最终响应的完整数据流,便于问题排查和系统优化。 --- ## 一、整体架构概览 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ RAG 数据流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 文档上传 │───▶│ 文档解析 │───▶│ 切片入库 │───▶│ 向量检索 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ API 层 MinerU ChromaDB 混合检索 │ │ 入口 解析器 向量库 BM25+向量 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 图片匹配 │───▶│ LLM 生成 │───▶│ 响应输出 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 相关性打分 AgenticRAG SSE 流式 │ │ 图片选择 问答引擎 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 二、文档解析层 ### 2.1 MinerU 解析输出结构 **入口函数**:`parsers/mineru_parser.py::parse_with_mineru()` **输出文件**: ``` .data/mineru_temp/{file_hash}/ ├── auto/ │ ├── {doc_name}.md # Markdown 内容 │ ├── {doc_name}_content_list.json # 结构化内容列表 ⭐ │ └── images/ # 提取的图片 │ ├── abc123.jpg │ └── def456.png ``` ### 2.2 content_list.json 结构 这是 MinerU 解析的核心输出,包含文档的完整结构化信息: ```json [ { "type": "text", "text": "第一章 水情分析", "page_idx": 0, "bbox": [x0, y0, x1, y1], "text_level": 1 }, { "type": "text", "text": "正文内容...", "page_idx": 0, "bbox": [x0, y0, x1, y1], "text_level": 0 }, { "type": "table", "table_body": "...
", "table_caption": "表1.1 数据统计", "img_path": "table_001.jpg", "page_idx": 1, "bbox": [x0, y0, x1, y1] }, { "type": "image", "img_path": "abc123.jpg", "caption": "", // ⚠️ MinerU 未提取,通常为空 "page_idx": 2, "bbox": [x0, y0, x1, y1] }, { "type": "chart", "img_path": "chart_001.jpg", "caption": "", // ⚠️ 同样通常为空 "page_idx": 3, "bbox": [x0, y0, x1, y1] } ] ``` ### 2.3 content_list 各类型字段详解 | 类型 | 字段 | 说明 | 示例值 | |------|------|------|--------| | **text** | `text` | 文本内容 | "第一章 概述" | | | `page_idx` | 页码索引(0-based) | 0 | | | `bbox` | 边界框坐标 | [50, 100, 500, 150] | | | `text_level` | 标题级别(0=正文,1=h1...) | 1 | | **table** | `table_body` | 表格 HTML | `"...
"` | | | `table_caption` | 表格标题 | "表1.1 统计数据" | | | `img_path` | 表格图片路径(可选) | "table_001.jpg" | | **image** | `img_path` | 图片路径 | "abc123.jpg" | | | `caption` | 图片标题 ⚠️ | "" (通常为空) | | **chart** | `img_path` | 图表图片路径 | "chart_001.jpg" | | | `caption` | 图表标题 ⚠️ | "" (通常为空) | ### 2.4 MinerUChunk 数据结构 **定义位置**:`parsers/mineru_parser.py` 第 95-116 行 ```python @dataclass class MinerUChunk: content: str # 文本内容 chunk_type: str # 类型: text, table, image, chart, equation page_start: int = 1 # 起始页码 page_end: int = 1 # 结束页码 text_level: int = 0 # 标题级别 (0=body, 1=h1, 2=h2...) title: str = "" # 标题文本 section_path: str = "" # 章节路径 "第一章 > 1.1 概述" bbox: Optional[List[float]] = None # 边界框 [x0, y0, x1, y1] source_file: str = "" # 源文件名 table_html: Optional[str] = None # 表格 HTML(如果是表格) image_path: Optional[str] = None # 图片路径(独立图片) images: Optional[List[Dict]] = None # 关联图片列表 ``` --- ## 三、切片入库层 ### 3.1 入库流程 **入口函数**:`knowledge/manager.py::add_file_to_kb()` **流程图**: ``` add_file_to_kb() │ ├── parse_document() → 调用 MinerU 解析 │ ├── convert_to_rag_format() → 转换为 RAG 格式 │ └── 遍历 pages_content: │ ├── 文本切片 → 生成 embedding → 存入 ChromaDB │ ├── 表格切片 → 生成摘要 → 存入 ChromaDB │ └── 图片切片 → 生成描述 → 存入 ChromaDB ``` ### 3.2 文本切片存储 **代码位置**:`knowledge/manager.py` 第 1050-1150 行 ```python text_meta = { 'source': filename, # 源文件名 'page': page_info.get('page', 0), # 页码 'chunk_type': 'text', # 类型 'section': section, # 章节标题 'section_path': section_path, # 章节路径 'level': page_info.get('level', 0), # 标题级别 'doc_type': _get_doc_type(filename), # 文档类型 'has_table': False, **extra_metadata } # document 字段 = 文本内容 document = page_info.get('text', '') # 向量化 vector = embedding_model.encode(document).tolist() collection.add( ids=[chunk_id], embeddings=[vector], documents=[document], # ⭐ 文本内容 metadatas=[text_meta] ) ``` ### 3.3 表格切片存储 **代码位置**:`knowledge/manager.py` 第 1150-1200 行 ```python table_meta = { 'source': filename, 'page': page_info.get('page', 0), 'chunk_type': 'table', 'section': section, 'caption': caption, # 表格标题 'has_table': True, 'table_html': table_html, # 表格 HTML ... } # document 字段 = 表格摘要(LLM 生成)或表格 Markdown document = summary if summary else markdown_table collection.add( ids=[chunk_id], embeddings=[vector], documents=[document], # ⭐ 表格摘要/Markdown metadatas=[table_meta] ) ``` ### 3.4 图片切片存储(重点!) **代码位置**:`knowledge/manager.py` 第 1195-1255 行 ```python # caption 获取(问题根源!) caption = page_info.get('caption') or chunk.title # ⚠️ 两者都是默认值 # 元数据 image_meta = { 'source': filename, 'page': page_info.get('page', 0), 'chunk_type': 'image', # 或 'chart' 'section': section_path, 'caption': caption, # ⚠️ 存入默认值 "图片"/"图表" 'figure_number': _extract_figure_number(caption, section), # 图号 'image_path': image_path, # 图片路径 'has_vlm_desc': False, ... } # ⭐ document 字段 = 轻量级描述(正确!) description = self.generate_lightweight_image_description(full_image_path, chunk, page_info) # 结果: "图表:位于「第一章」> 1.1 概述,第5页" # 向量化 vector = embedding_model.encode(description).tolist() collection.add( ids=[chunk_id], embeddings=[vector], documents=[description], # ⭐ 正确的描述信息 metadatas=[image_meta] # ⚠️ caption 是默认值 ) ``` ### 3.5 generate_lightweight_image_description 函数 **代码位置**:`knowledge/manager.py` 第 1418-1459 行 ```python def generate_lightweight_image_description(self, image_path: str, chunk, page_info: dict) -> str: """ 生成轻量级图片描述(不用 VLM) 信息来源:文件名 + 标题/caption + 章节路径 + 页码 """ parts = [] # 1. 图片类型 chunk_type = page_info.get('chunk_type', 'image') type_label = "图表" if chunk_type == 'chart' else "图片" # 2. 标题或 caption title = chunk.title if hasattr(chunk, 'title') and chunk.title else "" caption = page_info.get('caption', '') # 3. 章节路径 section = page_info.get('section_path', '') or page_info.get('section', '') # 4. 页码 page = page_info.get('page', 0) # 组装描述 if caption: parts.append(caption) elif title and title not in ("图片", "图表"): parts.append(title) if section: parts.append(f"位于「{section}」") parts.append(f"第{page}页") return f"{type_label}:{','.join(parts)}" # 输出示例: "图表:位于「Tracing the s-Process」> 2.1 The M-S-C sequence,第5页" ``` --- ## 四、向量库结构 ### 4.1 ChromaDB 存储结构 每个切片包含三个核心字段: | 字段 | 类型 | 说明 | 示例 | |------|------|------|------| | `ids` | str | 切片唯一 ID | "doc.pdf_text_0" | | `embeddings` | List[float] | 向量表示 | [0.1, 0.2, ...] | | `documents` | str | 文本内容/描述 | "图表:位于「xxx」第5页" | | `metadatas` | dict | 元数据 | 见下表 | ### 4.2 元数据字段详解 #### 文本切片 metadata ```python { 'source': 'report.pdf', # 源文件名 'page': 5, # 页码 'chunk_type': 'text', # 类型 'section': '水情分析', # 章节标题 'section_path': '第一章 > 1.1 水情分析', # 章节路径 'level': 0, # 标题级别 'doc_type': 'pdf', # 文档类型 'has_table': False, 'collection': 'public_kb' } ``` #### 表格切片 metadata ```python { 'source': 'report.pdf', 'page': 6, 'chunk_type': 'table', 'section': '数据统计', 'caption': '表1.1 月度统计数据', # 表格标题 'has_table': True, 'table_html': '...
', # 表格 HTML 'collection': 'public_kb' } ``` #### 图片/图表切片 metadata ```python { 'source': 'report.pdf', 'page': 7, 'chunk_type': 'image', # 或 'chart' 'section': '水情分析', 'caption': '图片', # 默认值(从 MinerU 获取) 'figure_number': '', # 图号(依赖 caption) 'image_path': 'abc123.jpg', # 图片路径 'has_vlm_desc': False, # 是否有 VLM 描述 'bbox': '[x0,y0,x1,y1]', # 边界框 JSON 'preview': '图表:位于「第一章」...', # 预览文本 'collection': 'public_kb' } ``` #### 图片切片 document 字段(优化后) ```python # 优化后的 document 字段包含上下文,便于语义检索命中 """ 图表:位于「第一章 > 水情分析」,第5页 前文:2022年汛期长江流域出现汛期反枯,三峡水库出入库流量呈现明显下降趋势... 后文:由图2.1可见,水位呈现先升后降趋势,最高水位出现在8月中旬... """ ``` --- ## 五、检索层 ### 5.1 混合检索流程 **入口**:`core/engine.py::search_knowledge()` 或 `core/agentic.py` ``` search_knowledge(query) │ ├── 向量检索 (ChromaDB) │ └── collection.query(query_embeddings=[vector], n_results=20) │ ├── 关键词检索 (BM25) │ └── bm25_index.search(query, top_k=20) │ └── 结果合并 (RRF) └── reciprocal_rank_fusion(vector_results, bm25_results) ``` ### 5.2 检索结果结构 ```python { 'ids': ['doc.pdf_text_0', 'doc.pdf_image_1', ...], 'documents': ['文本内容...', '图表:位于「xxx」第5页', ...], 'metadatas': [{...}, {...}, ...], 'distances': [0.1, 0.2, ...] } ``` 转换为 `contexts` 格式: ```python contexts = [ { 'id': 'doc.pdf_text_0', 'doc': '文本内容...', 'meta': {...}, 'score': 0.9 }, { 'id': 'doc.pdf_image_1', 'doc': '图表:位于「xxx」第5页', # ⭐ document 字段 'meta': { 'chunk_type': 'image', 'caption': '图片', # ⚠️ 默认值 'image_path': 'abc.jpg', ... }, 'score': 0.85 } ] ``` --- ## 六、图片匹配层 ### 6.1 图片选择流程 **代码位置**:`api/chat_routes.py` 第 246-293 行 ```python def select_images(contexts: list, query: str) -> list: """ 选择要展示的图片(打分排序 + 预算控制) """ scored_images = [] for ctx in contexts: meta = ctx.get('meta', {}) if meta.get('chunk_type') in ('image', 'chart') and meta.get('image_path'): # 调用打分函数 s = score_image_relevance(query, meta) # ⚠️ 未传入 doc 字段 if s >= MIN_SCORE: scored_images.append({ 'score': s, 'id': os.path.basename(meta['image_path']), 'url': f"/images/{os.path.basename(meta['image_path'])}", 'type': meta['chunk_type'], 'source': meta.get('source'), 'page': meta.get('page'), 'description': ctx.get('doc', '')[:100] }) scored_images.sort(key=lambda x: x['score'], reverse=True) return scored_images[:MAX_IMAGES] ``` ### 6.2 图片相关性打分 **代码位置**:`api/chat_routes.py` 第 186-243 行 ```python def score_image_relevance(query: str, meta: dict) -> float: """ 图片相关性打分 问题:使用 meta.get('caption') 获取的是默认值 "图片"/"图表" 解决:应该使用 ctx['doc'] 字段进行匹配 """ score = 0.0 # 1. 检测查询中的图片编号 caption = meta.get('caption', '') or '' # ⚠️ 获取默认值 figure_matches = re.findall(r'图\s*(\d+\.?\d*)', query) if figure_matches: for fig_num in figure_matches: if f"图{fig_num}" in caption: # ⚠️ 永远不匹配 score += 5.0 # 2. 查询内容与图片 caption 匹配 if caption: overlap = len(set(query) & set(caption)) # ⚠ 使用默认值匹配 score += min(overlap * 0.15, 3.0) # ... 其他加分逻辑 return score ``` --- ## 七、问题排查指南 ### 7.1 常见问题定位 | 问题现象 | 可能原因 | 排查位置 | |----------|----------|----------| | 图片不显示 | caption 为默认值 | 检索结果 `meta['caption']` | | 图片匹配错误 | 打分逻辑未使用 doc 字段 | `score_image_relevance()` | | 表格未识别 | table_html 为空 | 检索结果 `meta['table_html']` | | 切片丢失 | 解析失败或过滤 | MinerU 输出 `content_list.json` | ### 7.2 调试命令 ```python # 1. 查看 MinerU 解析结果 import json with open('.data/mineru_temp/{hash}/auto/{doc}_content_list.json') as f: content_list = json.load(f) for item in content_list[:10]: print(f"类型: {item.get('type')}, 内容: {str(item)[:100]}") # 2. 查看向量库切片 from knowledge.manager import KnowledgeBaseManager kb = KnowledgeBaseManager() collection = kb.get_collection('public_kb') # 获取所有图片切片 result = collection.get( where={"chunk_type": "image"}, include=['documents', 'metadatas'] ) for i, (doc, meta) in enumerate(zip(result['documents'][:5], result['metadatas'][:5])): print(f"图片 {i+1}:") print(f" document: {doc}") print(f" caption: {meta.get('caption')}") print(f" image_path: {meta.get('image_path')}") ``` ### 7.3 数据流检查清单 ``` □ MinerU 解析 ├─ content_list.json 是否生成? ├─ 图片项 caption 字段是否为空? └─ 图片文件是否正确提取? □ 切片入库 ├─ document 字段是否包含描述? ├─ metadata.caption 是否为默认值? └─ image_path 是否正确? □ 向量检索 ├─ 检索结果是否包含图片切片? ├─ ctx['doc'] 是否有值? └─ ctx['meta']['caption'] 是什么? □ 图片匹配 ├─ score_image_relevance 是否使用 doc 字段? └─ 最终匹配分数是否足够? ``` --- ## 八、已知问题与解决方案 ### 8.1 图片 caption 为默认值 **问题**:MinerU 未提取图片标题,导致 `meta['caption']` 为 "图片"/"图表" **影响**:`score_image_relevance()` 无法正确匹配图片 **临时解决**:修改 `score_image_relevance()` 使用 `ctx['doc']` 字段 **长期解决**:在 MinerU 解析层从文档上下文提取图片标题 ### 8.2 figure_number 未提取 **问题**:图号提取依赖 caption,caption 为空时 figure_number 也为空 **影响**:无法按图号精确检索 **解决**:改进 `_extract_figure_number()` 从 section 或上下文提取 --- ## 九、参考文件 | 文件 | 作用 | 关键函数 | |------|------|----------| | `parsers/mineru_parser.py` | 文档解析 | `parse_with_mineru()`, `MinerUChunk` | | `knowledge/manager.py` | 切片入库 | `add_file_to_kb()`, `generate_lightweight_image_description()` | | `api/chat_routes.py` | 图片匹配 | `select_images()`, `score_image_relevance()` | | `core/engine.py` | 向量检索 | `search_knowledge()` | | `core/agentic.py` | 问答引擎 | `AgenticRAG` |