跳转至

第 06 章:检索策略与动态计划

上一讲意图分类与路由入口 下一讲查询改写与变体生成

本讲目标

第 05 章已经完成入口路由:用户问题进来后,系统能判断是直接回答、source 边界提示,还是进入检索链路。

本章继续沿着同一条代码主线往后走:当 RouteDecision.route="retrieval" 时,把 IntentResult 转成一份可执行的 RetrievalPlan

也就是说,本章要解决的问题是:

既然这个问题需要检索,那 FAQ 查不查?文档查不查?各查多少?阈值多高?是否需要查询变体?

本章不连接 Milvus,不执行真实检索,只生成后续检索会消费的计划参数。

本讲地图

本图对应本讲功能闭环,展示从输入到本讲交付物的主干路径。节点与主项目代码文件和函数保持一致,后续章节消费的能力只作为交付边界出现。

图 1:第 06 讲功能闭环地图

flowchart TD
    C06_INPUT["检索入口<br/>decide_route()"]
    C06_INTENT["意图结果<br/>classify_intent()"]
    C06_CATEGORY["问题类别<br/>infer_question_category()"]
    C06_TABLE["表格偏好<br/>is_table_query()"]
    C06_BASE["参数基线<br/>Settings"]
    C06_PLAN["计划生成<br/>build_retrieval_plan()"]
    C06_OUT{{"章节输出<br/>RetrievalPlan"}}
    C06_INPUT --> C06_INTENT
    C06_INTENT --> C06_CATEGORY
    C06_INTENT --> C06_TABLE
    C06_INTENT --> C06_BASE
    C06_CATEGORY --> C06_PLAN
    C06_TABLE --> C06_PLAN
    C06_BASE --> C06_PLAN
    C06_PLAN --> C06_OUT
    style C06_INPUT fill:#F8FAFC,stroke:#64748B,stroke-width:2px
    style C06_INTENT fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
    style C06_CATEGORY fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
    style C06_TABLE fill:#FEF3C7,stroke:#D97706,stroke-width:2px
    style C06_BASE fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
    style C06_PLAN fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
    style C06_OUT fill:#DCFCE7,stroke:#16A34A,stroke-width:2px

节点与代码对齐

节点 对齐文件 函数/对象 本章职责
检索入口 qa_core/pipeline/steps.py decide_route() 只有 route=retrieval 才继续进入本章逻辑。
意图结果 qa_core/intent/classifier.py classify_intent() 沿用第 05 章的 FAQ_QUERY、KNOWLEDGE_QUERY、FOLLOW_UP。
问题类别 qa_core/intent/question_category.py infer_question_category() 识别 pricing、compliance、troubleshooting、summary 等风险类别。
表格偏好 qa_core/intent/question_category.py is_table_query() 表格/清单/字段类问题禁用模糊 FAQ 直出。
参数基线 qa_core/config/settings.py Settings 定义 top_k、阈值、上下文长度和短问题阈值。
计划生成 qa_core/retrieval/strategy.py build_retrieval_plan() 按意图、短问题、风险类别、表格偏好逐层调整参数。
章节输出 qa_core/retrieval/strategy.py RetrievalPlan 输出 run_faq、run_doc、top_k、阈值和 use_query_variants。

和第 05 章的关系

第 05 章当前输出两类可执行分支:

第 05 章输出 系统动作 第 06 章是否继续
route="direct_answer"reason="greeting_rule/human_service_rule/safety_rule" 问候、转人工、越界等确定性问题,直接返回固定文案 不继续
route="direct_answer"reason="source_boundary" 用户选择的 source 与问题明显不匹配,提示切换分类 不继续
route="retrieval" 需要进入 RAG 检索链路 继续生成 RetrievalPlan

所以第 06 章不是重新写一套流程,而是在第 05 章的基础上增量开发:

1
2
3
4
5
第 05 章
用户问题 -> decide_route() -> classify_intent()

第 06 章新增
IntentResult -> build_retrieval_plan() -> RetrievalPlan

