开源智能客服系统实战:从架构设计到生产环境部署
1. 背景痛点:为什么自己造轮子
去年公司把客服外包团队裁掉一半,领导一句“用 AI 顶上去”,需求单直接拍脸上。真正动手才发现,企业级场景远比 Demo 难缠:
- 意图识别准确率:用户一句“我密码丢了”能衍生出 20 多种表达方式,Rasa 默认的 DIET 模型在内部语料上只有 78% F1,离可用差一截。
- 会话状态管理:ToB 业务平均对话轮次 12+,状态要跨天、跨设备,还要支持客服随时切入,Redis 宕机 5 分钟就是客诉洪流。
- 多租户隔离:集团下有 6 个品牌,数据不能串门,又要共享基础模型,否则 GPU 预算直接爆炸。
- 高并发:大促峰值 3k QPS,平均响应超过 600ms 就会触发微信告警,而 Dialogflow 按量计费,账单一眼望不到头。
这些坑逼着我们走上“开源 + 自研”路线,既省钱又能把方向盘握在自己手里。
2. 技术选型:Rasa vs Dialogflow vs Botpress
| 维度 | Rasa 3.x | Dialogflow ES | Botpress 12 |
|---|---|---|---|
| NLU 可拔插 | 支持自定义组件 | 黑盒 | 可插 Transformer |
| 对话管理 | 基于 Story & Rule,可写单元测试 | 基于上下文状态机 | 流程可视化拖拽 |
| 扩展性 | Python 源码级,无并发限制 | 强制云调用,有配额 | 单进程 Node,水平扩展需自改 |
| 离线部署 | 完全可行 | 不可 | 可,但组件多 |
| 费用 | 0 美元 | 0.002 美元/请求 | 0 美元 |
结论:
- Dialogflow 出局——成本不可控,且状态机对长对话支持差。
- Botpress 的 GUI 很香,但单进程架构在 3k QPS 下直接跪;改到多进程成本≡重构。
- Rasa 开源、可离线、社区活跃,虽然 Story 语法学习曲线陡,但换来的是“想怎么改就怎么改”。
最终拍板:以 Rasa 作为 NLU + Core 核心,外围业务服务全用 Python 自研,保持组件可替换。
3. 架构设计:一张图看懂全貌
先上图,再解释。
PlantUML 源码贴在文末附录,想二次绘制的同学自取。图中关键链路:
- 网关统一做 JWT 校验、限流、灰度。
- NLU Service 只负责意图/实体识别,无状态,可水平扩容。
- Dialogue Engine 维护状态机,通过 RabbitMQ 把“意图事件”广播给下游:
- 知识图谱服务补填槽位;
- 工单服务写数据库;
- 审计服务写日志。
- 所有消息带 UUID,消费端做幂等(Redis SETNX 实现),保证重复投递不翻车。
事件流转全部异步, Dialogue Engine 返回 ACK 即可,平均 RT 从 600 ms 降到 120 ms。
4. 代码落地:FastAPI + Redis 实战片段
以下示例均从生产仓库脱敏而来,可直接跑通。
4.1 意图识别端点(含 JWT)
# nlu_service/main.py import jwt from fastapi import FastAPI, Depends, HTTPException from rasa.nlu.model import Interpreter import time app = FastAPI(title="NLUService") interpreter = Interpreter.load("/app/models/nlu.pt") async def verify_token(token: str): try: payload = jwt.decode(token, "secret", algorithms=["HS256"]) return payload["tenant_id"] except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token") @app.post("/predict") def predict(text: str, tenant_id: str = Depends(verify_token)): start = time.time() result = interpreter.parse(text) latency = time.time() - start # 时间复杂度 O(L) L=字符数,DIET 内部 Transformer 线性 return {"tenant_id": tenant_id, "intent": result["intent"], "latency": latency}JWT 中带上tenant_id,下游所有表按tenant_id分库分表,天然隔离。
4.2 Redis 会话上下文(TTL+LRU)
# session_store.py import redis import json import os r = redis.Redis(host=os.getenv("REDIS_HOST"), decode_responses=True) def get_session(sid: str): data = r.get(sid) return json.loads(data) if data else {"stack": []} def set_session(sid: str, data: dict, ttl: int = 360过得去): # 使用 LRU + TTL 双保险 r.setex(sid, ttl, json.dumps(data))实测 8 GB Redis 可扛 200 万会话,命中率 96%,比放 MySQL 减少 80% IO。
5. 生产实践:K8s 弹性与合规
5.1 HPA 基于 QPS
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nlu-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nlu-deploy minReplicas: 3 maxReplicas: 50 metrics: - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: "100"自定义指标通过 Prometheus + Prometheus Adapter 暴露,大促峰值 50 副本稳定运行。
5.2 敏感词 & 审计
- 敏感词采用 Aho-Corasick 多模式匹配,时间复杂度 O(n + m),2 万条词库 1 ms 内完成过滤。
- 审计日志写进 Loki,保留 30 天,对接公司 CIEM,满足 GDPR“可被删除”条款:给
user_id建倒排索引,删除时走后台批量任务。
6. 避坑指南:那些凌晨 2 点的告警
对话流超时反模式
早期把timeout=30s写死在 Story 里,结果用户睡一觉回来继续聊,状态机早被回收,直接跳到欢迎语。
正确姿势:把超时事件也当一种意图external_intent:timeout,让 Story 显式处理,再给用户“已重置”提示。多语言编码陷阱
中文繁体、Emoji、全角符号混一起,MySQL 若用 utf8(mb3) 会炸“Incorrect string value”。
统一用utf8mb4并设置charset-collate=utf8mb4_unicode_ci,否则半夜会收到 500 连环 call。RabbitMQ 消息帧过大
把整段 20 KB 语音丢给 MQ,结果 broker 报FRAME_ERROR。
正确做法:传对象存储 URL,消费者自己拉取,帧大小 < 128 KB,QPS 再高也不怕。
7. 延伸思考:LLM 加持的混合架构
规则引擎优点是可解释、可控;缺点是泛化差。大模型优点恰好反过来。
我们内部已跑通“双轨”方案:
- 冷启动:用 Rasa 规则兜底,保证 100% 合规回答。
- 灰度:把用户问题同时发给 LLM(私有化 Llama-2-13B),取置信度 > 0.85 且敏感词检测通过时,才用 LLM 回复。
- 线上 A/B 显示,LLM 轮次转化率提升 18%,但幻觉率 2%;通过知识图谱检索增强(RAG)后,幻觉率降到 0.6%。
论文参考:Google《LaMDA: Language Models for Dialog Applications》指出“Fine-tune + Retrieval”是降低幻觉的有效路径,与我们的落地结果一致。
8. 小结:开源不是省钱,是买自由
整套系统上线半年,跑了 3k QPS 大促,机器成本只是同量 Dialogflow 的 1/5。
更重要的是,遇到业务变更,不必等云厂商排期,自己改代码就能上线。
如果你也在被“降本增效”支配,不妨从 Rasa 这条船开始,先跑通一条核心 Story,再逐步替换成微服务。
文末附录:
PlantUML 源码(复制到 plantuml.com 即可渲染):
@startuml !define ms(name,desc) rectangle name as desc ms(gw, "API Gateway\nJWT&限流") ms(nlu, "NLU Service\nRasa") ms(core, "Dialogue Engine\n状态机") ms(kb, "知识图谱") ms(mq, "RabbitMQ") ms(db, "PostgreSQL\ntenant分片") ms(cache, "Redis\n会话+幂等") ms(log, "审计日志\nLoki") gw --> nlu : 文本 nlu --> mq : 意图事件 mq --> core mq --> kb core --> cache core --> db core --> log @enduml祝你也能早日脱离“人工客服荒”,用开源的代码,把智能客服真正落地到生产。