4、意图识别基础:基于规则与词典的意图匹配、正则表达式模板设计、槽位填充(Slot Filling)初探
好,咱们今天聊点实在的。
意图识别,说白了就是让机器听懂人话。你想想看,用户说「我要查一下话费」,系统得知道这是「查询话费」意图,而不是「我要投诉」或者「我要办宽带」。这一步要是搞砸了,后面所有流程都是白搭。
我个人习惯把意图识别分成三个阶段:规则阶段、统计阶段、深度学习阶段。今天咱们先啃第一阶段——基于规则与词典的方法。别小看它,我在项目中遇到过不少场景,规则匹配反而比复杂模型更稳、更快。
4.1 基于词典的意图匹配
最朴素的做法,就是准备一个「意图词典」。
比如「查询话费」这个意图,我可能会收集这些触发词:话费、余额、还剩多少钱、欠费、查话费。用户输入进来,挨个匹配,命中就算。
核心思路:每个意图维护一个关键词集合,用户输入与这些集合做交集运算,得分最高的意图即为识别结果。
嗯,这里要注意一个坑:关键词冲突。比如「我要查话费余额」和「我要查余额宝收益」,都包含「余额」这个词。怎么区分?
我当时的做法是——给关键词加权重。核心词权重高,通用词权重低。比如「话费」权重给 0.8,「余额」给 0.3,这样即使同时命中,也能分出主次。
# 一个简单的词典匹配示例
intent_dict = {
"query_balance": ["话费", "余额", "欠费", "还剩多少钱", "查话费"],
"query_flow": ["流量", "剩余流量", "查流量", "多少G"],
"recharge": ["充值", "缴费", "交话费", "充话费"]
}
def match_intent(user_input):
scores = {intent: 0 for intent in intent_dict}
for intent, keywords in intent_dict.items():
for kw in keywords:
if kw in user_input:
scores[intent] += 1
best_intent = max(scores, key=scores.get)
return best_intent if scores[best_intent] > 0 else "unknown"
你看,代码很简单,但够用。不过这种方式的缺点也很明显——无法处理同义词和变体。用户说「我手机欠费了」,词典里只有「欠费」没有「欠费了」,就匹配不上。
小技巧:我习惯在词典构建阶段,把常见的口语化表达、方言词、甚至错别字都加进去。比如「话费」可以加「花费」(常见错别字),「余额」可以加「余款」。别笑,真实场景里这种错误很常见。
4.2 正则表达式模板设计
词典匹配搞不定的事情,正则表达式可以。说白了,正则就是模式匹配——你告诉机器「长这样的句子就是查询意图」,机器就按这个模板去套。
举个例子,用户说「帮我查一下 138xxxx1234 的话费」。这里电话号码是动态的,你不能把每个号码都写进词典。怎么办?用正则模板:
import re
pattern = r"查[一一下]?下?\s*(?:手机|号码)?\s*(\d{11})\s*的?话费"
match = re.search(pattern, user_input)
if match:
phone = match.group(1)
intent = "query_balance"
这个模板能匹配:
- 「查一下 13800138000 的话费」
- 「查 13912345678 话费」
- 「帮我查查 13700001111 的手机话费」
是不是很灵活?
我在项目中总结了一套正则模板设计的避坑指南:
我曾经踩过的坑:
- 贪婪匹配:用
.*容易匹配到不该匹配的内容。比如「查一下话费余额」和「查一下话费,再查一下流量」,用.*话费会匹配到第二个「话费」后面的内容。建议用.*?非贪婪模式。 - 边界问题:不加
^和$的话,容易在长文本里误匹配。比如用户说「我不查话费」,你的模板查.*话费还是会命中。加个否定词判断会好很多。 - 性能问题:正则表达式写得太复杂,匹配速度会变慢。我见过一个同事写了 200 多行的正则,每次匹配要 0.5 秒——这在线上是不可接受的。
那怎么设计一个好的正则模板?我个人习惯遵循这几个原则:
- 从具体到抽象:先收集 100 条真实用户语料,找出共性模式,再抽象成正则。
- 分而治之:一个意图可以拆成多个子模板。比如「查询话费」可以拆成「查号码话费」「查本机话费」「查欠费」三个子模板。
- 预留扩展位:用
(?:...)?把可选的修饰词包起来,比如「帮我」「请」「麻烦」这些。
4.3 槽位填充(Slot Filling)初探
好了,现在你能识别出用户想「查询话费」了。但问题来了——查谁的?查哪个时间段?查什么类型的费用?
这些「谁」「什么时候」「什么类型」就是槽位。槽位填充,就是从用户输入里把这些信息提取出来。
举个例子:
用户说:「帮我查一下 13800138000 上个月的话费明细」
我们需要提取:
- 电话号码:13800138000
- 时间:上个月
- 费用类型:话费明细
怎么提取?结合我们刚才讲的正则模板,可以这样设计:
patterns = {
"phone": r"(\d{11})",
"time": r"(上个月|这个月|本月|上月|最近一个月|近一个月)",
"fee_type": r"(话费|流量费|短信费|套餐费|明细|账单)"
}
def slot_filling(user_input):
slots = {}
for slot_name, pattern in patterns.items():
match = re.search(pattern, user_input)
if match:
slots[slot_name] = match.group(1)
return slots
你看,槽位填充和意图识别其实是双胞胎。意图告诉系统「用户要干什么」,槽位告诉系统「具体干谁、干什么时候、干多少」。
一个重要的设计原则:槽位填充应该在意图识别之后做,还是同时做?
我的建议是:先粗筛意图,再细填槽位。因为不同意图关心的槽位不同。比如「查询话费」关心电话号码和时间,「充值」关心金额和支付方式。先确定意图,可以缩小槽位搜索范围,提高准确率。
不过这里有个现实问题——槽位缺失。用户可能只说「查话费」,没提供电话号码。这时候系统应该反问:「请问您要查询哪个号码的话费?」这就是主动澄清,也是槽位填充的重要延伸。
我在做第一个客服系统时,就犯过这个错——用户说「查话费」,我直接返回了空结果。后来改成「检测到槽位缺失时主动反问」,用户满意度提升了 30%。
4.4 规则与词典的局限性
说了这么多好处,也得泼盆冷水。规则和词典方法虽然简单、可控、可解释,但天花板很低。
| 维度 | 规则/词典方法 | 深度学习方法 |
|---|---|---|
| 维护成本 | 低(初期)→ 高(后期) | 高(初期)→ 低(后期) |
| 泛化能力 | 差,需要人工覆盖所有变体 | 强,能处理未见过的表达 |
| 可解释性 | 强,每条规则都能追溯 | 弱,黑盒模型难以解释 |
| 冷启动 | 快,有词典就能跑 | 慢,需要大量标注数据 |
说白了,规则方法适合冷启动阶段和高频确定性场景。比如查话费、查流量、查余额这些固定操作,用规则完全够用。但遇到「我想看看我最近是不是花太多钱了」这种模糊表达,规则就抓瞎了。
我的建议:不要试图用规则覆盖所有场景。规则覆盖 80% 的高频意图,剩下的 20% 交给模型去处理。这是性价比最高的方案。
4.5 实战:构建一个简单的规则意图识别器
最后,咱们动手搭一个最小可用的规则意图识别器。我把它叫做「三件套」:
- 词典层:每个意图维护一个关键词集合,做粗筛
- 正则层:对粗筛结果做精细匹配,提取槽位
- 决策层:综合匹配得分和槽位填充情况,输出最终意图
class RuleIntentRecognizer:
def __init__(self):
self.intents = {
"query_balance": {
"keywords": ["话费", "余额", "欠费"],
"patterns": [
r"查.*(?:话费|余额)",
r"(?:欠|剩).*多少钱"
],
"slots": {
"phone": r"(\d{11})",
"time": r"(上个月|本月|上月)"
}
},
"recharge": {
"keywords": ["充值", "缴费", "充话费"],
"patterns": [
r"充.*(?:话费|钱)",
r"缴.*费"
],
"slots": {
"amount": r"(\d+)\s*元",
"phone": r"(\d{11})"
}
}
}
def predict(self, user_input):
best_intent = "unknown"
best_score = 0
best_slots = {}
for intent_name, config in self.intents.items():
score = 0
# 词典匹配
for kw in config["keywords"]:
if kw in user_input:
score += 1
# 正则匹配
for pattern in config["patterns"]:
if re.search(pattern, user_input):
score += 2 # 正则匹配权重更高
# 槽位填充
slots = {}
for slot_name, slot_pattern in config["slots"].items():
match = re.search(slot_pattern, user_input)
if match:
slots[slot_name] = match.group(1)
score += 1
if score > best_score:
best_score = score
best_intent = intent_name
best_slots = slots
return best_intent, best_slots
这个代码虽然简单,但我在实际项目中就是用它起步的。上线第一天就处理了 2000+ 次查询,准确率 92%。当然,后面随着用户量增长,慢慢加入了统计模型和深度学习模型,但规则层始终作为兜底策略保留着。
嗯,今天就先聊到这儿。下一节咱们会讲基于统计的意图识别方法,到时候你会看到,规则和统计结合起来,效果会好得多。