这里要特别区分两件事:source_boundary 是路由阶段的确定性拦截,目的是防止用户选错资料分类;RetrievalPlan 是进入检索后的参数计划,只有 route="retrieval" 的问题才会生成。

动画节点对照

第 06 章对应业务流程中的 Stage 2 检索计划部分。下表用于把业务流程位置和本章代码对应起来。

动画位置 代码节点 本章学习内容
Stage 1 decide_route() 复用第 05 章入口路由,只接住 route="retrieval"
Stage 2 classify_intent() 复用第 05 章意图分类,得到 IntentResult
Stage 2 build_retrieval_plan() 本章新增,把意图变成检索计划
Stage 2 run_faq/run_doc/top_k/threshold 本章新增,决定后续如何检索
Stage 2 use_query_variants 本章只给出开关,第 07 章实现查询变体

本章涉及的项目代码

本章讲的是主项目中的检索计划生成逻辑,核心文件如下:

顺序 文件 只看什么
1 scripts/demo_query_prepare.py --plan-only 命令行入口,只看 route、intent、retrieval_plan 三层输出
2 qa_core/pipeline/steps.py 第 05 章入口路由,只有 retrieval 才进入本章逻辑
3 qa_core/intent/classifier.py 第 05 章意图分类,输出 IntentResult
4 qa_core/intent/question_category.py 问题类别和表格问题识别
5 qa_core/retrieval/strategy.py 本章核心:生成 RetrievalPlan
6 tests/test_retrieval_and_prompt.py 主项目测试,确认检索计划和后续上下文行为

这就是本章在主项目中的代码执行主线:

1
2
3
4
5
6
7
8
9
scripts/demo_query_prepare.py --plan-only
低成本路由预览:direct_answer / source_boundary / retrieval
  ↓ route="retrieval"
classify_intent()
build_retrieval_plan()
RetrievalPlan JSON

RetrievalPlan 是什么

RetrievalPlan 是后续检索阶段要读取的参数包。后面的代码不需要到处写 if intent == ...,而是统一消费这份计划。

它定义在:

qa_core/retrieval/strategy.py

核心字段如下:

字段 含义
run_faq 是否检索 FAQ 集合
run_doc 是否检索文档集合
faq_top_k FAQ 初始召回数量
doc_top_k 文档初始召回数量
rerank 后续是否进入重排
faq_direct_threshold FAQ 相似直出的保护阈值
final_context_top_n 最终进入 Prompt 的上下文条数
min_context_score 上下文最低相关性分数
max_context_chars 上下文总字符上限
max_context_doc_chars 单条文档字符上限
use_query_variants 第 07 章是否生成查询变体
question_category 问题类别
prefer_table 是否偏向表格、清单、字段类资料
faq_direct_exact_only 是否只允许精确 FAQ 直出
reason 本次计划的原因标签

source 不放在 RetrievalPlan 中。它属于检索过滤范围,而不是检索策略本身:前端显式选择的 source_filter 优先,其次才使用 IntentResult.suggested_source。第 08 章真正执行 Milvus 检索时,再把最终生效的 source 转成过滤条件。

FAQ/Doc 检索和 Hybrid Search 的边界

run_faq/run_doc 控制的是业务上的两路/分层检索:是否查 FAQ collection、是否查 Doc collection。它不是 Milvus Hybrid Search 的定义。

第 08 章的 Hybrid Search 指每个被执行的 collection 内部用 Dense 向量召回 + BM25 Sparse 关键词召回做融合排序。真实业务中,企业问答常见默认策略是 FAQ 和 Doc 都查,但最终仍要服从 RetrievalPlan:直答类问题不查,FAQ-only 或 Doc-heavy 问题也可以只查一路或偏向一路。

代码执行主线

1. 运行入口

scripts/demo_query_prepare.py --plan-only 的核心逻辑是:

1
2
3
4
5
route = decide_low_cost_route(query, scenario, source_filter)

if route.route == "retrieval":
    intent = classify_intent(query, history, scenario)
    plan = build_retrieval_plan(query, intent)

