背景痛点:企业自研智能客服的三道坎
过去两年,我帮三家零售公司搭过“自研智能客服”,上线前大家都信心满满,上线后却集体踩坑。最集中的反馈可以浓缩成三句话:
- NLU 准确率不到 80%,用户换种问法就“答非所问”。
- 多轮对话状态说丢就丢,刷新页面后机器人“失忆”。
- 大促峰值 QPS 涨 5 倍,横向扩容 30 分钟才生效,期间大量 502。
官方指标更直观:
- 意图识别 F1-score 0.62,低于 SaaS 方案 0.85 的基线。
- 对话完成率 54%,导致 30% 工单仍需人工兜底。
- 平均响应 1200 ms,P99 直逼 4 s,用户体验“秒变人工”。
痛定思痛,我们决定用开源方案重新设计一套“高可用对话系统”,目标只有一个——把可用性拉到 99.9%,同时让老板不再为按量计费肉疼。
技术选型:为什么敲定 Rasa + Transformer
先给出对比表(数据来自 2023 年官方文档与实测):
| 方案 | NLU 可拔插 | 本地部署 | 按量计费 | 二次开发自由度 | 社区活跃度(GitHub star) |
|---|---|---|---|---|---|
| Dialogflow ES | × | × | 有 | 低 | 无公开仓库 |
| Microsoft Bot | △ | △ | 有 | 中 | 11k |
| Rasa 3.x | √ | √ | 0 | 高 | 18k |
核心原因:
- Rasa 把 NLU 与 Core 拆开,正好给我们“换脑袋”的空间——把 DIET 分类器换成基于中文 RoBERTa-wwm-ext 的 Transformer,F1-score 立涨 12%。
- 事件源架构(Event Broker)+ Tracker Store 天然支持分布式,横向扩容不需要改代码。
- 纯 Python,Action Server 与内部 CRM、ERP 对接时,直接 import 现有 SDK,零学习成本。
核心实现:微服务 + 分布式状态
1. 整体架构
- 用户流量 → Nginx → Rasa Core (多副本)
- Core 通过 gRPC 调 NLU 服务;通过 Redis 读写对话状态
- Action Server 独立进程,处理订单查询、退货等本地业务
2. Docker Compose 最小可运行骨架
目录结构:
bot/ ├─ docker-compose.yml ├─ config.yml ├─ domain.yml ├─ actions/ │ └─ actions.py └─ models/ └─ 20240515-zh.tar.gzdocker-compose.yml(省略版本号,方便阅读):
services: redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: ["./data/redis:/data"] rasa-nlu: image: rasa/rasa:3.6-full command: run --enable-api -m models/20240515-zh.tar.gz volumes: ["./models:/app/models"] rasa-core: image: rasa/rasa:3.6-full command: run --core --endpoints endpoints.yml --credentials credentials.yml environment: - REDIS_URL=redis://redis:6379/0 depends_on: [redis, rasa-nlu] actions: build: ./actions command: python -m rasa_sdk --actions actions environment: - LOG_LEVEL=INFO3. 基于 Redis 的分布式 Tracker Store
config.yml 片段:
tracker_store: type: redis url: redis port: 6379 db: 0 key_prefix: rasa_trackers record_exp: 3600实测:
- 单台 Redis 4C8G 可支撑 1.2 万并发 Tracker 读写,QPS 3 w+。
- 故障演练:kill 掉任一 Core 节点,用户侧无感知“失忆”,因为状态在 Redis 中持久化。
4. 自定义 Action Server 代码示例
actions/actions.py(符合 PEP8,带类型注解与异常捕获):
import os import logging from typing import Dict, Text, Any from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher from rasa_sdk.events import SlotSet import requests logger = logging.getLogger(__name__) class ActionOrderStatus(Action): def name(self) -> Text: return "action_order_status" def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ): order_id = tracker.get_slot("order_id") if not order_id: dispatcher.utter_message(text="请问您的订单号是多少?") return [] try: url = f"{os.getenv('ORDER_API')}/orders/{order_id}" rsp = requests.get(url, timeout=2) rsp.raise_for_status() data = rsp.json() msg = f"订单 {order_id} 当前状态:{data['status']}" except requests.exceptions.Timeout: logger.warning("Order API timeout") msg = "查询超时,请稍后再试" except Exception as e: logger.exception("Order API error") msg = "系统异常,已通知管理员" dispatcher.utter_message(text=msg) return [SlotSet("order_status", msg)]埋点:所有异常都进 Sentry,方便复盘。
性能优化:压测与缓存
1. Locust 压测脚本
from locust import HttpUser, task, between class ChatUser(HttpUser): wait_time = between(1, 3) @task(10) def hello(self): self.client.post("/webhooks/rest/webhook", json={"sender": "user1", "message": "你好"})结果(4C8G 单节点):
- 无缓存:平均 1120 ms,RPS 210,CPU 95%。
- 加 Redis 缓存意图结果 5 min TTL:平均 480 ms,RPS 460,CPU 55%。
2. 缓存策略
- NLU 结果缓存:Key=“intent:hash(用户原句)” TTL=300 s,命中率 68%,减少 40% GPU 推理。
- Action 查询缓存:对订单/库存等只读接口缓存 60 s,命中率 55%,P99 下降 35%。
避坑指南:Policy、数据与 K8s
Policy 组合误区
- 同时开 “RulePolicy + TEDPolicy” 时,优先级一定把 RulePolicy 放最前,否则规则会被 TED 覆盖。
- 别在规则里写死
utter_xxx,建议跳到自定义 Action,方便埋日志。
意图训练数据增强
- 用 back-translation(中→英→中)扩写 2 倍语料,F1 提升 4%。
- 引入 10% 口语错字(“定单”→“订单”)+ 拼音(“dd”→“订单”),鲁棒性再涨 3%。
K8s 资源限制
- NLU 容器必须加
nvidia.com/gpu: 1,否则会在 CPU 上跑,延迟瞬间翻倍。 - Action Server 的 CPU limit 不要小于 1000m,否则并发 50 即触发 502。
- NLU 容器必须加
代码规范小结
- 统一用
black+isort做强制格式化,CI 阶段检测。 - 所有接口函数写类型注解,覆盖率 100%。
- 异常捕获必须打
logger.exception,附带sender_id,方便追踪单轮对话。
延伸思考:LLM 如何无缝加持
Rasa 3.6 已支持LLMCommandGenerator,可以把大模型当“后备 Policy”:
- 当 TED 置信度 < 0.3 时,把当前状态序列化成 prompt,调用本地部署的 ChatGLM3-6B。
- LLM 返回的回复再走一层“安全过滤”——正则+敏感词,耗时增加 400 ms,但兜底率提升 18%。
- 日志侧记录“LLM 介入”标签,后续人工纠偏,再回流到 Rasa 训练集,实现数据飞轮。
实测:在 5 k 轮会话中,LLM 介入 7%,整体满意度从 3.6 提到 4.2(5 分制),而成本仅增加 1 台 A10 GPU 节点。
写在最后的用户视角
系统上线三个月,跑了两次大促,Redis 扩容一次,GPU 节点升配一次,其余时间基本“放养”。监控大盘里那条 99.94% 的绿线,让老板终于不再半夜@我查日志。开源不是银弹,但把坑踩完、文档补全后,它确实能让预算和自主权都回到自己手里——对打工人来说,这就是最实在的安全感。