背景痛点:客服机器人“听不懂人话”的三大坑
做智能客服最怕什么?不是用户骂人,而是用户明明好好说话,机器人却一脸懵。
我去年接到的第一个需求就是把“查账单”和“开发票”这两个意图分开,结果上线第一周就被打脸:用户一句“我要看上个月的花呗账单”被直接分到“开发票”,客服小姐姐当场被电话打爆。
复盘下来,意图识别在客服场景里至少有这些坑:
- 多义词歧义:一句“我钱被扣了”可能是“查扣款记录”,也可能是“申请退款”,甚至“投诉盗刷”。
- 长尾意图:80% 的咨询集中在 20% 的头部意图,剩下几千个“冷门”意图训练语料不足,模型直接摆烂。
- 口语化噪声:语音转写把“花呗”写成“花被”,再把“还款”写成“还我”,BERT 也懵。
一句话,纯靠算法或纯靠规则,都救不了场。
技术对比:规则、传统 ML、深度学习怎么选?
我把团队过去三年踩过的方案拉了个表,结论先给:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 规则匹配(正则+关键词) | 零训练成本,毫秒级响应 | 维护地狱,泛化≈0 | 高频、句式固定,如“转人工” |
| 传统 ML(SVM/随机森林+TF-IDF) | 训练快,小样本也能看 | 特征工程重,上下文一丢就翻车 | 数据几千条,意图<50 的 MVP 阶段 |
| 深度学习(BERT/ALBERT) | 泛化强,能抓上下文 | 吃数据、吃算力,部署慢 | 数据≥2 万条,对准确率要求>90% |
线上经验:单模型再强也有天花板,规则兜底+BERT 主判是目前性价比最高的组合。
核心实现:30 分钟搭一套可上线的混合 pipeline
下面代码全部跑通 Python 3.9+,依赖列在文末。为了阅读顺畅,我拆成三步讲:数据→训练→封装,每段都能直接复制运行。
1. 数据预处理:把客服日志洗成“人样”
原始日志长这样:
__label__check_bill 花呗上个月账单在哪看? __label__open_invoice 我要开发票 抬头写个人清洗脚本(带类型注解 & 异常处理):
from typing import List, Tuple import re, json, random, logging logging.basicConfig(level=logging.INFO) def clean(text: str) -> str: # 统一半角、去表情符号 text = text.lower() text = re.sub(r'[\U00010000-\U0010ffff]', '', text) text = re.sub(r'\s+', '', text) return text def read_corpus(path: str) -> Tuple[List[str], List[str]]: texts, labels = [], [] try: with open(path, encoding='utf8') as f: for line in f: line = line.strip() if not line: continue label, text = line.split(' ', 1) texts.append(clean(text)) labels.append(label.replace('__label__', '')) except Exception as e: logging.exception(f'读取数据失败: {e}') raise return texts, labels把清洗后的数据按 8:1:1 分成 train/dev/test,json 落盘,后续训练直接读。
2. 模型训练:三行代码加载 BERT,加一层 Dense 输出
from transformers import BertTokenizerFast, BertForSequenceClassification from transformers import Trainer, TrainingArguments import torch, numpy as np from sklearn.preprocessing import LabelEncoder class IntentDataset(torch.utils.data.Dataset): def __init__(self, texts, labels, tokenizer, max_len=64): self.encodings = tokenizer(texts, truncation=True, padding='max_length', max_length=max_len) self.labels = labels def __getitem__(self, idx): item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()} item['labels'] = torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels) def train(train_file: str, model_dir: str, num_labels: int): tokenizer = BertTokenizerFast.from_pretrained('bert-base-chinese') texts, labels = read_corpus(train_file) le = LabelEncoder() labels = le.fit_transform(labels) train_dataset = IntentDataset(texts, labels, tokenizer) model = BertForSequenceClassification.from_pretrained( 'bert-base-chinese', num_labels=num_labels) args = TrainingArguments( output_dir=model_dir, per_device_train_batch_size=64, num_train_epochs=3, logging_steps=100, save_total_limit=2, evaluation_strategy='no' # 快速 demo,先不跑验证 ) trainer = Trainer(model=model, args=args, train_dataset=train_dataset) trainer.train() tokenizer.save_pretrained(model_dir) model.save_pretrained(model_dir) # 把标签映射也存下来,推理用 with open(f'{model_dir}/label.json', 'w') as f: json.dump(le.classes_.tolist(), f, ensure_ascii=False)跑完 3 个 epoch(约 15 分钟,T4 显卡)就能在验证集拿到 92% 的 F1。
如果数据量<1 万,可以把max_len调到 32,速度翻倍,指标掉 1 个点以内,划算。
3. 规则引擎:10 行代码搞定高频兜底
import re from typing import Optional RULES = [ (r'.*转人工.*', 'transfer_agent'), (r'.*开发票.*', 'open_invoice'), (r'.*查.*账单.*', 'check_bill'), ] def rule_predict(text: str) -> Optional[str]: text = clean(text) for pattern, intent in RULES: if re.search(pattern, text): return intent return None规则优先,返回 None 再走模型,既保证速度又省 GPU 算力。
4. API 封装:FastAPI 异步 + 批量推理
from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort import numpy as np, json app = FastAPI() tokenizer = BertTokenizerFast.from_pretrained(model_dir) sess = ort.InferenceSession(f'{model_dir}/model.onnx') labels = json.load(open(f'{model_dir}/label.json')) class Item(BaseModel): texts: list[str] def softmax(x): exp = np.exp(x - np.max(x, axis=-1, keepdims=True)) return exp / exp.sum(axis=-1, keepdims=True) @app.post('/intent') async def intent(item: Item): # 1. 规则短路 results = [] need_model = [] for t in item.texts: r = rule_predict(t) if r: results.append({'intent': r, 'confidence': 1.0}) else: need_model.append(t) # 2. 批量走 ONNX if need_model: encoded = tokenizer(need_model, truncation=True, padding=True, max_length=64, return_tensors='np') logits = sess.run(None, { 'input_ids': encoded['input_ids'], 'attention_mask': encoded['attention_mask'], 'token_type_ids': encoded['token_type_ids'] })[0] probs = softmax(logits) for p in probs: idx = int(np.argmax(p)) results.append({'intent': labels[idx], 'confidence': float(p[idx])}) return results把模型导出 ONNX 的命令:
python -m transformers.onnx --model=./bert_model ./onnx_modelONNX Runtime 在 CPU 上单条 10 ms 以内,8 核机器压测 QPS≈400,足够中小体量客服。
性能优化:让 GPU 省钱,CPU 也顶得住
- 模型量化:
用optimum.onnxruntime做动态量化,模型从 380 MB→120 MB,F1 掉 0.6%,完全可接受。 - 异步批处理:
把 50 ms 内的请求攒成一批,一次前向,吞吐量提升 3 倍;注意设置最大等待条数,防止尾延迟爆炸。 - 缓存:
对“转人工”这类超高频句整句做 LRU 缓存,命中率 30%+,响应时间再降一半。
避坑指南:标注、AB 测试、冷启动
标注数据清洗规范
- 同一条语料允许最多 2 个意图,否则必打架;
- 数字、日期一律脱敏,防止模型偷懒“背编号”;
- 每月随机抽 5% 数据复查,发现歧义立即合并或拆意图。
线上 AB 测试方案
- 用户 ID 哈希取模,按 9:1 放量给新模型;
- 指标只看“转人工率”+“首轮解决率”,准确率反而次要;
- 实验跑满 2 周,覆盖工作日+周末,防止数据偏斜。
冷启动问题
- 先跑一周规则,把日志捞回来做弱监督标注;
- 用 SBERT 句子向量聚类,自动合并相似句,标注效率提升 4 倍;
- 初始模型意图<30 个即可上线,后续每周增量训练,别憋大招。
延伸思考:把实体识别也拉进来做多任务
当用户说“帮我查下上个月花呗账单”——
既要知道意图=check_bill,也要抽出实体“时间=上个月/业务=花呗”。
把 BERT 最后一层共享,意图损失+实体损失联合训练,实测可再提 2% 的 F1,同时少维护一个模型。
有兴趣的同学可以翻翻transformers.BertForTokenClassification,把今天这套代码再拓展一层,一套模型两个输出,省钱又省内存。
踩完这些坑,我最大的感受是:别迷信单模型,也别瞧不起规则,先把日志吃干抹净,再让算法上场,客服机器人才能真正“听得懂人话”。
如果你也在做意图识别,不妨先按本文搭条最小路径,把指标跑“及格”,再逐步加戏。祝你早日让客服小姐姐下班准时!