Files
rag/docs/RAG检索流程逻辑.md
lacerate551 ee5295cc97 fix(parser): PDF/DOCX 切片层级结构修复与 MinerU V2 解析增强
核心修复:
- _post_process_chunks: 用 _buffer_has_body 标志替代 buffer.text_level=0,
  标题+正文合并后保留 text_level 不置零,修复层级信息丢失
- heading_rules numeric_level2: 正则从 ^\d+\.\d+[\.、\s] 改为
  ^\d+\.\d+(?!\.\d),修复无空格标题如"2.1运行调度"无法匹配
- V2 title handler: heading_rules 优先(模式匹配可靠),VLM 仅兜底
  (VLM 常给所有标题 level=1)

MinerU V2 解析增强:
- 两轮 TOC 过滤:多行目录块检测 + 孤立标题/单字符残留清理
- 封面 logo 过滤、重复标题去重
- chart VLM 描述和 Markdown 数据表提取
- ChromaDB metadata 新增 text_level/bbox/table_type/sub_type 字段

配置调整:
- CLUSTER_SECTION_PREFIX_LEVELS 1→2(两级 section 聚类更精确)
- MINERU_LOCAL_BACKEND 默认改为 pipeline

文档:
- RAG检索流程逻辑.md 更新 MinerUChunk 字段、ChromaDB metadata、
  MinerU 解析策略等章节
- 新增 RAG引用跳转-优化计划.md、云端MinerU输出分析与优化方案.md
2026-06-19 23:56:13 +08:00

48 KiB
Raw Blame History

RAG 检索流程逻辑

本文档描述 RAG 知识库服务从文档解析入库到 LLM 回答的完整数据流,供开发排查和系统优化参考。


目录

  1. 整体架构
  2. 文档解析与入库流程
  3. ChromaDB 存储字段详解
  4. BM25 索引两种实现
  5. 检索管线完整数据流
  6. distances / scores 语义变化
  7. 路由层处理流程
  8. 三重救援机制详解
  9. 图片召回与选择
  10. 返回格式与溯源信息
  11. 配置项完整列表
  12. 单/多知识库路径说明
  13. 切片策略与检索策略兼容性分析
  14. MinerU 解析策略

一、整体架构

┌─────────────────────────────────────────────────────────────────┐
│                       文档入库流程                                │
│                                                                 │
│  文件上传 → MinerU 解析 → MinerUChunk → 语义增强 → ChromaDB 存储  │
│                                            ↓                    │
│                                       BM25 索引构建              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       检索问答流程                                │
│                                                                 │
│  用户提问                                                        │
│    ↓                                                             │
│  意图分析(改写/子查询/意图分类)                                   │
│    ↓                                                             │
│  语义缓存检查 ──命中──→ 直接返回缓存答案                           │
│    ↓ 未命中                                                       │
│  混合检索(向量 + BM25 + 图片 + FAQ                              │
│    ↓                                                             │
│  RRF 融合 → 过滤链 → MMR → Rerank → 后处理 → 扩展 → 自适应 TopK  │
│    ↓                                                             │
│  search_hybriddistances → scores 转换)                        │
│    ↓                                                             │
│  路由层contexts 构建 → 三重救援 → 排序 → 预算截断                │
│    ↓                                                             │
│  Prompt 构建 → LLM 流式生成 → 后处理 → 返回                       │
└─────────────────────────────────────────────────────────────────┘

二、文档解析与入库流程

2.1 入口方法

# knowledge/manager.py
def add_file_to_kb(self, kb_name, filepath, embedding_model=None,
                   extra_metadata=None, enable_table_summary=True,
                   enable_image_description=False, file_content=None) -> int

完整流程

1. 调用 parsers.parse_document(filepath) 进行 MinerU 解析
2. 合并跨页表格_merge_cross_page_tables
3. 清理同名旧切片(查询 source == filename 的旧切片并删除)
4. 逐切片处理:
   a. 获取 chunk_type、page、section_path
   b. 构建语义增强内容semantic_content
   c. 构建 metadata 字典
   d. 生成 embedding 向量
   e. 可选LLM 生成表格摘要 / VLM 生成图片描述
5. 批量写入 ChromaDBcollection.add
6. 更新 BM25 索引bm25.add_documents + save

2.2 MinerU 解析流程

三个解析入口

函数 用途 调用方式
parse_with_mineru() 本地 GPU 解析 parse_with_mineru_persistent 在在线失败时调用
parse_with_mineru_online() 在线 API 解析 parse_with_mineru_persistent 优先调用
parse_with_mineru_persistent() 持久化解析(主入口) parsers.parse_document() 调用

实际主入口是 parse_with_mineru_persistent(),内部根据 config.MINERU_PREFER_ONLINE 自动选择在线或本地,在线失败自动回退本地。

