Files
rag/docs/生产路径优化计划.md
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

317 lines
11 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 路径优化计划
> 原则:小步快跑,每阶段独立可测,打完 commit 验证质量后再推进下一阶段。
> 如果某阶段导致回答质量下降,直接 `git revert` 回退到上一个 commit。
---
### Phase 0基线建立 + 评估基础设施
**目标**:在改动任何代码之前,先有量化基线和自动化评估手段。
**现状**`scripts/evaluate_rag.py` 只评估检索层Recall/MRR/nDCG不评估端到端回答质量。`data/eval_dataset.json` 不存在。
**工作内容**
1. 创建 `data/eval_dataset.json`:从现有知识库中挑选 15-20 个测试问题,覆盖以下类型:
- 事实查询("XX标准是多少"
- 列举查询("有哪些禁止情形"
- 对比查询("A和B的区别"
- 流程查询("如何申请XX"
- 跨文档查询(答案涉及多个文件)
每个问题记录:`query``query_type``expected_keywords`(答案应包含的关键信息点)、`relevant_sources`(应命中的文件名)
2. 创建 `scripts/eval_e2e.py`:端到端评估脚本
- 启动本地服务(或连接已运行的服务)
- 对每个测试问题调用 `POST /rag`
- 收集回答,用 LLM 评分(相关性 1-5、完整性 1-5、准确性 1-5
- 同时记录检索层指标Rerank 分数分布、上下文切片数、上下文总字数)
- 输出 JSON 报告 + 终端汇总
3. 运行一次基线评估,保存为 `data/eval_results/baseline.json`
4. 打 Git tag`git tag v1.0-rag-baseline`
**产出文件**
- `data/eval_dataset.json`
- `scripts/eval_e2e.py`
- `data/eval_results/baseline.json`
**验收标准**:评估脚本能正常运行,基线报告包含每个问题的评分。
---
### Phase 1Rerank 分数传递与上下文过滤
**目标**:让 Rerank 分数在 chat_routes 的上下文构建中发挥作用,过滤低分切片。
**改动范围**:仅 `api/chat_routes.py`,不涉及 engine.py。
**具体改动**
1.`contexts.append` 时已有 score 字段,确认它包含 Rerank 分数(当前 `scores` 来自 `search_result.get('scores')`,是 RRF 融合分数还是 Rerank 分数需要确认)
2.`_order_text_contexts_for_prompt` 函数中增加分数过滤参数:
```python
def _order_text_contexts_for_prompt(contexts, query, max_chunks,
min_score=0.0):
# 过滤低于分数阈值的切片
if min_score > 0:
text_contexts = [c for c in text_contexts
if c.get('score', 0) >= min_score]
```
3. 在 `generate()` 中调用时传入阈值:
```python
text_contexts = _order_text_contexts_for_prompt(
contexts, message, MAX_CONTEXT_CHUNKS,
min_score=RERANK_CONTEXT_MIN_SCORE # 新增配置项,初始值 0.05
)
```
4. 在 `config.py` 中新增配置:
```python
RERANK_CONTEXT_MIN_SCORE = 0.05 # 上下文最低 Rerank 分数阈值
```
5. 在 SSE `context_built` 事件中增加分数信息DEV 模式),方便调试。
**风险**:低。`min_score` 初始设 0.05(几乎不过滤),逐步调高观察效果。
**验证方法**
- 运行 `eval_e2e.py`,对比基线
- 重点关注:是否有问题因为过滤了切片而回答变差
- 观察 `context_built` 事件中 `chunk_count` 的变化
**Git commit**`feat(rag): pass rerank scores through to context building with min-score filter`
---
### Phase 2Token 预算控制
**目标**:用字数/token 预算替代纯计数截断,避免上下文过长稀释 LLM 注意力。
**改动范围**`api/chat_routes.py` + `config.py`。
**具体改动**
1. 在 `config.py` 新增:
```python
CONTEXT_MAX_CHARS = 8000 # 上下文最大字符数(约 4000 token
CONTEXT_SOFT_LIMIT = 6000 # 软限制,超过后只保留高分切片
```
2. 在 `_order_text_contexts_for_prompt` 返回后,构建 `context_text` 时:
```python
# 按 Rerank 分数降序逐个加入,直到达到 token 预算
sorted_contexts = sorted(text_contexts,
key=lambda c: c.get('score', 0),
reverse=True)
context_parts = []
total_chars = 0
for ctx in sorted_contexts:
doc = ctx.get('doc', '')
if total_chars + len(doc) > CONTEXT_MAX_CHARS:
break
context_parts.append(doc)
total_chars += len(doc)
context_text = "\n\n".join(context_parts)
```
3. 注意:排序后需要保持同一文档切片的连续性。改为先按 (source, section, chunk_index) 分组,再按组内最高分降序排列各组,逐组加入直到预算满。
**风险**:中。如果预算设太小,可能丢掉关键信息。初始 `CONTEXT_MAX_CHARS=8000` 比较保守(当前 20 个切片平均约 10000-15000 字)。
**验证方法**
- 对比基线的评分变化
- 关注列举类查询("有哪些禁止情形")是否因为预算截断而漏条
- 观察 `context_built.context_length` 分布
**Git commit**`feat(rag): add character-based token budget for context building`
---
### Phase 3上下文扩展精细化
**目标**Rerank 后的上下文扩展只对高置信度切片执行,避免低分切片引入噪声邻居。
**改动范围**`core/engine.py` 的 `_expand_contiguous_chunks` 及其调用处。
**具体改动**
1. MMR 前的扩展(第 573 行)保持不变——它的目的是防止邻居被 MMR 误删
2. Rerank 后的扩展(第 615 行)增加条件:
```python
# 只对 Rerank 分数 > EXPANSION_SCORE_THRESHOLD 的切片扩展邻居
EXPANSION_SCORE_THRESHOLD = 0.3
```
3. 给扩展进来的邻居切片在 metadata 中记录扩展来源:
```python
n_meta['_expanded_from_score'] = seed_score # 种子切片的 Rerank 分数
```
4. 在 `_order_text_contexts_for_prompt` 中限制扩展邻居的数量:
```python
MAX_EXPANDED_NEIGHBORS = 4 # 最多 4 个扩展邻居进入上下文
```
**风险**:中。如果阈值设太高,一些中等分数的切片不会扩展邻居,可能丢失上下文。初始 0.3 比较宽松。
**验证方法**
- 对比 `retrieval_debug` 事件中第二次 `context_expansion` 的 before/after 数量
- 检查是否有回答因为缺少邻居切片而变得不连贯
- 评估分数不应低于 Phase 2 的结果
**Git commit**`feat(rag): restrict post-rerank context expansion to high-confidence chunks`
---
### Phase 4置信度兜底
**目标**:当检索质量整体偏低时,在 prompt 中告知 LLM 谨慎回答,减少幻觉。
**改动范围**`api/chat_routes.py`(仅 prompt 层面,不改检索逻辑)。
**具体改动**
1. 在上下文构建完成后(`context_text` 已生成),检查 top-3 切片的平均 Rerank 分数:
```python
top_scores = [ctx.get('score', 0) for ctx in text_contexts[:3]]
avg_top3_score = sum(top_scores) / len(top_scores) if top_scores else 0
```
2. 根据平均分数在 prompt 中注入不同指令:
```python
if avg_top3_score < CONFIDENCE_WARN_THRESHOLD: # 比如 0.15
confidence_note = (
"【重要提示】参考资料与问题的相关性较低。"
"请仅基于参考资料中明确包含的信息回答,"
"如果资料不足以回答问题,请直接说明'知识库中未找到直接相关的信息'。"
)
elif avg_top3_score < CONFIDENCE_CAUTION_THRESHOLD: # 比如 0.3
confidence_note = (
"参考资料的相关性一般,请优先引用资料中的原文,避免推测。"
)
else:
confidence_note = ""
```
3. 在 `config.py` 新增:
```python
CONFIDENCE_WARN_THRESHOLD = 0.15
CONFIDENCE_CAUTION_THRESHOLD = 0.30
```
4. 在 SSE `finish` 事件中附带 `confidence_score` 字段,前端可用于提示用户。
**风险**:低。纯 prompt 层面的改动,不影响检索链路。最坏情况是 LLM 过于保守拒绝回答——可以通过调低阈值解决。
**验证方法**
- 构造几个"知识库中确实没有答案"的问题,检查 LLM 是否正确拒绝
- 正常问题的评分不应下降
- 观察 `finish` 事件中的 `confidence_score` 分布
**Git commit**`feat(rag): add confidence-based prompt guidance for low-quality retrieval`
---
### Phase 5上下文排序优化
**目标**:对所有查询类型(不仅是列举类)都做同章节聚合排序,保证同一文件同一章节的切片连续排列。
**改动范围**`api/chat_routes.py` 的 `_order_text_contexts_for_prompt`。
**具体改动**
1. 将当前只对 `_is_enum_query` 执行的排序逻辑推广为所有查询类型的默认行为
2. 排序策略:
- 第一优先级:按 Rerank 分数降序(高分切片先进入上下文)
- 第二优先级:同一 source + section 的切片按 chunk_index 连续排列
- 具体做法:先按 (source, section) 分组,组内按 chunk_index 排序,组间按组内最高分降序
3. 列举类查询保持当前行为不变(作为特例)
**风险**:低。排序变化不影响切片内容,只影响 LLM 看到的顺序。
**验证方法**
- 对比 `context_built.chunks_used` 的排列顺序
- 评估分数不应低于 Phase 4
**Git commit**`feat(rag): apply section-aware context ordering for all query types`
---
### Phase 6引用标注改进
**目标**:提升 `_attach_citations` 的匹配精度,支持多引用。
**改动范围**`api/chat_routes.py` 的 `_attach_citations` 函数。
**具体改动**
1. 动态阈值:短段落(< 50 字)使用更高的 overlap 阈值0.55),长段落保持 0.45
2. 允许一个段落匹配最多 2 个 chunk如果两个 chunk 的 overlap 分数都超过阈值且差距小于 0.1
3. 引用标记改为 `[ref:chunk_id_1][ref:chunk_id_2]`,前端 `extractCitations` 已支持多个 `[ref:xxx]`
**风险**:中。多引用可能导致引用列表变长、前端展示变化。
**验证方法**
- 检查引用数量是否合理增加(不应翻倍)
- 引用溯源弹窗的跳转是否仍然正确
- 评估分数不应下降
**Git commit**`feat(rag): improve citation matching with dynamic thresholds and multi-citation support`
---
### 阶段依赖关系
```
Phase 0 (基线+评估)
Phase 1 (Rerank分数传递) ← 风险最低,收益最直接
Phase 2 (Token预算) ← 依赖 Phase 1 的分数传递
Phase 3 (扩展精细化) ← 依赖 Phase 1 的分数信息
Phase 4 (置信度兜底) ← 依赖 Phase 1 的分数信息
Phase 5 (上下文排序) ← 独立,可与 Phase 4 互换顺序
Phase 6 (引用标注) ← 独立,放在最后因为改动面较大
```
### 回退策略
每个 Phase 完成后执行:
```bash
# 运行评估
python scripts/eval_e2e.py --output data/eval_results/phase_N.json
# 对比上一阶段
# 如果评分下降 > 5%,回退:
git revert HEAD
# 如果评分持平或提升,打 tag
git tag v1.0-phase-N
```
### 预估时间
| 阶段 | 代码改动量 | 预估时间 |
|------|----------|---------|
| Phase 0 | 新建 2 个文件 | 2-3 小时 |
| Phase 1 | 改 2 个文件,约 30 行 | 1 小时 |
| Phase 2 | 改 2 个文件,约 40 行 | 1-2 小时 |
| Phase 3 | 改 1 个文件,约 20 行 | 1 小时 |
| Phase 4 | 改 2 个文件,约 25 行 | 30 分钟 |
| Phase 5 | 改 1 个文件,约 30 行 | 1 小时 |
| Phase 6 | 改 1 个文件,约 40 行 | 1-2 小时 |