Skip to main content

检索策略

Written: 2026.06
第 06 章跟敲代码:codealong/chapters/ch06_retrieval_strategy。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的 qa_core/scripts/ 等路径为准。
上一讲意图分类
下一讲查询改写与变体生成

1. 本讲目标

  • 理解为什么不同问题需要不同的检索参数
  • 掌握 RetrievalPlan 的完整结构和每个字段的含义
  • 理解动态阈值的设计哲学
  • 读懂 build_retrieval_plan() 的策略分支逻辑

2. 本讲项目交付闭环

第 5 讲已经把用户问题识别成 IntentResult。这一讲要继续往后推进一步:把“意图”转换成一份可执行的检索计划。也就是说,主流程后面不再到处写 if intent == ...,而是统一消费 RetrievalPlan 本讲实现完成后的代码结构: 闭环验证方式:
验证时重点看:FAQ_QUERY 是否使用 FAQ 优先策略,KNOWLEDGE_QUERY 是否扩大文档召回,短问题是否提高保护阈值,费用/合规/表格类问题是否触发更谨慎的参数。

3. 前置知识 — 检索策略中的关键概念

3.1 为什么不能所有问题用一套参数

很多 RAG 教程和 Demo 会这样做:
这个做法的问题: 检索策略的核心思想:不同问题类型,用不同的检索参数。这是一个”动态计划”,而非”全局常量”。

3.2 关键参数概念


4. RetrievalPlan 数据结构

@dataclass(frozen=True) 的设计:检索计划一旦构建就不应被修改。它是一个确定性的决策结果,不是可变的状态。

5. build_retrieval_plan() 详解

5.1 基础参数

上面代码中 is_table_query() 用于判断用户问题是否涉及表格/清单类查询(详见 3.5 节),定义如下:
infer_question_category() 的实现 上面代码中 is_table_query()infer_question_category() 都定义在 qa_core/intent/question_category.py 中。infer_question_category() 通过正则匹配将问题分为五类,供检索策略和提示词模板共同使用:

5.2 意图分支

分支 1:直接答案类 — 不检索
分支 2:FAQ 查询 — FAQ 优先策略
设计分析
  • doc_top_k // 2:FAQ 问题不需要太多文档上下文,减少文档候选。这是项目策略,不是固定公式。
  • threshold - 0.08:降低直出门槛,相信 FAQ 标准答案。0.08 是相对调整幅度,来自项目对 FAQ 优先策略的保守设定。
  • max(0.62, ...):项目保护底线,防止配置过低导致误直出。生产环境应结合误直出样本继续校准。
分支 3:知识咨询 — 加强文档检索
设计分析
  • 知识咨询通常需要多段资料拼出完整答案(如入职流程需要制度、材料清单、审批步骤)
  • doc_complex_query_top_k:项目配置项,用来给复杂问题更大的搜索空间;当前默认值以代码 Settings 为准。
  • final_context_top_n 增加到 5:让 LLM 看到更多完整片段;这个值受模型上下文窗口、文档粒度和噪声率影响,需要通过评测校准。
分支 4:追问 — 谨慎策略
设计分析
  • direct_threshold ≥ 0.78:项目保护阈值,原则是宁可多检索也不要误直出。追问的信息不完整,FAQ 相似分数可能虚高。
  • 扩大候选数:给检索更多机会找到正确内容

5.3 短问题保护

设计分析
  • 短问题(如”权限”、“发票”)歧义很大
  • 收缩文档检索范围,减少噪声
  • 提高直出阈值,防止”权限”误匹配到 FAQ 中某个具体权限的答案
  • 但排除 FOLLOW_UP:短追问可以通过历史改写补全,不应简单按短句收缩

5.4 问题类别保护

以下是对特定高频、高风险问题类别的特殊策略: 费用类 — 强口径保护
为什么费用类需要特殊保护:“退款多少钱”、“什么时候到账”这类问题,如果 FAQ 直出了一个相似但不准确的答案,后果很严重。策略上宁可多召回资料让模型基于上下文回答,也不能凭一个 0.75 分的 FAQ 直接承诺金额。 合规类 — 最严格保护
为什么合规类阈值最高:合规判断涉及法律风险,不能只靠 FAQ 的标准回答。需要多条款交叉验证。0.86 是本项目当前最高保护阈值,用来表达“高风险类别更谨慎”的策略,而不是通用行业标准。 排障类 — 扩大搜索
总结归纳类 — 多片段聚合

5.5 表格问题特殊处理

为什么表格问题要禁用相似 FAQ 直出
prefer_table 标志还会影响后续的上下文选择逻辑:在同等分数下,优先保留表格行而非普通文本段落。

6. 动态阈值的设计哲学

6.1 项目当前阈值阶梯

这些数字不是行业标准,也不是模型自动学习出来的结果,而是本项目当前的策略参数。解释时重点讲“为什么不同场景要分层”,不要把 0.62/0.78/0.86 讲成放之四海皆准的阈值。上线前要用 FAQ 误直出率、insufficient_context 比例、人工评测集和 LangSmith Evaluation 继续校准。

6.2 一个贯穿始终的原则

