init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn) - RAG 引擎(混合检索 + 云端 Reranker + 引用溯源) - 文档解析(MinerU + 多格式支持) - Docker 生产部署配置 - 排除前端项目、敏感配置、模型文件
This commit is contained in:
316
docs/生产路径优化计划.md
Normal file
316
docs/生产路径优化计划.md
Normal file
@@ -0,0 +1,316 @@
|
||||
## 生产 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 1:Rerank 分数传递与上下文过滤
|
||||
|
||||
**目标**:让 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 2:Token 预算控制
|
||||
|
||||
**目标**:用字数/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 小时 |
|
||||
Reference in New Issue
Block a user