这段代码说明本章的边界非常清楚:

  • direct_answer:已经有答案或边界提示,不生成计划
  • retrieval:继续意图分类,并生成检索计划
  • --plan-only:只看第 06 章交付物,不执行第 07 章的追问改写和查询变体

完整在线链路里的 decide_route() 还会尝试 FAQ 精确 fast path;那一步会访问 FAQ collection。第 06 章为了聚焦“计划怎么生成”,验证脚本只保留确定性路由预览,不执行 Milvus 检索。

2. 问题类别识别

qa_core/intent/question_category.py 负责识别问题类型:

1
2
3
4
5
6
7
QuestionCategory = Literal[
    "default",
    "pricing",
    "compliance",
    "troubleshooting",
    "summary",
]

这些类别不是新的用户意图,而是检索策略的风险标签。

这里要先把三个维度分清楚,否则很容易觉得参数没有感觉:

维度 代码字段 解决的问题 例子
用户意图 intent.intent 这类问题更像 FAQ、知识查询,还是追问? FAQ_QUERYKNOWLEDGE_QUERYFOLLOW_UP
风险标签 question_category 这类问题错答成本高不高,需不需要更谨慎? pricingcompliancetroubleshooting
资料形态 prefer_table 这类问题是不是更依赖表格、清单、字段、行记录? true / false

它们不是互斥关系,而是可以叠加。比如:

报销费用超过5000需要谁审批

这句话可能同时触发:

  • 意图:像 FAQ 查询,所以先偏向 FAQ。
  • 风险:包含“费用、5000、审批”,所以进入 pricing 保护。
  • 资料形态:如果问到清单、字段、明细表,还会继续触发 prefer_table=true

所以最后的 reason 可能不是一个词,而是一串策略叠加结果,例如:

faq_first_short_query_guard_pricing_guard
faq_first_pricing_guard_table_row_preferred

reason 时,要从左到右理解为:先按意图定主方向,再叠加短问题、风险类别、表格偏好的保护规则。

类别 典型问题 策略倾向
default 普通业务问题 使用默认检索计划
pricing 费用、金额、报销、付款 提高 FAQ 直出门槛,扩大候选
compliance 合规、隐私、合同、审计 使用更高保护阈值
troubleshooting 报错、失败、异常、排查 扩大文档候选,保留更多步骤
summary 总结、归纳、对比、大纲 扩大文档候选,覆盖更多资料

表格类问题由 is_table_query() 判断,例如清单、台账、字段、金额、责任人、付款节点等。

这里的“表格类问题”不是指第 06 章已经去解析 Excel,也不是指用户问题里一定出现了 .xlsx 文件。它指的是:用户问法明显依赖某个清单、台账、字段、行记录或明细项,答案很可能藏在结构化资料的一行或一列里。

不是表格类问题 是表格类问题
报销流程是什么 报销材料清单里发票字段怎么填
VPN 连不上怎么处理 故障台账里的责任人字段是谁
合同审批流程有哪些步骤 付款节点明细表里超过 5000 的审批要求是什么

第 06 章只负责识别这种问题形态,并把保护信号写进 RetrievalPlan;真正的表格文件加载、切分和入库在第 16 章,真正按计划执行 Milvus 检索在第 08 章。

3. 检索计划规则表

build_retrieval_plan() 不是一次性返回固定参数,而是先生成默认参数,再按固定顺序应用规则补丁。当前代码用 PlanPatch 描述每条规则要改哪些字段,用 _apply_plan_rules() 统一执行规则。

默认参数
意图规则 _intent_rules()
短问题保护规则
问题类别规则 _category_rules()
表格偏好规则
RetrievalPlan

这样做的好处是:参数规则集中在表里,主函数只负责识别问题形态和组装 RetrievalPlan,不会在多个长分支之间来回跳。

规则一:按意图调整