解析核心流程(以在线为例):

  1. 文件校验(存在性、大小 ≤ 100MB、格式校验
  2. 计算文件 MD5 hash前12位用于隔离输出目录
  3. 申请上传链接 → PUT 上传文件 → 轮询查询结果5秒间隔→ 下载 zip 包
  4. 解析 content_list优先 v2 格式 _content_list_v2.json
  5. 逐项解析 content_list 构建 MinerUChunk

MinerUChunk 数据结构

@dataclass
class MinerUChunk:
    content: str                       # 文本内容
    chunk_type: str                    # text / heading / table / image / chart / equation
    page_start: int = 1                # 起始页码(1-based)
    page_end: int = 1                  # 结束页码
    text_level: int = 0                # 标题级别 (0=正文, 1=h1, 2=h2, 3=h3)
    title: str = ""                    # 标题文本
    section_path: str = ""             # 章节路径," > " 连接
    bbox: Optional[List[float]] = None # 边界框 [x0, y0, x1, y1]
    source_file: str = ""              # 源文件名
    table_html: Optional[str] = None   # 表格 HTML仅 table
    image_path: Optional[str] = None   # 图片路径(独立图片)
    images: Optional[List[Dict]] = None # 关联图片: [{"id":"abc.jpg","order":1}]
    context_before: str = ""           # 图片前文本上下文
    context_after: str = ""            # 图片后文本上下文
    # VLM 增强信息(云端 MinerU V4 VLM 后端提供)
    vlm_description: str = ""          # VLM 视觉描述(图片/图表)
    chart_markdown: str = ""           # VLM 提取的图表数据表Markdown 格式)
    # MinerU 结构化元数据
    table_type: str = ""               # 表格类型cflow/table/text来自 V2 _v2_table_type
    table_nest_level: str = ""         # 表格嵌套层级(来自 V2 _v2_table_nest_level
    sub_type: str = ""                 # 图片/图表子类型natural_image/table_image/bar/line 等)

2.3 section_path 生成逻辑

维护 section_stack: [(level, title), ...],遇到新标题时:

  • 弹出栈中 level >= 当前 level 的项
  • 压入 (level, title)
  • section_path = " > ".join([栈中所有项的 title])

2.4 后处理三阶段

  1. 过滤空切片:移除 content 为空的 chunk
  2. 合并碎片:标题+正文合并、短文本合并(阈值 MIN_CHUNK_SIZE//2),表格/图片/公式不参与
  3. 拆分超长:超过 MAX_CHUNK_SIZE 的文本用 split_text_with_limit 拆分

2.5 图片处理

图片来源三类

  1. 独立图片/图表content_list 中 type=image/chartchunk.image_path
  2. 表格的图片形式type=table 有 img_pathchunk.image_path
  3. 嵌入表格 HTML 的图片(<img src="...">)→ chunk.images 列表

图片路径重映射parse_with_mineru_persistent 中):

  1. 从 MinerU 输出找到图片源文件
  2. 按文件内容 MD5 重命名,移动到 .data/images/ 目录
  3. 更新 chunk.image_path 和 chunk.images 中的路径

2.6 语义内容构建

文本类型 _build_semantic_content_for_text()

{标题}
主题:{章节路径截断到3级}
{正文内容}

表格类型 _build_semantic_content_for_table()

主题:{章节路径}
表格:{标题}(非"表格"时)
字段:{表头列表}
描述该表包含N行数据记录各字段信息
示例字段1=值1, 字段2=值2

表格内容:
| ... | ... |

图片类型 generate_lightweight_image_description()

图片图2.1,系统架构图,位于「第一章 > 1.2 概述」第5页
前文:系统由三个模块组成...
后文:如图所示,各模块之间...

设计意图:语义增强内容存入 ChromaDB 的 documents 字段,用于向量检索。增强后的内容比原始文本包含更多上下文信息,提升检索召回率。


三、ChromaDB 存储字段详解

3.1 存储结构

ChromaDB 使用 collection.add(ids, documents, metadatas, embeddings) 批量写入。

字段 类型 说明
ids List[str] 切片唯一标识,格式 {filename}_{index},如 "1.docx_14"
documents List[str] 语义增强后的内容(非原始 content
metadatas List[dict] 切片元数据(见下表)
embeddings List[List[float]] 768维向量embedding_model.encode(semantic_content) 生成

3.2 metadatas 字段详解

字段 类型 来源 作用 示例
chunk_id str {filename}_{index} 切片唯一标识(与 ids 相同) "1.docx_14"
chunk_index int 入库时赋值 i 切片在文件中的序号,用于邻居扩展和救援 14
chunk_type str MinerU 解析 切片类型,驱动检索和展示逻辑 "text"
source str 文件名 源文件名,用于黑名单过滤和来源展示 "1.docx"
collection str 入库参数 所属向量库名,确保跨库可追溯 "public_kb"
doc_type str 文件扩展名映射 文档类型,驱动前端差异化溯源展示 "word"
page int MinerU 解析 起始页码(仅 PDF 可靠) 5
page_end int MinerU 解析 结束页码 5
section str MinerU section_path 章节路径用于章节过滤、聚类救援、prompt 标题注入 "第一章 > 1.1 概述"
status str 默认 "active" 切片状态,deprecated 时被引擎过滤 "active"
version str 默认 "v1" 版本号,用于缓存失效 "v1"
images_json str json.dumps(chunk.images) 关联图片列表的 JSON 序列化,图片选择时反序列化 '[{"id":"abc.jpg","order":1}]'
image_path str MinerU 解析 图片文件名(不含目录),用于图片 URL 生成 "0569dd285537.jpg"
text_level int MinerU 解析 标题级别0=正文, 1=H1, 2=H2, 3=H3用于层次感知检索 2
bbox str MinerU 解析PDF 边界框 JSON 序列化 [x0,y0,x1,y1],用于前端页内精准定位。仅 PDF 有值DOCX 为 None 不存储 "[100,200,500,600]"
table_type str MinerU V2 解析 表格类型cflow/table/text用于区分表格渲染方式 "table"
table_nest_level str MinerU V2 解析 表格嵌套层级,标识嵌套表格的深度 "2"
sub_type str MinerU V2 解析 图片/图表子类型natural_image/table_image/bar/line/bar_line 等),用于前端差异化展示 "bar"

注意chunk_idids 重复存储。ids 是 ChromaDB 的主键,chunk_id 存在 metadata 中便于路由层按 metadata 查找。两者值相同但用途不同。

命名不一致MinerU 中字段名 section_path,入库时映射为 metadata 的 section。路由层代码中两种名称混用(meta.get('section')meta.get('section_path')),通常用 or 兼容:meta.get('section', '') or meta.get('section_path', '')

3.3 chunk_type 取值

chunk_type 说明 检索行为 展示行为
text 普通文本 正常参与向量检索 正常展示
heading 标题 正常参与向量检索 正常展示
table 表格 走图片独立检索通道;有表格保护 max_chunks 豁免;可被表格救援找回
image 图片 走图片独立检索通道 full_description 替换 doc
chart 图表 走图片独立检索通道 同 image
equation 公式 正常参与向量检索 正常展示
faq FAQ FAQ 独立检索通道 享受分数加权

四、BM25 索引两种实现

项目中存在两个 BM25Index 类,返回格式有重要差异。

4.1 对比表

维度 core/bm25_index.py knowledge/base.py
使用场景 单知识库路径(当前不执行) 多知识库路径(当前活跃)
实例管理 全局单例 RAGEngine.bm25_index 每个向量库独立实例
持久化 单一 bm25_index.pkl {kb_name}.pkl 每库独立
search() 返回 DictChromaDB 兼容格式) Tuple[List, List, List, List]
返回值解构 result['ids'][0] ids, docs, metas, scores = bm25.search()
列表嵌套 [['id1', 'id2']](嵌套列表) ['id1', 'id2'](扁平列表)
分数字段名 distances 第4个返回值匿名
add_documents 模式 替换(覆盖) 追加 + 去重

