背景痛点:规则引擎的“硬编码”天花板
做客服系统的老同学都有体会,用 if-else 堆出来的“关键词回复”在前三年还能跑,一旦业务线超过 5 条、意图超过 200 个,维护成本就像滚雪球:
- 每新增一个问法,要在十几层嵌套里找位置,生怕改错分支
- 多轮对话想记录“用户已提供手机号”这种中间状态,只能靠全局变量或写文件,并发一高就互相覆盖
- 上线后用户问法千奇百怪,规则覆盖不了就只能转人工,导致 30% 以上会话仍需人工坐席兜底
一句话:规则引擎适合“固定问答”,却扛不住“自然语言”的随机与模糊。
技术选型:Rasa 开源自由 vs 云厂商“黑盒”
| 维度 | Rasa 3.x | Dialogflow / Lex |
|---|---|---|
| 代码可见性 | 完全开源,可 Debug 到每一层 | 黑盒,只能调 API |
| 数据隐私 | 本地训练,敏感语料不外传 | 明文上传云端,合规审批麻烦 |
| 自定义逻辑 | Python 任意写,直接 import 业务库 | 只能用云函数/蓝图,受限于沙箱 |
| 中文支持 | 自带 MITIE、Jieba 分词,可自训练 | 中文模型更新慢,方言识别弱 |
| 费用 | 0 授权费,只出服务器 | 按调用量计费,量大后成本陡增 |
一句话总结:想省钱、想深度定制、想内网部署——Rasa 是“真香”。
Rasa 3.x 架构速览:一张图看懂数据流
- User Input 先被 Interpreter 做 Natural Language Understanding/NLU,输出意图 + 实体
- Tracker 维护多轮状态(类似 Session),随时可序列化到 Redis
- Policy 根据当前状态选动作(如回复、调用 API、跳转表单)
- Action Server 执行自定义 Python 代码,可读写 DB、调用外部微服务
- 最终 Response 返回通道(Web、微信、钉钉等)
整个流程全用异步协程,官方宣称单核可扛 300 并发,实测 4C8G 容器能到 1000+ QPS。
核心实现:从 domain.yml 到自定义 Action
1. 意图与实体定义规范
domain.yml 是“数据字典”,建议把业务前缀带上,方便后期检索:
version: "3.1" intents: - faq.refund_policy # 业务域.具体意图 - faq.exchange_size - greet - goodbye entities: - order_id # 订单号,正则提取 - phone_number # 手机号,预训练 DIETClassifier 直接识别Tips:中文语料里数字和字母经常混写,例如“订单号 A 12345”,记得在 regex 里加\s*兼容空格。
2. 训练数据增强(少样本也能玩)
nlu.yml 示例,用同义词 + 结构化写法,减少重复:
- intent: faq.refund_policy examples: | - 我想[退款](order_id:NULL)可以么 - 退货政策是啥 - 能退钱不 - 不想要了,能[退](faq.refund_policy)吗再跑一条命令自动生成 5 倍样本:
rasa data augment --nlu nlu.yml --out nlu_aug.yml --factor 53. 自定义 Action:带 API + DB 的完整示例
actions/ refund_action.py(符合 PEP8,关键处加类型注解)
from typing import Any, Dict, List, Text from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher from rasa_sdk.events import SlotSet import aiohttp import aioredis class ActionQueryRefund(Action): def name(self) -> Text: return "action_query_refund" async def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> List[Dict[Text, Any]]: order_id: Text = tracker.get_slot("order_id") phone: Text = tracker.get_slot("phone_number") # 参数校验 if not order_id or not phone: dispatcher.utter_message(text="缺少订单号或手机号,请检查后重试") return [] # 1. 调内部退款接口 url = f"http://api.retail.com/refund/{order_id}" async with aiohttp.ClientSession() as session: async with session.get(url, headers={"phone": phone}) as resp: if resp.status != 200: dispatcher.utter_message(text="系统开小差了,稍后再试") return [] data = await resp.json() # 2. 把结果写缓存,15 min 过期 redis = await aioredis.create_redis_pool("redis://localhost") key = f"refund:{order_id}" await redis.set(key, data["status"], expire=900) # 3. 回复用户 msg = f"订单 {order_id} 的退款状态:{data['status']}" dispatcher.utter_message(text=msg) return [SlotSet("refund_status", data["status"])]注意:
- 所有 IO 都用 async,避免阻塞 Policy 的事件循环
- 返回值里把
refund_status写回 slot,方便后续节点做分支判断
生产考量:并发、缓存与灰度
1. 对话状态 Redis 缓存方案
Rasa 默认把 Tracker 存内存,容器一重启就丢丢。生产环境需在 endpoints.yml 加:
tracker_store: type: redis url: redis://redis-cluster:6379 db: 0 key_prefix: rasa:tracker ttl: 1800 # 30 min 无活动自动过期这样多 Pod 水平扩展无状态,用户中途掉线再回来也能续聊。
2. 使用 Locust 压测
locustfile.py 片段:
from locust import HttpUser, task, between class ChatUser(HttpUser): wait_time = between(1, 3) host = "http://rasa-server:5005" @task def refund_flow(self): self.client.post("/webhooks/rest/webhook", json={"sender": "tester", "message": "我要退款"})运行:
locust -f locustfile.py -u 300 -r 50 -t 5m观察两个指标:
- 95% 响应时间 < 600 ms
- 错误率 < 1%
若超时占比高,优先检查 Action Server 的同步阻塞点。
3. 模型灰度发布
- 打镜像带版本号:
rasa/model:2024.06.01 - 在 K8s 用
canaryDeployment,流量按 Header 切 5% 到新版 - 对比上一周期意图置信度分布、Fallback 触发率,无异常再全量
避坑指南:中文场景的小细节
中文 NLU 数据增强技巧
用rasa data convert把 JSON 转成 Markdown,再跑rasa data split做交叉验证;少样本时叠加同音词 + 拼音替换,能把 F1 提升 3-5 个百分点。避免对话死循环
在 config.yml 里给 RulePolicy 加core_fallback_threshold: 0.3与core_fallback_action: action_default_fallback,连续两次 Fallback 就自动转人工。敏感词过滤
在nlu_pipeline最前面插一个自定义 Component,正则匹配敏感词库,命中后直接返回intent: sensitive并走统一拒绝模板,既安全又不污染训练数据。
互动挑战:动手扩展 slot 验证逻辑
现在手机号 slot 只靠正则,有时用户会输“1381234567”(少一位)。请你:
- 新建
actions/validation_action.py - 实现
validate_phone_number函数,带类型注解,长度 =11 且前缀在{'13','15','16','17','18','19'}才算合法 - 非法时给友好提示,并返回
{"phone_number": None}让 Rasa 重新提问
完成后跑rasa test验证是否通过单元测试,欢迎把 GitHub 链接贴在评论区一起 review!
写在最后
整套流程跑下来,最大的感受是:Rasa 把“算法黑盒”拆成可编程的乐高,只要会 Python,就能像写 CRUD 一样写对话逻辑。AI 辅助开发不是口号——当你能在 30 分钟内给新意图加数据、训练、灰度、上线,而隔壁团队还在同步排期改规则,就知道这份自由有多香。祝大家玩得开心,少踩坑,多复用!