intent 调整结果
FAQ_QUERY FAQ 优先,文档候选减半,降低基础 FAQ 直出阈值
KNOWLEDGE_QUERY 扩大文档候选,增加最终上下文数量
FOLLOW_UP 扩大 FAQ 和文档候选,提高 FAQ 直出阈值,打开查询变体
GREETING/HUMAN_SERVICE/OUT_OF_SCOPE 关闭 FAQ 和文档检索

本章正常情况下只会给 route="retrieval" 的问题生成计划。保留直答类处理,是为了让 build_retrieval_plan() 单独调用时仍然有明确行为。

规则二:短问题保护

短问题信息少,更容易误命中。比如:

1
2
3
登录
审批呢
费用

如果它不是追问,本章会:

  • 收缩文档候选
  • 提高 FAQ 直出门槛
  • reason 中追加 short_query_guard

追问不套用这个保护,因为第 07 章会先结合历史问题做改写。

规则三:风险类别保护

费用、合规、排障、总结类问题更需要谨慎。

类别 策略变化
pricing 扩大 FAQ 和文档候选,FAQ 直出阈值至少 0.84
compliance 扩大文档候选,FAQ 直出阈值至少 0.86
troubleshooting 扩大文档候选和上下文数量
summary 扩大文档候选和上下文数量

规则四:表格偏好

表格类问题通常要定位具体行列,例如:

1
2
3
材料清单里的付款金额字段是什么
验收表里责任人字段在哪里
付款节点明细有哪些

这类问题最怕“看起来差不多”的误命中。比如 FAQ 里有“报销材料需要哪些”,但用户真正问的是“材料清单里的付款金额字段”,两者都和报销材料相关,却不是同一个答案。

所以本层会把检索计划改得更保守:

策略变化 含义 后续影响
扩大文档候选 不只看少量 FAQ 候选 第 08 章检索时给文档集合更多召回机会
增加最终上下文数量 给 LLM 更多证据 第 10/11 章生成答案时更容易引用到正确行或字段
prefer_table=True 标记这是表格、清单、字段类问题 后续上下文选择时优先保留表格化资料
faq_direct_exact_only=True 禁止模糊 FAQ 高分直接返回 第 09/10 章只有精确 FAQ 命中才允许直出

换句话说,表格偏好不是“现在查表”,而是告诉后续链路:这一问要谨慎,宁愿多查一点证据,也不要因为一个相似 FAQ 分数高就直接回答。

五类问题的参数体感

理解第 06 章时,可以把参数看成五个旋钮:

旋钮 变大或打开以后意味着什么 代价
faq_top_k FAQ 候选更多,更不容易漏掉标准问答 后续重排和判断成本更高
doc_top_k 文档候选更多,更适合复杂知识问题 噪声更多,检索和重排更慢
faq_direct_threshold FAQ 直出更谨慎,分数必须更高 可能少一些快速直出
final_context_top_n 给 LLM 的证据更多 Prompt 更长,答案可能更慢
use_query_variants 第 07 章会生成查询变体扩大召回 多一次改写/变体成本

下面把五类常见问题放到同一张表里对比。

问题类型 典型问法 核心参数变化 设计目的
FAQ 查询 异地入职材料办理时需要准备哪些资料和审批信息 doc_top_k 从 20 收到 10;use_query_variants=false;基础 FAQ 直出阈值从 0.72 降到 0.64 先相信标准 FAQ,文档只作为兜底证据,避免简单问题走复杂链路
知识查询 公司会议室预约规则在哪里查看以及需要遵守哪些流程要求 doc_top_k 提到 24;final_context_top_n 提到 5;use_query_variants=true 问题通常需要多段资料拼接,先扩大文档召回,再由后续章节生成更完整答案
追问 那审批呢,历史问题是 报销流程是什么 faq_top_k=24doc_top_k=24faq_direct_threshold 至少 0.78;use_query_variants=true 当前问题信息不足,必须依赖历史改写,不能被短词误命中后直接回答
费用类问题 报销费用超过5000需要谁审批 doc_top_k 至少 24;final_context_top_n 至少 6;faq_direct_threshold 至少 0.84 金额、报销、付款类问题错答成本高,宁愿多找证据,也不轻易 FAQ 模糊直出
表格类问题 材料清单里的付款金额字段是什么 prefer_table=truefaq_direct_exact_only=truefinal_context_top_n 至少 7 这类问题常藏在表格行、清单字段或台账明细里,必须抑制“相似 FAQ 直接回答”

