项目记录

意图识别与检索策略

使用规则候选、分类模型与网关仲裁,把用户问题转换为可解释、可治理的检索计划。

发布于
本文目录 · 8 节

用户的问法只有那么几种,但系统要做的决定一点不少: 这句话该直答、该查 FAQ、该检索文档,还是该先追问一句?

如果所有问题都直接丢给向量库,会出现两种典型事故:用户发一句「你好」也被检索, 模型硬着头皮拿无关文档回答;用户搜一个具体编号,语义检索却召不回。

先给结论

这套项目的意图链路是三段式:规则候选 + BERT 模型 + 网关仲裁。

text
第一段  路由层收口:问候 / 人工客服 / 越界请求,直接返回,不进检索
第二段  规则候选 + 模型打分:规则给可解释的高频判断,BERT 负责长尾表达
第三段  网关仲裁:按优先级决策树融合两路结果,产出可治理、可回放的意图结论

一、先分层,别急着上模型

很多项目一上来就训一个意图分类模型,结果高频场景反而退化了—— 因为「你好」这种问题,规则判断的准确率是 100%,模型的准确率是 99%, 而后者需要 GPU、需要训练数据、需要每次改分类都重训。

所以这里先做收口:问候、转人工、越界请求由路由层的 classify_direct_intent() 直接判定并返回,根本不进网关。网关只处理三种检索类意图:

text
FAQ_QUERY         高频标准问题,走 FAQ 快路径
KNOWLEDGE_QUERY   制度、流程、方案类知识问题,走文档检索
FOLLOW_UP         依赖上下文的追问,需要放宽召回

这个边界很重要:能用确定性规则解决的部分,一行模型推理都不该花。

二、规则和模型,各自负责什么

规则分数不是概率,它是确定性规则的排序权重,写在 config/rules.toml 里:

toml
[intent_rule_scores]
strong_faq = 0.82            # 强 FAQ 特征(问法完整、命中 FAQ 句式)
knowledge = 0.84             # 知识查询特征
source_question_shape = 0.85 # 问法结构指向某个 source
direct_faq_shape = 0.86      # 典型 FAQ 句式

模型侧是一个本地 BERT 分类器(models/bert_intent_classifier_v1, max_length=64、CPU 推理),输出标签和置信度。

两者的分工是明确的:

规则BERT 模型
擅长高频、结构固定、可枚举的问法长尾表达、口语化、新出现的说法
优点完全可解释,改一行配置就生效泛化好,不用为每种说法写规则
风险覆盖不到没写过的说法给不出理由,误判难定位

三、网关仲裁:冲突时听谁的

规则和模型一定会打架。网关的解法不是「加权平均」,而是一棵优先级决策树:

text
1. 模型误判检查:没有历史对话却判成追问 -> 以规则为准
2. 低置信度检查:模型置信度不足 -> 退回规则结论
3. 一致性检查:两路结论一致 -> 直接采用,并提高最终置信度
4. 冲突处理:两路结论不同 -> 按风险等级选择更保守的一边

网关输出的不只一个标签,而是一个完整的 IntentResult:规则分数、模型分数、 最终置信度、候选列表、风险标签、策略版本号。这就是它能被回放和审计的前提—— 线上出现一次误判,你能从 Trace 里还原当时两路分别给了什么分。

冷启动时 warmup_intent_decision_gateway() 会把模型加载进内存, 所以意图判断不会成为首问的瓶颈。

四、意图只是输入,检索计划才是输出

拿到意图之后,真正决定召回质量的是动态检索计划(qa_core/retrieval/strategy.py), 它按五层决策给出参数:

text
1. 意图分支        FAQ / 知识查询 / 追问 -> 基础 top_k 与阈值
2. 短问题保护      查询短于 20 字 -> 限制文档检索,提高直出门槛(FAQ top_k 提到 30)
3. 规则分数保护    规则判断越弱 -> FAQ 直出越保守,文档召回越充分
4. 风险类别        费用 / 合规 / 排障 / 总结 -> 各自使用不同阈值
5. 表格偏好        表格类查询 -> 扩大文档候选池

阈值本身是业务护栏,写在 [retrieval_strategy] 里,和模型参数分开管理:

toml
faq_direct_floor = 0.62                 # FAQ 直出的最低底线
pricing_direct_threshold = 0.84         # 费用类问题必须更确定才敢直出
compliance_direct_threshold = 0.86      # 合规类更高
follow_up_faq_top_k_min = 24            # 追问场景放宽 FAQ 召回
table_context_top_n_min = 7             # 表格问题多给上下文

注意最后两个:风险越高,直出越保守;上下文越依赖,召回越宽。 这不是模型参数, 是业务判断,所以它必须放在业务能改的地方。

五、查询改写:同一句话的多种问法

企业知识库里的用词和员工口语经常对不上。[query_variants] 用配置化的替换规则生成变体:

toml
[[query_variants.replacements]]
when_any = ["发票"]
replace = [["发票", "开票"], ["发票", "账单"]]

[[query_variants.replacements]]
when_any = ["怎么排查"]
replace = [["怎么排查", "如何处理"], ["怎么排查", "处理步骤"]]

短且结构化的查询(不超过 24 字)才触发变体生成,避免长问题被改写跑偏。

动手验证

powershell
# 用意图策略评测集跑一遍,看每类的判定准确率和冲突分布
python scripts/intent/evaluate_intent_policy.py

# 重新训练 / 校准意图模型与阈值(有标注数据时)
python scripts/intent/train_intent_bert.py
python scripts/intent/calibrate_thresholds.py

更快的体感验证:把 config/rules.toml 里的 faq_direct_floor 从 0.62 调到 0.9, 重启后问几个 FAQ 问题,你会看到原本直出的问题开始走文档检索—— 这就是阈值作为护栏的直观效果。

取舍与边界

  • 规则 + 模型的混合方案维护成本更高,换来的是可解释性和长尾覆盖; 如果你的场景问法极窄(比如只有十种查询),纯规则就够了。
  • 阈值必须校准,不能凭感觉设:eval_sets/threshold_calibration_cases.json 就是干这个的。
  • 意图类别不要无限扩张:类别越多,规则冲突和模型混淆越难排查, 新增类别前先问一句「能不能归到现有的三类里」。

下一篇进入检索本体:dense 和 sparse 为什么必须一起用, 以及召回之后那一步重排到底救回了多少正确答案。