背景痛点:传统客服系统到底卡在哪?
做客服系统的老同学都有体会,用户一句“我订单呢?”就能让后台瞬间懵圈。早期关键词+正则的套路,遇到口语化、倒装、省略,就像用鱼网捞空气——看似努力,实则啥也没捞着。再加上多轮对话里状态全靠 session 硬怼,用户中途换个问法,上下文瞬间断片,客服机器人秒变“复读机”。痛点总结起来就三条:
- 意图识别准确率低,口语稍一变形就翻车
- 对话状态维护困难,session 一丢就“失忆”
- 多轮场景难以扩展,新业务流程动辄返工
于是,我们决定用 AI 重新打地基,目标只有一个:让客服机器人像人一样“记得住、答得准、回得快”。
技术选型:Rasa + BERT 为何胜出?
先把主流框架拉出来遛一遛:
| 框架 | 优点 | 缺点 |
|---|---|---|
| Dialogflow | 谷歌全家桶,集成快 | 中文支持一般,黑盒收费 |
| LangChain | 组件丰富,LLM 友好 | 对话管理偏轻量,生产级需自补 |
| Rasa | 开源可定制,NLU+Core 分离 | 上手曲线陡,调优工作量大 |
我们团队对“可私有部署 + 白盒调优”有硬需求,最终拍板 Rasa 3.x 做骨架,BERT 做语义底座。理由简单粗暴:
- Rasa 的 Tracker 把对话状态抽象成事件流,天然适合“多轮”
- BERT 中文预训练模型开源多,Fine-tuning 成本低,意图识别 SOTA
- 二者通过自定义 Component 就能拼在一起,不破坏原有 pipeline
核心实现一:用 BERT 把意图识别拉回 90%+
1. 数据准备
把历史客服日志清洗后得到 2.1 万条 query,覆盖 37 个意图。按 8:1:1 切训练 / 验证 / 测试,保证同义词分布一致。
2. Fine-tuning 代码(PEP8 规范,关键注释已标)
# bert_intent_classifier.py from transformers import BertTokenizer, BertForSequenceClassification from torch.utils.data import DataLoader import torch, os class IntentDataset(torch.utils.data.Dataset): def __init__(self, encodings, labels): self.encodings = encodings 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(run_path, lr=2e-5, epochs=3): tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=37 ) # 假设 train_texts, train_labels 已准备好 train_encodings = tokenizer(train_texts, truncation=True, padding=True) train_dataset = IntentDataset(train_encodings, train_labels) loader = DataLoader(train_dataset, batch_size=32, shuffle=True) opt = torch.optim.AdamW(model.parameters(), lr=lr) model.train() for epoch in range(epochs): for batch in loader: opt.zero_grad() outputs = model(**batch) loss = outputs.loss loss.backward() opt.step() print(f"Epoch {epoch} loss: {loss.item()}") os.makedirs(run_path, exist_ok=True) model.save_pretrained(run_path) tokenizer.save_pretrained(run_path) if __name__ == "__main__": train("./bert_intent_model")训练 3 轮,验证集准确率 93.4%,比原始 Rasa 默认的SklearnIntentClassifier提升 17 个百分点。
3. 接入 Rasa NLU Pipeline
在config.yml里把自定义组件插进去:
language: zh pipeline: - name: JiebaTokenizer - name: BertIntentClassifier model_path: ./bert_intent_model - name: DIETClassifier epochs: 100重启rasa train,pipeline 无缝切换,线上灰度一周,TOP5 意图错误率下降 60%。
核心实现二:基于 Graph 的对话状态管理
多轮场景最怕“状态爆炸”。我们把业务节点抽象成有向图,顶点是对话状态,边是用户事件。Tracker 里只存“当前顶点 ID + 关键槽位”,内存占用从 MB 级降到 KB 级。
Rasa Core 的CustomPolicy只需实现一个简单接口:
# graph_policy.py from rasa.core.policies.policy import Policy from rasa.core.events import SlotSet class GraphPolicy(Policy): def predict_action_probabilities(self, tracker, domain): node_id = tracker.get_slot("cur_node") or "START" # 根据 node_id 查图,返回可执行动作概率分布 ... return prob_vector图结构用 JSON 描述,热更新不重启服务,产品运营也能改流程。
性能优化:让 200 ms 成为常态
1. 压测数据
单机 4C8G,Docker 限 CPU 2 核,并发 200 线程,平均响应 180 ms,P99 320 ms。瓶颈主要在 BERT 推理。
2. 冷启动优化
- 把模型转 ONNX,再用 ONNXRuntime-GPU,首响从 3 s 降到 0.9 s
- 容器启动时预加载 tokenizer + 模型, readinessProbe 延迟 30 s,防止流量提前涌入
- 采用进程池预热,批量喂“假请求”,让 GPU 显存提前分配
避坑指南:那些深夜踩过的雷
多轮上下文丢失
现象:用户中途说“算了”,机器人继续追问。
解决:在 Graph 节点里加“全局退出”超级边,任何时刻识别到“取消、算了、人工”关键词,直接跳到 EXIT 节点,清空槽位。槽位冲突
现象:手机号、订单号都是数字,串槽导致查不到订单。
解决:给每个槽加正则校验,失败时触发澄清动作,并把候选值写入candidate_slots,让策略优先匹配历史准确率高的实体。生产资源分配
经验值:每 100 QPS 对应 1 核 CPU + 2 GB 内存,GPU 可选 T4,显存 8 GB 可扛 500 并发。务必给 worker 数配RASA_ENVIRONMENT=production,否则默认单进程,流量一上来就 502。
上线效果与复盘
上线三个月,日均对话 12 万轮,意图识别准确率 93.2%,多轮完成率 87%,客服人力节省 42%。复盘发现,Graph 结构让运营改流程的平均耗时从 3 天缩到 2 小时,成为最大惊喜。
留给读者的三个开放式问题
- 当业务图节点超过 500 个时,如何自动检测“ unreachable node”并保持热更新安全?
- 如果未来把 LLM 作为“兜底策略”,怎样设计置信度闸门,避免大模型幻觉污染现有精准流程?
- 对于多租户 SaaS 客服,如何隔离各自的对话图与槽位,同时共享公共的 BERT 推理池?
欢迎在评论区抛出你的方案,一起把 AI 客服做得更“像人”。