阶段三 · BERT 微调与意图识别

意图识别:管理指令解析

一句话总结

意图识别 = 从一句话中判断用户"想做什么". 本篇把它应用到群聊管理场景:从管理员的自然语言消息中解析出操作指令(禁言/踢人/警告),以及操作对象和参数.

前置回顾

上一篇完成了 BERT 微调的完整流程:加载模型 → 准备数据 → 训练 → 评估. 那个任务是"对骂检测"(二/三分类). 本篇扩展到另一个实用场景:管理指令解析. 从分类的角度看,意图识别就是多分类--不过类别变成了"禁言""踢人""警告""查询""闲聊"等操作指令.

群聊管理的指令场景

群管理员的日常操作往往用自然语言表达,而不是固定格式的命令:

固定命令格式(程序员思维):
/mute @张三 30m
/kick @李四
/warn @王五

管理员的实际表达(自然语言):
"把张三禁言半小时"
"踢了那个发广告的"
"警告一下王五,下次再犯直接踢"
"最近谁被禁言了"
"这群怎么这么安静"

如果只做正则匹配,会面临表达多样性的问题:

表达"禁言"的 10+ 种方式:
"禁言张三"
"把张三禁了"
"张三禁言30分钟"
"封一下张三的嘴"
"让张三闭嘴"
"mute张三"
"张三别说话了先"
"管管张三"
...

正则穷举 → 维护噩梦

正则匹配 = Go 的 switch-case 路由. 每种 URL 写一个 case,新增路由就加代码. 10 种 URL 还行,100 种就崩了.
意图识别 = Go 的 http.HandleFunc 模式匹配. 框架理解 URL 的结构,自动路由. 意图识别让系统理解自然语言的结构,自动映射到操作.

任务分解:意图 + 槽位

一条管理指令包含两部分信息:

"把张三禁言半小时"

意图(Intent): mute(禁言)
槽位(Slots):
- target: 张三 ← 操作对象
- duration: 30min ← 持续时间
组件 做什么 模型任务
意图识别 判断整句话的操作类型 句子级分类(和对骂检测一样)
槽位填充 提取具体参数 序列标注(每个词打标签)

本篇先聚焦意图识别(分类),槽位填充在后续阶段展开.

意图体系设计

第一步是定义管理场景的意图标签体系:

群聊管理意图体系(v1):

管理指令:
mute 禁言某人 "把xx禁了" "禁言xx"
kick 踢出某人 "踢了xx" "请xx出去"
warn 警告某人 "警告xx" "提醒一下xx"
unmute 解除禁言 "解禁xx" "让xx说话"
ban_media 禁止发图/链接 "xx不许发图" "禁止xx发链接"

查询类:
query_log 查询操作记录 "最近谁被禁言了" "操作日志"
query_rule 查询群规 "群规是什么" "什么不能发"

非指令:
chat 普通闲聊 "今天天气好" "晚上吃什么"
abuse 辱骂(非指令) "你有病吧" "废物"

意图粒度的权衡

意图拆得越细,每个意图的训练样本越少. 初期建议 5-8 个意图,等数据量够了再细分.

一个实用的判断标准:两个意图在系统中是否触发不同的处理逻辑? 如果"禁言"和"禁言并警告"最终都调同一个 API,就不需要拆成两个意图.

训练数据构建

意图识别的标注比对骂检测更直接--只需要给每句话标一个意图类别:

data/intent_train.csv:

text,intent
"把张三禁言半小时",mute
"把那个发广告的禁了",mute
"踢了李四",kick
"请广告号出去",kick
"警告一下王五",warn
"最近有谁被禁言",query_log
"群规发一下",query_rule
"今天天气好",chat
"你有病吧",abuse

数据量建议

意图 最低样本量 推荐样本量
高频意图(mute, kick, chat) 100 300+
中频意图(warn, query) 50 150+
低频意图(unmute, ban_media) 30 80+

样本量不均衡是常见问题. "chat" 和 "abuse" 的样本通常远多于 "unmute". 两种处理方法:

  1. 过采样少数类:对样本不足的意图做重复采样,使各类别数量均衡
  2. 训练时加类别权重:给少数类更高的损失权重(class_weights = 1.0 / 各类样本数),让模型更重视少数类的错误

Few-Shot 场景:数据极少时怎么办

