多库检索与存储修复: - 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 - 更新多篇现有文档
15 KiB
生产 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 尚未生成。
工作内容:
-
创建
data/eval_dataset.json:从现有知识库中挑选 15-20 个测试问题,覆盖以下类型:- 事实查询("XX标准是多少")
- 列举查询("有哪些禁止情形")
- 对比查询("A和B的区别")
- 流程查询("如何申请XX")
- 跨文档查询(答案涉及多个文件)
每个问题记录:
query、query_type、expected_keywords(答案应包含的关键信息点)、relevant_sources(应命中的文件名) -
创建
scripts/eval_e2e.py:端到端评估脚本- 启动本地服务(或连接已运行的服务)
- 对每个测试问题调用
POST /rag - 收集回答,用 LLM 评分(相关性 1-5、完整性 1-5、准确性 1-5)
- 同时记录检索层指标(Rerank 分数分布、上下文切片数、上下文总字数)
- 输出 JSON 报告 + 终端汇总
-
运行一次基线评估,保存为
data/eval_results/baseline.json -
打 Git tag:
git tag v1.0-rag-baseline
产出文件:
data/eval_dataset.jsonscripts/eval_e2e.pydata/eval_results/baseline.json
验收标准:评估脚本能正常运行,基线报告包含每个问题的评分。
Phase 1:Rerank 分数传递与上下文过滤 已实施
目标:让 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等调试字段
原始设计:
-
在
contexts.append时已有 score 字段,确认它包含 Rerank 分数(当前scores来自search_result.get('scores'),是 RRF 融合分数还是 Rerank 分数需要确认) -
在
_order_text_contexts_for_prompt函数中增加分数过滤参数: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] -
在
generate()中调用时传入阈值:text_contexts = _order_text_contexts_for_prompt( contexts, message, MAX_CONTEXT_CHUNKS, min_score=RERANK_CONTEXT_MIN_SCORE # 新增配置项,初始值 0.05 ) -
在
config.py中新增配置:RERANK_CONTEXT_MIN_SCORE = 0.05 # 上下文最低 Rerank 分数阈值 -
在 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。
实施说明:
config.py已新增CONTEXT_MAX_CHARS = 8000、CONTEXT_SOFT_LIMIT = 6000、DIRECT_CONTEXT_MAX_CHARS = 2000api/chat_routes.py已新增_build_context_with_budget()函数(当前第 312 行),按 Rerank 分数降序逐个加入直到预算满- 列举类 / 对比类查询走独立分支,保持原始顺序直接拼接(当前第 1754-1758 行)
原始设计:
-
在
config.py新增:CONTEXT_MAX_CHARS = 8000 # 上下文最大字符数(约 4000 token) CONTEXT_SOFT_LIMIT = 6000 # 软限制,超过后只保留高分切片 -
在
_order_text_contexts_for_prompt返回后,构建context_text时:# 按 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) -
注意:排序后需要保持同一文档切片的连续性。改为先按 (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控制
原始设计(行号为撰写时快照,已偏移):
-
MMR 前的扩展(第 573 行)保持不变—— 已重构,实际位置见core/engine.py -
Rerank 后的扩展(第 615 行)增加条件—— 已实施,EXPANSION_SCORE_THRESHOLD控制# 只对 Rerank 分数 > EXPANSION_SCORE_THRESHOLD 的切片扩展邻居 EXPANSION_SCORE_THRESHOLD = 0.3 -
给扩展进来的邻居切片在 metadata 中记录扩展来源:
n_meta['_expanded_from_score'] = seed_score # 种子切片的 Rerank 分数 -
在
_order_text_contexts_for_prompt中限制扩展邻居的数量: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.30generate()内已计算_confidence_score(top-3 平均 Rerank 分数,当前第 1760-1762 行)- 根据分数在 prompt 中注入不同的置信度提示(当前第 1821-1827 行)
- SSE
finish事件已附带confidence_score字段(当前第 1926 行)
原始设计:
-
在上下文构建完成后(
context_text已生成),检查 top-3 切片的平均 Rerank 分数: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 -
根据平均分数在 prompt 中注入不同指令:
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 = "" -
在
config.py新增:CONFIDENCE_WARN_THRESHOLD = 0.15 CONFIDENCE_CAUTION_THRESHOLD = 0.30 -
在 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 行),直接拼接不做预算截断 - 尚未将"同章节聚合排序"推广为所有查询类型的默认行为
原始设计:
-
将当前只对
_is_enum_query执行的排序逻辑推广为所有查询类型的默认行为 -
排序策略:
- 第一优先级:按 Rerank 分数降序(高分切片先进入上下文)
- 第二优先级:同一 source + section 的切片按 chunk_index 连续排列
- 具体做法:先按 (source, section) 分组,组内按 chunk_index 排序,组间按组内最高分降序
-
列举类查询保持当前行为不变(作为特例)
风险:低。排序变化不影响切片内容,只影响 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 模式提供独立的引用标注
原始设计:
-
动态阈值:短段落(< 50 字)使用更高的 overlap 阈值(0.55),长段落保持 0.45
-
允许一个段落匹配最多 2 个 chunk(如果两个 chunk 的 overlap 分数都超过阈值且差距小于 0.1)
-
引用标记改为
[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 完成后执行:
# 运行评估
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 小时 |