FAQ 误直出比”信息不足”更危险。
  • 信息不足 → 提示用户联系人工客服 → 用户知道系统不能确定答案
  • FAQ 误直出 → 系统自信地给出错误信息 → 用户被误导 → 可能产生严重后果
这就是为什么在费用、合规、追问等场景下宁可提高阈值也不降低的原因。

6.3 Reason 字段的意义

每个策略分支都有一个 reason 字符串,它会写入 LangSmith trace,并在必要时用于本地排查:
这保证了可解释性:后续排查 bad case 时,可以直接看到这个请求为什么走了某套参数,而不是猜测。

7. 检索计划与下游的衔接

7.1 build_retrieval_plan() 完整决策流程

7.2 在 RAG 流程中的使用

上面流程图展示了 build_retrieval_plan() 内部的 4 层决策。但读者容易困惑的是:RetrievalPlan 创建出来后,到底是怎么被下游函数消费的? 下面逐段展示代码中的实际衔接点。 Step 1 — plan 的创建:prepare_retrieval()
Step 2 — plan 的消费①:search_faq()
Step 3 — plan 的消费②:get_faq_direct_answer()
direct_faq_answer() 的实现 被调用的 direct_faq_answer() 定义在 qa_core/pipeline/context.py 中,判断 FAQ 结果是否可以不经过 LLM 直接返回标准答案。只允许两种情况:
  1. 用户问题和 FAQ 标准问题完全一致;
  2. 检索/重排分数达到当前 RetrievalPlan 给出的动态阈值。
FAQ 答案通常是费用、流程、账号、合同等强口径内容,直出可以减少幻觉,但相似问题误直出风险很高,不能只要召回就返回。
Step 4 — plan 的消费③:select_context_docs()
数据流总结 — 从流程图到代码的定位指引:

7.3 FAQ Store 和 Doc Store 的工厂模式

注意:缓存(@lru_cache)位于 get_hybrid_store 而非 get_faq_store / get_doc_store 上。两个高层函数接收可选的 collection_name 参数(而非 scenario_id),在未指定时自动回落为当前 active 场景的默认 collection。这样设计使得缓存 key 始终是 collection_name 字符串,避免了因 scenario 元数据变化导致的缓存膨胀;同时 get_faq_store / get_doc_store 本身保持无状态,便于在运行时切换默认场景。

8. 本讲实践闭环

通过标准:FAQ、文档、表格、追问等问题能得到不同 top_k、阈值、是否 rerank、是否 query variants 的计划。

8.1 本讲从 0 到 1 实现闭环

本讲承接第 5 讲的 IntentResult,把“用户是什么问题”转换成“应该怎么检索”。它不直接查 Milvus,只负责生成检索计划。 实现完成后,相关代码结构应该是下面这张图:

8.1.1 :定义检索计划结构

目标:把“怎么检索”变成一个可传递、可测试、可追踪的数据对象。 来源:真实代码节选,见 qa_core/schemas.py 中的 RetrievalPlan
设计解释:Pipeline 不应该到处散落 top_k=8 这类魔法数字,而是先得到一个计划,再按计划检索。

8.1.2 :实现 6 步动态检索计划

目标:把真实代码里的 6 步决策完整讲清楚,而不是只展示几个意图分支。 来源:真实代码逻辑压缩版,对应 qa_core/retrieval/strategy.py::build_retrieval_plan()
设计解释:真实实现不是一次性返回某个固定 RetrievalPlan,而是从 settings 基线出发,按“意图 → 短问题 → 风险类别 → 表格偏好”逐层叠加,最后组装不可变计划。

8.1.3 :理解四个决策层的完整分支

目标:知道每个 helper 到底改变了哪些字段。 来源:真实代码逻辑压缩版,对应 qa_core/retrieval/strategy.py::_apply_intent_branching()_apply_short_query_guard()_apply_risk_category()_apply_table_preference()
设计解释:高风险问题宁可信息不足,也不能因为相似 FAQ 误直出;表格问题还会打开 faq_direct_exact_only,避免“看起来相似”的 FAQ 抢答具体行列数据。

8.1.4 :写测试验证动态参数

验收命令: 来源:命令行验收,对应 tests/test_retrieval_and_prompt.py
闭环验证重点: 通过标准:
  • FAQ 问题和知识咨询问题生成不同 faq_direct_threshold
  • 追问启用 use_query_variants 或提高阈值。
  • 表格类问题扩大文档候选,并避免相似 FAQ 误直出。
  • reason 能表达策略叠加过程,方便 Trace 排查。

9. 重点掌握

10. 本讲小结

  • 检索策略不是全局常量,是根据意图、问题类别、问题长度动态生成的 RetrievalPlan
  • 六个意图对应不同的检索参数:直接答案不检索、FAQ 优先低阈值、知识咨询扩大文档、追问提高阈值
  • 问题类别保护:费用类(0.84)、合规类(0.86) — FAQ 误直出比信息不足更危险
  • 短问题保护:歧义大的短问题提高阈值、收缩文档检索,但排除可通过历史改写的追问
  • 表格问题:扩大文档候选,禁用相似 FAQ 直出,优先保留表格行
  • 动态阈值的每一档都有原因,体现在 reason 字段中
下一讲查询改写与变体生成 — 追问改写、查询扩展、多轮对话历史管理