4.2 兼容处理

多知识库路径中,_search_multi_kb 方法对 knowledge/base.py 的 tuple 返回值做了兼容转换:

# core/engine.py _search_multi_kb 中
if isinstance(bm25_res, tuple):
    _ids, _docs, _metas, _dists = bm25_res
    bm25_res = {
        'ids': [_ids], 'documents': [_docs],
        'metadatas': [_metas], 'distances': [_dists]
    }

设计建议:两个 BM25Index 实现有重叠功能,未来可考虑统一为 dict 返回格式,消除兼容转换代码。


五、检索管线完整数据流

5.1 入口方法

# api/chat_routes.py
search_result = search_hybrid(retrieval_query, top_k, candidates, allowed_collections, sub_queries)

# search_hybrid 内部调用
result = engine.search_knowledge(query, top_k, allowed_levels, collections, sub_queries)
# 然后添加 scores 字段

5.2 search_knowledge 完整流程

query (str)
  │
  ├─ [1] 缓存检查 ──命中──→ 直接返回
  │
  ├─ [2] 外部子查询IntentAnalyzer 生成的 sub_queries
  │       → _search_with_sub_queries() → 合并 → 返回
  │
  ├─ [3] 查询拆分QueryDecomposer 判断)
  │       → _search_with_decomposition() → 合并 → 返回
  │
  ├─ [4] 查询扩展(当前仅构建 expanded_queries未用于多查询检索
  │
  ├─ [5] 分支USE_MULTI_KB=True → _search_multi_kb() → 返回
  │       USE_MULTI_KB=False → 单知识库路径(当前不执行)
  │
  └─ 单知识库路径(以下步骤在 _search_multi_kb 中对称存在):
      │
      ├─ [6]  where 过滤构建security_level / source
      │
      ├─ [7]  向量编码 → query_vector (768维)
      │
      ├─ [8]  向量检索 → vector_results
      │         {ids:[[]], documents:[[]], metadatas:[[]], distances:[[]]}
      │
      ├─ [9]  图片独立检索 → image_results → 合并
      │
      ├─ [10] FAQ 独立检索 → faq_results → 合并
      │
      ├─ [11] BM25 检索 → bm25_results
      │         + 捕获 BM25 原始 top-3 → _bm25_raw_top3
      │
      ├─ [12] 动态 RRF 权重 → (vector_w, bm25_w)
      │
      ├─ [13] RRF 融合 → fused_results
      │         {distances:[[rrf_score]], '_score_source':'rrf'}
      │         ids 带 collection 前缀:"public_kb/filename_3"
      │
      ├─ [14] 废止过滤 → 移除 status != "active" 的切片
      │
      ├─ [15] 枚举标记 → fused_results['_enum_query'] = bool
      │
      ├─ [16] BM25 top3 传递 → fused_results['_bm25_top3']
      │
      ├─ [17] 章节过滤 → 按查询中的章节关键词过滤
      │
      ├─ [18] 上下文扩展 1MMR前min_score=0→ 补齐邻居
      │
      ├─ [19] MMR 去重 → 缩减到 MMR_TOP_K=30
      │
      ├─ [20] Rerank 重排 → distances 变为 rerank 分数 [0,1]
      │         '_reranked'=True, '_score_source' 仍为 'rrf'
      │
      ├─ [21] FAQ 加权 → FAQ 切片 distances -= 0.1
      │
      ├─ [22] 黑名单过滤 → 移除黑名单 source
      │
      ├─ [23] 时间衰减 → 老 FAQ distances += decay
      │
      ├─ [24] 章节聚类提升 → 低分但聚类的切片 distances = 0.65
      │
      ├─ [25] 上下文扩展 2Rerank后min_score=0.3)→ 高分种子邻居
      │
      ├─ [26] 自适应 TopK → 可能截断结果
      │         ⚠️ 当前 bug: _score_source='rrf' 导致此步被跳过
      │
      └─ [27] 缓存写入 + 返回

5.3 特殊键在管线中的传递

以下键在过滤/截断方法中需要手动复制(否则会丢失):

for key in ('_debug', '_score_source', '_enum_query', '_expanded_context', '_bm25_top3'):
    if key in results:
        filtered[key] = results[key]
写入位置 读取位置 作用
_debug 每步追加 step 返回给前端dev 模式) 检索调试信息
_score_source RRF 融合写入 'rrf' 自适应 TopK 判断 区分 distances 语义
_enum_query 枚举检测后写入 MMR lambda 选择、自适应 TopK 枚举查询保留更多上下文
_expanded_context 扩展时写入 返回给前端 扩展统计
_bm25_top3 BM25 检索后写入 路由层分歧检测救援 BM25 原始 top-3 切片信息
_reranked Rerank 后写入 search_hybrid 中判断 区分 distances 是距离还是分数
_cluster_boosted 聚类提升写入 meta 上下文扩展时作为种子资格 标记被引擎层提升的切片

5.4 多知识库并发检索

_search_multi_kb 使用 ThreadPoolExecutor 并行查询各向量库:

with ThreadPoolExecutor(max_workers=len(target_collections)) as executor:
    futures = {executor.submit(_query_single_collection, name): name for name in target_collections}
    for future in as_completed(futures):
        coll_results, bm25_raw_items = future.result()
        all_results.extend(coll_results)
        _bm25_raw_top3.extend(bm25_raw_items)

每个向量库独立查询:向量检索 + BM25 检索,结果标记 _collection = coll_name