这张表要注意两点。

第一,FAQ 查询不等于只查 FAQ。本项目默认仍然允许 run_doc=true,只是把文档候选收小,让文档作为兜底;真正是否返回 FAQ 标准答案,要等第 08/09/10 章拿到真实 FAQ hit 后判断。

第二,费用类和表格类是叠加保护,不是新的主意图。一个问题可以既是 FAQ 查询,又是费用类问题,还可以同时是表格类问题。最终计划会把这些规则叠加起来。

参数叠加示例

假设默认配置是:

1
2
3
4
5
faq_top_k=20
doc_top_k=20
final_context_top_n=4
faq_direct_score_threshold=0.72
doc_complex_query_top_k=24

普通 FAQ 问题:

异地入职材料办理时需要准备哪些资料和审批信息

计划会偏向 FAQ:

1
2
3
4
5
6
7
{
  "doc_top_k": 10,
  "faq_direct_threshold": 0.64,
  "final_context_top_n": 4,
  "use_query_variants": false,
  "reason": "faq_first"
}

这表示:标准 FAQ 很可能能回答,所以文档候选先收小;问题本身已经完整,不需要第 07 章生成查询变体。

如果问题变成:

报销费用超过5000需要谁审批

计划会叠加费用保护:

1
2
3
4
5
6
{
  "doc_top_k": 24,
  "faq_direct_threshold": 0.84,
  "final_context_top_n": 6,
  "reason": "faq_first_short_query_guard_pricing_guard"
}

这表示:虽然它看起来仍像 FAQ,但涉及费用和审批,不能轻易模糊直出;要多召回文档证据,给后续答案生成更多上下文。

如果问题再变成:

材料清单里的付款金额字段是什么

计划会继续叠加表格保护:

1
2
3
4
5
6
{
  "prefer_table": true,
  "faq_direct_exact_only": true,
  "final_context_top_n": 7,
  "reason": "faq_first_pricing_guard_table_row_preferred"
}

这表示:答案更可能来自某个清单字段或表格行。即使 FAQ 相似度高,也不能只靠“相似”直接返回,除非是精确 FAQ 命中。

典型运行结果

在项目根目录执行以下命令。第 06 章统一带上 --plan-only,这样输出会停在 RetrievalPlan,不会调用第 07 章的 LLM 改写或查询变体。

cd D:\workspace\knowforge-rag-platform

FAQ 查询

python scripts\demo_query_prepare.py "异地入职材料办理时需要准备哪些资料和审批信息" --plan-only

关键输出:

{
  "intent": {
    "intent": "FAQ_QUERY"
  },
  "retrieval_plan": {
    "run_faq": true,
    "run_doc": true,
    "doc_top_k": 10,
    "use_query_variants": false,
    "reason": "faq_first"
  }
}

知识查询

python scripts\demo_query_prepare.py "公司会议室预约规则在哪里查看以及需要遵守哪些流程要求" --plan-only

关键输出:

{
  "intent": {
    "intent": "KNOWLEDGE_QUERY"
  },
  "retrieval_plan": {
    "doc_top_k": 24,
    "final_context_top_n": 5,
    "use_query_variants": true,
    "reason": "knowledge_doc_enriched"
  }
}

追问

python scripts\demo_query_prepare.py "那审批呢" --history "报销流程是什么" --plan-only

关键输出:

{
  "intent": {
    "intent": "FOLLOW_UP",
    "requires_rewrite": true
  },
  "retrieval_plan": {
    "faq_top_k": 24,
    "doc_top_k": 24,
    "use_query_variants": true,
    "reason": "history_aware_follow_up"
  }
}

费用类问题

python scripts\demo_query_prepare.py "报销费用超过5000需要谁审批" --plan-only

关键输出:

