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 秒——这在线上是不可接受的。

那怎么设计一个好的正则模板?我个人习惯遵循这几个原则:

  1. 从具体到抽象:先收集 100 条真实用户语料,找出共性模式,再抽象成正则。
  2. 分而治之:一个意图可以拆成多个子模板。比如「查询话费」可以拆成「查号码话费」「查本机话费」「查欠费」三个子模板。
  3. 预留扩展位:用 (?:...)? 把可选的修饰词包起来,比如「帮我」「请」「麻烦」这些。

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 实战:构建一个简单的规则意图识别器

最后,咱们动手搭一个最小可用的规则意图识别器。我把它叫做「三件套」:

  1. 词典层:每个意图维护一个关键词集合,做粗筛
  2. 正则层:对粗筛结果做精细匹配,提取槽位
  3. 决策层:综合匹配得分和槽位填充情况,输出最终意图
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%。当然,后面随着用户量增长,慢慢加入了统计模型和深度学习模型,但规则层始终作为兜底策略保留着。

嗯,今天就先聊到这儿。下一节咱们会讲基于统计的意图识别方法,到时候你会看到,规则和统计结合起来,效果会好得多。