背景痛点:传统客服系统“三座大山”
过去两年,我先后接手过三套“祖传”客服系统,痛点出奇一致:
- 意图识别靠关键词,用户换种说法就“已读乱回”,准确率不足60%。
- 多轮对话靠 session 里硬编码 if/else,一旦业务调整,开发比需求还多。
- 对接订单、物流、CRM 时,每新增一个接口就要发版,高峰期一发布就掉线。
这些问题在流量低峰期还能“人肉”兜底,大促活动一来,客服群里“人工智障”截图满天飞。痛定思痛,我们决定用 Rasa 3.6.1 搭一套可私有化、可灰度、可热更新的智能客服智能体,把对话管理、意图识别、第三方调用全部做成微服务,一次搭建,持续复用。
技术选型:Rasa vs Dialogflow vs Lex
中文场景下,NER 和意图分类的准确率直接决定用户体验。我们把同样 2 万条客服语料分别喂给三家引擎,结果如下:
| 引擎 | NER F1 | 意图 Acc | 训练成本 | 私有化部署 | 备注 |
|---|---|---|---|---|---|
| Dialogflow ES | 0.82 | 0.85 | 按次收费 | 不可 | 中文分词用 Google 自带,领域词无法干预 |
| AWS Lex V2 | 0.79 | 0.83 | 按次收费 | 不可 | 中文支持 beta,槽位抽取容易丢 |
| Rasa 3.6.1 | 0̇.88 | 0.91 | 免费 | 一键 Helm | 可用 BERT 微调,词典、规则、策略全部可控 |
训练成本这块,Rasa 只需要一张 2080Ti,6 小时收敛;而云厂商按调用量计费,半年就能买一台服务器。再加上私有化合规需求,Rasa 直接胜出。
核心实现:Rasa 3.x 微服务架构
1. 系统总览
我们采用“对话核心 + 技能微服务”两层架构:
- 对话核心:Rasa Pro 3.6.1,负责意图分类、实体抽取、对话策略。
- 技能微服务:订单查询、物流跟踪、退换货,各自独立部署,通过 gRPC 暴露接口。
- 消息总线:RabbitMQ 3.11,做异步削峰;连接池用 aio-pika 8 answers 版。
- 状态缓存:Redis Cluster 7.0,存储 sender_id 对应的对话状态及槽位。
2. domain.yml 片段
version: "3.1" session_config: session_expiration_time: 3600 carry_over_slots_to_new_session: true intents: - query_order - cancel_order - human_handoff entities: - order_id - phone slots: order_id: type: text influence_conversation: true mappings: - type: from_entity entity: order_id responses: utter_ask_order_id: - text: "请问您的订单号是多少?"3. BERT 微调意图分类(PyTorch 1.13)
数据预处理:把 2 万条语料按 8:1:1 切分,统一转小写,去掉表情符号。
# dataset.py import torch from torch.utils.data import Dataset class IntentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __getitem__(self, idx): enc = self.tokenizer( self.texts[idx], truncation=True, padding='max_length', max_length=self.max_len, return_tensors='pt' ) item = {k: v.squeeze(0) for k, v in enc.items()} item['labels'] = torch.tensor(self.labels[idx], dtype=torch.long) return item def __len__(self): return len(self.texts)训练脚本(关键步骤已加注释,学习率 2e-5,batch 32,单卡 2080Ti 3 epoch 收敛):
# train_intent.py from transformers import BertForSequenceClassification, Trainer, TrainingArguments from sklearn.metrics import accuracy_score, f1_score def compute_metrics(pred): labels = pred.label_ids preds = pred.predictions.argmax(-1) acc = accuracy_score(labels, preds) f1 = f1_score(labels, preds, average='weighted') return {'accuracy': acc, 'f1': f1} model = BertForSequenceClassification.from_pretrained( 'bert-base-chinese', num_labels=12) args = TrainingArguments( output_dir='./intent_model', per_device_train_batch_size=32, learning_rate=2e-5, num_train_epochs=3, evaluation_strategy='epoch', save_strategy='epoch', logging_dir='./logs', load_best_model_at_end=True, metric_for_best_model='f1' ) trainer = Trainer( model=model, args=args, train_dataset=train_ds, eval_dataset=val_ds, compute_metrics=compute_metrics ) trainer.train()时间复杂度:样本数 n,序列长度 L,BERT 底模层数 12,自注意力 O(L²·d),整体训练复杂度 O(n·L²·d),在 L=128 时显存占用约 7G。
4. 异步消息队列
用户请求先进 RabbitMQ,再由 rasa-consumer 拉取,防止核心引擎被突发流量冲垮。
连接池配置要点:
- 每个 pod 启动时预建 10 条 TCP 通道,心跳 30s;
- 采用 aio-pika 的 RobustConnection,断线自动重连;
- 消费端 prefetch_count=20,保证单 pod 内存不爆。
# consumer.py import aio_pika import asyncio from rasa.core.agent import Agent agent = Agent.load('/app/models') async def on_message(message: aio_pika.IncomingMessage): async with message.process(): user_text = message.body.decode() sender_id = message.headers.get('sender_id') res = await agent.handle_text(user_text, sender_id=sender_id) # 回包通过回调队列返回性能优化:压测与缓存
1. Locust 脚本示例
# locustfile.py from locust import HttpUser, task, between class ChatUser(HttpUser): wait_time = between(1, 3) host = "http://rasa-prod:5005" @task def ask_order(self): self.client.post("/webhooks/rest/webhook", json={ "sender": "test_user", "message": "我的订单 12345 到哪了" })单机 4 核 8G 可模拟 800 并发,平均响应 420 ms,CPU 85%,低于 90% 安全水位。
2. Redis 缓存策略
- 对话状态以
sender_id:state为 key,TTL 3600s; - 采用 Redis Pipeline 批量写,减少 RTT;
- 雪崩防护:过期时间加随机 jitter 0~300s,防止集中失效;
- 大促前提前扩容 20% 节点,并用
redis-cli --hotkeys找出热 key,做本地缓存兜底。
避坑指南:中文场景血泪史
中文分词器与领域词典协同
默认 jieba 会把“订单号”切成“订单/号”,导致实体抽取失败。我们给 Rasa 的WhitespaceTokenizer加了自定义词典,并把业务高频词(订单号、物流单号、优惠券)整体加入,NER F1 提升 5 个点。对话超时重试的幂等性
用户可能因网络重复发送同样消息。我们在 RabbitMQ 消息头里加入msg_id,消费端用 Redis setnx 做幂等,key 过期 5 min,保证同一条消息只处理一次。模型灰度发布
采用 Kubernetes 的 Canary Deployment,新模型起 10% pod,对比意图置信度分布与回答准确率,无异常再全量。回滚策略:只要 5 min 内异常率>1%,自动切回旧版。
代码规范与上线 checklist
- 所有 Python 代码通过 black 22.3 格式化,行宽 88;
- 函数复杂度不超过 10,圈复杂度用 radon 检测;
- 关键路径打日志采用 structlog,保留 request_id,方便链路追踪;
- 上线前跑一遍
make test:单元测试覆盖率>85%,Locust 压测 RT<600 ms,错误率<0.5%。
结尾:下一步,让客服听懂方言?
目前系统已能稳定支撑 5 万 QPS,大促零事故。但新的需求已经来了:华南地区用户习惯粤语语音输入,如何在不增加太多延迟的前提下,把粤语 ASR 与意图模型联合训练?欢迎一起探讨。若你对多语种语音-语义端到端方案感兴趣,推荐先读这两篇论文:
- 《Code-Switching ASR for Cantonese-Mandarin》
- 《Joint Training of ASR and NLU for Low-Resource Dialect》
期待评论区听到你的实践。