检索策略
Written: 2026.06第 06 章跟敲代码:上一讲:意图分类codealong/chapters/ch06_retrieval_strategy。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的qa_core/、scripts/等路径为准。
下一讲:查询改写与变体生成
1. 本讲目标
- 理解为什么不同问题需要不同的检索参数
- 掌握 RetrievalPlan 的完整结构和每个字段的含义
- 理解动态阈值的设计哲学
- 读懂 build_retrieval_plan() 的策略分支逻辑
2. 本讲项目交付闭环
第 5 讲已经把用户问题识别成IntentResult。这一讲要继续往后推进一步:把“意图”转换成一份可执行的检索计划。也就是说,主流程后面不再到处写 if intent == ...,而是统一消费 RetrievalPlan。
| 项目交付项 | 说明 |
|---|---|
| 核心模块 | qa_core/retrieval/strategy.py |
| 核心函数 | build_retrieval_plan()、_apply_intent_branching()、_apply_short_query_guard()、_apply_risk_category()、_apply_table_preference() |
| 输入对象 | 第 5 讲输出的 IntentResult |
| 输出对象 | RetrievalPlan |
| 下游衔接 | FAQ 检索、文档检索、FAQ 直出、上下文筛选、查询变体 |
| 验证入口 | tests/test_retrieval_and_prompt.py |
FAQ_QUERY 是否使用 FAQ 优先策略,KNOWLEDGE_QUERY 是否扩大文档召回,短问题是否提高保护阈值,费用/合规/表格类问题是否触发更谨慎的参数。
3. 前置知识 — 检索策略中的关键概念
3.1 为什么不能所有问题用一套参数
很多 RAG 教程和 Demo 会这样做:| 场景 | 问题 | 固定参数的问题 |
|---|---|---|
| FAQ | ”忘记密码怎么办” | k=5 不够,FAQ 里最匹配的那条可能在 top-3 之外 |
| 知识咨询 | ”入职流程包含哪些步骤” | k=5 可能不够,需要多段资料拼出完整流程 |
| 追问 | ”那审批呢” | 信息不完整,k=5 可能全是无关内容 |
| 合规 | ”这个合同条款合法吗” | 只看 5 条可能导致断章取义 |
| 问候 | ”你好” | 根本不需要检索,白白消耗资源 |
3.2 关键参数概念
| 参数 | 含义 | 调大效果 | 调小效果 |
|---|---|---|---|
faq_top_k | FAQ 召回条数 | 更多候选,召回更全 | 更快,减少噪音 |
doc_top_k | 文档召回条数 | 更多上下文 | 更快,减少噪音 |
faq_direct_threshold | FAQ 直出分数阈值 | 更谨慎,更少直出 | 更激进,更多直出 |
final_context_top_n | 最终进 Prompt 的条数 | LLM 看到更多资料 | Prompt 更短,响应更快 |
min_context_score | 最低上下文分数 | 更宽松,更多资料 | 更严格,只保留高质量 |
max_context_chars | 上下文总字符数上限 | 更长上下文 | 更短上下文 |
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:直接答案类 — 不检索doc_top_k // 2:FAQ 问题不需要太多文档上下文,减少文档候选。这是项目策略,不是固定公式。threshold - 0.08:降低直出门槛,相信 FAQ 标准答案。0.08是相对调整幅度,来自项目对 FAQ 优先策略的保守设定。max(0.62, ...):项目保护底线,防止配置过低导致误直出。生产环境应结合误直出样本继续校准。
- 知识咨询通常需要多段资料拼出完整答案(如入职流程需要制度、材料清单、审批步骤)
doc_complex_query_top_k:项目配置项,用来给复杂问题更大的搜索空间;当前默认值以代码Settings为准。final_context_top_n增加到 5:让 LLM 看到更多完整片段;这个值受模型上下文窗口、文档粒度和噪声率影响,需要通过评测校准。
direct_threshold ≥ 0.78:项目保护阈值,原则是宁可多检索也不要误直出。追问的信息不完整,FAQ 相似分数可能虚高。- 扩大候选数:给检索更多机会找到正确内容
5.3 短问题保护
- 短问题(如”权限”、“发票”)歧义很大
- 收缩文档检索范围,减少噪声
- 提高直出阈值,防止”权限”误匹配到 FAQ 中某个具体权限的答案
- 但排除 FOLLOW_UP:短追问可以通过历史改写补全,不应简单按短句收缩
5.4 问题类别保护
以下是对特定高频、高风险问题类别的特殊策略: 费用类 — 强口径保护0.86 是本项目当前最高保护阈值,用来表达“高风险类别更谨慎”的策略,而不是通用行业标准。
排障类 — 扩大搜索
5.5 表格问题特殊处理
prefer_table 标志还会影响后续的上下文选择逻辑:在同等分数下,优先保留表格行而非普通文本段落。
6. 动态阈值的设计哲学
6.1 项目当前阈值阶梯
| 场景 | 项目当前最低阈值 | 原因 |
|---|---|---|
| FAQ_QUERY | 0.62 | FAQ 标准答案可信,可以相对宽松 |
| KNOWLEDGE_QUERY | 0.72(配置默认值) | 基线阈值 |
| FOLLOW_UP | 0.78 | 追问信息不完整,更谨慎 |
| 短问题 | 0.78+ | 歧义大,更谨慎 |
| 费用类 | 0.84 | 涉及金额,更谨慎 |
| 合规类 | 0.86 | 涉及法律风险,最谨慎 |
0.62/0.78/0.86 讲成放之四海皆准的阈值。上线前要用 FAQ 误直出率、insufficient_context 比例、人工评测集和 LangSmith Evaluation 继续校准。
6.2 一个贯穿始终的原则
FAQ 误直出比”信息不足”更危险。- 信息不足 → 提示用户联系人工客服 → 用户知道系统不能确定答案
- FAQ 误直出 → 系统自信地给出错误信息 → 用户被误导 → 可能产生严重后果
6.3 Reason 字段的意义
每个策略分支都有一个reason 字符串,它会写入 LangSmith trace,并在必要时用于本地排查:
7. 检索计划与下游的衔接
7.1 build_retrieval_plan() 完整决策流程
7.2 在 RAG 流程中的使用
上面流程图展示了build_retrieval_plan() 内部的 4 层决策。但读者容易困惑的是:RetrievalPlan 创建出来后,到底是怎么被下游函数消费的? 下面逐段展示代码中的实际衔接点。
Step 1 — plan 的创建:prepare_retrieval()
search_faq()
get_faq_direct_answer()
direct_faq_answer() 的实现
被调用的 direct_faq_answer() 定义在 qa_core/pipeline/context.py 中,判断 FAQ 结果是否可以不经过 LLM 直接返回标准答案。只允许两种情况:
- 用户问题和 FAQ 标准问题完全一致;
- 检索/重排分数达到当前
RetrievalPlan给出的动态阈值。
select_context_docs()
| 流程图分支 | 产生的 plan 字段 | 下游消费函数路径 |
|---|---|---|
| 直接答案 | run_faq=False, run_doc=False | qa_core/pipeline/retrieval_steps.py::search_faq()、search_doc() |
| FAQ_QUERY | faq_direct_threshold = max(0.62, base-0.08) | qa_core/pipeline/retrieval_steps.py::get_faq_direct_answer() |
| FOLLOW_UP | faq_top_k ≥ 24, threshold ≥ 0.78 | qa_core/pipeline/retrieval_steps.py::search_faq()、get_faq_direct_answer() |
| pricing 类别 | threshold ≥ 0.84, final_context_top_n ≥ 6 | qa_core/pipeline/retrieval_steps.py、qa_core/pipeline/context.py::select_context_docs() |
| compliance 类别 | threshold ≥ 0.86, final_context_top_n ≥ 6 | qa_core/pipeline/retrieval_steps.py、qa_core/pipeline/context.py::select_context_docs() |
| prefer_table | faq_direct_exact_only=True, doc_top_k 扩大 | qa_core/pipeline/retrieval_steps.py、qa_core/pipeline/context.py::select_context_docs() |
| 所有分支 | use_query_variants | qa_core/pipeline/query_variants.py::generate_query_variants() |
| 所有分支 | reason | qa_core/pipeline/steps.py → retrieval_info → LangSmith trace |
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. 本讲实践闭环
| 项目 | 内容 |
|---|---|
| 本讲类型 | 项目实现 |
| 实践产物 | qa_core/retrieval/strategy.py 与 RetrievalPlan |
| 是否进入最终项目 | 是 |
| 验收方式 | 运行检索策略单测,验证不同意图和问题类别生成不同参数 |
| 后续落点 | 第 8 讲按 plan 检索,第 10 讲由 Pipeline 调用 |
top_k、阈值、是否 rerank、是否 query variants 的计划。
8.1 本讲从 0 到 1 实现闭环
本讲承接第 5 讲的IntentResult,把“用户是什么问题”转换成“应该怎么检索”。它不直接查 Milvus,只负责生成检索计划。
实现完成后,相关代码结构应该是下面这张图:
8.1.1 :定义检索计划结构
目标:把“怎么检索”变成一个可传递、可测试、可追踪的数据对象。 来源:真实代码节选,见qa_core/schemas.py 中的 RetrievalPlan。
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_direct_exact_only,避免“看起来相似”的 FAQ 抢答具体行列数据。
8.1.4 :写测试验证动态参数
验收命令: 来源:命令行验收,对应tests/test_retrieval_and_prompt.py。
| 验证项 | 输入条件 | 期望结果 |
|---|---|---|
| 直接答案 | IntentResult.direct_answer 有值 | run_faq=False,run_doc=False |
| FAQ 优先 | intent=FAQ_QUERY | FAQ 阈值较低,允许高置信 FAQ 直出 |
| 知识咨询 | intent=KNOWLEDGE_QUERY | 扩大文档候选,保留 rerank |
| 追问 | intent=FOLLOW_UP | 启用 query variants 或提高直出阈值 |
| 短问题保护 | 非追问短句 | 收缩 doc_top_k,提高阈值 |
| 费用类 | question_category=pricing | 扩大候选,阈值至少 0.84 |
| 合规类 | question_category=compliance | 阈值至少 0.86 |
| 排障/总结 | troubleshooting/summary | 扩大文档候选和上下文数量 |
| 表格问题 | prefer_table=True | 扩大 doc_top_k,设置 faq_direct_exact_only=True |
| 计划组装 | 任意非直接问题 | 包含 min/max context、category、reason 等完整字段 |
- FAQ 问题和知识咨询问题生成不同
faq_direct_threshold。 - 追问启用
use_query_variants或提高阈值。 - 表格类问题扩大文档候选,并避免相似 FAQ 误直出。
reason能表达策略叠加过程,方便 Trace 排查。
9. 重点掌握
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | RetrievalPlan 的完整结构和每个字段的含义(run_faq/run_doc、faq_top_k/doc_top_k、rerank、faq_direct_threshold、final_context_top_n、use_query_variants 等) | 检索计划是整个 RAG 策略的数据核心 |
| ★★★ 必会 | build_retrieval_plan() 的四层决策链:意图分支 → 短问题保护 → 问题类别保护 → 表格偏好 | 理解动态检索策略如何逐层叠加 |
| ★★★ 必会 | 动态阈值设计哲学:FAQ_QUERY(0.62) → KNOWLEDGE_QUERY(0.72) → FOLLOW_UP(0.78) → 费用类(0.84) → 合规类(0.86),FAQ 误直出比信息不足更危险 | 面试高频,理解”宁可不说也不乱说”的原则 |
| ★★ 理解 | 意图分支的四种策略:直接答案(不检索)、FAQ 优先(doc_top_k 减半)、知识咨询(扩大文档)、追问(提高阈值) | 意图到检索参数的映射逻辑 |
| ★★ 理解 | 短问题保护(_apply_short_query_guard):歧义大的短问题收缩检索范围、提高阈值 | 防误判的重要保护机制 |
| ★★ 理解 | 问题类别保护:费用类(0.84)需确认金额、合规类(0.86)不能自行判断、排障类扩大搜索 | 高风险场景的特殊处理 |
| ★ 了解 | reason 字段的可解释性设计(如 “faq_first_short_query_guard_pricing_guard”) | Trace 排查时使用 |
| ★ 了解 | 表格问题(prefer_table)时禁用相似 FAQ 直出 | 边缘场景的保护策略 |
10. 本讲小结
- 检索策略不是全局常量,是根据意图、问题类别、问题长度动态生成的 RetrievalPlan
- 六个意图对应不同的检索参数:直接答案不检索、FAQ 优先低阈值、知识咨询扩大文档、追问提高阈值
- 问题类别保护:费用类(0.84)、合规类(0.86) — FAQ 误直出比信息不足更危险
- 短问题保护:歧义大的短问题提高阈值、收缩文档检索,但排除可通过历史改写的追问
- 表格问题:扩大文档候选,禁用相似 FAQ 直出,优先保留表格行
- 动态阈值的每一档都有原因,体现在
reason字段中

