背景痛点:传统客服系统为何“听不懂人话”
去年双十一,公司老客服系统差点把“我要退货”识别成“我要睡觉”,结果用户被气得直接投诉。复盘发现,规则引擎在面对口语化、错别字、领域缩写时几乎全线崩溃。总结下来,三大硬伤:
- 意图识别靠关键词+正则,稍一变体就翻车
- 多轮对话状态机写得像“蜘蛛网”,一改需求就牵一发动全身
- 新业务上线要先堆规则,维护成本指数级上涨
痛定思痛,我们决定用 AI 辅助开发,把“堆人天”变成“调模型”。
技术对比:规则、纯BERT、Rasa+BERT谁更扛得住?
在同样 4 核 8 G 的容器里压测,结果如下:
| 方案 | 平均QPS | 意图准确率 | 周维护人时 |
|---|---|---|---|
| 规则引擎 | 1200 | 72 % | 18 h |
| 纯BERT服务 | 350 | 91 % | 3 h |
| Rasa+BERT混合 | 820 | 89 % | 4 h |
纯BERT 准确率高,但推理延迟大;规则引擎快却笨;Rasa 负责对话管理,BERT 只干“听懂话”一件事,两者互补,QPS 翻倍,维护量也没增加多少。于是敲定“混合架构”路线。
核心实现:让BERT听懂话,让Rasa管对话
1. BERT意图分类Fine-tuning(含数据增强)
训练数据只有 1.2 万条,先上增强:
- 随机删词、同义词替换、拼音混淆,数据量扩到 5 万
- 用 whole word masking,防止中文被切成乱码
代码如下,可直接丢进 Colab 跑:
# intent_train.py import torch, random, jieba from transformers import BertTokenizerFast, BertForSequenceClassification from sklearn.model_selection import train_test_split from torch.utils.data import Dataset, DataLoader MAX_LEN = 64 BATCH = 32 LR = 2e-5 EPOCHS = 4 def aug(text): """简单数据增强:随机同义词替换""" seg = jieba.lcut(text) for i, w in enumerate(seg): if random.random() < 0.15: seg[i] = random.choice(SYNONYM_DICT.get(w, [w])) return ''.join(seg) class IntentDataset(Dataset): def __init__(self, texts, labels, tokenizer, aug_prob=0.5): self.encodings = tokenizer( [aug(t) if random.random() < aug_prob else t for t in texts], truncation=True, padding='max_length', max_length=MAX_LEN) self.labels = labels def __getitem__(self, idx): return {k: torch.tensor(v[idx]) for k, v in self.encodings.items()} | { 'labels': torch.tensor(self.labels[idx])} def __len__(self): return len(self.labels) # 读取原始数据 texts, labels = load_raw_data('intent.csv') train_txt, val_txt, train_lbl, val_lbl = train_test_split(texts, labels, test_size=0.1) tokenizer = BertTokenizerFast.from_pretrained('bert-base-chinese') model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=len(set(labels))) train_set = IntentDataset(train_txt, train_lbl, tokenizer) val_set = IntentDataset(val_txt, val_lbl, tokenizer, aug_prob=0) loader = DataLoader(train_set, batch_size=BATCH, shuffle=True) optimizer = torch.optim.AdamW(model.parameters(), lr=LR) for epoch in range(EPOCHS): model.train() for batch in loader: optimizer.zero_grad() out = model(**{k: v for k, v in batch.items() if k != 'labels'}) loss = torch.nn.functional.cross_entropy(out.logits, batch['labels']) loss.backward() optimizer.step() # 省略验证代码 torch.save(model.state_fam(), 'intent_cls.pt')训练 4 轮,验证集准确率 91.3 %,够用了。
2. Rasa对话管理:Domain 与自定义 Action
Rasa 3.x 版本把“故事”和“域”拆得很干净,维护起来像写接口文档。核心文件就三:
domain.yml:定义意图、实体、槽位、回复模板rules.yml:单轮直达场景stories.yml:多轮跳转
示例片段(domain.yml):
intents: - request_return - affirm - deny entities: - order_id slots: order_id: type: text mappings: - entity: order_id type: from_entity responses: utter_ask_order_id: - text: 请问您的订单号是多少? actions: - action_query_return_status自定义 Action 里调内部 API,把订单状态捞回来:
# actions.py from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher import requests, os class ActionQueryReturnStatus(Action): def name(self): return "action_query_return_status" def run(self, dispatcher, tracker: Tracker, domain): order_id = tracker.get_slot("order_id") if not order_id: dispatcher.utter_message(text="订单号还没给我呢") return [] # 内部服务走 Kubernetes DNS rsp = requests.get( f"http://order-svc.default.svc.cluster.local/api/return?oid={order_id}", timeout=1.5) if rsp.status_code != 200: dispatcher.utter_message(text="系统开小差了,稍后再试") return [] data = rsp.json() dispatcher.utter_message(text=f"订单{order_id}退货进度:{data['status']}") return []把镜像打成rasa-action:1.0.0,在 values 里配好extraContainers,一条命令helm upgrade就上线。
性能优化:让GPU“省一点”,让Redis“快一点”
1. ONNX+量化,延迟腰斩
BERT 原模型 400 MB,FP32 推理 180 ms;走 ONNX Runtime 动态量化后,体积 110 MB,延迟 82 ms,准确率只掉 0.6 %,划算。
# export_onnx.py from transformers import BertTokenizerFast, BertForSequenceClassification import torch, onnx, onnxruntime as ort model = BertForSequenceClassification.from_pretrained('./intent_cls.pt') tokenizer = BertTokenizerFast.from_pretrained('bert-base-chinese') dummy = tokenizer("退货", return_tensors='pt') torch.onnx.export( model, (dummy['input_ids'], dummy['attention_mask']), 'intent_cls.onnx', input_names=['input_ids', 'attention_mask'], output_names=['logits'], dynamic_axes={'input_ids': {0: 'batch'}, 'logits': {0: 'batch'}}, opset_version=11) # 动态量化 from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic('intent_cls.onnx', 'intent_cls.quant.onnx', weight_type=QuantType.QInt8)2. 异步+Redis缓存,QPS翻倍
对话状态默认走 SQLite,高并发下锁等待惨不忍睹。改成 Redis,把 Tracker 序列化成 JSON 扔进去,key 用sender_id,TTL 30 min,接口层再用aioredis做连接池,QPS 从 400 涨到 820,P99 延迟降 40 %。
# redis_tracker_store.py import json, aioredis from rasa.core.tracker_store import TrackerStore from rasa.shared.core.trackers import DialogueStateTracker class RedisTrackerStore(TrackerStore): def __init__(self, domain, host='redis', port=6379, db=0): self.redis = aioredis.from_url(f"redis://{host}:{port}/{db}") async def save(self, tracker: DialogueStateTracker): key = f"tracker:{tracker.sender_id}" await self.redis.setex(key, 1800, json.dumps(tracker.as_dialogue().as_dict())) async def retrieve(self, sender_id: str) -> DialogueStateTracker: data = await self.redis.get(f"tracker:{sender_id}") if data: return DialogueStateTracker.from_dict( self.domain, json.loads(data), sender_id) return None避坑指南:那些线上踩过的坑
领域术语 OOV
用户说“我要退差价”,BERT 切成“退/价/差”,结果“差价”不在词表。把 tokenizer 换成BertTokenizerFast(do_basic_tokenize=False),再开sentencepiece子词,OOV 率从 5 % 降到 0.8 %。对话幂等
用户狂点“查询退货”,自定义 Action 被重复调,订单系统压力爆炸。在 Action 里加redis.setnx(order_id, ttl=5),5 秒内同一订单号拒绝重入,保证幂等。冷启动
新模型刚发布,Pod 一次性拉 200 并发,GPU 显存直接 OOM。用k8s readinessProbe先测/health,返回 200 才放流量;同时加initialDelaySeconds=60,让模型充分加载。
延伸思考:把知识图谱拉进来
目前回答只能查订单状态,如果用户问“蓝牙耳机和有线耳机退货政策一样吗”,得靠提前写故事。把商品-政策-场景三元组灌进 NebulaGraph,再在 Action 里加一道图谱检索:
# kggs.py from nebula3.gclient.net import ConnectionPool def query_policy(product, scene): stmt = f"USE customer; MATCH (p:Product{{name:'{product}'}})-[:hasPolicy]->(po:Policy) RETURN po.{scene};" return pool.execute(stmt)把返回结果塞进回复模板,就能做到“千人千面”的精准回答,后续再慢慢把图谱推理权重和 Rasa 的 Policy 融合,实现可解释的对话决策。
整个系统上线三个月,意图准确率稳定在 89 % 左右,平均响应 220 ms,客服人力减少 40 %。回头看,最大感受是:AI 辅助开发不是“模型万能”,而是让模型做最擅长的事,把脏活累活交给规则与工程。下一步,想把多模态用户情绪也接进来,让客服机器人不仅“听得懂”,还能“读得懂表情”。如果你也在踩智能客服的坑,欢迎留言一起交流。