背景痛点:Linux 上跑智能客服,踩过的坑比代码还多
去年公司把客服从“人工小姐姐”换成“AI 小助手”,需求一落地,才发现 Linux 环境比 Demo 难伺候:
- 会话(Session)状态保持:HTTP 无状态,用户问两句“订单在哪”后,第三句突然变成“你是谁”,体验瞬间翻车。
- 多轮对话管理:Rasa 的 Tracker 默认放内存,Gunicorn 多 worker 一重启,对话历史直接蒸发。
- 资源竞争:流量高峰时,Nginx + 4 个 Gunicorn worker 直接把 4 核 CPU 吃满,Redis 单线程也被打爆,延迟飙到 2 s。
痛定思痛,决定用开源方案撸一套“能扛大促”的智能客服,目标:高并发、低延迟、易扩容、不烧钱。
技术选型:Rasa 不是唯一,却是最顺手的
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| Dialogflow ES | 谷歌全家桶,中文支持尚可 | 按次收费,流量大就破产;数据出境合规难 | 放弃 |
| 自研 NLU + DM | 自主可控,可炫技 | 工期 3 个月起,分词、意图、槽位全自己啃 | 放弃 |
| Rasa Open Source + Redis | 开源免费,社区活跃,插件多;Tracker 可外置;Docker 镜像成熟 | 需要自己动手写“状态仓库” | 就选它 |
一句话:Rasa 负责“听懂”,Redis 负责“记住”,Nginx 负责“分发”,三者组合成本最低,可控性最高。
核心实现:Docker Compose 一把梭
1. 目录结构
chatops/ ├── docker-compose.yml ├── nginx/ │ └── default.conf ├── rasa/ │ ├── credentials.yml │ ├── endpoints.yml │ └── actions/ ├── redis/ │ └── redis.conf └── auth/ └── jwt_middleware.py2. Docker Compose 编排
version: "3.8" services: nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - rasa-production networks: - chat rasa-production: image: rasa/rasa:3.6-full working_dir: /app volumes: - ./rasa:/app command: > run --cors "*" --port 5005 --endpoints endpoints.yml --credentials credentials.yml environment: - REDIS_URL=redis://redis:6379/0 depends_on: - redis networks: - chat redis: image: redis:7-alpine volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf networks: - chat networks: chat: driver: bridge3. Redis 数据结构设计
把 Rasa Tracker 序列化后按session_id拆分,Hash 存最后一轮,List 存历史,TTL 保活 30 min。
# redis_tracker_store.py import json import redis from rasa.core.tracker_store import TrackerStore from rasa.shared.core.trackers import DialogueStateTracker class RedisTrackerStore(TrackerStore): """ 将对话状态持久化到 Redis,支持多 worker 共享。 """ def __init__(self, domain, url="redis://localhost:6379/0"): self.red = redis.from_url(url, decode_responses=False) super().__init__(domain) def save(self, tracker: DialogueStateTracker) -> None: key = f"tracker:{tracker.sender_id}" # 存全量 self.red.hset(key, "data", json.dumps(tracker.current_state())) # 存历史 self.red.lpush(key+":events", *[json.dumps(e) for e in tracker.events]) self.red.expire(key, 1800) # 30 min 过期 def retrieve(self, sender_id: str) -> DialogueStateTracker: key = f"tracker:{sender_id}" data = self.red.hget(key, "data") if data: state = json.loads(data) return DialogueStateTracker.from_dict( sender_id, state, self.domain) return None4. JWT 会话鉴权中间件
用 PyJWT 生成 Token,过期 1 h,Nginx 转发时把Authorization头带过来。
# jwt_middleware.py import jwt from functools import wraps from flask import request, jsonify SECRET = "change-me-in-prod" def generate_token(user_id: str) -> str: """生成 JWT,有效期 1 小时""" payload = {"user": user_id} return jwt.encode(payload, SECRET, algorithm="HS256") def login_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get("Authorization", "").split(" "")[-1] try: jwt.decode(token, SECRET, algorithms=["HS256"]) except jwt.ExpiredSignatureError: return jsonify({"error": "token expired"}), 401 return f(*args, **kwargs) return decorated在 Rasa 的自定义 Action 里加装饰器即可保护敏感接口,如“查询订单”。
性能优化:压测数据说话
1. Locust 场景
- 脚本:模拟 1000 并发,每秒发 5 轮对话,持续 5 min。
- 指标:P95 响应时间、错误率。
| 场景 | P95 延迟 | 错误率 |
|---|---|---|
| 无 Redis,内存 Tracker | 2100 ms | 12 % |
| 有 Redis,开启缓存 | 380 ms | 0.4 % |
结论:Redis 缓存把 Tracker 查询从磁盘/重启风险中解放,延迟降 5 倍。
2. Nginx worker 数调优
公式:worker_processes = min(cpu_cores, 8)
实测 4 核云主机:
- 设 4 个 worker,QPS 峰值 1200
- 设 8 个 worker,QPS 峰值 1250,CPU 空转,收益递减
建议:容器场景下,把worker_processes auto;交给 Nginx 自己识别即可,别硬写死。
避坑指南:中文环境特供版
中文分词器配置陷阱
Rasa 默认用 WhitespaceTokenizer,中文一句话被当成一个 token,意图识别直接瞎猜。
解决:换成JiebaTokenizer,在config.yml里加:pipeline: - name: JiebaTokenizer dictionary_path: ./dict.txt记得把业务专有词(如“免息券”)写进自定义词典,否则照样分错。
Redis 持久化导致延迟飙升
默认开启 AOF,每秒刷盘,流量高峰时fsync阻塞主线程。
解决:- 只开 RDB,关闭 AOF:
appendonly no - 若必须 AOF,改
appendfsync everysec为no,让系统调度刷盘,再外挂 SSD。
- 只开 RDB,关闭 AOF:
对话超时机制
客服场景用户可能“已读不回”,Tracker 一直占内存。
实现:- Redis 存 Tracker 时加 30 min TTL
- 用户再说话时,若 key 不存在,返回欢迎语并重新创建 Tracker,体验无感。
代码规范:PEP8 是底线
- 每行 79 字符,函数名小写加下划线
- 关键函数必须写 docstring,已在上文示例体现
- 提交前
black redis_tracker_store.py && flake8 --max-line-length=79
延伸思考:把日志接到 ELK,排查不再靠 grep
Rasa 日志默认 stdout,多容器并排时,查一个问题要docker logs十几遍。
下一步:
- 容器里装 Filebeat,采集
/app/logs/*.log - 日志格式改 JSON,字段含
sender_id、intent、response_time - Elasticsearch 按
sender_id建索引,Kibana 做 Dashboard:- 平均响应时间趋势
- 意图命中率热力图
- 异常 Top 意图告警
一旦客服“答非所问”,5 min 内就能定位是 NL 模型问题还是 Action 超时,运维小姐姐再也不用熬夜 tail 日志。
整套方案上线后,撑过了去年双十二 3 倍流量,CPU 稳定在 60 %,P95 延迟 400 ms 以内。代码和 Ansible 脚本已放到 GitLab,团队新人 30 min 可复制一套测试环境。
如果你也在 Linux 上折腾智能客服,希望这篇笔记能让你少走点弯路,多点时间喝咖啡。祝调试顺利,对话不翻车!