限时福利领取


基于Rasa的智能客服机器人实战:从意图识别到上下文对话管理


背景痛点:规则机器人为何总“答非所问”

很多团队第一次做客服机器人时,都会先写一堆“if-contains”硬编码:用户消息里出现“退款”就回 A,“密码”就回 B。上线初期看似能跑,但很快暴露三大硬伤:

  1. 意图覆盖不全
    用户说“想把钱退回来”,规则里只有“退款”关键词,匹配失败直接丢默认回复。

  2. 缺乏上下文
    上一句刚问“你们营业时间?”,下一句追问“周末呢?”,规则脚本没有“营业时间”这一上下文节点,只能再丢一次全量答案。

  3. 维护噩梦
    每新增一个意图,要在 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

即可把机器人对接到微信通道。


生产考量:上线后最怕“两眼一抹黑”

  1. 对话日志监控

    • 打开 endpoints.yml 中的 TrackerStore: SQL 把每条事件落库。
    • 用 Grafana + Loki 收集 rasa-production.log,对 action_listen 超时、fallback 次数建告警。
    • 关键指标:意图置信度分布、槽位识别 F1、会话完成率、人工转接率。
  2. 模型版本控制

    • 训练脚本里把 model-<git-sha>.tar.gz 上传到内部 Artifactory。
    • 线上通过 rasa run --model model-xxx.tar.gz 灰度,10% 流量观察 fallback 上升还是下降。
    • 回滚只需切换软链,30 秒完成。

避坑指南:中文场景的血泪经验

  1. 中文实体识别不准

    • 除了加 lookup,再跑 DIETClassifierpretrained BERT 联合:
      pip install rasa[transformers],config 里换 LanguageModelFeaturizer
    • 领域新词(如“极光宽带”)在 nlu 样本不足时,用 RegexEntityExtractor 兜底,先保证召回。
  2. 对话中断恢复

    • 默认 session_expiration_time=0 会无限续期,容易把上周的槽带到本周。
    • 建议设 5~10 分钟,并在前端传 session_id,用户换设备可强制刷新。
    • 对电话语音通道,把“重说”关键词映射到 action_restart,清空 tracker。

扩展思考:增量训练,让机器人“边睡边学”

传统做法:攒一个月数据 → 人工标注 → 全量训练 → 上线。周期长,新意图迟迟上不了线。

增量思路:

  1. 日志 → 主动学习(Rasa Pro 的 rasa x)→ 高不确定性样本人工确认。
  2. rasa train --finetune 在旧权重基础上继续跑,只改最后几层,避免灾难遗忘。
  3. 配置 MIN_NEW_SAMPLES=200 触发自动训练,CI 跑完自动化回归测试(内置 rasa test),F1 下降>2% 就阻断合并。
  4. 上线后对比 fallback 率,若下降则保留,否则回滚。整个闭环 1 天内完成。

rasa-workflow


结尾体验

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

限时福利领取


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