对某些意图(如 "unmute"),可能只收集到 10-20 条样本. 三种应对策略:

  1. 数据增强:改写同义表达("解禁张三" → "给张三解除禁言" / "让张三可以说话了"),手动扩充到 50+ 条
  2. 合并意图:样本太少的意图合并进相近类别,在槽位里区分(如 "unmute" 并入 "mute",用 action 槽位区分禁言/解禁)
  3. Prompt-based 分类:冷启动阶段用 ChatGPT/通义千问做 zero-shot 分类,等积累够数据再训练专用模型

BERT 意图分类实战

意图识别的训练流程和第 12 篇的对骂检测几乎一样--都是 BERT 微调做分类:

INTENT_LABELS = ["mute", "kick", "warn", "unmute", "query_log", "query_rule", "chat", "abuse"]
label2id = {label: i for i, label in enumerate(INTENT_LABELS)}
id2label = {i: label for i, label in enumerate(INTENT_LABELS)}

model = AutoModelForSequenceClassification.from_pretrained(
"hfl/chinese-roberta-wwm-ext",
num_labels=len(INTENT_LABELS),
label2id=label2id,
id2label=id2label,
)

def preprocess(examples):
tokens = tokenizer(examples["text"], padding="max_length", truncation=True, max_length=64)
tokens["labels"] = [label2id[intent] for intent in examples["intent"]]
return tokens

# 其余训练代码和第 12 篇完全一致,只需修改:
# - num_labels = 8(意图数)
# - max_length = 64(管理指令更短)
# - num_train_epochs = 5(意图类别多,多训几轮)

max_length=64 的原因

管理指令通常很短("把张三禁了"只有 5 个字). 设 max_length=64 既节省计算资源,又不会截断任何管理指令. 对比对骂检测用 128--因为辱骂消息可能更长("你这个人怎么这样啊真的有毛病吧每次都这样...").

规则 + 模型:混合架构

实际系统中,纯模型或纯规则都不是最优解. 混合架构结合两者的优势:

混合处理管线:

输入消息

┌─────────────────┐
│ 规则引擎(第一层) │ ← 精确匹配,零延迟
│ /mute @xxx 30m │ → 直接执行,跳过模型
│ /kick @xxx │
└─────────────────┘
↓ (未命中规则)
┌─────────────────┐
│ 关键词过滤(第二层)│ ← 简单模式匹配
│ "禁言" + @提及 │ → 高置信度,直接执行
└─────────────────┘
↓ (未命中)
┌─────────────────┐
│ BERT 意图分类 │ ← 处理自然语言表达
│ "管管那个人" │ → 概率输出,按阈值决策
└─────────────────┘

后处理 + 执行

为什么保留规则层

覆盖场景 延迟 准确率
规则引擎 /mute, /kick 等固定格式命令 < 1ms 100%(精确匹配)
关键词 "禁言" + @提及 < 1ms 95%+(模式明确)
BERT 模型 "管管那个人""让他闭嘴" 10-50ms 85-90%(复杂表达)

规则能处理的就不走模型--节省算力,减少延迟,避免模型出错.

实现示例

import re

class IntentRouter:
def __init__(self, bert_classifier):
self.bert = bert_classifier
self.command_pattern = re.compile(
r"^/(mute|kick|warn|unmute)\s+@(\S+)(?:\s+(\d+[mhd]))?"
)

def parse(self, text, sender_role="member"):
if sender_role != "admin":
return {"intent": "not_admin", "confidence": 1.0}

cmd_match = self.command_pattern.match(text)
if cmd_match:
return {
"intent": cmd_match.group(1),
"target": cmd_match.group(2),
"duration": cmd_match.group(3),
"confidence": 1.0,
"source": "rule",
}

result = self.bert.predict(text)

if result["confidence"] < 0.7:
return {"intent": "uncertain", "confidence": result["confidence"]}

return {**result, "source": "model"}

置信度阈值

模型输出的 softmax 概率不等于"真实置信度". 一个 0.85 的概率不意味着 85% 的准确率. 实际部署时需要用测试数据校准阈值:

  • 画 precision-confidence 曲线
  • 找到满足 precision > 95% 的最低 confidence 阈值(比如 0.75)
  • 低于阈值的 case 走兜底逻辑(提示管理员确认,或忽略)

槽位填充基础

意图确定后,还需要提取操作参数. 简单场景用规则提取,复杂场景用序列标注模型:

规则提取(推荐起步)

import re

def extract_slots(text, intent):
"""从文本中提取槽位(规则方式)"""
slots = {}

at_match = re.search(r"@(\S+)", text)
if at_match:
slots["target"] = at_match.group(1)