5.5 子查询路径的 BM25 top3 传递

_search_with_sub_queries_search_with_decomposition 合并结果时,会收集各子查询的 _bm25_top3,按 bm25_score 降序取全局 top-3确保路由层分歧检测在子查询路径下也能工作。


六、distances / scores 语义变化

distances 字段在管线各阶段含义不同,是理解检索逻辑的关键。

6.1 语义变化表

阶段 distances 含义 方向 范围 标记
ChromaDB 向量检索 cosine distance 越小越好 [0, 2] -
BM25 检索 BM25 原始分数 越大越好 [0, ∞) -
RRF 融合后 RRF 分数 越大越好 [0, ~0.05] _score_source='rrf'
Rerank 后 CrossEncoder 相关性分数 越大越好 [0, 1] _reranked=True
FAQ 加权后 rerank 分数 - 0.1 越大越好 [-0.1, 1] -
时间衰减后 距离 + decay 越大越差 - -
聚类提升后 被设为 1.0 - CLUSTER_SEED_FLOOR = 0.65 越大越好 - _cluster_boosted=True

6.2 search_hybrid 中的 distances → scores 转换

# api/chat_routes.py search_hybrid()
if result.get('_reranked'):
    # Rerank 后distances 就是相关性分数,直接使用
    scores = [float(d) for d in distances]
else:
    # 未 Rerank将向量距离转为相似度分数
    scores = [1.0 - d if d <= 1.0 else 1.0 / (1.0 + d) for d in distances]
result['scores'] = [scores]

6.3 自适应 TopK 中的距离→相似度转换

# engine.py 第 790 行
top_score = 1.0 - fused_results['distances'][0][0]  # 距离转相似度

⚠️ 已知问题RRF+Rerank 后,_score_source 仍为 'rrf',导致自适应 TopK 被跳过(第 788 行条件 fused_results.get('_score_source') != 'rrf' 不满足)。但此时 distances 已是有效的 rerank 分数,自适应 TopK 本应可以应用。此 bug 导致高置信度查询无法收缩结果,低置信度查询无法扩展。


七、路由层处理流程

7.1 generate_stream 完整流程

用户提问 (message)
  │
  ├─ [0] 意图分析 → IntentAnalysis(rewritten_query, need_retrieval, use_context, sub_queries, intent)
  │       need_retrieval=False + use_context=True → 直接用历史上下文回答 → return
  │
  ├─ [0.5] retrieval_query = intent.rewritten_query
  │
  ├─ [1] 语义缓存检查 ──命中──→ 流式返回缓存答案 → return
  │
  ├─ [2] 混合检索 → search_hybrid() → search_result
  │
  ├─ [3] 构建 contexts 列表
  │       从 search_result 的 documents[0] / metadatas[0] / scores[0] 三数组构建
  │       contexts = [{'doc': display_doc, 'meta': meta, 'score': score}, ...]
  │       图片/图表切片doc 替换为 meta.full_description如果存在
  │
  ├─ [3.5] 补充检索:从文本切片提取图号/表号引用,补充检索缺失图片
  │
  ├─ [4] 懒加载增强(当前禁用)
  │
  ├─ [5] 图片选择 → select_images()
  │
  ├─ [6] 三重救援
  │       ├─ _rescue_bm25_divergenceBM25 分歧检测救援)
  │       ├─ _rescue_lexical_match词法匹配救援
  │       └─ _rescue_section_cluster章节聚类救援
  │
  ├─ [7] 排序 + 预算截断
  │       _order_text_contexts_for_prompt() → min_score 过滤 → max_chunks 截断
  │
  ├─ [8] 表格救援 → _rescue_table_chunks()
  │
  ├─ [9] Prompt 构建
  │       ├─ 置信度分数top-3 平均 rerank 分数)
  │       ├─ 图片描述注入
  │       ├─ 意图驱动指令注入(对比/推理/操作/枚举)
  │       └─ 置信度指令注入
  │
  ├─ [10] LLM 流式生成 → engine.generate_answer_stream()
  │
  └─ [11] 后处理
          ├─ 答案对齐过滤(提取图号/表号,反向筛选图片)
          ├─ 去引用标记(移除 [1][2]
          ├─ 附加引用标注([ref:chunk_id]
          ├─ 敏感信息过滤
          ├─ 保存会话
          └─ 写入语义缓存

7.2 contexts 构建关键转换

# search_result 格式(来自 search_hybrid
search_result = {
    'documents': [[doc1, doc2, ...]],
    'metadatas': [[meta1, meta2, ...]],
    'scores': [[score1, score2, ...]],      # search_hybrid 添加
    'ids': [['public_kb/filename_3', ...]], # 多知识库模式带前缀
    'distances': [[rerank_score, ...]],     # 原始 distances 仍保留
    '_bm25_top3': [{id, doc, meta, bm25_score, rank}, ...],
    '_debug': {...},
}

# contexts 列表格式(路由层使用)
contexts = [
    {'doc': display_doc, 'meta': meta, 'score': score},
    ...
]

注意contexts 中没有 id 字段。切片 ID 只能通过 meta.chunk_id 获取(格式如 1.docx_14),而 engine 的 ids 可能带 collection 前缀(如 public_kb/1.docx_14)。

7.3 min_score 过滤

RERANK_CONTEXT_MIN_SCORE = 0.05

_order_text_contexts_for_prompt 中:

  • score >= min_score 的切片直接通过
  • 表格保护:同 section 有切片通过阈值时,同 section 的 table 切片保留下限为 min_score * 0.3 = 0.015

八、三重救援机制详解

三重救援是双层保护架构:引擎层在 rerank 后提升低分但可信的切片(分数较高),路由层在 min_score 过滤前做最终安全网(分数较低)。

8.1 BM25 分歧检测救援 _rescue_bm25_divergence

触发条件

  • BM25_DIVERGENCE_RESCUE_ENABLED = True
  • search_result._bm25_top3 非空
  • BM25 项的 rank <= BM25_DIVERGENCE_MAX_RANK= 3

保底分数CLUSTER_RESCUE_FLOOR = 0.06

两种情况

情况 条件 处理 示例
A — 分数压制 切片在 contexts 中但 score < min_score 提升 score 至 CLUSTER_RESCUE_FLOOR q003: 0.0034 → 0.06
B — 截断丢失 切片不在 contexts 中(被 rerank top_k 截断) _bm25_top3 备份注入新 context q014: 不在 → 注入 score=0.06

保护机制

  • 仅救援 BM25 rank ≤ 3 的切片top-3 是精确关键词匹配的强信号)
  • 救援分数固定为 CLUSTER_RESCUE_FLOOR0.06),不会高于 rerank 正常通过的切片
  • 匹配使用 meta.chunk_id(不带 collection 前缀),与 BM25 top3 的 id 字段匹配

