Files
rag/docs/RAG检索流程逻辑.md
lacerate551 4afa30a946 docs(rag): 编写完整的 RAG 检索流程逻辑文档
覆盖从 MinerU 解析入库到 LLM 回答的全链路数据流:
- 文档解析与入库流程(MinerU → Chunk → ChromaDB + BM25)
- ChromaDB 存储字段详解(每个 metadata 字段的作用)
- BM25 索引两种实现的差异分析
- distances/scores 语义在各管线阶段的变化
- 检索管线完整数据流图
- 三重救援机制详解(BM25 分歧/词法匹配/章节聚类)
- 配置项完整列表(含死代码标注)
- 单/多知识库路径说明
2026-06-17 19:58:31 +08:00

33 KiB
Raw Blame History

RAG 检索流程逻辑

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


目录

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

一、整体架构

┌─────────────────────────────────────────────────────────────────┐
│                       文档入库流程                                │
│                                                                 │
│  文件上传 → 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 = ""            # 图片后文本上下文

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"

注意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 1 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 格式不兼容