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

1073 lines
48 KiB
Markdown
Raw Permalink 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 知识库服务从文档解析入库到 LLM 回答的完整数据流,供开发排查和系统优化参考。
---
## 目录
1. [整体架构](#一整体架构)
2. [文档解析与入库流程](#二文档解析与入库流程)
3. [ChromaDB 存储字段详解](#三chromadb-存储字段详解)
4. [BM25 索引两种实现](#四bm25-索引两种实现)
5. [检索管线完整数据流](#五检索管线完整数据流)
6. [distances / scores 语义变化](#六distances--scores-语义变化)
7. [路由层处理流程](#七路由层处理流程)
8. [三重救援机制详解](#八三重救援机制详解)
9. [图片召回与选择](#九图片召回与选择)
10. [返回格式与溯源信息](#十返回格式与溯源信息)
11. [配置项完整列表](#十一配置项完整列表)
12. [单/多知识库路径说明](#十二单多知识库路径说明)
13. [切片策略与检索策略兼容性分析](#十三切片策略与检索策略兼容性分析)
14. [MinerU 解析策略](#十四mineru-解析策略)
---
## 一、整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 文档入库流程 │
│ │
│ 文件上传 → MinerU 解析 → MinerUChunk → 语义增强 → ChromaDB 存储 │
│ ↓ │
│ BM25 索引构建 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 检索问答流程 │
│ │
│ 用户提问 │
│ ↓ │
│ 意图分析(改写/子查询/意图分类) │
│ ↓ │
│ 语义缓存检查 ──命中──→ 直接返回缓存答案 │
│ ↓ 未命中 │
│ 混合检索(向量 + BM25 + 图片 + FAQ
│ ↓ │
│ RRF 融合 → 过滤链 → MMR → Rerank → 后处理 → 扩展 → 自适应 TopK │
│ ↓ │
│ search_hybriddistances → scores 转换) │
│ ↓ │
│ 路由层contexts 构建 → 三重救援 → 排序 → 预算截断 │
│ ↓ │
│ Prompt 构建 → LLM 流式生成 → 后处理 → 返回 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 二、文档解析与入库流程
### 2.1 入口方法
```python
# 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` 数据结构**
```python
@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/chart`chunk.image_path`
2. 表格的图片形式type=table 有 img_path`chunk.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_id` 与 `ids` 重复存储。`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() 返回** | `Dict`ChromaDB 兼容格式) | `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 返回值做了兼容转换:
```python
# 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 入口方法
```python
# 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 特殊键在管线中的传递
以下键在过滤/截断方法中需要手动复制(否则会丢失):
```python
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` 并行查询各向量库:
```python
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 转换
```python
# 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 中的距离→相似度转换
```python
# 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 构建关键转换
```python
# 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_FLOOR`0.06),不会高于 rerank 正常通过的切片
- 匹配使用 `meta.chunk_id`(不带 collection 前缀),与 BM25 top3 的 `id` 字段匹配
**数据来源**`_bm25_top3``engine.search_knowledge` 在 BM25 搜索后保存,格式为:
```python
[{'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.35`dist=0.65 | `CLUSTER_RESCUE_FLOOR = 0.06` |
| 覆盖范围 | 聚类切片在 rerank 之前就能被后续步骤看到 | 最终安全网,确保不遗漏 |
> **设计意图**引擎层提升分数较高0.35 对应 dist=0.65使得这些切片能在后续的扩展步骤中被当作种子。路由层是最终安全网分数较低0.06)仅确保通过 min_score 过滤。
---
## 九、图片召回与选择
### 9.1 独立图片检索P0 通道)
图片/图表切片走独立检索通道,不被文本切片挤占名额:
```python
image_recall_k = max(5, top_k // 2) # 图片独立召回数量
image_results = _search_image_chunks(query_vector, image_recall_k, where_filter)
```
- 仅检索 `chunk_type``image``chart``table` 的切片
- 多知识库模式下对每个 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
```json
{
"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_index`core/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_index`core/bm25_index.py仅在此路径使用
### 12.4 独立检索路径
`knowledge/search.py``SearchMixin``SearchResult` 是独立于 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
```
**合并规则**
- 标题 chunk`text_level > 0`)刷新缓冲并开始新合并组
- 正文 chunk 若 `< min_merge_size`100字则并入缓冲
- 正文 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_chunks``engine.py:1334`)在 section 过滤时使用**精确匹配**
```python
where_filter = {"$and": [{"source": source}, {"section": section}]}
```
当种子是父级标题(如 `section = "三、做好货源投放前的基础工作"`)时:
- 子标题下的正文切片 `section = "三 > (一)货源投放要求 > ..."` **不匹配**
- 回退到 source-only 模式,从整个文档中拉取相邻 `chunk_index` 的切片
- 对大文档,回退模式可能拉取到不相关的切片
**影响**:父级标题被检索到时,无法通过 section 过滤拉取其子节内容,降低了上下文完整性。
#### 失配 3~~`text_level` 未传入检索管线~~ ✅ 已修复
**原问题**`text_level`(标题级别 0/1/2/3在切片阶段被精确计算但未存入 ChromaDB metadata检索管线无法感知标题层级。
**修复**`knowledge/manager.py` 的 metadata 构建处已新增 `text_level` 字段:
```python
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_LEVELS``1` 调整为 `2`
```python
CLUSTER_SECTION_PREFIX_LEVELS = 2 # section_path 归一化保留的层级数(按章节前两级分组,提升聚类精确度)
```
**当前状态**:归一化到前两级(如 `(二)货源投放要求 > 7.关于主导品规投放`聚类信号精确度提升。对深层嵌套文档3 级及以上 section的影响已通过 `CLUSTER_MAX_SECTIONS=3``CLUSTER_MIN_TYPES=2` 约束。
#### 失配 5预算构建器中标题切片的开销轻微
**问题**`_build_context_with_budget``chat_routes.py:866`)按 `(source, section)` 分组后,每个组添加 `━ {section} ━` 分隔行。纯标题切片形成单例组:
```
━ 三、做好货源投放前的基础工作 ━ ← ~25字符开销
三、做好货源投放前的基础工作 ← ~14字符内容信息量≈0
```
**影响**:约 40 字符的预算被浪费在无信息量的组上。在 `CONTEXT_MAX_CHARS = 8000` 的预算下,少量标题切片影响可控,但 2.docx 的 61 个标题切片如果被拉入就会累积显著开销。
#### 失配 6MMR 去重对标题切片的过度消除(模式相关)
**问题**Embedding-based MMR`MMR_USE_EMBEDDING=True`)模式下,标题切片因 Embedding 高度相似而相互惩罚。Jaccard 模式(`MMR_USE_EMBEDDING=False`,当前设置)因词级分词有一定区分度,问题较轻。
**影响**:当标题切片恰好是某子章节的唯一入口时,被 MMR 消除后该子章节在检索结果中完全丢失。当前使用 Jaccard 模式,此问题暂未触发。
#### 失配 7~~3.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 过滤从精确匹配改为前缀匹配:
```python
# 当前:精确匹配
{"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_description``chart_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_content`PDF回退 `paragraph_content`DOCX
- `paragraph`:读取 `paragraph_content`
- `table`:提取 `table_caption`(列表格式解析)、`table_footnote``image_source.path``table_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`