背景痛点:为什么传统规则引擎撑不住客服量?
去年双十一,我们把一套“关键词+正则”规则引擎丢给电商客户做灰度,结果凌晨两点被叫醒:并发一上万,CPU飙到 90%,用户问“我订单怎么还没发?”被识别成“开发票”,直接转售后,导致投诉率翻三倍。
复盘发现三大硬伤:
- 意图漂移衰减:规则冲突随业务线膨胀,NLU 准确率从 92% 掉到 74%。
- 多轮状态丢失:Redis 里只存了
session_id→last_intent,用户中途换商品就全乱。 - 异常兜底粗糙:命中“other”就甩人工,高峰期客服排队 600+,体验崩盘。
痛定思痛,决定用 AI 把对话引擎重做一遍,目标只有一个:准确率↑、延迟↓、能扛 3w QPS。
技术方案选型:规则 vs 机器学习 vs 深度学习
先给一份压测对比(4C8G Docker 内网,200 并发持续 5min):
| 方案 | 平均 QPS | 平均延迟 | 意图准确率 | 备注 |
|---|---|---|---|---|
| 规则引擎 | 1200 | 160 ms | 74% | 后期规则爆炸,难维护 |
| FastText | 3500 | 45 ms | 83% | 训练快,但语义泛化弱 |
| BERT+BiLSTM | 4200 | 78 ms | 93.6% | 模型大,需 GPU |
结论:
- 对并发与准确率双高场景,深度模型是必选项;
- 只要工程化做得好,78 ms 延迟完全可接受。
混合模型架构设计
- 表示层:Chinese-BERT-base 输出 768 维向量,捕获深层语义;
- 序列层:双向 LSTM 接 CRF,解决“地点+时间”槽位依赖;
- 意图头:简单 Dense+Softpool,把整句向量压到 64 维,再分类;
- 蒸馏分支:训练时同步蒸馏到 4 层 TinyBERT,线上做双轨——GPU 节点跑大模型,CPU 节点跑蒸馏模型,故障自动降级。
代码实现:Rasa 自定义 Policy 与数据增强
1. 自定义 Policy 实现对话状态机
# policies/bertrule_policy.py from rasa.core.policies.policy import Policy from rasa.core.events import ConversationPaused import torch, json, redis class BertRulePolicy(Policy): def __init__(self, featurizer, model_path, r_host, r_port): super().__init__(featurizer) self.model = torch.jit.load(model_path) # 加载蒸馏模型 self.redis = redis.Redis(r_host, r_port, decode_responses=True) def predict_action_probabilities(self, tracker, domain): last_msg = tracker.latest_message.text slots = {e["entity"]: e["value"] for e in tracker.latest_message.entities} # 幂等键:user_id+msg_hash key = f"dup:{tracker.sender_id}:{hash(last_msg)}" if self.redis.exists(key): return self._action_probs("action_duplicate_reply", domain) self.redis.setex(key, 60, 1) # 60s 防重 # 意图识别 with torch.no_grad(): inputs = self.tokenizer(last_msg, return_tensors="pt") logits = self.model(**inputs).logits intent_id = int(logits.argmax(-1)) # 状态机:商品→支付→物流 state = tracker.get_slot("flow_state") or "init" if state == "init" and intent_id == 2: # 查商品 return self._action_probs("action_set_flow_state_product", domain) if state == "product" and intent_id == 5: # 转支付 return self._action_probs("action_set_flow_state_pay", domain) # ... 更多状态略 return self._action_probs("action_default_fallback", domain)2. 微调时的数据增强技巧
# augment.py from textaugment import EDA eda = EDA(synonym_prob=0.15, random_prob=0.1) def augment(df, times=3): new_rows = [] for _, row in df.iterrows(): for i in range(times): text = eda(row["text"]) new_rows.append({"text": text, "intent": row["intent"]}) return pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True) # 关键超参数 # lr=2e-5, batch=32, epoch=5, max_seq=128, warmup=0.1, weight_decay=0.01经验:
- 电商场景做 EDA 别用同义词替换价格数字,否则“ 199 元”变“ 198 元”会触发价格保护投诉;
- 槽位样本不足时,用模板+随机实体填充 10 倍扩增,比单纯回译更稳。
生产考量:让模型活着比跑分更重要
1. 对话服务的幂等性设计
- 每条用户消息生成
msg_id=UUID,进入 Kafka 前按 key 去重; - 推理侧返回结果带
msg_id,APP 端若重试同一 ID 直接返回缓存,避免重复发券/扣款。
2. Prometheus 意图识别监控
# intent_monitor.py from prometheus_client import Counter, Histogram intent_counter = Counter("intent_total", "Intent counts", ["intent"]) latency_hist = Histogram("intent_latency_seconds", "Model latency") def monitor(intent, seconds): intent_counter.labels(intent=intent).inc() latency_hist.observe(seconds)Grafana 看板加两条告警:
- 意图“other”占比 > 15% 持续 5min → 电话告警;
- P99 延迟 > 300 ms → 自动扩容 GPU Pod。
避坑指南:三次深夜事故复盘
OOM 崩溃
原因:忘了关torch.no_grad()外又加torch.cuda.empty_cache(),显存碎片把 T4 卡撑爆。
解决:推理服务改fp16+batch=1,并给 Gunicorn 加--max-requests 1000定期重启 Worker。方言识别失效
原因:训练集全是普通话,粤语用户“咁快发货?”全部进“other”。
解决:拉 5k 方言音频,用 ASR 文本+拼音对齐做伪标签,再蒸馏回主模型,准确率拉回 90%。Redis 单点丢状态
原因:主节点宕机,哨兵切备机丢 30s 数据,多轮对话全乱。
解决:状态快照双写 Redis+MySQL,每轮结束异步落盘;重启时先读 MySQL 基线,再 Redis 补增量。
上线效果与可复制的调优路径
灰度两周,核心指标:
- 意图准确率:74% → 93.6%,提升 26%;
- 平均响应:160 ms → 78 ms;
- 人工转接率:38% → 18%,直接释放 40+ 坐席。
后续三步走:
- 把蒸馏 TinyBERT 推到移动端,离线也能跑;
- 引入 RLHF,用坐席点踩数据做奖励模型,持续迭代 Policy;
- 做多模态,用户发截图就能定位订单,进一步降低描述成本。
延伸思考:复杂度与延迟的天平怎么摆?
模型越做越深,效果饱和,但 GPU 预算有限。
如果让你来拍板,你会:
- 继续增大模型,还是
- 把算力挪去做更聪明的特征工程+规则兜底?
欢迎留言聊聊你们的“抠延迟”骚操作。