背景痛点:传统客服为什么总“答非所问”
去年双十一,公司老客服系统直接“翻车”:用户问“我昨天下的单怎么还没发货”,机器人却回复“请提供订单号”。看似礼貌,实则完全没抓住“催发货”这个意图,更谈不上把“昨天”这个时间实体抽出来。
复盘发现三大硬伤:
- 关键词匹配式意图识别,换个说法就失效
- 没有上下文记忆,每轮对话都是“陌生人”
- 业务系统耦合在代码里,改一句回复就要上线
痛定思痛,我们决定用开源方案重新造轮子,最终选型落在了 Rasa——理由下面慢慢聊。
技术选型:Rasa 凭啥脱颖而出
先把当时做的打分表搬出来(满分 5 分):
| 维度 | Dialogflow | LUIS+BotFram | Rasa |
|---|---|---|---|
| 数据隐私 | 2 | 2 | 5 |
| 中文效果 | 3 | 3 | 4 |
| 可定制 | 2 | 3 | 5 |
| 费用 | 2 | 2 | 5 |
| 社区活跃度 | 4 | 3 | 4 |
结论简单粗暴:
- 数据不出内网,GDPR、等保合规一票否决
- 需要深度对接内部 CRM、订单、物流接口,黑盒 SaaS 改不动
- 老板批的预算只够买两台 8C16G 云主机
于是 Rasa 成了唯一选项——Python 原生、MIT 协议、可离线训练、组件都能拆,自己改得起。
核心实现一:NLU 模型训练与数据标注
Rasa 把对话拆成 NLU(听懂话)和 Core(会接话)两大块。先搞定“听懂”。
准备语料
用 Excel 拉了个模板,三列:text、intent、entities。发给客服小姐姐,按真实聊天记录标注。两周撸出 1.2 万条,覆盖 37 个意图、18 类实体。
标注规范重点:- 意图动词+名词,如
order_cancel、shipping_inquiry,别出现want_to_know_shipping这种啰嗦写法 - 实体用 BILOU,同一实体不同表述都标,比如“昨天”“昨晚”全标
date:yesterday
- 意图动词+名词,如
训练配置
中文先用LanguageModelFeaturizer加载哈工大 LTP,再接DIETClassifier联合训练意图+实体。config.yml关键片段:
language: zh pipeline: - name: JiebaTokenizer - name: LanguageModelFeaturizer model_name: bert model_weights: bert-base-chinese - name: DIETClassifier epochs: 100 transformer_size: 256 batch_size: 64- 交叉验证
rasa test nlu一把梭,precision 0.89,recall 0.86,F1 0.87,基本可上线。对误分类样本再喂回去,三轮迭代后 F1 提到 0.91。
核心实现二:用 Rasa Core 设计多轮对话
有了意图,还得让机器人“记得住、聊得动”。Core 负责策略,官方推荐 3.x 新 Rule+Story 双轨制:
Rule 兜底高频单轮
例如“你好”“谢谢”直接回固定模板,不走复杂模型,省算力。Story 覆盖多轮
把客服金牌话术抽象成流程图,再转成 Story 文件。举个例子——用户要改地址:
## shipping_change_address * shipping_change_address - address_form - form{"name": "address_form"} - form{"name": null} - action_query_shipping - utter_change_success- Domain 声明 slot
地址改完写进slot: delivery_address,后续物流查询节点直接复用,不用再问。
核心实现三:自定义 Action Server 实战
光会聊天不够,还得查订单、调接口、写数据库。Action Server 就是干这个的。
- 项目结构
actions/ ├── actions.py ├── utils/ │ └── crm.py └── requirements_actions.txt- 代码示例:查询订单并返回物流状态
import os import logging from typing import Any, Dict, List, Text from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher from actions.utils.crm import get_order_shipping logger = logging.getLogger(__name__) # 日志落盘,方便排查 handler = logging.FileHandler("/var/log/rasa/actions.log") logger.addHandler(handler) class ActionQueryShipping(Action): def name(self) -> Text: return "action_query_shipping" def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]: order_id = tracker.get_slot("order_id") if not order_id: dispatcher.utter_message(text="亲,需要提供订单号哦~") return [] try: data = get_order_shipping(order_id) status = data.get("status", "unknown") dispatcher.utter_message( text=f"订单 {order_id} 当前状态:{status}" ) return [SlotSet("shipping_status", status)] except Exception as e: logger.exception("CRM exception") dispatcher.utter_message(text="物流查询开小差了,稍后再试") return []- 错误处理要点
- 任何第三方超时不抛异常,用
asyncio.wait_for包一层 - 返回统一错误话术,避免把栈追踪甩给用户
- 关键节点打日志,
order_id必须可追踪
- 任何第三方超时不抛异常,用
生产考量:性能、安全两手抓
性能测试与优化
- 压测工具:Locust 模拟 300 并发,平均响应 650 ms,95 线 1.2 s,老板嫌慢。
- 异步改造:
- Action Server 用
asyncio改写 IO 等待,响应降到 280 ms - Redis 缓存热点订单,命中率 62%,再减 90 ms
- Action Server 用
- 模型瘦身:
- 把
DIETtransformer_size 从 256 压到 128,F1 掉 0.8%,但推理速度翻倍 - 开启
rasa shell --production的SANIC_WORKERS=4,8C 机器 CPU 吃到 70%,刚好
- 把
安全性设计
- 输入校验:正则+长度截断,防 SQL 注入和超长攻击
- 敏感过滤:手机号、身份证用
presidio打码,日志落盘前脱敏 - HTTPS & JWT:Action 与业务系统走内网 HTTPS,Header 带 JWT,过期 30 s
- 限流:Nginx
limit_req_zone每 IP 10 r/s,超频直接 503
避坑指南:那些踩过的坑
- 模型版本管理
最初git push把模型文件也传上去,仓库膨胀到 2 G。后来改用 DVC 指向 OSS,只存model.tar.gz的指纹,回滚只需dvc checkout。 - 对话回退
用户突然说“不对,我要取消”,记得在 Story 里加* deny或* stop,否则机器人继续追问,体验极差。 - 槽位冲突
日期实体既想存“今天”又想存“2024-06-01”,结果互相覆盖。解决:用slot_mapping分date_str和date_obj两个槽,逻辑层再统一格式化。 - 中文数字归一化
“三十七” 得先转成 “37”,再喂给后端。写个number_normalize放custom_component,否则 CRM 查不到订单。
写在最后
整套流程跑下来,客服机器人上线首周就接住 62% 的咨询量,人工坐席从 24×7 轮班改成白天高峰兜底,省下的预算够我们再开两条产品线。
不过新烦恼也来了:广东用户一句“俾我睇下單嘢”直接让 NLU 原地懵圈——方言识别还没解。你的业务场景里遇到过类似问题吗?都怎么破?欢迎留言一起拆坑。