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

15 KiB
Raw Permalink Blame History

生产 RAG 路径优化计划

原则:小步快跑,每阶段独立可测,打完 commit 验证质量后再推进下一阶段。 如果某阶段导致回答质量下降,直接 git revert 回退到上一个 commit。


实施进度总览(更新于 2026-06

阶段 内容 状态 说明
Phase 0 基线建立 + 评估基础设施 部分实施 scripts/eval_e2e.py 已创建,但 data/eval_dataset.jsonbaseline.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.jsondata/eval_results/baseline.json 尚未生成。

工作内容

  1. 创建 data/eval_dataset.json:从现有知识库中挑选 15-20 个测试问题,覆盖以下类型:

    • 事实查询("XX标准是多少"
    • 列举查询("有哪些禁止情形"
    • 对比查询("A和B的区别"
    • 流程查询("如何申请XX"
    • 跨文档查询(答案涉及多个文件)

    每个问题记录:queryquery_typeexpected_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 taggit 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_filterscore_statsconfidence_top3 等调试字段

原始设计

  1. contexts.append 时已有 score 字段,确认它包含 Rerank 分数(当前 scores 来自 search_result.get('scores'),是 RRF 融合分数还是 Rerank 分数需要确认)

  2. _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]
    
  3. generate() 中调用时传入阈值:

    text_contexts = _order_text_contexts_for_prompt(
        contexts, message, MAX_CONTEXT_CHUNKS,
        min_score=RERANK_CONTEXT_MIN_SCORE  # 新增配置项,初始值 0.05
    )
    
  4. config.py 中新增配置:

    RERANK_CONTEXT_MIN_SCORE = 0.05  # 上下文最低 Rerank 分数阈值
    
  5. 在 SSE context_built 事件中增加分数信息DEV 模式),方便调试。

风险:低。min_score 初始设 0.05(几乎不过滤),逐步调高观察效果。

验证方法

  • 运行 eval_e2e.py,对比基线
  • 重点关注:是否有问题因为过滤了切片而回答变差
  • 观察 context_built 事件中 chunk_count 的变化

Git commitfeat(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 = 8000CONTEXT_SOFT_LIMIT = 6000DIRECT_CONTEXT_MAX_CHARS = 2000
  • api/chat_routes.py 已新增 _build_context_with_budget() 函数(当前第 312 行),按 Rerank 分数降序逐个加入直到预算满
  • 列举类 / 对比类查询走独立分支,保持原始顺序直接拼接(当前第 1754-1758 行)

原始设计

  1. config.py 新增:

    CONTEXT_MAX_CHARS = 8000      # 上下文最大字符数(约 4000 token
    CONTEXT_SOFT_LIMIT = 6000     # 软限制,超过后只保留高分切片
    
  2. _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)
    
  3. 注意:排序后需要保持同一文档切片的连续性。改为先按 (source, section, chunk_index) 分组,再按组内最高分降序排列各组,逐组加入直到预算满。

风险:中。如果预算设太小,可能丢掉关键信息。初始 CONTEXT_MAX_CHARS=8000 比较保守(当前 20 个切片平均约 10000-15000 字)。

验证方法

  • 对比基线的评分变化
  • 关注列举类查询("有哪些禁止情形")是否因为预算截断而漏条
  • 观察 context_built.context_length 分布

Git commitfeat(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.3MAX_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 控制

    # 只对 Rerank 分数 > EXPANSION_SCORE_THRESHOLD 的切片扩展邻居
    EXPANSION_SCORE_THRESHOLD = 0.3
    
  3. 给扩展进来的邻居切片在 metadata 中记录扩展来源:

    n_meta['_expanded_from_score'] = seed_score  # 种子切片的 Rerank 分数
    
  4. _order_text_contexts_for_prompt 中限制扩展邻居的数量:

    MAX_EXPANDED_NEIGHBORS = 4  # 最多 4 个扩展邻居进入上下文
    

风险:中。如果阈值设太高,一些中等分数的切片不会扩展邻居,可能丢失上下文。初始 0.3 比较宽松。

验证方法

  • 对比 retrieval_debug 事件中第二次 context_expansion 的 before/after 数量
  • 检查是否有回答因为缺少邻居切片而变得不连贯
  • 评估分数不应低于 Phase 2 的结果

Git commitfeat(rag): restrict post-rerank context expansion to high-confidence chunks


Phase 4置信度兜底 已实施

目标:当检索质量整体偏低时,在 prompt 中告知 LLM 谨慎回答,减少幻觉。

改动范围api/chat_routes.pyprompt 层面 + SSE 输出)。

实施说明

  • config.py 已新增 CONFIDENCE_WARN_THRESHOLD = 0.15CONFIDENCE_CAUTION_THRESHOLD = 0.30
  • generate() 内已计算 _confidence_scoretop-3 平均 Rerank 分数,当前第 1760-1762 行)
  • 根据分数在 prompt 中注入不同的置信度提示(当前第 1821-1827 行)
  • SSE finish 事件已附带 confidence_score 字段(当前第 1926 行)

原始设计

  1. 在上下文构建完成后(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
    
  2. 根据平均分数在 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 = ""
    
  3. config.py 新增:

    CONFIDENCE_WARN_THRESHOLD = 0.15
    CONFIDENCE_CAUTION_THRESHOLD = 0.30
    
  4. 在 SSE finish 事件中附带 confidence_score 字段,前端可用于提示用户。

风险:低。纯 prompt 层面的改动,不影响检索链路。最坏情况是 LLM 过于保守拒绝回答——可以通过调低阈值解决。

验证方法

  • 构造几个"知识库中确实没有答案"的问题,检查 LLM 是否正确拒绝
  • 正常问题的评分不应下降
  • 观察 finish 事件中的 confidence_score 分布

Git commitfeat(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 commitfeat(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 commitfeat(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 小时