基于Rasa的智能客服机器人实战:从意图识别到上下文对话管理
intents:- affirm- deny- goodbyeentities:- date- time- phoneslots:type: textmappings:date:type: textmappings:time:type: textmappings:phone:type: textmappings:responses:- text: "请问您需要哪种服务?目前支持宽带/路由器/摄像头
基于Rasa的智能客服机器人实战:从意图识别到上下文对话管理
背景痛点:规则机器人为何总“答非所问”
很多团队第一次做客服机器人时,都会先写一堆“if-contains”硬编码:用户消息里出现“退款”就回 A,“密码”就回 B。上线初期看似能跑,但很快暴露三大硬伤:
-
意图覆盖不全
用户说“想把钱退回来”,规则里只有“退款”关键词,匹配失败直接丢默认回复。 -
缺乏上下文
上一句刚问“你们营业时间?”,下一句追问“周末呢?”,规则脚本没有“营业时间”这一上下文节点,只能再丢一次全量答案。 -
维护噩梦
每新增一个意图,要在 N 个文件里加关键词、回话术、兜底逻辑,合并冲突频繁,回滚困难。
痛点总结:规则系统把“对话”当字符串匹配,而不是“用户状态机”。想真正解决问题,需要可训练、可推理、可追踪状态的框架——Rasa 正是为此而生。
技术选型:Rasa vs 云端方案
| 维度 | Rasa(开源) | Dialogflow / Lex(云端) |
|---|---|---|
| 数据隐私 | 本地训练,日志不落第三方 | 文本需上传云端 |
| 自定义模型 | 改网络结构、加自家词典、魔改损失函数都行 | 只能用黑盒 API |
| 中文实体 | 可接入 BERT+CRF、词向量、领域词典 | 中文支持一般,新词识别弱 |
| 费用 | 服务器成本固定 | 按调用量计费,量大后成本陡增 |
| 部署 | Docker 一键,可离线 | 必须公网,内网业务需代理 |
一句话:对中文场景、数据敏感、需要深度定制的团队,Rasa 几乎是唯一选择。
核心实现:30 分钟搭出最小可用机器人
下面以“预约上门安装”机器人为例,演示 Rasa 3.x 的完整流程。
1. 环境准备
# 建议 Python 3.8
python -m venv rasa3
source rasa3/bin/activate
pip install rasa==3.6.2
rasa init --init-dir ./appointment_bot # 生成骨架
cd appointment_bot
2. 定义业务要素:domain.yml
version: "3.1"
intents:
- request_appointment
- affirm
- deny
- goodbye
entities:
- service_type
- date
- time
- phone
slots:
service_type:
type: text
influence_conversation: true
mappings:
- type: from_entity
entity: service_type
date:
type: text
influence_conversation: true
mappings:
- type: from_entity
entity: date
time:
type: text
influence_conversation: true
mappings:
- type: from_entity
entity: time
phone:
type: text
influence_conversation: true
mappings:
- type: from_entity
entity: phone
responses:
utter_ask_service_type:
- text: "请问您需要哪种服务?目前支持宽带/路由器/摄像头安装。"
utter_ask_date:
- text: "您希望预约到哪一天?(格式:YYYY-MM-DD)"
utter_ask_time:
- text: "大概几点方便?(如 14:30)"
utter_ask_phone:
- text: "留个手机号,方便师傅联系:"
utter_confirm:
- text: "好的,{service_type}服务预约在{date} {time},手机{phone}。确认请说“是”。"
utter_ok:
- text: "预约成功,稍后会有短信提醒,感谢使用!"
session_config:
session_expiration_time: 60 # 1 分钟无交互即过期
carry_over_slots_to_new_session: true
要点:
influence_conversation: true让槽位值参与 TED 策略特征,防止上下文丢失。- 四个待填槽位,全部用
from_entity映射,后续在 action 里做二次校验。
3. NLU 训练数据:nlu.yml 片段
nlu:
- intent: request_appointment
examples: |
- 我想预约装宽带
- 装路由器要什么流程
- 摄像头安装怎么预约
- 申请上门服务
- 想装个宽带,明天有空
- intent: affirm
examples: |
- 是
- 对的
- 确认
- 没问题
- intent: deny
examples: |
- 不是
- 错了
- 重填
- intent: goodbye
examples: |
- 谢谢
- 拜拜
- 没事了
# 实体标注采用 BILOU 格式,Rasa 会自动解析
- regex: date
examples: |
- \d{4}-\d{1,2}-\d{1,2}
- lookup: service_type
examples: |
- 宽带
- 路由器
- 摄像头
技巧:
- 中文实体容易缺样本,用
lookup表强制词典特征,再叠加 CRF 提升召回。 - 日期正则只是初步,真正生产建议接 duckling(支持中文)或直接调
spaCy zh的 NER。
4. 对话策略:config.yml
recipe: default.v1
language: zh
pipeline:
- name: JiebaTokenizer
dictionary_path: ./dict
- name: RegexFeaturizer
- name: LexicalSyntacticFeaturizer
- name: CountVectorsFeaturizer
analyzer: char_wb
min_ngram: 1
max_ngram: 4
- name: DIETClassifier
epochs: 100
constrain_similarities: true
- name: EntitySynonymNormalizer
- name: ResponseSelector
epochs: 100
policies:
- name: RulePolicy
core_fallback_threshold: 0.3
core_fallback_action_name: action_default_fallback
- name: TEDPolicy
epochs: 50
max_history: 10
constrain_similarities: true
说明:
- RulePolicy 兜底单轮问答、固定流程;TEDPolicy 负责多轮状态泛化。
max_history=10让模型能看到过去 10 轮,足够记住“已问日期→待确认”这类上下文。
5. 多轮校验:actions.py
from typing import Any, Dict, List, Text
from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import ValidationAction
from rasa_sdk.types import DomainDict
import re
class ValidateAppointmentForm(ValidationAction):
"""对四个槽位做二次校验,防止 NLU 误识别"""
def name(self) -> Text:
return "validate_appointment_form"
async def required_slots(
self,
domain_slots: List[Text],
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: "DomainDict",
) -> List[Text]:
"""动态返回待填槽位,支持跳过逻辑"""
return ["service_type", "date", "time", "phone"]
async def validate_service_type(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
if slot_value not in ["宽带", "路由器", "摄像头"]:
dispatcher.utter_message(text="服务类型只能选宽带/路由器/摄像头,请重新说。")
return {"service_type": None}
return {"service_type": slot_value}
async def validate_date(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
# 简单正则,可换成 chinese-calendar 库校验工作日
if not re.match(r"\d{4}-\d{1,2}-\d{1,2}", slot_value):
dispatcher.utter_message(text="日期格式不对,应为 YYYY-MM-DD,如 2024-07-18")
return {"date": None}
return {"date": slot_value}
async def validate_phone(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
mobile = re.sub(r"\D", "", slot_value)
if len(mobile) != 11 or not mobile.startswith("1"):
dispatcher.utter_message(text="手机号似乎不对,请重新输入。")
return {"phone": None}
return {"phone": mobile}
要点:
- 继承
ValidationAction后,Rasa 会在表单填充前后自动调用对应validate_<slot_name>。 - 返回
{"slot": None}即视为校验失败,机器人会重新提问。
6. 自定义连接器:把机器人接到企业微信
# wechat_connector.py
from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
from sanic import Blueprint, response
import json
import hmac
import hashlib
class WechatOutputChannel(OutputChannel):
"""发送消息到企业微信 API"""
def __init__(self, corpsecret, access_token_url):
self.secret = corpsecret
self.token_url = access_token_url
async def send_text_message(self, recipient_id: Text, text: Text, **kwargs):
# 实际调用企业微信 message/send 接口
pass
class WechatInputChannel(InputChannel):
def name(cls):
return "wechat"
def blueprint(self, on_new_message):
wechat_webhook = Blueprint("wechat_webhook", __name__)
@wechat_webhook.route("/webhook", methods=["POST"])
async def receive(request):
signature = request.headers.get("WX_SIGNATURE")
body = request.raw_body
if not self.verify_signature(body, signature):
return response.json({"code": 403})
msg = json.loads(body)
user_id = msg["UserID"]
text = msg["Content"]
await on_new_app_message(
UserMessage(text, WechatOutputChannel(), sender_id=user_id)
)
return response.json({"code": 0})
return wechat_webhook
在 credentials.yml 里加一行:
wechat.WechatInputChannel:
corpsecret: "xxxx"
access_token_url: "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
启动时:
rasa run --credentials credentials.yml --connector wechat
即可把机器人对接到微信通道。
生产考量:上线后最怕“两眼一抹黑”
-
对话日志监控
- 打开
endpoints.yml中的TrackerStore: SQL把每条事件落库。 - 用 Grafana + Loki 收集
rasa-production.log,对action_listen超时、fallback 次数建告警。 - 关键指标:意图置信度分布、槽位识别 F1、会话完成率、人工转接率。
- 打开
-
模型版本控制
- 训练脚本里把
model-<git-sha>.tar.gz上传到内部 Artifactory。 - 线上通过
rasa run --model model-xxx.tar.gz灰度,10% 流量观察 fallback 上升还是下降。 - 回滚只需切换软链,30 秒完成。
- 训练脚本里把
避坑指南:中文场景的血泪经验
-
中文实体识别不准
- 除了加
lookup,再跑DIETClassifier与pretrained BERT联合:pip install rasa[transformers],config 里换LanguageModelFeaturizer。 - 领域新词(如“极光宽带”)在
nlu样本不足时,用RegexEntityExtractor兜底,先保证召回。
- 除了加
-
对话中断恢复
- 默认
session_expiration_time=0会无限续期,容易把上周的槽带到本周。 - 建议设 5~10 分钟,并在前端传
session_id,用户换设备可强制刷新。 - 对电话语音通道,把“重说”关键词映射到
action_restart,清空 tracker。
- 默认
扩展思考:增量训练,让机器人“边睡边学”
传统做法:攒一个月数据 → 人工标注 → 全量训练 → 上线。周期长,新意图迟迟上不了线。
增量思路:
- 日志 → 主动学习(Rasa Pro 的
rasa x)→ 高不确定性样本人工确认。 - 用
rasa train --finetune在旧权重基础上继续跑,只改最后几层,避免灾难遗忘。 - 配置
MIN_NEW_SAMPLES=200触发自动训练,CI 跑完自动化回归测试(内置rasa test),F1 下降>2% 就阻断合并。 - 上线后对比 fallback 率,若下降则保留,否则回滚。整个闭环 1 天内完成。

结尾体验
把上面的代码全部跑通后,我第一次在微信里打出“我想装宽带”,机器人立刻问日期,然后时间、手机一路确认,最后回我“预约成功”。那一刻的爽点,不亚于第一次跑通“Hello World”。
当然,真实场景远比 demo 复杂:口音噪声、同义词、用户反悔……但有了 Rasa 的可训练框架,再遇到新痛点,我们不再改几千行 if,而是——
“加样本、再训练、灰度、观察”,四个步骤循环。
如果你也在为客服机器人头疼,不妨拉下代码,按文里步骤试一轮;半小时后,你会拥有一条可扩展、可观测、可回滚的对话流水线。祝训练愉快,fallback 越来越少!
更多推荐




所有评论(0)