1
2
3
4
5
6
7
8
{
  "retrieval_plan": {
    "question_category": "pricing",
    "faq_direct_threshold": 0.84,
    "final_context_top_n": 6,
    "reason": "faq_first_short_query_guard_pricing_guard"
  }
}

表格类问题

python scripts\demo_query_prepare.py "材料清单里的付款金额字段是什么" --plan-only

关键输出:

1
2
3
4
5
6
7
8
{
  "retrieval_plan": {
    "prefer_table": true,
    "faq_direct_exact_only": true,
    "final_context_top_n": 7,
    "reason": "faq_first_pricing_guard_table_row_preferred"
  }
}

这段输出可以这样读:

字段 说明
prefer_table=true 这个问题像是在问清单、字段或表格行,后续证据选择要偏向表格化资料。
faq_direct_exact_only=true FAQ 不能只因为相似度高就直接返回,必须是精确匹配才允许直出。
final_context_top_n=7 最终给 LLM 的上下文条数增加,避免漏掉正确行或字段。
reason 包含 table_row_preferred 诊断标签,说明本轮计划触发了表格/行记录保护。

这里要明确一个边界:第 06 章只输出计划,不负责加载表格文件,也不负责执行检索;它把“这可能是表格类问题”的信号传给第 08 章及后续 Pipeline。

容易混淆的边界

问题 正确理解
run_faq/run_doc 是不是 Hybrid Search? 不是。它决定查 FAQ collection 还是 Doc collection;第 08 章每个 collection 内部的 Dense + BM25 才是 Milvus Hybrid Search。
为什么 source 不在 RetrievalPlan 里? source 是数据过滤范围,来自前端选择或意图推断;RetrievalPlan 是检索策略参数,两者职责不同。
faq_direct_threshold 是不是本章会直接返回 FAQ? 不是。本章只计算阈值;是否直出要等第 08/09/10 章拿到真实 FAQ hit 后再判断。
use_query_variants=true 是不是本章已经生成变体? 不是。本章只打开开关;第 07 章才真正改写追问并生成查询变体。
prefer_table=true 是不是本章已经读取 Excel? 不是。本章只识别“像表格/清单/字段类问题”;第 16 章负责多格式文件加载和表格行入库。

参数数字怎么解释

本章参数对齐主项目默认配置,但这些数字不是官方标准,也不是固定承诺。

参数 默认值 参数理解
faq_top_k 20 FAQ 先召回一批候选,后续再精筛
doc_top_k 20 文档先召回一批候选,避免过早漏召回
faq_short_query_top_k 30 短问题信息少,FAQ 多取一些候选
doc_complex_query_top_k 24 复杂知识问题需要更多文档候选
faq_direct_score_threshold 0.72 FAQ 相似直出的基础保护线
0.78 短问题保护线 短问题更容易误命中,所以阈值更高
0.84 费用类保护线 金额、报销、付款类问题更谨慎
0.86 合规类保护线 合规、隐私、合同类问题更谨慎

这些值来自当前项目样例和默认配置。真实项目上线时,需要结合评测集、召回率、误直出率、延迟和人工抽检继续校准。

测试

cd D:\workspace\knowforge-rag-platform
python -m pytest tests/test_retrieval_and_prompt.py

测试应该覆盖:

  • 第 05 章入口路由仍然正常
  • FAQ_QUERY 会生成 FAQ 优先计划,不在本章本地返回标准答案
  • FAQ 查询生成 FAQ 优先计划
  • 知识查询扩大文档候选并打开查询变体
  • 追问扩大候选并要求第 07 章改写
  • 短问题提高 FAQ 直出门槛
  • 费用/合规类问题提高保护阈值
  • 表格类问题偏向表格行资料

本章小结

本章完成了从“用户意图”到“检索计划”的转换。

第 05 章解决:

这个问题要不要进入检索?

第 06 章解决:

进入检索以后,应该按什么策略检索?

下一章会继续使用 IntentResult.requires_rewriteRetrievalPlan.use_query_variants,实现追问改写和查询变体生成。