数据来源_bm25_top3engine.search_knowledge 在 BM25 搜索后保存,格式为:

[{'id': '1.docx_14', 'doc': '...', 'meta': {...}, 'bm25_score': 12.4, 'rank': 1}, ...]

8.2 词法匹配救援 _rescue_lexical_match

触发条件

  • contexts 非空且 retrieval_query 非空
  • 清理后查询长度 ≥ 2
  • 能提取 bigram

保底分数CLUSTER_RESCUE_FLOOR = 0.06

Phase 1 — 词法匹配救援

  1. 清理查询(去除 markdown 格式和标点)
  2. 提取查询 bigram 集合(连续两字组)
  3. 对每个 score < min_score 的切片,计算 bigram 命中率
  4. 命中率 > 0.35 → 提升 score 至 max(原score, CLUSTER_RESCUE_FLOOR)
  5. 记录被救援的 (source, chunk_index) 作为种子

Phase 2 — 邻居救援

  • 对每个词法匹配救援的种子,同时救援同 source 下 chunk_index 后续 8 个相邻切片
  • 目的:枚举类问题的 header 切片被救援后,其后续子条目也应被保留

8.3 章节聚类救援 _rescue_section_cluster

触发条件

  • SECTION_CLUSTER_RESCUE_ENABLED = True
  • 存在"全灭 section":某 section 的成员数 ≥ CLUSTER_MIN_MEMBERS= 3类型多样性 ≥ CLUSTER_MIN_TYPES= 2且所有成员 score < min_score

保底分数CLUSTER_RESCUE_FLOOR = 0.06

处理逻辑

  1. (source, normalized_section) 分组
  2. 检测"全灭 section":所有成员 score < min_score
  3. 计算聚类强度 = 成员数 × 类型多样性 × (1 + 查询匹配度)
  4. 按强度降序,救援 top CLUSTER_MAX_SECTIONS= 3个 section
  5. 每个 section 最多救援 CLUSTER_MAX_RESCUE_PER_SECTION= 6个切片

8.4 引擎层 vs 路由层的双层保护

维度 引擎层 _section_cluster_boost 路由层 _rescue_section_cluster
执行时机 Rerank 后、扩展前 min_score 过滤前
提升方式 修改 distances 修改 contexts score
保底分数 CLUSTER_SEED_FLOOR = 0.35dist=0.65 CLUSTER_RESCUE_FLOOR = 0.06
覆盖范围 聚类切片在 rerank 之前就能被后续步骤看到 最终安全网,确保不遗漏

设计意图引擎层提升分数较高0.35 对应 dist=0.65使得这些切片能在后续的扩展步骤中被当作种子。路由层是最终安全网分数较低0.06)仅确保通过 min_score 过滤。


九、图片召回与选择

9.1 独立图片检索P0 通道)

图片/图表切片走独立检索通道,不被文本切片挤占名额:

