- RAG数据流程.md: 更新模型配置为 deepseek-v4-flash,补充 v7.1 变更 - 新增十二~十六章节:缓存系统、图片选择管线、VLM 懒加载、 文档解析与切片、性能插桩 - 待解决风险项.md: 记录 7 个已识别但未实施的风险项
917 lines
34 KiB
Markdown
917 lines
34 KiB
Markdown
# 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-plus(VLM) │
|
||
│ 流式输出 · 引用标注 │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ 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 API(qwen3-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 采用文本相似度模式进行 MMR(Maximal 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 流式生成
|
||
|
||
使用 SSE(Server-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` 非空 | VLM(qwen-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
|
||
```
|