项目记录
意图识别与检索策略
使用规则候选、分类模型与网关仲裁,把用户问题转换为可解释、可治理的检索计划。
本文目录 · 8 节
用户的问法只有那么几种,但系统要做的决定一点不少: 这句话该直答、该查 FAQ、该检索文档,还是该先追问一句?
如果所有问题都直接丢给向量库,会出现两种典型事故:用户发一句「你好」也被检索, 模型硬着头皮拿无关文档回答;用户搜一个具体编号,语义检索却召不回。
先给结论
这套项目的意图链路是三段式:规则候选 + BERT 模型 + 网关仲裁。
第一段 路由层收口:问候 / 人工客服 / 越界请求,直接返回,不进检索
第二段 规则候选 + 模型打分:规则给可解释的高频判断,BERT 负责长尾表达
第三段 网关仲裁:按优先级决策树融合两路结果,产出可治理、可回放的意图结论
一、先分层,别急着上模型
很多项目一上来就训一个意图分类模型,结果高频场景反而退化了—— 因为「你好」这种问题,规则判断的准确率是 100%,模型的准确率是 99%, 而后者需要 GPU、需要训练数据、需要每次改分类都重训。
所以这里先做收口:问候、转人工、越界请求由路由层的 classify_direct_intent()
直接判定并返回,根本不进网关。网关只处理三种检索类意图:
FAQ_QUERY 高频标准问题,走 FAQ 快路径
KNOWLEDGE_QUERY 制度、流程、方案类知识问题,走文档检索
FOLLOW_UP 依赖上下文的追问,需要放宽召回
这个边界很重要:能用确定性规则解决的部分,一行模型推理都不该花。
二、规则和模型,各自负责什么
规则分数不是概率,它是确定性规则的排序权重,写在 config/rules.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 模型 | |
|---|---|---|
| 擅长 | 高频、结构固定、可枚举的问法 | 长尾表达、口语化、新出现的说法 |
| 优点 | 完全可解释,改一行配置就生效 | 泛化好,不用为每种说法写规则 |
| 风险 | 覆盖不到没写过的说法 | 给不出理由,误判难定位 |
三、网关仲裁:冲突时听谁的
规则和模型一定会打架。网关的解法不是「加权平均」,而是一棵优先级决策树:
1. 模型误判检查:没有历史对话却判成追问 -> 以规则为准
2. 低置信度检查:模型置信度不足 -> 退回规则结论
3. 一致性检查:两路结论一致 -> 直接采用,并提高最终置信度
4. 冲突处理:两路结论不同 -> 按风险等级选择更保守的一边
网关输出的不只一个标签,而是一个完整的 IntentResult:规则分数、模型分数、
最终置信度、候选列表、风险标签、策略版本号。这就是它能被回放和审计的前提——
线上出现一次误判,你能从 Trace 里还原当时两路分别给了什么分。
冷启动时 warmup_intent_decision_gateway() 会把模型加载进内存,
所以意图判断不会成为首问的瓶颈。
四、意图只是输入,检索计划才是输出
拿到意图之后,真正决定召回质量的是动态检索计划(qa_core/retrieval/strategy.py),
它按五层决策给出参数:
1. 意图分支 FAQ / 知识查询 / 追问 -> 基础 top_k 与阈值
2. 短问题保护 查询短于 20 字 -> 限制文档检索,提高直出门槛(FAQ top_k 提到 30)
3. 规则分数保护 规则判断越弱 -> FAQ 直出越保守,文档召回越充分
4. 风险类别 费用 / 合规 / 排障 / 总结 -> 各自使用不同阈值
5. 表格偏好 表格类查询 -> 扩大文档候选池
阈值本身是业务护栏,写在 [retrieval_strategy] 里,和模型参数分开管理:
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] 用配置化的替换规则生成变体:
[[query_variants.replacements]]
when_any = ["发票"]
replace = [["发票", "开票"], ["发票", "账单"]]
[[query_variants.replacements]]
when_any = ["怎么排查"]
replace = [["怎么排查", "如何处理"], ["怎么排查", "处理步骤"]]
短且结构化的查询(不超过 24 字)才触发变体生成,避免长问题被改写跑偏。
动手验证
# 用意图策略评测集跑一遍,看每类的判定准确率和冲突分布
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 为什么必须一起用, 以及召回之后那一步重排到底救回了多少正确答案。