image_recall_k = max(5, top_k // 2)   # 图片独立召回数量
image_results = _search_image_chunks(query_vector, image_recall_k, where_filter)
  • 仅检索 chunk_typeimagecharttable 的切片
  • 多知识库模式下对每个 collection 分别调用
  • 图片结果与文本结果通过 _merge_results() 合并

9.2 图片相关性提升Boost

在路由层对图片/图表切片做相关性评估:

  • 编号匹配:查询提到"图2.1"且图片 caption 匹配 → boost_factor = 2.0
  • 语义重叠caption 与查询有足够字符重叠 → boost_factor = 1.5
  • Boost 以 _image_boost 标记记录在 metadata 中,不改变排序

9.3 图片选择(select_images

从召回结果中筛选最终展示给 LLM 的图片:

  1. 动态预算:精确查图 2 张,有图片数据 5 张,有引用 3 张,默认 2 张
  2. 对有 image_path 的切片用 score_image_relevance 打分
  3. VLM 相关性筛选 + 章节关联检测 + 图号/表号匹配加分
  4. 按分数降序取 top MAX_IMAGES

9.4 图片后置过滤

LLM 生成回答后,用回答内容反向过滤图片:

  • 提取回答关键词
  • 检查每张图片描述与回答关键词的重叠度
  • 超过阈值的保留;兜底:若全部被过滤则保留最高分 1 张

十、返回格式与溯源信息

10.1 SSE 事件序列

事件类型 说明
intent_result 意图分析结果(仅 dev
retrieval_debug 检索管线调试信息(仅 dev
start 开始生成
sources 检索来源列表
chunks_retrieved 召回切片详情(仅 dev
section_cluster_rescue 章节聚类救援(仅 dev
chunk 每个 token流式
finish 完成事件(含完整回答和元数据)

10.2 来源信息sources

{
  "source": "文件名.docx",
  "page": 12,
  "page_end": 14,
  "page_range": "12-14",
  "section": "第三章 > 第二节 > 小节名",
  "chunk_type": "text",
  "doc_type": "word",
  "section_chunk_id": 5,
  "score": 0.892
}

10.3 引用格式citations

_attach_citations() 自动插入 [ref:chunk_id] 标记:

根据相关规定,安全检查应包括以下几个方面[ref:3.docx_154]...

十一、配置项完整列表

检索管线配置

配置项 默认值 作用 状态
USE_MULTI_KB True 多向量库模式 活跃
USE_HYBRID_SEARCH True 向量+BM25混合检索 活跃
USE_RERANK True Rerank 重排 活跃
RERANK_CANDIDATES 20 Rerank 候选数 活跃
RERANK_CONTEXT_MIN_SCORE 0.05 路由层最低分数阈值 活跃
RERANK_BACKEND "local" local/cloud/fallback 活跃
RERANK_USE_ONNX env("true") ONNX 加速 活跃
DYNAMIC_RRF_ENABLED True 动态 RRF 权重 活跃
RRF_K 60 RRF 常数 活跃
MMR_ENABLED True MMR 去重 活跃
MMR_USE_EMBEDDING env("false") 高精度/轻量版 活跃
MMR_TOP_K 30 MMR 保留数量 活跃
MMR_LAMBDA 0.5 相关性vs多样性 活跃
ENUM_QUERY_MMR_LAMBDA 0.85 枚举查询 lambda 活跃
QUERY_EXPANSION_ENABLED True 查询扩展 ⚠️ 构建但未用于多查询
SECTION_FILTER_ENABLED True 章节过滤 活跃
ADAPTIVE_TOPK_ENABLED True 自适应 TopK ⚠️ RRF+Rerank 后被跳过bug
CONTEXT_EXPANSION_ENABLED True 上下文扩展 活跃
CONTEXT_EXPANSION_BEFORE 1 向前扩展数 活跃
CONTEXT_EXPANSION_AFTER 8 向后扩展数 活跃
CONTEXT_EXPANSION_MAX_CHUNKS 50 最大扩展总数 活跃
SECTION_CLUSTER_BOOST_ENABLED True 引擎层聚类提升 活跃
SECTION_CLUSTER_RESCUE_ENABLED True 路由层聚类救援 活跃
BM25_DIVERGENCE_RESCUE_ENABLED True BM25 分歧救援 活跃
BM25_DIVERGENCE_MAX_RANK 3 救援 BM25 排名阈值 活跃
RAG_SEARCH_TOP_K 30 传给 search_hybrid 的 top_k 活跃
RAG_SEARCH_CANDIDATES 100 传给 search_hybrid 的 candidates 不传递给 engine
ENABLE_WEB_SEARCH False 网络搜索 预留接口,未实现
VECTOR_WEIGHT / BM25_WEIGHT 0.5 静态 RRF 权重 ⚠️ 动态 RRF 启用时被覆盖

缓存配置

配置项 默认值 作用 状态
QUERY_CACHE_ENABLED True 查询缓存 活跃
EMBEDDING_CACHE_ENABLED True Embedding 缓存 活跃
RERANK_CACHE_ENABLED True Rerank 分数缓存 活跃
SEMANTIC_CACHE_ENABLED True 语义缓存 活跃

聚类/救援配置

配置项 默认值 作用 状态
CLUSTER_MIN_MEMBERS 3 触发聚类最小成员数 活跃
CLUSTER_MIN_TYPES 2 触发聚类最小类型数 活跃
CLUSTER_SEED_FLOOR 0.35 引擎层聚类提升阈值 活跃
CLUSTER_RESCUE_FLOOR 0.06 路由层救援保底分数 活跃
CLUSTER_MAX_BOOST_PER_SECTION 8 引擎层每 section 最大提升数 活跃
CLUSTER_MAX_SECTIONS 3 全局最大提升 section 数 活跃
CLUSTER_MAX_RESCUE_PER_SECTION 6 路由层每 section 最大救援数 活跃
CLUSTER_SECTION_PREFIX_LEVELS 2 section 归一化层级(按章节前两级分组) 活跃

LLM 预算配置

配置项 默认值 作用 状态
MAX_LLM_CALLS_PER_QUERY 2 每查询 LLM 调用上限 ⚠️ 仅 llm_budget.py 使用(模块未集成到主流程)
MAX_QUERY_REWRITES 1 查询改写上限 ⚠️ 同上

十二、单/多知识库路径说明

12.1 当前配置

USE_MULTI_KB = True(硬编码在 config.py

效果search_knowledge 在第 612 行直接分支到 _search_multi_kb(),跳过第 628-811 行的单知识库路径。

12.2 两条路径对比

维度 单知识库路径 多知识库路径
向量库 self.collection self.kb_manager.get_collection(coll_name)
BM25 self.bm25_indexcore/bm25_index.py kb_manager.get_bm25_index(coll_name)knowledge/base.py
并发 ThreadPoolExecutor 并行查各库
RRF 权重 [VECTOR_WEIGHT, BM25_WEIGHT] [vector_w, bm25_w, ...] 交替
ID 前缀 带 collection 前缀
安全过滤 ChromaDB where 过滤 source 过滤

12.3 单知识库路径保留原因

  • 逻辑与多知识库路径对称,作为回退方案
  • 小规模部署可能不需要多知识库
  • self.bm25_indexcore/bm25_index.py仅在此路径使用

12.4 独立检索路径

knowledge/search.pySearchMixinSearchResult 是独立于 engine 的检索路径:

  • knowledge/router.py 使用(知识库路由推荐功能)
  • 不走 engine 主流程
  • 返回 SearchResult dataclass扁平列表格式与 engine 的 ChromaDB 格式不兼容

十三、切片策略与检索策略兼容性分析

本节基于 public_kb 三份文档1.docx / 2.docx / 3.docx的实际切片数据分析切片策略与检索管线各环节的搭配情况识别已确认的失配点并提出优化方向。

13.1 public_kb 切片统计

指标 1.docx 2.docx 3.docx
总切片数 65 193 411
文本切片 60 144 385
表格切片 3 49 21
图片切片 2 0 5
纯标题切片(<20字 16 (27%) 61 (42%) 97 (25%)
正文碎片(<100字 2 4 5
文本中位长度 122字 24字 66字
文本最大长度 725字 798字 757字
连续标题链≥2个 3条 22条 34条

关键发现2.docx 的纯标题切片占比高达 42%3.docx 有 34 条连续标题链(其中一条包含 21 个连续标题——附件列表)。这些标题切片不包含实质内容,但在向量库中占据存储空间,并参与检索管线的所有阶段。

13.2 切片策略概述

当前切片策略以标题层级为核心拆分依据:

MinerU 解析 → content_list → 逐项构建 MinerUChunk
  → heading_rules 引擎识别标题级别text_level
  → 每个标题开始新的 section_path
  → _post_process_chunks 三阶段后处理:
      Phase 1: 过滤空切片
      Phase 2: 合并碎片(标题+正文、短文本 < min_merge_size=100
      Phase 3: 拆分超长(> max_chunk_size=1000

合并规则

  • 标题 chunktext_level > 0)刷新缓冲并开始新合并组
  • 正文 chunk 若 < min_merge_size100字则并入缓冲
  • 正文 chunk 若 ≥ min_merge_size 且缓冲已满则直接输出
  • 合并上限 max_merged_size = 800

后果每个标题级别H1/H2/H3的段落都会产生独立的 chunk。当标题下方没有正文或正文在子标题下产生"纯标题切片"。

13.3 已确认的失配点

失配 1纯标题切片的语义内容冗余严重

问题:纯标题切片存入 ChromaDB 的 documents 字段是同一文本的三次重复:

三、做好货源投放前的基础工作          ← titletext_level > 0 时追加)
主题:三、做好货源投放前的基础工作     ← section_path最多3级
三、做好货源投放前的基础工作          ← content原始文本

生成代码见 knowledge/base.py:231-254_build_semantic_content_for_text)。

影响

  • Embedding 向量几乎无区分度——所有标题的向量高度相似
  • 向量检索几乎不会命中这些切片(内容查询与重复标题的相似度极低)
  • 浪费向量库存储空间2.docx 有 61 个此类切片)

失配 2上下文扩展的 section 精确匹配(严重)

问题_expand_contiguous_chunksengine.py:1334)在 section 过滤时使用精确匹配

where_filter = {"$and": [{"source": source}, {"section": section}]}

当种子是父级标题(如 section = "三、做好货源投放前的基础工作")时:

  • 子标题下的正文切片 section = "三 > (一)货源投放要求 > ..." 不匹配
  • 回退到 source-only 模式,从整个文档中拉取相邻 chunk_index 的切片
  • 对大文档,回退模式可能拉取到不相关的切片

影响:父级标题被检索到时,无法通过 section 过滤拉取其子节内容,降低了上下文完整性。

失配 3text_level 未传入检索管线 已修复

原问题text_level(标题级别 0/1/2/3在切片阶段被精确计算但未存入 ChromaDB metadata检索管线无法感知标题层级。

修复knowledge/manager.py 的 metadata 构建处已新增 text_level 字段:

metadata = {
    ...
    'text_level': getattr(chunk, 'text_level', 0),  # 已新增
}

当前状态text_level 已存入 ChromaDB metadata为后续层次感知检索提供了数据基础。引擎层和路由层尚未利用此字段做特殊处理如标题切片降权、导航锚点扩展但数据层已就绪。

失配 4章节聚类归一化粒度过粗 已调整

原问题CLUSTER_SECTION_PREFIX_LEVELS = 1 将 section_path 归一化到第一级,不同二级章节的切片被归入同一聚类组,可能触发误提升。

修复config.py 中已将 CLUSTER_SECTION_PREFIX_LEVELS1 调整为 2

CLUSTER_SECTION_PREFIX_LEVELS = 2  # section_path 归一化保留的层级数(按章节前两级分组,提升聚类精确度)

当前状态:归一化到前两级(如 (二)货源投放要求 > 7.关于主导品规投放聚类信号精确度提升。对深层嵌套文档3 级及以上 section的影响已通过 CLUSTER_MAX_SECTIONS=3CLUSTER_MIN_TYPES=2 约束。

失配 5预算构建器中标题切片的开销轻微

问题_build_context_with_budgetchat_routes.py:866)按 (source, section) 分组后,每个组添加 ━ {section} ━ 分隔行。纯标题切片形成单例组:

━ 三、做好货源投放前的基础工作 ━        ← ~25字符开销
三、做好货源投放前的基础工作             ← ~14字符内容信息量≈0

影响:约 40 字符的预算被浪费在无信息量的组上。在 CONTEXT_MAX_CHARS = 8000 的预算下,少量标题切片影响可控,但 2.docx 的 61 个标题切片如果被拉入就会累积显著开销。

失配 6MMR 去重对标题切片的过度消除(模式相关)

问题Embedding-based MMRMMR_USE_EMBEDDING=True)模式下,标题切片因 Embedding 高度相似而相互惩罚。Jaccard 模式(MMR_USE_EMBEDDING=False,当前设置)因词级分词有一定区分度,问题较轻。

影响:当标题切片恰好是某子章节的唯一入口时,被 MMR 消除后该子章节在检索结果中完全丢失。当前使用 Jaccard 模式,此问题暂未触发。

失配 73.docx 的 section_path 污染 已修复

原问题3.docx 中存在一个完整的合同条款段落被误识别为 H1 标题,导致 section_path 极长且无语义意义,该 section 下 59 个切片分组失真。

修复parsers/heading_rules.py 中新增了两层防护:

  1. _validate_level 超长文本降级H1 > 40字、H2 > 60字、H3 > 50字时自动降为正文level=0。覆盖所有返回路径v1 常规匹配、v2 style 匹配、bold_short_text 兜底)。
  2. 规则级 max_length + exclude_pattern:部分 H1 规则(如 numeric_level1)设置 max_length=50 和排除句末标点(;。,、::)的 exclude_pattern,在匹配阶段即过滤段落文本。

当前状态长段落不再被误判为标题section_path 污染问题已消除。

13.4 切片-检索兼容性总结

失配点 严重度 影响范围 现状
标题语义内容冗余 🔴 严重 全部文档 670 个向量库切片中约 174 个是纯标题
section 精确匹配 🔴 严重 父级标题检索 回退到 source-only可能拉取不相关内容
text_level 未传入 🟠 显著 全局 已修复text_level 已存入 ChromaDB metadata
聚类归一化过粗 🟡 中等 聚类提升/救援 已修复CLUSTER_SECTION_PREFIX_LEVELS 调整为 2
预算构建开销 🟢 轻微 上下文构建 标题单例组浪费字符预算
MMR 过度消除 🟢 模式相关 MMR 去重 当前 Jaccard 模式暂未触发
section_path 污染 🟡 中等 3.docx 已修复heading_rules 超长文本降级防护

13.5 优化建议

短期(改动小,收益明确)

1. 后处理合并连续标题链 已实施

已在 _post_process_chunks Phase 2 中实现连续标题链合并:

  • H1 级标题是章节边界,强制断开,不参与链合并
  • 非 H1 连续标题H2/H3合并为单个 chunk合并后取更高层级数值更小保留第一个标题的 section_path
  • 合并上限仍为 max_merged_size = 800

2. 存储 text_level 到 ChromaDB metadata 已实施

knowledge/manager.py 中已新增 text_level 字段存储,为后续层次感知检索提供数据基础。

3. 后处理合并标题与首个子节正文

当一个标题切片的下一个切片是子标题(更高 text_level 数值)下的正文时,将标题文本前置到子节切片中:

当前:#7 [H2] "四、相关的指标与分类"10字 → #8 [H2] "(一)货源属性分类\n1.紧俏品规..."349字
优化:#7 [H2] "四、相关的指标与分类\n货源属性分类\n1.紧俏品规..."359字

预期收益:消除父级标题的孤立切片,同时为子节切片提供上层上下文。

中期(需要评测验证)

4. 上下文扩展支持 section 前缀匹配

_expand_contiguous_chunks 的 section 过滤从精确匹配改为前缀匹配:

# 当前:精确匹配
{"section": section}
# 优化:前缀匹配(拉取所有子节切片)
{"section": {"$regex": f"^{re.escape(section)}"}}

需评估:前缀匹配可能拉取过多切片,需要配合 CONTEXT_EXPANSION_MAX_CHUNKS 上限控制。

5. 调整 CLUSTER_SECTION_PREFIX_LEVELS 已实施

已从 1 调整为 2,聚类信号精确度提升。

长期(架构级改进)

6. 层次感知检索

利用已存入 metadata 的 text_level 实现结构感知的检索策略:

  • 标题切片作为"导航锚点",被检索到时自动拉取其 section_path 前缀下的所有子切片
  • MMR 去重时对标题切片设置保护阈值,确保每个主要章节至少保留一个入口
  • 预算构建器对标题单例组做特殊处理(合并到子节组或跳过)

7. 修复 section_path 污染 已实施

parsers/heading_rules.py_validate_level 方法已实现超长文本降级防护H1>40字、H2>60字、H3>50字降为正文

8. bbox 空间感知检索(新增)

利用已存入 metadata 的 bbox 坐标实现空间感知的检索策略:

  • 同一页面相邻区域的切片在检索时可做空间聚类
  • 图片/图表切片的 bbox 可用于判断其在文档中的物理位置关系
  • 仅 PDF 切片有 bboxDOCX 切片无此信息

9. VLM 描述增强图片检索(新增)

利用已存入 MinerUChunk 的 vlm_descriptionchart_markdown 字段:

  • 将 VLM 视觉描述注入图片切片的 semantic_content提升向量检索的语义匹配度
  • chart_markdown图表数据表可增强图表切片的 BM25 关键词匹配
  • 仅云端 MinerU V4 VLM 后端提供,本地 pipeline 后端无此信息

十四、MinerU 解析策略

14.1 云端优先 + 本地备选

当前解析策略为云端 MinerU V4 API 优先,本地 MinerU 备选

维度 云端 MinerU V4 本地 MinerU
入口 parse_with_mineru_online() parse_with_mineru()
后端 VLM视觉语言模型 pipeline传统 OCR
解析速度 ~5 秒(含网络) 取决于 GPU/CPU
V2 数据丰富度 丰富bbox、title_content、VLM 描述、list_items、sub_type 基础:无 bbox、无 title type、无 VLM 描述
配置项 MINERU_PREFER_ONLINE=True MINERU_LOCAL_BACKEND="pipeline"
回退逻辑 失败自动回退本地 最终回退方案

调度流程

parse_with_mineru_persistent()
  → config.MINERU_PREFER_ONLINE=True
  → parse_with_mineru_online()  ← 优先
  → 成功 → 返回
  → 失败 → parse_with_mineru()  ← 本地回退pipeline 后端)

14.2 V2 content_list 解析

_parse_v2_content_list() 负责将 MinerU V2 格式的 content_list 转换为 MinerUChunk 列表。V2 是 MinerU 的结构化输出格式,比 V1 包含更丰富的元数据。

V2 与 V1 的关键差异

维度 V2 格式 V1 格式
标题文本 title_content 字段 text 字段
图片路径 image_source.path img_path
VLM 描述 content.content
列表 list_items 数组 无(直接文本)
表格标题 table_caption(列表格式) table_caption(字符串)
bbox 所有元素都有 仅部分元素

V2 解析已处理的类型

  • title:读取 title_contentPDF回退 paragraph_contentDOCX
  • paragraph:读取 paragraph_content
  • table:提取 table_caption(列表格式解析)、table_footnoteimage_source.pathtable_nest_level
  • image / chart:提取 image_source.path(回退 img_path、VLM 描述、chart_markdown、sub_type、caption列表格式
  • equation:读取 text
  • list:拼接 list_items 为段落文本

14.3 DOCX vs PDF 后端差异

云端 MinerU 对 DOCX 和 PDF 使用不同的解析后端,导致 V2 输出有显著差异:

特征 DOCXoffice 后端) PDFhybrid/VLM 后端)
bbox 所有元素都有
title type/level 无(仅 bold style H1/H2/H3
VLM 描述 图片/图表有视觉描述
chart_markdown 图表有数据表 Markdown
list_items 结构化列表
sub_type natural_image/bar/line 等
title 字段名 paragraph_content title_content
图片路径 image_source.path image_source.path

策略DOCX 的标题层级由本地 heading_rules.py 引擎基于文本模式补充推断PDF 的标题层级直接使用 MinerU 提供的 text_level