AI智能客服云服务实战:从架构设计到生产部署的避坑指南
背景痛点:流量一涨,客服先“宕”
去年双十一,我们给电商客户上线了一套“AI 客服”,结果凌晨 00:05 并发冲到 3w QPS,P99 延迟飙到 4.8 s,用户疯狂吐槽“机器人卡成 PPT”。复盘发现三大坑:
- 单容器部署,ASR 与 NLP 抢 CPU,线程池打满,响应延迟指数级上升
- 多轮对话靠内存 Map 存上下文,Pod 一重启会话全丢,用户得从头“你好”开始
- 敏感词、地址纠错都走的百度/腾讯 HTTP 接口,对方限流 429,我们直接 502
痛定思痛,决定把客服系统拆成“可横向扩展的云服务”,目标只有一个:流量再大,也要让机器人“张嘴说话”不卡壳。
技术选型:买现成的还是自己搭?
| 维度 | 阿里云/腾讯云智能客服 | Rasa + FastAPI 自建 | |---|---|---|---| | 上手速度 | 5 分钟开通,界面配 FAQ 即可 | 需自己搭 NLU、DST、Policy | | 定制深度 | 意图模板固定,多轮逻辑黑盒 | 代码级控制,可插任意模型 | | 并发弹性 | 按“调用次数”计费,突发流量钱包先受不了 | 按 CPU/GPU 扩 Pod,成本可控 | | 租户隔离 | 平台级,不可二次分库 | 自己做多租户路由,灵活但需代码 |
结论:
- POC 阶段直接买云厂商,快速验证
- 正式上线后,为了“业务定制 + 成本可控”,我们选了Rasa 3.x + FastAPI的微服务方案,全部容器化,跑在 ACK(阿里云 K8s)上,方便后续混合云输出。
核心实现:把“对话工厂”拆成 5 个小微服务
整体思路:
“ASR → NLP-Core → DM(对话管理)→ 敏感词→ TTS” 五段流水线,每段都是独立 Deployment,通过gRPC内网调用,对外只暴露FastAPI-Gateway。
1. Docker Compose 编排(本地开发版)
# docker-compose.yml services: asr: image: registry.cn-sh/ai-cust/asr:2.1 ports: ["30001:50051"] environment: [*GPU_IDX=0*] nlp-core: image: registry.cn-sh/ai-cust/nlp:3.0 ports: ["30002:50051"] environment: [*MODEL_DIR=/models/bert-base-chinese*] dm: image: registry.cn-sh/ai-cust/dm:3.0 ports: ["30003:50051"] environment: [*REDIS_URL=redis://redis:6379/1*] gateway: build: ./gateway ports: ["8000:8000"] depends_on: [asr, nlp-core, dm]开发机 GPU 只有一张 2080Ti,用deploy.resources.reservations.devices把 GPU 按序号绑给 ASR,避免抢卡。
2. Python 熔断层:让第三方“挂”得优雅
第三方敏感词服务常 429,我们包一层gRPC + CircuitBreaker:
# grpc_client.py from grpc import insecure_channel, RpcError from circuitbreaker import circuit import backoff class SensitiveClient: @circuit(failure_threshold=5, recovery_timeout=30) @backoff.on_exception(backoff.expo, RpcError, max_tries=3) def check(self, text: str) -> bool: with insecure_channel('sensitive:50051') as chan: stub = SensitiveStub(chan) return stub.Scan(SensitiveReq(text=text)).has_sensitive- 熔断器 30 s 后自动半开,失败 5 次即跳闸
- 指数退避重试,避免瞬时打爆
- 类型注解 + 日志埋点,方便 SRE 排障
3. 对话状态机 Redis 缓存设计
DM 服务用Redis Hash存每轮特征,Key 设计:
cust:{tenant_id}:{session_id} → {"slots": "...", "history": "...", "ttl": 1700000000}代码片段:
# dm/state_repo.py import redis, json, time from typing import Dict, Optional class StateRepo: def __init__(self, url: str): self.r = redis.from_url(url, decode_responses=True) def get_state(self, tenant: str, sid: str) -> Optional[Dict]: data = self.r.hgetall(f"cust:{tenant}:{sid}") if not data: return None if int(data["ttl"]) < time.time(): return None # 过期会话自动回收 return json.loads(data["slots"]) def save_state(self, tenant: str, sid: str, slots: Dict, ttl: int = 600): pipe = self.r.pipeline() key = f"cust:{tenant}:{sid}" pipe.hset(key, mapping={"slots": json.dumps(slots), "ttl": int(time.time()) + ttl}) pipe.expire(key, ttl) pipe.execute()- 10 min 无交互自动过期,节省内存
- 用 Pipeline 打包两次写,RTT 减半
- 支持多租户前缀,逻辑隔离
性能优化:把 GPU 当“共享单车”用
1. 负载测试数据
用 k6 写脚本,模拟 5 千路并发语音问答,持续 10 min:
- 纯 CPU 版:P99 2.1 s,CPU 打满,ASR 识别掉字 8%
- GPU 共享版:P99 0.6 s,掉字 <1%,GPU 利用率 68%
2. GPU 动态分配策略
K8s 侧装NVIDIA Device Plugin,给 ASR Pod 加nvidia.com/gpu: 1的 request。
高峰时利用HPA + Prometheus GPU Duty指标:
avg(gpu_util) > 70% ⇒ 副本数 +1 avg(gpu_util) < 30% ⇒ 副本数 -1低峰期只跑 1 个 Pod,成本砍半。
3. 对话超时回收
上文 Redis 已带 TTL,此外 DM 起了一个asyncio 定时任务,每 30 s 扫描“僵尸会话”(用户已离开但 Key 残留),调用DEL释放。
经验值:10 万并发会话,Redis 内存峰值从 14 GB 降到 6 GB。
避坑指南:上线前一定要踩的 3 个坑
1. 敏感词异步调用陷阱
早期图简单,把敏感词放 FastAPI 的sync路由里直接requests.post,结果线程池被占满,QPS 从 2 k 跌到 300。
解决:
- 路由函数加
async def,httpx.AsyncClient全程 await - 超时 200 ms 直接放行,宁可漏杀,不可卡顿
2. 会话 ID 碰撞
最早用uuid4().hex16 字节,结果压测 5 千万次出现 2 次碰撞,用户 A 看到用户 B 订单。
解决:
- 改
uuid1(node=mac)+ 进程 PID + 时间戳,理论碰撞 <1e-12 - 数据库层加唯一索引,真撞了抛异常,前端重新建会话
3. 语音转文字编码
ASR 模型训练语料全是 UTF-8,但部分安卓机上传 PCM 附带charset=binary,按 GBK 解码直接崩。
解决:
- 网关层统一
chardet.detect(),异常编码转 UTF-8 后再喂给 ASR - 二进制头加 Magic Number 校验,防止脏数据入队
代码规范:让后人少掉几根头发
- 全项目强制
mypy --strict,函数签名一个都不能少 - 所有 I/O 函数加
tenacity重试 + 日志埋点,出错能定位到第几行 - 关键循环写
# cython: N注释,提醒后续可编译.pyx提升性能 - 单元测试覆盖率 >85%,PR 自动跑
locust冒烟,性能 regress 直接打回
延伸思考:实时情感分析能不能再快点?
目前情感模型(RoBERTa-large)跑在 GPU 上,单句 150 ms,叠加到对话链路后 P99 增加 0.2 s。
问题是:
- 如果提前把情感任务放CPU 异步队列,用户侧不等待,结果通过WebSocket 推回去,就能“零延迟”感知情绪。
但带来的挑战:
- 乱序推送,前端如何对齐句子与情感标签?
- 异步失败重试,可能用户已离开,情感结果是否还有意义?
欢迎有经验的朋友一起聊聊,看还有没有其他“不堵主链路”的实时情感方案。
小结:让客服系统像自来水一样“随开随有”
整套微服务化 + 熔断 + 自动扩缩下来,去年双十二同并发水位,P99 降到 0.5 s,GPU 成本节省 42%,至今没再被用户吐槽“卡”。
回头想,最大心得不是堆多高大上的算法,而是:把每个可能“堵”的环节都做成可横向扩展的小模块,再提前把失败场景写进代码里。
希望这份避坑笔记,能帮你在下一个流量洪峰到来前,把智能客服真正做成“随开随有”的自来水服务。