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

18 KiB
Raw Blame History

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 行)

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 列表

@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 行)

# 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 字段

{
    '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

方案

# 优先使用 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 重建向量库

# 方式1删除向量库目录后同步
rm -rf knowledge/vector_store/chroma/public_kb
curl -X POST http://localhost:5001/sync

# 方式2通过 API 重新上传文档

6.2 测试检索

# 测试 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 检查向量库内容

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()