duration_match = re.search(r"(\d+)\s*(分钟|小时|天|min|h|d|m)", text)
if duration_match:
num = int(duration_match.group(1))
unit = duration_match.group(2)
unit_map = {"分钟": "m", "min": "m", "m": "m",
"小时": "h", "h": "h",
"天": "d", "d": "d"}
slots["duration"] = f"{num}{unit_map.get(unit, 'm')}"

return slots

# 示例
extract_slots("把@张三禁言30分钟", "mute")
# → {"target": "张三", "duration": "30m"}

序列标注(进阶)

当规则覆盖不了时(比如"把那个一直发广告的禁了",没有 @ 提及),需要用 NER/序列标注模型:

"把 那个 一直 发 广告 的 禁 了"
O B-TGT I-TGT I-TGT I-TGT O O O

B-TGT = 目标实体开始
I-TGT = 目标实体内部
O = 非实体

提取出: target = "那个一直发广告的"
→ 需要进一步解析:这描述的是哪个用户(结合上下文/最近发广告的人)

序列标注用 AutoModelForTokenClassification 实现,架构和分类类似,但输出从"句子级标签"变成"每个 token 的标签". 这超出了本篇范围,后续有需要时再展开.

评估意图识别系统

意图识别的评估除了标准 F1,还需要关注业务指标:

技术指标:
- Macro F1(所有意图的平均 F1)
- 各意图单独的 Precision / Recall
- Confusion Matrix(哪些意图容易混淆)

业务指标:
- 指令执行准确率(管理员说"禁言",系统是否正确执行了禁言)
- 误触发率(管理员在闲聊,系统错误地执行了管理操作)
- 兜底率(多少比例的消息走到了"uncertain"兜底逻辑)

误触发比漏识别更危险

管理员说"今天谁做饭",系统理解成"踢人"然后真的踢了人--这种误触发的影响远大于漏识别一条禁言指令. 部署时宁可保守(高阈值 + 二次确认),也不能激进触发管理操作.

混淆矩阵分析

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

cm = confusion_matrix(true_labels, pred_labels)
disp = ConfusionMatrixDisplay(cm, display_labels=INTENT_LABELS)
disp.plot(xtick_rotation=45)
plt.tight_layout()
plt.savefig("intent_confusion_matrix.png")

常见混淆:

真实意图 容易误判为 原因
mute warn "管管他" 语义模糊
kick mute "让他消失" 可能是踢人或禁言
query_log chat "最近群里怎么样" 可能是查询或闲聊
abuse chat 轻度吐槽("服了")和闲聊边界模糊

端到端流程串联

把意图识别和对骂检测放进完整的群聊处理管线中:

群聊消息进入

身份判断: 是管理员?
↓ 是 ↓ 否
意图识别管线 内容审核管线
规则 → 关键词 → BERT 对骂检测(BERT)
↓ ↓
管理指令? 违规?
↓ 是 ↓ 否 ↓ 是 ↓ 否
执行操作 正常放行 自动处理 正常放行
(禁言/踢人) (禁言/警告)

两个模型可以是同一个 BERT 的不同微调版本,也可以合并成一个多任务模型(同时输出"内容分类"和"意图类别"). 初期建议分开训练,简化调试.

快速回顾

  • 意图识别 = 句子级多分类:判断"想做什么",本质和对骂检测一样是分类任务
  • 槽位填充 = 参数提取:初期用正则,复杂场景用序列标注模型
  • 混合架构:规则(精确) → 关键词(快速) → BERT(兜底),三层逐步兜底
  • 置信度阈值:模型输出概率需要校准,低置信度走兜底逻辑
  • 误触发 > 漏识别:管理操作不可撤销(踢人/禁言),宁可保守
  • 数据不均衡:少数类用过采样或类别权重处理

动手练习

  1. 设计意图体系:根据自己的群管理经验,设计一套 5-8 个意图的标签体系. 为每个意图写 10 条示例
  2. 训练意图分类器:用上面的代码模板,在自己的数据上训练意图分类模型. 目标 macro F1 > 0.80
  3. 混淆矩阵分析:画出混淆矩阵,找出最容易混淆的两对意图,分析原因并提出改进方案
  4. 实现混合路由:参考 IntentRouter 的代码,实现一个完整的三层路由(规则 → 关键词 → 模型),用 20 条测试 case 验证正确性
  5. 阈值校准:在测试集上画 precision-confidence 曲线,找到 precision > 95% 对应的最低阈值