Files
rag/docs/生产路径优化计划.md
lacerate551 cb75b9b274 fix(boundary): 修复多库边界问题、版本管理及删除清理
多库检索与存储修复:
- RRF 融合去重改用 (collection, chunk_id) 复合键,修复同名文件结果被吞
- DocStore 存储路径加 collection 前缀,修复跨库同名切片数据覆盖
- search_multiple 去重改用复合键
- chunk_id 解析改用 rsplit 兼容下划线文件名

上传与版本管理修复:
- 同名文件上传改为覆盖模式,自动清理旧切片
- 修复首次上传不创建版本记录
- 修复覆盖上传版本号回退到 v1
- sync ADDED 分支改用动态版本号生成
- _generate_version_id 改为基于全部版本递增
- 废止/恢复操作同步 SQLite 版本记录
- mark_document_as_superseded 改为仅更新 SQLite

删除清理修复:
- 删除文档时同步清理 SQLite 版本记录和变更日志
- 删除向量库时同步清理该库所有版本记录
- cleanup 改为清理 SQLite 记录而非 ChromaDB

测试:
- test_version_management.py: 27 条版本管理单元测试
- test_edge_cases.py: 28 条边界用例测试
- test_upload_dedup.py: 5 条上传去重测试
- e2e_risk_test.py: 27 条端到端风险测试

文档:
- 新增风险边界问题修复注意事项.md(面向后端的对接文档)
- 新增向量库边界风险分析.md
- 更新多篇现有文档
2026-06-04 23:58:44 +08:00

368 lines
15 KiB
Markdown
Raw Permalink 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。
---
### 实施进度总览(更新于 2026-06
| 阶段 | 内容 | 状态 | 说明 |
|------|------|------|------|
| Phase 0 | 基线建立 + 评估基础设施 | **部分实施** | `scripts/eval_e2e.py` 已创建,但 `data/eval_dataset.json``baseline.json` 尚未生成 |
| Phase 1 | Rerank 分数传递与上下文过滤 | **已实施** | `RERANK_CONTEXT_MIN_SCORE` 已配置,`_order_text_contexts_for_prompt` 已接受 `min_score` 参数 |
| Phase 2 | Token 预算控制 | **已实施** | `CONTEXT_MAX_CHARS=8000` / `CONTEXT_SOFT_LIMIT=6000` 已配置,`_build_context_with_budget()` 已实现 |
| Phase 3 | 上下文扩展精细化 | **已实施** | `EXPANSION_SCORE_THRESHOLD=0.3` / `MAX_EXPANDED_NEIGHBORS=4` 已配置,`_expanded_from_score` 元数据已记录 |
| Phase 4 | 置信度兜底 | **已实施** | `CONFIDENCE_WARN_THRESHOLD=0.15` / `CONFIDENCE_CAUTION_THRESHOLD=0.30` 已配置,`confidence_score` 已通过 SSE 输出 |
| Phase 5 | 上下文排序优化 | **部分实施** | `_build_context_with_budget()` 实现了通用排序逻辑,但列举类查询仍走独立的 `_is_enum_query` 分支 |
| Phase 6 | 引用标注改进 | **已实施** | 已迁移到 `core/agentic_citation.py`,支持动态阈值(短段落 0.55 / 长段落 0.45)和每段最多 2 引用 |
> **注**:下文各 Phase 中引用的具体函数名、行号为文档撰写时的快照,经多次迭代后已偏移。实际位置请以当前代码为准。
---
### Phase 0基线建立 + 评估基础设施 `部分实施`
**目标**:在改动任何代码之前,先有量化基线和自动化评估手段。
**现状**`scripts/eval_e2e.py` 已创建但尚未运行基线评估。`data/eval_dataset.json``data/eval_results/baseline.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 分数在上下文构建中发挥作用,过滤低分切片。
**改动范围**`api/chat_routes.py` + `config.py`
**实施说明**
- `config.py` 已新增 `RERANK_CONTEXT_MIN_SCORE = 0.05`
- `_order_text_contexts_for_prompt()` 已接受 `min_score` 参数(当前位于 `api/chat_routes.py` 第 188 行)
- 调用处已传入 `min_score=RERANK_CONTEXT_MIN_SCORE`(当前位于第 1750-1751 行)
- SSE `context_built` 事件已包含 `min_score_filter``score_stats``confidence_top3` 等调试字段
**原始设计**
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`。
**实施说明**
- `config.py` 已新增 `CONTEXT_MAX_CHARS = 8000`、`CONTEXT_SOFT_LIMIT = 6000`、`DIRECT_CONTEXT_MAX_CHARS = 2000`
- `api/chat_routes.py` 已新增 `_build_context_with_budget()` 函数(当前第 312 行),按 Rerank 分数降序逐个加入直到预算满
- 列举类 / 对比类查询走独立分支,保持原始顺序直接拼接(当前第 1754-1758 行)
**原始设计**
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`(当前第 1089 行)及其调用处。
**实施说明**
- `config.py` / `engine.py` 已新增 `EXPANSION_SCORE_THRESHOLD = 0.3`、`MAX_EXPANDED_NEIGHBORS = 4`
- 扩展时传入 `min_score=EXPANSION_SCORE_THRESHOLD` 过滤低分切片
- 邻居切片 metadata 已记录 `_expanded_from_score`(种子分数)
- `MAX_EXPANDED_NEIGHBORS` 限制每个种子的邻居数量
- 上下文扩展同时在 `chat_routes.py`(第 281 行)和 `engine.py` 中实现,参数由 `CONTEXT_EXPANSION_ENABLED/BEFORE/AFTER/MAX_CHUNKS` 控制
**原始设计**(行号为撰写时快照,已偏移):
1. ~~MMR 前的扩展(第 573 行)保持不变~~ —— 已重构,实际位置见 `core/engine.py`
2. ~~Rerank 后的扩展(第 615 行)增加条件~~ —— 已实施,`EXPANSION_SCORE_THRESHOLD` 控制
```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 层面 + SSE 输出)。
**实施说明**
- `config.py` 已新增 `CONFIDENCE_WARN_THRESHOLD = 0.15`、`CONFIDENCE_CAUTION_THRESHOLD = 0.30`
- `generate()` 内已计算 `_confidence_score`top-3 平均 Rerank 分数,当前第 1760-1762 行)
- 根据分数在 prompt 中注入不同的置信度提示(当前第 1821-1827 行)
- SSE `finish` 事件已附带 `confidence_score` 字段(当前第 1926 行)
**原始设计**
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`(当前第 188 行)。
**当前状态**
- `_build_context_with_budget()`(第 312 行)已实现了通用的分数排序 + 预算控制逻辑
- 但列举类查询(`_is_enum_query`)和对比类查询仍走独立分支(第 1754 行),直接拼接不做预算截断
- 尚未将"同章节聚合排序"推广为所有查询类型的默认行为
**原始设计**
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引用标注改进 `已实施`
**目标**:提升引用匹配精度,支持多引用。
**改动范围**:已迁移至 `core/agentic_citation.py`(独立模块),原 `api/chat_routes.py` 中也保留了 `_attach_citations`(第 393 行)作为备用。
**实施说明**
- 动态阈值已实现:短段落(< 50 字)使用 overlap 阈值 0.55,长段落 0.45
- 每段最多匹配 2 个 chunk
- 引用标记格式 `[ref:chunk_id]`,前端 `extractCitations` 已支持多个引用
- `agentic_citation.py` 中 `_attach_citations` 方法(第 220 行)为 agentic 模式提供独立的引用标注
**原始设计**
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 小时 |