Files
rag/docs/RAG数据流程.md
lacerate551 475900e103 docs: RAG数据流程新增缓存/图片/VLM/切片/性能章节 + 待解决风险项
- RAG数据流程.md: 更新模型配置为 deepseek-v4-flash,补充 v7.1 变更
- 新增十二~十六章节:缓存系统、图片选择管线、VLM 懒加载、
  文档解析与切片、性能插桩
- 待解决风险项.md: 记录 7 个已识别但未实施的风险项
2026-06-21 20:25:25 +08:00

917 lines
34 KiB
Markdown
Raw 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 数据流程
> 本文档基于 v7.0.0 架构,详细梳理 RAG 系统从用户查询到回答生成的完整数据流。
> 涵盖意图分析、混合检索、云端重排序、MMR 去重、上下文构建、LLM 生成和引用溯源等核心环节。
---
## 一、概述
### 1.1 检索管线总览
```
用户查询
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. 意图分析 (intent_analyzer) │
│ 问题改写 · 指代消解 · 是否需要检索 · 重复提问检测 │
│ 模型qwen-turbo │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. 混合检索 (engine.search_knowledge) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 向量检索 │ │ BM25 检索 │ │
│ │ (ChromaDB) │ │ (关键词匹配) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ └────────┬──────────┘ │
│ ▼ │
│ RRF 融合 (Reciprocal Rank Fusion) │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. 云端 Rerank (DashScope API) │
│ 模型qwen3-rerank │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. MMR 去重 (文本 Jaccard 模式) │
│ MMR_USE_EMBEDDING=false │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 5. 上下文扩展 + 图片选择 │
│ 上下文增强 · 图文关联补充 · 懒加载 VLM 描述 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 6. LLM 生成 (AgenticRAG 引擎) │
│ 模型qwen3.6-flash主 LLM/ qwen-vl-plusVLM
│ 流式输出 · 引用标注 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 7. 返回结果 │
│ { answer, sources, images } │
└─────────────────────────────────────────────────────────────────────────┘
```
### 1.2 模型配置一览
| 用途 | 模型 | 说明 |
|------|------|------|
| 主 LLM | deepseek-v4-flash | 回答生成、上下文理解 |
| 意图分析 | deepseek-v4-flash | JSON 结构化输出(`INTENT_RESPONSE_FORMAT=True` |
| VLM | qwen-vl-plus | 图片理解与描述生成 |
| 云端重排序 | qwen3-rerank | DashScope API 调用 |
| 本地重排序 | bge-reranker-base | ONNX Runtime用于图片 CrossEncoder 二次评分 |
| Embedding | bge-base-zh-v1.5 | 文本向量化 |
### 1.3 架构变更要点
- **Reranker**:从本地 BGE-reranker 切换为云端 DashScope APIqwen3-rerank本地 reranker 保留用于图片 CrossEncoder 二次评分
- **MMR 去重**:使用文本相似度模式(`MMR_USE_EMBEDDING=false`),基于 Jaccard 系数
- **Agentic 引擎**拆分为多个子模块agentic_search / agentic_answer / agentic_citation 等)
- **Graph RAG**:模块已清空,不再使用
**v7.1 新增变更**
- **性能插桩**`/rag` 流式响应新增 `stages_ms` 字段,记录 8 个管线阶段耗时intent / cache / search / vlm / image / rescue / llm / post
- **CrossEncoder 图片二次评分**`select_images` 最终排序前新增 P3 阶段,用本地 bge-reranker-base 对 `(query, vlm_desc)` 做语义精排,映射为 ±5 分调整量CE 负分图片直接剔除
- **意图分析 JSON 强制输出**:新增 `INTENT_RESPONSE_FORMAT` 配置开关,推理模型需设为 False
- **缓存管理端点**:新增 `POST /cache/clear` 一键清除精确缓存 + 语义缓存
---
## 二、请求入口
### 2.1 API 路由
**入口文件**`api/chat_routes.py`
用户通过 API 发送查询请求,由 `generate()` 函数统一调度:
```
POST /rag 或 POST /chat
Body: { "query": "用户问题", "kb_name": "知识库名称" }
```
`generate()` 的职责:
1. 调用意图分析模块,获取改写后的查询与检索决策
2. 若需要检索,调用混合检索管线
3. 执行图片选择与上下文构建
4. 调用 LLM 生成回答(流式输出)
5. 组装最终响应(回答 + 引用来源 + 图片)
### 2.2 请求数据结构
```python
# 请求
{
"query": "蓄水以来逐年发电量",
"kb_name": "public_kb",
"history": [...] # 可选:对话历史
}
# 响应
{
"type": "finish",
"answer": "完整回答文本",
"sources": [...], # 引用来源列表
"images": [...] # 精选图片列表
}
```
---
## 三、意图分析
**入口文件**`core/intent_analyzer.py`
**使用模型**qwen-turbo轻量快速
### 3.1 核心功能
意图分析器在检索前对用户查询进行预处理,输出结构化决策:
```python
@dataclass
class IntentAnalysis:
rewritten_query: str # 改写后的查询(指代消解、省略补全)
use_context: bool # 是否使用历史上下文
need_retrieval: bool # 是否需要检索知识库
```
### 3.2 处理逻辑
| 功能 | 说明 |
|------|------|
| 问题改写 | 指代消解("它" → 具体实体)、省略补全(补全缺失主语) |
| 上下文判断 | 根据对话历史决定是否需要前文信息 |
| 检索决策 | 判断是否需要查询知识库(闲聊类问题可跳过) |
| 重复提问检测 | 用户重复提问相同问题时,强制设置 `need_retrieval=true` |
### 3.3 重复提问规则
当用户重复提问相同或相似问题时,意图分析器必须设置 `need_retrieval = true`。原因:用户可能对之前的回答不满意,或之前的回答包含错误信息,需要重新检索以获取更准确的结果。
---
## 四、检索阶段
**入口文件**`core/engine.py`
**核心函数**`search_knowledge()`
### 4.1 查询缓存检查
检索前先检查缓存,命中则直接返回历史结果,避免重复计算:
```python
cached = cache.get_query_result(query, kb_name)
if cached:
return cached
```
### 4.2 向量检索
使用 embedding 模型将查询编码为向量,在 ChromaDB 中执行 ANN 检索:
```python
query_vector = embedding_model.encode(query)
text_results = collection.query(
query_embeddings=[query_vector],
n_results=100 # recall_k召回候选数量
)
```
**图片独立召回**:同时对图片切片进行独立检索,保证图片有足够的召回机会:
```python
image_results = collection.query(
query_embeddings=[query_vector],
where={'chunk_type': {'$in': ['image', 'chart', 'table']}},
n_results=5
)
```
### 4.3 BM25 关键词检索
基于 BM25 算法执行关键词匹配检索,弥补向量检索在精确匹配上的不足:
```python
bm25_results = bm25_index.search(query, top_k=100)
```
### 4.4 RRF 融合
使用 Reciprocal Rank Fusion 算法将向量检索和 BM25 检索的结果进行融合排序:
```python
fused_results = reciprocal_rank_fusion(
[text_results, image_results, bm25_results],
weights=[VECTOR_WEIGHT, IMAGE_WEIGHT, BM25_WEIGHT]
)
```
**融合参数**
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `recall_k` | 100 | 各通道召回候选数量 |
| `VECTOR_WEIGHT` | 0.6 | 向量检索权重 |
| `BM25_WEIGHT` | 0.4 | BM25 检索权重 |
---
## 五、重排序
### 5.1 云端 Rerank
**v7.0.0 变更**:从本地 BGE-reranker 切换为云端 DashScope API。
**调用模型**qwen3-rerank
RRF 融合后的候选结果通过云端 Reranker 进行精排,基于查询与文档的语义相关性重新打分排序:
```python
reranked = rerank_results(
query,
fused_results,
model="qwen3-rerank", # DashScope API
top_k=15
)
```
### 5.2 与旧版的区别
| 对比项 | 旧版(本地 BGE-reranker | v7.0.0(云端 qwen3-rerank |
|--------|--------------------------|---------------------------|
| 部署方式 | 本地模型加载 | DashScope API 远程调用 |
| 资源占用 | 需要 GPU 显存 | 无本地资源消耗 |
| 模型能力 | BGE-reranker较小 | qwen3-rerank更强 |
| 延迟 | 低(本地推理) | 中等(网络往返) |
---
## 六、MMR 去重
### 6.1 配置
v7.0.0 采用文本相似度模式进行 MMRMaximal Marginal Relevance去重
```
MMR_USE_EMBEDDING = false
```
### 6.2 工作原理
使用文本 Jaccard 相似度(而非 embedding 向量余弦相似度)来衡量候选文档之间的重复程度:
```python
def _apply_mmr(query, candidates, top_k=30, lambda_param=0.7):
"""
MMR 去重:在相关性和多样性之间取得平衡
lambda_param=0.7: 70% 权重给相关性30% 权重给多样性
相似度度量:文本 Jaccard 系数(基于词集合交集/并集)
"""
selected = []
for candidate in candidates:
if not selected:
selected.append(candidate)
continue
# Jaccard 相似度(文本模式)
max_sim = max(
jaccard_similarity(candidate.tokens, s.tokens)
for s in selected
)
mmr_score = lambda_param * relevance - (1 - lambda_param) * max_sim
if mmr_score > threshold:
selected.append(candidate)
return selected[:top_k]
```
### 6.3 选择文本模式的原因
- **速度更快**:无需计算 embedding 向量之间的余弦相似度
- **效果直观**Jaccard 系数直接反映文本内容的重叠程度
- **避免向量偏差**embedding 模型可能对格式化内容(如图片描述)产生不准确的相似度
---
## 七、上下文构建
### 7.1 检索结果处理
**入口文件**`api/chat_routes.py`
将检索结果转换为 LLM 可用的上下文列表:
```python
contexts = []
for result in search_results:
meta = result['metadata']
if meta['chunk_type'] in ('image', 'chart'):
# 图片切片:使用完整描述(而非轻量描述)
doc = meta.get('full_description', result['document'])
else:
doc = result['document']
contexts.append({'doc': doc, 'meta': meta})
```
### 7.2 懒加载增强
对没有 VLM 描述的图片切片,按需调用 VLM 生成更精准的语义描述:
```python
enhance_retrieved_chunks(contexts, query, kb_name)
# 对缺少 VLM 描述的图片 → 调用 qwen-vl-plus 生成描述
```
### 7.3 图片选择
**核心函数**`select_images()`
从检索结果中筛选与查询最相关的图片:
**步骤一:意图检测**
| 查询类型 | 参数调整 |
|----------|----------|
| 精确图号查询("图2.3" | `MAX_IMAGES=2, MIN_SCORE=5.0` |
| 数据驱动(检索含图片切片) | `MAX_IMAGES=5, MIN_SCORE=2.0` |
| 文本引用图表 | `MAX_IMAGES=3, MIN_SCORE=2.0` |
| 普通查询 | `MAX_IMAGES=2, MIN_SCORE=3.0` |
表格嵌入图片时 `MAX_IMAGES` 可扩展至 15。
**步骤二:提取图表引用**
从 top 5 文本块中提取 "见图2.3"、"如表2.2" 等引用,建立图号与来源文件的映射:
```python
referenced_figures = {'2.3': {'source_file': 'xxx.pdf'}, ...}
```
**步骤三:图片相关性打分**
`score_image_relevance()` 打分规则:
| 匹配项 | 加分 |
|--------|------|
| 图号精确匹配(查询中有"图2.3" | +10 分 |
| 表号精确匹配 | +10 分 |
| 关键词匹配("发电量"等) | +2 分/个,上限 8 |
| 字符重叠 | +0.2 分/字符,上限 3 |
| 章节匹配 | +1.5 分 |
| 图片类型chart > image | +2 / +1 分 |
**步骤四VLM 与章节关联调整**
| 条件 | 调整 |
|------|------|
| VLM 描述与查询相关(`_check_vlm_relevance >= 0.5` | +2 分 |
| VLM 描述与查询不相关(`< 0.3` | -3 分 |
| 图片章节与主检索不匹配(`section_similarity < 0.3` | -5 分 |
| 图号/表号被文本引用 + 章节匹配 | +8~+13 分 |
| 来源文件匹配 primary_sources | +2 分 |
**步骤五CrossEncoder 语义精排P3**
对所有过线候选图片,用本地 bge-reranker-base 对 `(query, vlm_desc_or_caption)` 做二次评分:
```python
ce_scores = engine.reranker.predict([(query, desc) for desc in descriptions])
# 映射ce_score > 0 → adjustment ∈ (0, +5]ce_score < 0 → adjustment ∈ [-5, 0)
img['score'] += adjustment
# CE 负分图片直接剔除
```
**步骤六:后置过滤**
`_filter_images_by_answer()` 根据 LLM 回答内容反向筛选图片,过滤回答中未提及的无关图片。
**步骤七:返回 top N 图片**
```python
scored_images.sort(key=lambda x: x['score'], reverse=True)
return scored_images[:MAX_IMAGES]
```
### 7.4 构建 LLM Prompt
```python
# 文本上下文
context_text = "\n\n".join([ctx['doc'] for ctx in contexts[:5]])
# 图片信息
if selected_images:
image_info = "【可用图片】\n" + 图片描述列表
# 最终上下文
enhanced_context = context_text + image_info
```
---
## 八、LLM 生成
### 8.1 AgenticRAG 引擎
v7.0.0 将 Agentic 引擎拆分为独立的子模块,各司其职:
| 子模块 | 职责 |
|--------|------|
| `agentic_search.py` | 检索调度:管理多轮检索、查询分解 |
| `agentic_answer.py` | 回答生成:基于上下文生成最终回答 |
| `agentic_citation.py` | 引用标注:在回答中插入来源引用标记 |
| `agentic_context.py` | 上下文管理:上下文窗口控制、截断策略 |
| `agentic_query.py` | 查询处理:查询改写、多查询生成 |
| `agentic_media.py` | 多媒体处理:图片理解、表格解析 |
| `agentic_quality.py` | 质量控制:回答质量评估、幻觉检测 |
| `agentic_meta.py` | 元数据管理:知识库信息、检索统计 |
### 8.2 流式生成
使用 SSEServer-Sent Events实现流式输出
```python
for token in engine.generate_answer_stream(query, enhanced_context):
yield token # 逐 token 推送给前端
```
**主 LLM 模型**qwen3.6-flash
**VLM 模型**qwen-vl-plus处理图片理解任务
### 8.3 回答结构
```python
{
"type": "finish",
"answer": "根据蓄水以来的统计数据,三峡电站逐年发电量呈现波动上升趋势...",
"sources": [
{
"source": "三峡公报_2022.pdf",
"page": 12,
"section": "综述 > 2.3 发电",
"chunk_id": "三峡公报_text_24"
}
],
"images": [
{
"id": "ab77281e7913.jpg",
"url": "/images/ab77281e7913.jpg",
"type": "chart",
"source": "三峡公报_2022.pdf",
"page": 12,
"description": "图2.3 柱状图2003-2022年逐年发电量"
}
]
}
```
---
## 九、引用溯源
### 9.1 引用标注机制
`agentic_citation.py` 负责在回答中插入引用标记,将回答内容与知识库来源关联:
```
根据统计数据[1]2022年三峡电站年度发电量为787.90亿千瓦时[2]。
[1] 来源三峡公报_2022.pdf第12页综述 > 2.3 发电
[2] 来源三峡公报_2022.pdf第15页表2.1
```
### 9.2 引用数据来源
每个引用标记对应检索结果中的一个切片,包含:
| 字段 | 说明 |
|------|------|
| `source` | 源文件名 |
| `page` | 页码 |
| `section` | 章节路径 |
| `chunk_id` | 切片唯一标识 |
| `chunk_type` | 类型text / table / image / chart |
### 9.3 前端引用跳转
前端解析回答中的引用标记(如 `[1]`),渲染为可点击的链接,点击后跳转到对应的来源文件或页面。
---
## 十、文档入库流程(补充参考)
> 入库流程为检索提供数据基础,以下简要说明关键环节。
### 10.1 文档解析
**入口文件**`parsers/mineru_parser.py`
**核心函数**`parse_with_mineru()`
MinerU 解析 PDF/Word/Excel 文件,输出结构化内容:
```
.data/mineru_temp/{file_hash}/
├── auto/
│ ├── {doc_name}.md # Markdown 内容
│ ├── {doc_name}_content_list.json # 结构化内容列表(核心)
│ └── images/ # 提取的图片文件
```
**content_list.json 中的条目类型**
| item_type | 处理方式 | 关键字段 |
|-----------|----------|----------|
| `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 |
### 10.2 MinerUChunk 数据结构
```python
@dataclass
class MinerUChunk:
content: str # 文本内容
chunk_type: str # 类型: text, table, image, chart
page_start: int = 1 # 起始页码
page_end: int = 1 # 结束页码
text_level: int = 0 # 标题级别 (0=正文, 1=h1, 2=h2...)
title: str = "" # 标题文本
section_path: str = "" # 章节路径
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 # 关联图片列表
context_before: str = "" # 图片前的文本上下文
context_after: str = "" # 图片后的文本上下文
```
### 10.3 切片入库
**入口文件**`knowledge/manager.py`
**核心函数**`add_file_to_kb()`
```
MinerUChunk 列表
├── 文本块 → 计算 embedding → 存入 ChromaDB
├── 表格块 → 生成语义增强摘要 → 存入 ChromaDB
└── 图片块 → VLM 缓存检查 → 生成描述 → 存入 ChromaDB
```
**图片描述策略**
1. 优先使用 VLM 缓存描述(语义更丰富,由 qwen-vl-plus 生成)
2. 若无缓存,生成轻量描述(基于文件名 + 章节路径 + 上下文)
轻量描述示例:
```
图表图2.3,位于「综述 > 2.3发电」第12页
前文受长江流域性严重枯水影响2022年三峡电站年度发电量为787.90亿千瓦时...
后文2.4航运 三峡船闸和葛洲坝船闸实行统一调度...
```
VLM 描述示例(更精准):
```
图2.3 柱状图 主要内容描述该柱状图展示了2003年至2022年每年的发电量单位亿千瓦时
发电量在2003年为86.07亿千瓦时随后逐年波动上升至2020年达到峰值1118.02亿千瓦时...
```
### 10.4 ChromaDB 存储结构
| 字段 | 类型 | 说明 |
|------|------|------|
| `ids` | str | 切片唯一标识,如 `doc.pdf_text_24` |
| `embeddings` | List[float] | 向量表示 |
| `documents` | str | 切片内容(文本/描述/摘要) |
| `metadatas` | dict | 元数据(见下表) |
**图片切片 metadata 示例**
```python
{
'source': '三峡公报_2022.pdf',
'page': 12,
'chunk_type': 'chart',
'section': '综述 > 2.3发电',
'figure_number': '2.3',
'image_path': 'ab77281e7913.jpg',
'has_vlm_desc': True,
'preview': '图2.3 柱状图 主要内容...'
}
```
---
## 十一、关键文件索引
| 文件 | 职责 | 关键函数/类 |
|------|------|-------------|
| `api/chat_routes.py` | API 路由与请求调度 | `generate()`, `select_images()`, `score_image_relevance()` |
| `core/intent_analyzer.py` | 意图分析 | `analyze()`, `IntentAnalysis` |
| `core/engine.py` | 检索引擎核心 | `search_knowledge()`, `reciprocal_rank_fusion()`, `rerank_results()` |
| `core/agentic_search.py` | 检索调度 | 多轮检索、查询分解 |
| `core/agentic_answer.py` | 回答生成 | 基于上下文生成最终回答 |
| `core/agentic_citation.py` | 引用标注 | 来源引用标记插入 |
| `core/agentic_context.py` | 上下文管理 | 上下文窗口控制、截断 |
| `core/agentic_query.py` | 查询处理 | 查询改写、多查询生成 |
| `core/agentic_media.py` | 多媒体处理 | 图片理解、表格解析 |
| `core/agentic_quality.py` | 质量控制 | 回答质量评估、幻觉检测 |
| `core/agentic_meta.py` | 元数据管理 | 知识库信息、检索统计 |
| `parsers/mineru_parser.py` | 文档解析 | `parse_with_mineru()`, `MinerUChunk` |
| `knowledge/manager.py` | 知识库管理 | `add_file_to_kb()`, `generate_lightweight_image_description()` |
| `knowledge/lazy_enhance.py` | 懒加载增强 | `lazy_vlm_description()`, `enhance_retrieved_chunks()` |
| `core/cache.py` | 三层精确缓存 | `RAGCacheManager`, `get_cache_manager()` |
| `core/semantic_cache.py` | 语义缓存 | `SemanticCache`, `get_semantic_cache()` |
| `knowledge/image_cleanup.py` | 孤儿文件清理 | `cleanup_image_orphans()`, `collect_referenced_images()` |
| `cleanup_orphans.py` | 孤儿文件清理脚本 | `--force` 执行删除,默认 dry-run |
| `sync_vlm_cache.py` | VLM 缓存同步 | `--re-embed` 强制重算 embedding |
| `eval_image_retrieval.py` | 图片检索评测 | 检索层 + 选择层分层评测 |
---
## 十二、缓存系统
### 12.1 三层精确缓存core/cache.py
`RAGCacheManager` 管理三个独立的 `LRUCache` 实例:
| 缓存层 | Key 构造 | TTL | 容量 | kb_version 关联 |
|--------|----------|-----|------|-----------------|
| Query Cache | `MD5(query:kb_name:kb_version)` | 1h | 500 | ✅ 版本失效 |
| Embedding Cache | `MD5(emb:{text})` | 24h | 2000 | ✅ 版本失效 |
| Rerank Cache | `MD5(rerank:{query}:{sorted_doc_ids})` | 1h | 1000 | ❌ 变更时全清空 |
**知识库版本失效机制**
```python
def increment_kb_version(kb_name):
old_version = self._kb_versions.get(kb_name, 0)
self._kb_versions[kb_name] = old_version + 1
# 失效旧版本
self.query_cache.invalidate_by_version(old_version)
self.embedding_cache.invalidate_by_version(old_version)
self.rerank_cache.clear() # Rerank 无 kb_version全量清空
```
`sync.py` 文档变更时调用 `increment_kb_version()` 触发失效。
**写入保护**`CACHE_MIN_SCORE = 0.3`,检索结果最高分低于此值时不写缓存,避免低质量结果被缓存。
### 12.2 语义缓存core/semantic_cache.py
`SemanticCache` 使用 FAISS 向量索引实现语义级缓存:
```
查找query_embedding → FAISS ANN 搜索 → 余弦相似度 > 0.92 → 候选
二次验证Jaccard(query, cached._raw_query) >= 0.5 → 确认命中
存储query_embedding + result → FAISS add
```
**Jaccard 二次验证**(字符级):
```python
set_a = set(query) # 字符集合
set_b = set(cached_query)
similarity = |intersection| / |union|
```
**Intent 与 RAG 共用 FAISS 索引**,通过 `cache_type` 字段隔离:
| 缓存类型 | cache_type 值 | Embedding 输入 | 读取条件 |
|----------|---------------|---------------|---------|
| RAG 回答 | `"rag_answer"` | `embedding(query|collections)` | `cache_type == "rag_answer"` |
| 意图分析 | `"intent_analysis"` | `embedding(query)` | `cache_type != "rag_answer"` |
**容量管理**:默认 `max_size=10000`,达到上限时 `self.clear()` 全清空(非 LRU 淘汰)。
**Intent 精确缓存**(独立于 FAISS`_exact_cache: Dict[str, IntentAnalysis]`key = `query + 最近2条历史摘要`,上限 500 条,无 TTL满后停止接收新条目。
### 12.3 缓存命中链路
一个 `/rag` 请求依次经过以下缓存检查点:
```
请求进入 chat_routes.rag()
├─ ① 语义缓存读取FAISS
│ key = embedding(retrieval_query|collections)
│ 要求 cache_type == "rag_answer"
│ 二次验证: Jaccard >= 0.5
│ ├─ 命中 → 直接流式返回缓存答案,流程结束
│ └─ 未命中 → 继续
├─ ② intent_analyzer.analyze()
│ ├─ 精确缓存: key = query + 历史摘要
│ ├─ 语义缓存: key = embedding(query)cache_type != "rag_answer"
│ └─ 未命中 → 调 LLM → 写入两层缓存
├─ ③ engine.search()
│ ├─ Query Cache: MD5(query:kb_name:kb_version)
│ ├─ Embedding Cache: MD5(emb:{text})
│ └─ Rerank Cache: MD5(rerank:{query}:{sorted_doc_ids})
├─ ④ LLM 生成回答
└─ ⑤ 语义缓存写入FAISS
存入 { cache_type: "rag_answer", answer, sources, images, _raw_query }
```
**缓存清除**`POST /cache/clear` 端点清除 Query/Embedding/Rerank 缓存 + 语义缓存(需网关认证)。
---
## 十三、图片选择管线
### 13.1 评分流程select_images
完整的图片选择管线包含 7 个阶段:
```
score_image_relevance() ← P1: 基础分(关键词/图号/章节/类型)
VLM 相关性调整 ← _check_vlm_relevance() -3 / +2 分
章节关联惩罚 ← section_similarity < 0.3 → -5 分
图号/表号引用加分 ← 文本引用匹配 → +8~+13 分
来源文件加分 ← primary_sources 匹配 → +2 分
MIN_SCORE 过滤 ← 动态阈值 2.0~5.0
CrossEncoder 二次评分 ← P3: 本地 reranker 对 (query, desc) 精排
↓ 正相关 +5负相关 -5 且直接剔除
MAX_IMAGES 预算控制 ← 动态上限 2~15 张
_filter_images_by_answer() ← 后置过滤LLM 回答关键词重叠
```
### 13.2 图片召回保障
图片切片在检索层面临 CrossEncoder 系统性低分0.002~0.08 vs 文本 0.3~0.9),有多重保障机制:
| 机制 | 位置 | 说明 |
|------|------|------|
| 图片独立召回 | `engine._search_image_chunks()` | 对 image/chart 切片单独查询 ChromaDB |
| `_image_boost` 标记 | `chat_routes.py` | 图片意图查询时,为匹配的图片切片打 1.5x/2.0x boost 标记 |
| 章节聚类救援 | `_rescue_section_cluster()` | 当整章节切片全被 rerank 压制时,保底分配分数 |
| BM25 分歧救援 | `_rescue_bm25_divergence()` | BM25 高排名但 rerank 低分的切片被恢复 |
| 词法匹配救援 | `_rescue_lexical_match()` | 切片文本精确包含查询关键词时提升分数 |
---
## 十四、VLM 懒加载增强
**入口文件**`knowledge/lazy_enhance.py`
**触发位置**`chat_routes.py` `generate()` 函数
### 14.1 触发条件
| 切片类型 | 触发条件 | 调用模型 |
|----------|---------|---------|
| image / chart | `has_vlm_desc=False``image_path` 非空 | VLMqwen-vl-plus |
| table | `has_summary=False``score > 0.7` | LLM表格摘要 |
| table + 关联图片 | `has_vlm_desc=False``image_path` 非空 | VLM |
### 14.2 增强流程
```
缓存检查(图片 MD5 哈希 → .data/cache/vlm/{hash}.txt
├─ 缓存命中(内容 ≥ 5 字符)→ 直接返回
└─ 缓存未命中 →
├─ 调用 VLM/LLM 生成描述/摘要
├─ 空描述保护(< 5 字符)→ 不写缓存,不更新向量库
├─ 写入缓存文件
└─ 更新 ChromaDB
├─ metadata: { has_vlm_desc: True, vlm_desc: description }
├─ embedding: 用 VLM 描述重新计算向量(通过 RAGEngine 单例获取模型)
└─ document: 更新为 VLM 描述文本
```
**embedding 模型获取**`KnowledgeBaseManager` 没有 `embedding_model` 属性,通过 `_get_embedding_model()``RAGEngine` 单例获取。
### 14.3 后台异步化
VLM/LLM 调用在后台线程执行,不阻塞主流程:
```python
# chat_routes.py 中
_bg_contexts = copy.deepcopy(contexts) # 深拷贝避免线程竞争
_t = threading.Thread(target=_background_enhance, daemon=True)
_t.start()
# 主流程继续 select_images + LLM 生成
```
后台线程使用 `asyncio.run()` + 60s 超时保护。首次查询时图片使用 caption + section 作为临时描述,后台生成完成后写入 ChromaDB下次查询直接使用缓存。
---
## 十五、文档解析与切片
### 15.1 MinerU 解析流程
**入口文件**`parsers/mineru_parser.py`
支持两种云端 API 模式:
| 模式 | 端点 | 延迟 | 适用场景 |
|------|------|------|---------|
| v4 precise | `/api/v4/` | 较长(需排队) | 高精度 PDF 解析 |
| v1 agent | `/api/v1/agent/` | ~10s | 轻量级文档处理 |
**V2 格式转换**`_parse_v2_content_list`
MinerU 返回的 content_list 被转换为 `MinerUChunk` 对象,提取的字段包括:`_v2_table_type``_v2_table_nest_level``_v2_list_type``sub_type`(部分字段在下游切片时被丢弃)。
**标题检测**`parsers/heading_rules.py`
基于规则的多级标题检测,`_validate_level()` 长度守卫防止长文本误判为标题:
| 级别 | 最大长度 | 超出后 |
|------|---------|--------|
| H1 | 40 字符 | 降级为普通文本 |
| H2 | 60 字符 | 降级为普通文本 |
| H3 | 50 字符 | 降级为普通文本 |
`chinese_article` 规则额外限制 `max_length=30``numeric_level1` 排除以标点结尾的误判。
### 15.2 切片后处理
**`_post_process_chunks`** 核心逻辑:
1. **连续标题链合并**:连续 title 类切片合并为一个 chunk保留最高 text_level
2. **body 合并后 text_level 重置**body 合并进 title buffer 后执行 `buffer.text_level = 0`,防止下一个标题被错误合并
3. **元数据写入**
| 字段 | 类型 | 说明 |
|------|------|------|
| `chunk_type` | str | `text` / `title` / `table` / `image` / `chart` |
| `section` | str | 章节路径(如 `综述 > 2.3发电` |
| `text_level` | int | 标题级别0=body, 1=H1, 2=H2, 3=H3 |
| `image_path` | str | 图片文件名 |
| `bbox` | str | 页面坐标(仅 PDF |
| `has_vlm_desc` | bool | 是否已有 VLM 描述 |
---
## 十六、性能插桩
**入口文件**`api/chat_routes.py` `generate()` 函数
### 16.1 各阶段计时
在 SSE 流式生成器中初始化计时字典,记录 8 个管线阶段耗时:
| 阶段 key | 记录时机 | 覆盖范围 |
|----------|----------|----------|
| `intent` | 意图分析完成后 | IntentAnalyzer.analyze() |
| `cache` | 语义缓存检查后 | semantic_cache.lookup() |
| `search` | 混合检索+切片处理完成后 | engine.search() + 上下文提取 |
| `vlm` | VLM 懒加载完成后 | enhance_retrieved_chunks() |
| `image` | select_images 完成后 | 图片选择 |
| `rescue` | 救援管线+上下文构建完成后 | 聚类/BM25/预算 |
| `llm` | LLM 流式生成完成后 | engine.generate_answer_stream() |
| `post` | finish 事件构建完成后 | 答案对齐+debug 事件 |
### 16.2 数据输出
**SSE finish 事件**
```json
{
"type": "finish",
"timing": {
"total_ms": 12345,
"stages_ms": {
"intent": 4200, "cache": 50, "search": 900,
"vlm": 10, "image": 30, "rescue": 200,
"llm": 6800, "post": 150
}
}
}
```
**服务端日志**
```
[性能] 总12345ms | 意图4200 缓存50 检索900 VLM10 图片30 救援200 LLM6800 后处理150
```