背景痛点:传统客服系统“三宗罪”
过去两年,我先后接手过三套“祖传”客服系统:一套基于正则的规则引擎,两套 Rasa 1.x 的老项目。它们在线上共同暴露出的问题,可以总结成三句话:
- 意图识别模糊——“我要退款”和“我要退货”只多了一个字,规则却得写两条,维护量指数级上涨。
- 上下文丢失——用户问完“订单能改地址吗?”紧接着说“那邮费怎么算?”,系统直接失忆,只能重新走 FAQ。
- 扩展性差——每逢大促,运营临时加活动,开发通宵改流程图,第二天还要被业务吐槽“上线慢”。
这些问题在流量低峰还能忍,一到 618、双 11,客服通道瞬间变“客服堵”。痛定思痛,我们决定用 Dify 重构一条真正生产可用的 AI 智能客服链路。
核心目标只有一个:在高并发场景下,让机器人像人一样记住上下文,并且随时可灰度、可回滚、可观测。
技术选型:Dify 为什么胜出
先把候选框架拉出来遛一圈:
| 维度 | Rasa | Botpress | Dify |
|---|---|---|---|
| 内置 LLM 微调 | 需外挂 | 需外挂 | 可视化 |
| 业务逻辑解耦 | 一般 | 一般 | 插件+工作流 |
| 高并发网关 | 自建 | 自建 | 自带 API Gateway |
| 运维友好度 | 中等 | 中等 | 容器化一键起 |
一句话总结:Rasa/Botpress 更像“DIY 乐高”,Dify 则是“半开箱即用”。尤其 LLM 微调与业务逻辑彻底解耦后,运营同学自己就能在界面里拖一条“新意图”上线,不用凌晨两点把开发从床上薅起来。
核心实现:四条链路,句句有代码
1. API Gateway 扛住 500 TPS
Dify 内置的 Gateway 已经做好 Nginx+Gunicorn 的横向扩容,我们只需要在docker-compose.yml里把WORKERS参数调成CPU*2+1,再挂一层 SLB,就轻松把峰值推到 500 TPS。压测脚本后面统一给出。
2. Redis 状态机:让机器人“有记忆”
对话状态用 Hash 存储,key 设计为session:{user_id},field 存intent、slots、turn_count,TTL 设为 30 min。核心代码如下(PEP8 合规,含类型标注):
import json import redis from typing import Dict, Optional class DialogueStore: def __init__(self, host: str = "redis", port: int = 6379, db: int = 0): self.r = redis.Redis(host=host, port=port, db=db, decode_responses=True) def get_state(self, user_id: str) -> Optional[Dict]: data = self.r.hgetall(f"session:{user_id}") return json.loads(data) if data else None def set_state(self, user_id: str, state: Dict, ex: int = 1800) -> None: key = f"session:{user_id}" self.r.hset(key, mapping={k: json.dumps(v) for k, v in state.items()}) self.r.expire(key, ex)状态机简图:
3. 异步日志:Celery+ELK,错误重试不丢话
客服日志如果同步写,高峰期 RT 直接 +200 ms。我们采用 Celery 任务队列,异常时自动指数退避重试三次,代码片段:
from celery import Task from typing import Any import logging logger = logging.getLogger(__name__) class LogTask(Task): autoretry_for = (Exception,) max_retries = 3 retry_backoff = 2 @app.task(base=LogTask) def log_conversation(user_id: str, msg: str, intent: str) -> None: body = {"user_id": user_id, "msg": msg, "intent": intent} es.index(index="chat-{date}", body=body)ELK 侧只负责聚合,磁盘 IO 与客服链路完全解耦。
4. 敏感词过滤 & 数据脱敏
采用双通道方案:
- 规则层:Aho-Corasick 算法,2 ms 内完成 10 万级敏感词匹配;
- 模型层:Dify 插件里再挂一个轻量 BERT 分类器,识别隐晦变形词。
脱敏函数示例:
import re from typing import List ID_PATTERN = re.compile(r"\d{6}(\d{8})\w{4}") PHONE_PATTERN = re.compile(r"1[3-9]\d{9}") def desensitize(text: str) -> str: text = ID_PATTERN.sub(r"********", text) text = PHONE_PATTERN.sub(r"***********", text) return text生产考量:压测、灰度、熔断,一个都不能少
1. 压测数据
JMeter 1000 并发、持续 15 min 结果:
- P99 延迟 450 ms
- 错误率 0.2%(全部因后端 GPU 池瞬时满负荷)
- 当 TPS>520 时延迟陡增,故线上按 400 TPS 做限流
2. 灰度发布
利用 K8scanary注解,按用户尾号 00-09 走新模型,其余走旧模型。Dify 的APP_ID与Model_Version天然支持多版本并存,回滚只需改一条路由权重。
3. 异常熔断
Hystrix 模式搬到 Python:失败率连续 5 次超过 50% 即熔断 30 s, fallback 返回“人工客服忙线中,请稍后再试”。熔断期间不再调用 GPU 池,保护后端。
避坑指南:踩过的坑,帮你填平
对话超时阈值
设置 30 min 看似合理,但大促凌晨用户“睡一觉继续聊”会导致 Redis 内存暴涨。最终按业务曲线调成 15 min,内存下降 40%。微调样本增强
直接拿线上日志做样本会“越训越偏”。我们用 back-translation:中文→英文→中文,再人工筛一遍,意图准确率从 82% 提到 91%。GPU 动态分配
线上共用一张 A10,白天客服+晚上训练互相抢卡。后来用 K8snvidia.com/gpu: 1+time-slicing策略,白天训练任务只给 20% 算力,客服优先,冲突降低 90%。
代码规范小结
- 统一
black格式化,行宽 88 - 公开函数必须带类型标注与
-> None - 所有 I/O 操作包
try/except,打日志不出异常给前端 - 单元测试覆盖 ≥ 85%,MR 自动跑
tox
互动环节:开放问题
用户聊到一半突然说“对了,你们 APP 最近闪退怎么回事?”,话题瞬间从订单售后来到软件 BUG。如果换作你,会怎么让状态机既保留旧上下文,又优雅地切换到新意图?欢迎在 示例仓库 提 PR,一起把“话题漂移”模块做成插件!
全文完。希望这份从架构到代码的实战笔记,能帮你在下一个 618 之前,把客服系统从“能用”做到“好用”。祝部署顺利,0 故障上线!