RAG 检索增强实战:两段式切块 + 四级检索流水线设计
RAG 两段式切块 + 四级检索流水线 — 设计文档
日期: 2026-07-03
状态: 待实现
作者: 协作设计(用户 + Claude)
目标
优化现有 RAG 管道的检索质量与上下文完整性。现有管道单一固定切块(1000字/300重叠),检索命中后直接送 LLM,存在两个问题:
- 上下文割裂 — 命中的 chunk 可能正好切断了一个完整景点介绍,LLM 拿到的是残缺上下文
- 检索粗放 — 只有 BM25→向量→reranker 三级,早期级无门控,低质量查询会浪费昂贵的 reranker API 调用
本设计通过 两段式文档切块 + 四级检索流水线 + 多级检索门控 解决上述问题。
非目标
- 不改
TravelRAG的对外接口(query()/query_with_metrics()/get_rag()签名不变) - 不引入新的外部依赖(继续用 jieba / rank-bm25 / sentence-transformers / qwen3-rerank / DeepSeek)
- 不做磁盘持久化缓存(用内存按城市懒缓存即可)
- 不改 Agent 侧调用代码、不改 API 端点签名
总体架构
1 | 用户问题 |
接口不变:TravelRAG.query() / query_with_metrics() 签名和行为不变,api/rag.py、agent/tools.py、tests/rag_eval.py 零改动。
详细设计
第 2 阶段:两段式切块
数据结构
1 |
|
切块参数
| 层 | chunk_size | overlap | 分隔符 |
|---|---|---|---|
| Parent | 1500 | 200 | \n\n, \n, 。, !, ?, ;, ., , "" |
| Child | 300 | 50 | 同上 |
流程
- 用
RecursiveCharacterTextSplitter切 parent(size=1500, overlap=200) - 对每个 parent,再用 splitter 切 child(size=300, overlap=50)
- 记录每个 child 的
parent_idx和position_in_parent - 同一城市的所有 parent/child 存进该城市的
CityIndex缓存
设计要点
- child overlap=50:child 已小,大 overlap 会导致大量重复向量编码;50 字足够防止句子被硬切。
- 图片文档:沿用现有
_call_vision_api转文字后,同样走两段式切块。 - parent-child 映射:child 通过
parent_idx反查 parent,是证据打包的基础。
第 3 阶段:四级检索流水线
一级 — BM25 召回
- 输入:该城市全部 child(从
CityIndex.bm25查询) - 输出:top 30 child(按 BM25 分)
- 门控:召回数 < 5 → 拒绝(
error=bm25_low_recall) - 说明:k 从现有 20 放宽到 30,因为 child 更小更碎,需要更高召回保证不漏。
二级 — 向量召回
- 输入:top 30 child
- 方法:用 BGE 编码 query + child,余弦相似度排序(沿用现有
normalize_embeddings=True+np.dot) - 输出:top 15 child
- 门控:top 余弦分 < 0.30 → 拒绝(
error=vector_low_score) - 说明:向量分数从
CityIndex.child_vectors直接取,无需重编码 child(只编码 query)。
三级 — 邻居滑窗交叉重排
- 窗口构建:对 top 15 的每个 child,同 parent 内取前后各 2 个邻居 child,拼成窗口(
5 child,1500 字,约等于一个 parent) - 边界处理:parent 边缘的 child 邻居不足 2 个时,取实际可得的(窗口可能 3-4 个 child)
- 去重:多个命中 child 可能生成相同窗口,按窗口文本哈希去重后再 rerank
- 重排:把每个窗口的拼接文本作为 document,送 qwen3-rerank 打分(沿用现有
_qwen3_rerank,document 内容从 child 单块变成窗口拼接文本) - 输出:top 5 窗口(带 rerank 分)
- 门控:top rerank 分 < 0.25 → 拒绝(
error=rerank_low_score)
四级 — 证据打包
- 取 parent:对 top 5 窗口,每个取其所属 parent 作为证据
- parent 去重:同 parent 的多个窗口只保留一份,取最高分
- 排序:按窗口 rerank 分排序 parent
- 输出:证据包(去重后的 parent 列表,最多 5 个)
- 送 LLM:拼接 parent 文本送 DeepSeek 生成回答(沿用现有
_ANSWER_PROMPT)
第 4 阶段:检索门控
| 级 | 门控条件 | 拒绝 error 码 | 拒绝行为 |
|---|---|---|---|
| 城市匹配 | 无文档 | no_docs_matched |
返回”未找到相关资料” |
| 一级 | BM25 召回数 < 5 | bm25_low_recall |
同上 |
| 二级 | 向量 top 余弦 < 0.30 | vector_low_score |
同上 |
| 三级 | rerank top 分 < 0.25 | rerank_low_score |
同上 |
拒绝行为统一:设 metrics.answer_refused = True,记录 metrics.error 和 metrics.gate_failed_at,立即返回”未找到与您问题足够相关的旅行资料”提示,不进入下一级。
阈值校准:初始阈值如上,上线后通过 rag_eval.py 实测结果调整。
按城市懒预计算缓存
数据结构
1 |
|
缓存管理
TravelRAG新增实例属性:self._city_cache: dict[str, CityIndex]- 构建时机:某城市首次被查询时,
_build_city_index(city, pdf_paths)切块 + 编码所有 child 向量 + 建 BM25,存入缓存 - 后续查询:命中缓存直接用,跳过 PDF 读取和切块,只需编码 query 向量
- 多城市查询(如”上海去苏州和杭州”):每个匹配城市独立建/查
CityIndex缓存。一级 BM25 在每个城市的缓存索引上各跑一次,各取 top 30,合并去重成一个候选池;二级起在合并池上操作。child 向量直接从各城市child_vectors汇总,无需重编码。这样缓存对多城市查询同样有效。 - 模型共享:
embedding_model仍是单例延迟加载,被所有城市共享
性能预期
| 场景 | 现有延迟 | 新延迟 |
|---|---|---|
| 首次查某城市 | 5-15s | 10-20s(多切 child + 编码所有 child 向量) |
| 复查同城市 | 5-15s(每次重算) | 1-2s(仅 query 向量 + rerank API) |
首次略慢是建立缓存的代价,复查大幅提速。
RAGMetrics 扩展
新增字段追踪新流程(现有字段保留):
1 |
|
字段兼容说明:bm25_candidates/vector_candidates/noise_removed 保留是为了不破坏现有 rag_eval.py 和 rag_eval_results.json 的字段名。新流程中 bm25_candidates 与 bm25_recall 同值,vector_candidates 与 vector_recall 同值,noise_removed 恒为 0(新流程无独立噪声过滤级,噪声过滤融入一级 BM25 之后的 child 质量由向量+rerank 保证)。
测试与评测
tests/rag_eval.py沿用,重写前先跑存基线 JSON,重写后再跑对比- 新增字段全部进
rag_eval_results.json - 汇总打印新增:各级门控拒绝数、平均 vector top 分、缓存命中率
- 评测脚本会自动 warm up(首次查某城市建缓存),复查查询能体现缓存收益
文件改动范围
| 文件 | 改动 |
|---|---|
app/rag/pipeline.py |
重写 TravelRAG 内部;新增 ParentChunk/ChildChunk/CityIndex;新增两段切块、四级流水线、门控、城市缓存;扩展 RAGMetrics |
tests/rag_eval.py |
适配新 metrics 字段 |
docs/tech-rag.md |
更新技术文档 |
docs/api-reference.md |
RAG 端点行为说明(接口不变,仅补门控说明) |
接口签名零改动:query() / query_with_metrics() / get_rag() 不变。
风险与缓解
| 风险 | 缓解 |
|---|---|
| 首次查某城市变慢(建缓存) | 评测脚本 warm up;生产可预热常见城市 |
| 阈值定不准导致误拒 | 初始值偏保守,通过 rag_eval 实测调整 |
| 窗口拼接后文本过长超 reranker 限制 | 窗口 ~1500 字,qwen3-rerank 单 document 上限远大于此,安全 |
| 多城市查询缓存膨胀 | 128 本 PDF 总量有限,内存可承受;必要时加 LRU |
成功标准
- 知识库覆盖范围内查询命中率 ≥ 现有水平(100%)
- 复查同城市延迟从 5-15s 降到 1-2s
- 低质量/无效查询通过早期门控拒绝,不浪费 reranker API 调用
rag_eval.py评测结果可对比基线
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 且听风吟!
评论
