- INTENT_MAX_TOKENS 2048 不再标注为'浪费',推理模型思考链需 ~1000 token - 删除 P2 建议'INTENT_MAX_TOKENS 降低 2048→512'(推理模型不可降) - P0 优化建议标注 deepseek-v4-flash 额度已尽,需寻找替代轻量模型 - Rerank 配置补充云端 xop3qwen8breranker(讯飞云)信息 - §6.4 休眠模块:AgenticRAG 类已在 v4.0 删除,更新描述 - 阶段 7 补充两管线断裂问题:chart_contexts 降门槛、P0 安全网、答案后过滤鸡生蛋问题 - 阶段 9 补充推理模型 content 为空问题和 reasoning_content 回退机制 - 阶段 10 图片后过滤补充 _filter_images_by_answer 函数名
20 KiB
RAG 知识库问答系统 — 性能分析报告
基于 15 题基准测试 + 代码静态分析 | 2026-06-22
基准测试环境:本地 deepseek-v4-flash 模型,服务器使用 mimo-v2.5 时延迟特征可能不同
一、完整请求管线总览
一次 /rag/stream 请求经历 10 个串行阶段,每个阶段必须等待前一阶段完成后才能开始。SSE 流式响应在 LLM 生成阶段才开始产出用户可见内容,前 8 个阶段用户感知为"等待中"。
用户提问
│
├─ 0. 输入校验 & 安全过滤 ~1ms
├─ 1. 意图分析(LLM 调用) ~3500-5000ms ◄── 瓶颈 #1
├─ 2. 语义缓存查找 ~50-200ms
├─ 3. 混合检索(向量+BM25+图片) ~200-600ms
├─ 4. RRF 融合 + Rerank ~300-800ms
├─ 5. MMR 去重 ~5-20ms
├─ 6. 救援管线(3层) ~5-30ms
├─ 7. 图片选择 + VLM 增强 ~10-50ms
├─ 8. 上下文构建 ~5-15ms
├─ 9. LLM 流式生成 ~5000-15000ms ◄── 瓶颈 #2
└─ 10. 后处理(对齐/引用/会话存储) ~50-200ms
总耗时:典型请求 10-20 秒(含流式输出时间),其中 LLM 调用占 80%以上。
二、各阶段详细分析
阶段 1:意图分析 — 最大瓶颈之一
位置:core/intent_analyzer.py → analyze() 方法
功能:一次非流式 LLM 调用,同时完成问题改写、指代消解、意图分类、子查询生成。
执行流程:
精确缓存查找(Dict, O(1))
↓ 未命中
语义缓存查找(FAISS, cosine ≥ 0.92)
↓ 未命中
LLM 调用(非流式,JSON 输出) ◄── 主要耗时
↓
写入双层缓存 + 返回 IntentAnalysis
关键参数:
| 参数 | 服务器值 | 说明 |
|---|---|---|
| INTENT_MODEL | mimo-v2.5 | 与主模型共用,无法使用轻量模型 |
| INTENT_TEMPERATURE | 0.1 | 低温度保证确定性输出 |
| INTENT_MAX_TOKENS | 2048 | 推理模型思考链 ~1000 token + JSON 输出 ~200 token,配置合理 |
| INTENT_HISTORY_WINDOW | 6 | 取最近 6 条历史消息 |
| 精确缓存上限 | 500 条 | 内存 Dict,重启清零 |
| 语义缓存阈值 | 0.92 | 余弦相似度,过于严格则命中率低 |
实测耗时(deepseek-v4-flash):3500-5000ms。mimo-v2.5 因模型更大,预期 4000-7000ms。
瓶颈分析:
- 非流式调用:必须等待完整 JSON 响应才能解析,无法提前返回。这是所有阶段中唯一必须完整等待的 LLM 调用。
- 模型过重:意图分析本质是分类+改写任务,不需要 mimo-v2.5 这样的推理大模型。换用轻量非推理模型可减少 30-40% 延迟,但百炼 deepseek-v4-flash 额度已尽,需寻找其他可用轻量模型。
- MAX_TOKENS 配置合理:mimo-v2.5 是推理模型,思考链消耗 ~1000 token,JSON 输出 ~200 token,2048 是必要预算(此前 1024 导致 content 为空、全部输出进入 reasoning_content 引发解析错误)。
- 缓存命中率低:精确缓存 key 包含历史上下文,同一用户连续问不同问题时不会命中;语义缓存阈值 0.92 过于严格。
阶段 2:语义缓存查找
位置:core/semantic_cache.py
功能:FAISS 向量索引,用 embedding 余弦相似度匹配历史问答,命中则跳过整个检索+生成流程。
查找逻辑:
- 将
"{query}|{collections}"编码为向量 - FAISS IndexFlatIP 搜索 top-1(向量已归一化,内积=余弦)
- 阈值 ≥ 0.92 → 命中
- 验证
cache_type == "rag_answer"(区分意图缓存和问答缓存)
性能:50-200ms(主要是 embedding 编码耗时,FAISS 搜索本身 <1ms)
瓶颈分析:此阶段本身不慢,但受限于 0.92 的高阈值,实际命中率较低。同一个问题的不同表述方式(如"智启平台支持哪些数据源" vs "智启能对接什么数据库")余弦相似度可能在 0.85-0.91 之间,无法命中。降低阈值会引入误命中风险。
阶段 3:混合检索
位置:core/engine.py → search_knowledge()
功能:三路并行召回 + RRF 融合
三路召回:
| 召回路径 | 数量 | 后端 | 耗时 |
|---|---|---|---|
| 向量检索 | recall_k = max(20, 30×3) = 90 | ChromaDB + bge-base-zh-v1.5 (CPU) | 100-300ms |
| BM25 关键词 | top_k=30 | rank_bm25 (jieba 分词) | 10-30ms |
| 图片独立召回 | n_results=5 | ChromaDB,过滤 chunk_type | 20-50ms |
RRF 融合(Reciprocal Rank Fusion):
score = weight / (k + rank + 1),k = 60
动态权重(按查询长度自适应):
alpha = clamp((len(query) - 15) / 35, 0, 1)
vector_weight = 0.3 + 0.4 × alpha # 短查询 0.3,长查询 0.7
bm25_weight = 1.0 - vector_weight
性能:总计 200-600ms。Embedding 编码是主要耗时(CPU 推理),ChromaDB 查询本身很快。
瓶颈分析:
- Embedding 在 CPU 上运行:bge-base-zh-v1.5 编码 90 个候选文档需要 100-200ms。GPU 可降至 10-30ms,但当前服务器未配置 GPU。
- 召回数量偏大:
recall_k=90意味着 embedding 编码量大。实际 Rerank 只取 20 个,多召回的 70 个是浪费。可降低RECALL_MULTIPLIER从 3 到 2。 - BM25 无瓶颈:纯内存计算,jieba 分词+BM25 打分共 10-30ms。
阶段 4:Rerank
位置:core/engine.py → rerank_results()
功能:CrossEncoder 精排,对融合后的候选重打分
服务器配置:RERANK_BACKEND = "local",使用 ONNX 格式的 bge-reranker-base。云端备选为 xop3qwen8breranker(讯飞云 API),RERANK_BACKEND = "cloud" 或 "fallback" 时启用。
流程:
- 检查 Rerank 缓存(MD5 of query + sorted_doc_ids)
- 缓存未命中 → 调用 ONNX 推理,predict(query, doc) × N
- 按分数排序,截取
RERANK_TOP_K=15
性能:
- 缓存命中:<1ms
- 缓存未命中(20 个候选):300-800ms(CPU ONNX 推理)
瓶颈分析:
- CPU 推理:CrossEncoder 是最重的 CPU 计算任务。每个 (query, doc) pair 需要一次 forward pass。20 个候选 × ~30ms/pair ≈ 600ms。
- max_length=512:长文档截断到 512 token,避免 OOM 但可能丢失信息。
- 缓存效果好:同一查询+同一文档集合直接命中缓存。但首次查询必经此阶段。
阶段 5:MMR 去重
位置:core/mmr.py
当前配置:MMR_USE_EMBEDDING = False(文本模式),使用 jieba 词级 Jaccard 相似度
算法:贪心选择,每次取与已选集合最不相似且与查询最相关的候选
性能:5-20ms(纯文本计算,无 embedding 开销)
瓶颈分析:无瓶颈。文本模式 Jaccard 非常轻量。
阶段 6:救援管线(3 层)
位置:api/chat_routes.py 内部函数 + core/engine.py
第一层 — BM25 发散救援:
- 当 BM25 top-3 中的文档被 Rerank 打到极低分时,恢复其分数到
CLUSTER_RESCUE_FLOOR=0.06 - 防止关键词完全匹配但语义分低的文档被丢弃
第二层 — 词法匹配救援:
- 提取查询 bigram,对每个低分文档计算 bigram 匹配率
- 匹配率 > 0.35 → 提升到 floor
- 附带邻居救援:同文档 ±8 个 chunk_index 的邻居也被提升
第三层 — 章节聚类救援(engine.py _section_cluster_boost):
- 按
(source, normalized_section)分组 - 检测"全灭章节":所有成员分数 < min_score
- 要求 ≥ 3 成员 + ≥ 2 种 chunk_type
- 提升最强聚类的 top-3 章节,每节最多 8 个 chunk
性能:5-30ms(纯规则计算)
瓶颈分析:无瓶颈。但在复杂表格/对比查询中,救援管线的效果直接影响回答质量。
阶段 7:图片选择
位置:api/chat_routes.py → select_images()
评分体系(多层叠加):
| 评分因子 | 分值 | 条件 |
|---|---|---|
| 图号精确匹配 | +10.0 | 查询提到"图2.3"且匹配 |
| 表号精确匹配 | +10.0 | 查询提到"表1"且匹配 |
| 关键词匹配 | +2.0/词 | jieba 分词后匹配,上限 +8.0 |
| 字符重叠 | +0.2/字 | 上限 +3.0 |
| 章节匹配 | +1.5/关键词 | 图片章节与查询关键词重叠 |
| 图表类型 | +2.0 (chart) / +1.0 (image) | chunk_type 加分 |
| 引用+章节匹配 | +8.0 + 5.0 | 被文本引用且章节/来源匹配 |
| VLM 相关性 < 0.3 | -3.0 | VLM 描述与查询不相关 |
| VLM 相关性 ≥ 0.5 | +2.0 | VLM 描述与查询相关 |
| 章节距离过远 | -5.0 | section_similarity < 0.3 且非引用 |
动态预算:
| 场景 | MAX_IMAGES | MIN_SCORE |
|---|---|---|
| 精确图号查询 | 2 | 5.0 |
| 检索结果含图片数据 | 5 | 2.0 |
| 文本中有图号引用 | 3 | 2.0 |
| 默认 | 2 | 3.0 |
性能:10-50ms(规则计算 + VLM 相关性检查)
瓶颈分析:图片选择本身不慢。瓶颈在 VLM 描述生成(knowledge/lazy_enhance.py),需要调用 VLM 模型为每张图片生成文字描述。当前设计为懒加载(首次查询时生成并缓存),首次查询可能有 60s 超时。
两管线断裂问题:select_images() 独立于文本上下文管线选择图片,但图片描述可能因 min_score 过滤未进入 LLM 上下文。具体问题链:
- chart_contexts 降门槛:CrossEncoder 对图片/图表打分系统性偏低(0.002-0.08 vs 文本 0.3-0.9),原
min_score=0.05会过滤掉几乎所有图表。已修复为min_score * 0.5 = 0.025。 - P0 安全网:即使降门槛后,图片描述仍可能被
_build_context_with_budget()截断。安全网在 LLM 生成前检查:若selected_images的描述不在context_text中,强制追加缺失描述。日志标记为[P0] 图片描述未完整注入 context。 - 答案后过滤鸡生蛋问题:
_filter_images_by_answer()根据 LLM 答案内容过滤图片。若 LLM 因上下文缺失而回答"未找到某图表",正确图片会被误过滤。P0 安全网可缓解此问题。
阶段 8:上下文构建
位置:api/chat_routes.py → _build_context_with_budget() / _order_text_contexts_for_prompt()
预算控制:
CONTEXT_MAX_CHARS = 8000 # 硬上限(约 4000 token)
CONTEXT_SOFT_LIMIT = 6000 # 软限制(超过后只接受高分 chunk)
MAX_CONTEXT_CHUNKS = 20 # 最大切片数
排序策略:
- 枚举/对比查询:保持原文顺序,注入
━ section ━分隔符 - 普通查询:按 (source, section) 分组 → 组内按 chunk_index 排序 → 组间按 max_rerank_score 排序
- 表格保护:即使分数低于 min_score,只要同章节有高分文本 chunk,表格 chunk 也保留(floor = min_score × 0.3)
性能:5-15ms
瓶颈分析:无瓶颈。但上下文质量直接影响 LLM 生成质量。8000 字符 ≈ 4000 token,对于需要对比多个文档的查询可能不够。
阶段 9:LLM 流式生成 — 最大瓶颈
位置:core/engine.py → generate_answer_stream()
功能:将上下文 + 历史 + 查询组装成 prompt,调用 LLM 流式输出
Prompt 结构:
[System] 你是一个专业的知识库问答助手...(规则指令)
[System] 参考资料:(context, 最多 8000 字符)
[User×N] 历史对话(最多 10 轮)
[User] 当前问题
关键参数:
| 参数 | 服务器值 | 说明 |
|---|---|---|
| MODEL | mimo-v2.5 | 主生成模型 |
| LLM_TEMPERATURE | 0.7 | 较高温度,生成更发散 |
| LLM_MAX_TOKENS | 3000 | 最大输出 token |
| LLM_TOP_P | (未设置) | 默认 1.0 |
实测耗时(deepseek-v4-flash):5000-15000ms(含流式输出)。mimo-v2.5 推理模型预计 8000-20000ms。
瓶颈分析:
- 模型延迟不可控:LLM API 的 TTFT(首 token 时间)和 TPS(token/秒)取决于服务端负载和网络。这是整个管线中唯一无法通过代码优化的瓶颈。
- 推理模型 content 为空问题:mimo-v2.5 是推理模型,
max_tokens不足时思考链消耗全部预算,content为空需从reasoning_content提取内容。当前LLM_MAX_TOKENS=3000足够,但若降低此值需注意思考链预算。 - Prompt 长度影响 TTFT:8000 字符上下文 + 10 轮历史 ≈ 6000-8000 input token。输入越长,TTFT 越高。
- 流式缓解:用户看到第一个 token 的等待时间 = 意图分析 + 检索 + TTFT ≈ 6-12 秒。流式输出减少了感知等待,但总耗时不变。
- MAX_TOKENS=3000 配置合理:推理模型思考链占用部分预算,实际回答通常 500-1500 token,3000 平衡速度与质量。
阶段 10:后处理
功能:答案对齐、图片过滤、引用附加、会话存储、语义缓存写入
子步骤:
| 步骤 | 耗时 | 说明 |
|---|---|---|
| 图号引用提取 | ~5ms | 正则匹配答案中的图/表引用 |
| 图片后过滤 | ~5ms | _filter_images_by_answer(),根据 LLM 答案内容裁剪不相关图片 |
| 引用清理 | ~2ms | 去除 LLM 添加的 [N] 标记 |
| 引用附加 | ~10ms | 添加结构化 [ref:chunk_id] |
| 敏感内容过滤 | ~5ms | prompt_guard 检查 |
| 会话存储 | ~20-100ms | SQLite 写入 |
| 语义缓存写入 | ~10-50ms | FAISS 索引添加 |
总计:50-200ms。无瓶颈。
三、缓存体系分析
五层缓存架构
精确缓存层
├─ Query Cache LRU 500, TTL 1h 最终结果缓存(含 rerank 分数)
├─ Embedding Cache LRU 2000, TTL 24h 文档向量缓存
├─ Rerank Cache LRU 1000, TTL 1h CrossEncoder 分数缓存
└─ Intent Cache Dict 500, 无 TTL 意图分析结果缓存
语义缓存层
└─ FAISS Cache max 10000, 阈值 0.92 向量化相似问答缓存
缓存命中场景
| 场景 | 命中层 | 节省时间 |
|---|---|---|
| 完全相同的问题 + 相同知识库 | Query Cache | 跳过整个检索(节省 ~1s) |
| 相似问题(余弦 ≥ 0.92) | Semantic Cache | 跳过检索 + 生成(节省 ~10s) |
| 相同文档集合被 rerank | Rerank Cache | 跳过 CrossEncoder(节省 ~600ms) |
| 相同文档需要 embedding | Embedding Cache | 跳过编码(节省 ~100ms) |
| 相同问题(意图层面) | Intent Cache | 跳过意图分析 LLM(节省 ~4s) |
缓存问题
- 内存存储,重启清零:所有缓存都在进程内存中,服务重启后第一次请求全部冷启动。
- 语义缓存阈值过高:0.92 的余弦阈值导致换一种说法就命中不了。
- Rerank 缓存不参与 kb_version 失效:文档更新后 rerank 缓存可能返回旧分数。
- 语义缓存淘汰策略粗暴:达到 10000 上限时全量清空(
clear()),不是 LRU。
四、瓶颈总结与优化优先级
耗时分布(典型请求,基于 deepseek-v4-flash 实测)
意图分析 ████████████████████ 25-35% ~4000ms
LLM 生成 ████████████████████████████████ 45-60% ~8000ms
检索+Rerank ████████ 10-15% ~1200ms
其他阶段 ██ 3-5% ~400ms
优化机会排序
| 优先级 | 方向 | 预期收益 | 难度 | 风险 |
|---|---|---|---|---|
| P0 | 意图分析换轻量模型 | 节省 2-3s/请求 | 低 | 低(需寻找可用轻量模型,百炼 deepseek-v4-flash 额度已尽) |
| P0 | 主模型 API 优化(换供应商/批处理) | 节省 3-5s/请求 | 中 | 中(需要评估质量) |
| P1 | 意图分析结果缓存优化 | 重复问题节省 4s | 低 | 低 |
| P1 | 语义缓存阈值调优(0.92→0.88) | 提高命中率,节省 10s | 低 | 中(可能误命中) |
| P2 | RECALL_MULTIPLIER 降低(3→2) | 减少 embedding 编码量 ~30ms | 低 | 低 |
| P2 | LLM_TEMPERATURE 降低(0.7→0.3) | 减少发散,回答更精准 | 低 | 中(可能影响创造性) |
| P3 | Rerank 换 GPU 推理 | 节省 ~500ms | 高(需硬件) | 低 |
| P3 | Embedding 换 GPU 推理 | 节省 ~150ms | 高(需硬件) | 低 |
| P3 | 语义缓存改 LRU 淘汰 | 避免全量清空抖动 | 中 | 低 |
五、配置参数速查表
模型与服务
| 参数 | 当前值 | 说明 |
|---|---|---|
| DASHSCOPE_MODEL | mimo-v2.5 | 主 LLM |
| INTENT_MODEL | mimo-v2.5 | 意图分析模型 |
| VLM_MODEL | mimo-v2.5 | 图片描述模型 |
| EMBEDDING_MODEL_PATH | bge-base-zh-v1.5 | 本地 embedding |
| RERANK_BACKEND | local | 本地 ONNX rerank(可选 cloud/fallback) |
| RERANK_CLOUD_MODEL | xop3qwen8breranker | 云端 rerank(讯飞云 API) |
检索参数
| 参数 | 当前值 | 说明 |
|---|---|---|
| RAG_SEARCH_TOP_K | 30 | 最终返回数 |
| RECALL_MULTIPLIER | 3 | 向量召回倍数 |
| RERANK_CANDIDATES | 20 | 送入 Rerank 的候选数 |
| RERANK_TOP_K | 15 | Rerank 后保留数 |
| RERANK_CONTEXT_MIN_SCORE | 0.05 | 最低 rerank 分数阈值 |
| MMR_TOP_K | 30 | MMR 处理后保留数 |
| MMR_LAMBDA | 0.5 | 相关性/多样性平衡 |
| MMR_USE_EMBEDDING | False | 使用 jieba Jaccard |
| RRF_K | 60 | RRF 平滑参数 |
| DYNAMIC_RRF_ENABLED | True | 按查询长度自适应权重 |
上下文构建
| 参数 | 当前值 | 说明 |
|---|---|---|
| CONTEXT_MAX_CHARS | 8000 | 上下文硬上限(字符) |
| CONTEXT_SOFT_LIMIT | 6000 | 软限制(超此只接受高分) |
| MAX_CONTEXT_CHUNKS | 20 | 最大切片数 |
| MAX_HISTORY_ROUNDS | 10 | 对话历史上限 |
LLM 生成
| 参数 | 当前值 | 说明 |
|---|---|---|
| LLM_TEMPERATURE | 0.7 | 生成温度 |
| LLM_MAX_TOKENS | 3000 | 最大输出 token |
| LLM_TOP_P | 未设置 | 默认 1.0(不限制) |
缓存
| 参数 | 当前值 | 说明 |
|---|---|---|
| QUERY_CACHE_SIZE | 500 | 查询缓存容量 |
| QUERY_CACHE_TTL | 3600s | 查询缓存有效期 |
| EMBEDDING_CACHE_SIZE | 2000 | 向量缓存容量 |
| EMBEDDING_CACHE_TTL | 86400s | 向量缓存有效期 |
| RERANK_CACHE_SIZE | 1000 | Rerank 缓存容量 |
| RERANK_CACHE_TTL | 3600s | Rerank 缓存有效期 |
| SEMANTIC_CACHE_THRESHOLD | 0.92 | 语义缓存余弦阈值 |
救援管线
| 参数 | 当前值 | 说明 |
|---|---|---|
| CLUSTER_MIN_MEMBERS | 3 | 聚类最少成员数 |
| CLUSTER_MIN_TYPES | 2 | 聚类最少类型数 |
| CLUSTER_SEED_FLOOR | 0.35 | 种子分数下限 |
| CLUSTER_RESCUE_FLOOR | 0.06 | 救援后分数 |
| CLUSTER_MAX_SECTIONS | 3 | 最多救援章节数 |
| BM25_DIVERGENCE_RESCUE_ENABLED | True | BM25 发散救援开关 |
| BM25_DIVERGENCE_MAX_RANK | 3 | BM25 top-N 参与救援 |
置信度
| 参数 | 当前值 | 说明 |
|---|---|---|
| CONFIDENCE_WARN_THRESHOLD | 0.15 | 低于此值:谨慎回答 |
| CONFIDENCE_CAUTION_THRESHOLD | 0.30 | 低于此值:引用原文 |
六、架构层面的潜在风险
6.1 单点故障
- LLM API 单点:所有 LLM 调用(意图分析 + 生成)都走同一个 DASHSCOPE_BASE_URL。API 限流或故障时整个服务不可用。意图分析有降级兜底(默认 need_retrieval=True),但生成阶段无降级。
- ChromaDB 单点:向量库文件损坏时所有检索失败。已有 ChromaDB corruption 风险分析文档。
6.2 内存压力
- FAISS 语义缓存:10000 条 × 768 维 float32 = ~30MB 向量数据 + 等量元数据。达到上限全量清空时可能有瞬间抖动。
- BM25 全量加载:所有文档的全文 + jieba 分词结果都在内存中。知识库增长时内存线性增长。
- 三层 LRU 缓存:Query(500) + Embedding(2000) + Rerank(1000) = 最多 3500 条缓存。Embedding 缓存存 768 维向量,2000 条 ≈ 6MB。
6.3 并发安全
- gunicorn gthread 模式 2 线程 + ChromaDB 文件锁。并发写入 ChromaDB 时可能冲突。
- FAISS 语义缓存使用 RLock,线程安全。
- 精确缓存使用 RLock,线程安全。
- IntentAnalyzer._exact_cache 是普通 Dict,Python GIL 保护下基本安全。
6.4 休眠模块
以下模块当前未接入生产流程,属于独立休眠模块(原属 AgenticRAG 类,该类已在 v4.0 删除):
confidence_gate.py— 置信度门控quality_assessor.py— 质量评估器reasoning_reflector.py— 推理反思器loop_guard.py— 循环守卫
这些模块增加了代码库的维护负担但不影响运行时性能。可按需启用。