背景痛点:传统客服系统“三座大山”
去年双十一,我们给电商客户做客服升级,原系统直接“炸”了:高峰期平均响应 4.8 s,意图识别准确率掉到 62%,CRM 调一次接口 800 ms,用户疯狂转人工。拆完代码发现三座大山:
- 规则引擎 if-else 层层嵌套,新增一个“退货”意图要改 7 个文件,上线周期 3 天。
- 多轮对话状态放在 Redis String,大 key 读写全串行,并发一高就“雪崩”。
- 与 CRM 是同步 REST 调用,对方超时无降级,线程池打满后整个 Bot 僵死。
痛点总结:延迟高、状态乱、集成重——不重构,AI 客服只能“人工”智能。
技术选型:规则引擎 vs 机器学习,为什么选了 Rasa+Transformer
我们拉了两条分支做 A/B,数据集 18 万条真实对话:
- 规则引擎(Artisan):F1=0.71,新增意图开发 3 天,逻辑冲突 23 处。
- Rasa+Transformer(DIET):F1=0.89,新增意图只需标注 200 条样本,训练 20 min。
再算一笔资源账:DIET 的 Transformer 层只有 2 层 hidden=256,推理 CPU 单核 30 ms,内存 180 MB,完全够跑在 2C4G 的容器里。规则引擎看似轻量,可维护成本是“人”,人比 GPU 贵。于是拍板:Rasa 负责 NLU,自研 Dialogue State Tracker(DST)负责多轮策略,两边用 gRPC 解耦,后续可独立扩容。
核心实现:Python 代码直接抄
1. 对话状态机 / Dialogue State Tracker(带持久化)
状态机思路:每个 user_id 维护一个 State 对象,回合级事件驱动,状态快照异步落盘。
# dst.py from __future__ import annotations import json import time from typing import Dict, Optional from redis import Redis from dataclasses import dataclass, asdict @dataclass class State: user_id: str intent: str = "" slots: Dict[str, str] = None turn: int = 0 ttl: int = 600 # 秒 class DST: def __init__(self, redis: Redis): self.r = redis def _key(self, uid: str) -> str: return f"dst:{uid}" def get(self, user_id: str) -> Optional[State]: raw = self.r.get(self._key(user_id)) return State(**json.loads(raw)) if raw else None def save(self, state: State) -> None: pipe = self.r.pipeline() pipe.set(self._key(state.user_id), json.dumps(asdict(state)), ex=state.ttl) pipe.execute() def update_intent(self, user_id: str, intent: str) -> State: state = self.get(user_id) or State(user_id=user_id) state.intent = intent state.turn += 1 self.save(state) return state时间复杂度:get/set 都是 O(1),Redis 单线程写+内存操作,10 万 QPS 无压力。
2. 异步消息端点 / FastAPI
用 FastAPI 的BackgroundTasks把耗时操作(调 CRM、落日志)丢后台,接口立刻返回,减少排队。
# main.py from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel import httpx app = FastAPI() dst = DST(Redis(host="redis", decode_responses=True)) class WebhookReq(BaseModel): user_id: str text: str async def call_crm(user_id: str, intent: str): async with httpx.AsyncClient(timeout=3.0) as client: await client.post("http://crm/api/event", json={"user": user_id, "intent": intent}) @app.post("/webhook") async def webhook(req: WebhookReq, bg: BackgroundTasks): intent = rasa_parse(req.text) # 伪代码,返回 str state = dst.update_intent(req.user_id, intent) bg.add_task(call_crm, req.user_id, intent) return {"reply": f"收到,已为您记录【{intent}】", "turn": state.turn}压测结果:4 核 8 G 容器,单实例 1200 RPS,P99 延迟 65 ms,CPU 70%,内存 1.1 G。
生产考量:压测、脱敏、灰度一个都不能少
1. JMeter 脚本示例
线程组:500 并发,Ramp-up 30 s,循环 300 次。HTTP Header 带X-User-ID做分片,方便后台按用户路由到同一 Pod,避免状态漂移。
<HTTPSamplerProxy> <stringProp name="HTTPSampler.path">/webhook</stringProp> <stringProp name="HTTPSampler.method">POST</stringProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp> <elementProp name="arguments"> <collectionProp name="Arguments.arguments"> <elementProp> <stringProp name="Argument.value">{"user_id":"${__RandomString(8)}","text":"我要退货"}</stringProp> </elementProp> </collectionProp> </elementProp> </HTTPSamplerProxy>监控指标:QPS、RT、Error%、CPU、Pod 重启次数。RT 突增 20% 即触发自动回滚。
2. 敏感信息过滤
用正则先跑匹配,再白名单兜底,防止误杀订单号。
import re PHONE = re.compile(r"(1[3-9]\d{9})(?!\d)") BANK = re.compile(r"(\d{16}|\d{19})(?!\d)") REPLACE = "***" def mask(text: str) -> str: text = PHONE.sub(REPLACE, text) text = BANK.sub(REPLACE, text) return text时间复杂度:单条正则 O(n),n<512 字符,单次 0.02 ms,可放网关统一处理。
避坑指南:对话超时、标签清洗血泪史
1. 对话超时 3 种模式对比
- 固定 TTL:Redis key 600 s 后过期,实现简单,但用户回来再聊就“失忆”。
- 滑动窗口:每次消息重置 TTL,适合高频场景,内存占用 +30%。
- 业务失效:订单关闭即清状态,需要业务方发事件,最省内存,对接最复杂。
线上采用“滑动窗口”,给 30 min 缓冲,兼顾体验与内存。
2. 意图标签清洗技巧
- 同义归并:把“退款”“退钱”“给我退”映射到 refund,用聚类+人工复核,两周把标签从 312 压到 98。
- 负样本补充:每个意图至少 10% 负样本,防止“退货”被“我要退换货”误触发。
- 时间切分:双 11 前 30 天数据单独训练,避免促销话术污染日常模型。
代码规范小结
- 全项目强制
mypy --strict,类型缺失直接阻断 CI。 - 所有 I/O 函数加
tenacity重试 + 限流,异常按“用户可见/不可见”分级,后者写 Sentry,用户侧返回统一 200+错误码。 - 关键路径打
debug日志,采样率 1%,方便线上排障。
延伸思考:拥抱大模型,也看看账单
GPT-4 做客服,Zero-shot 准确率就能到 0.93,但算一笔账:
- 输入 500 token、输出 150 token,每轮 650 token。
- 官方价 $0.03/1k token,每轮 $0.0195。
- 日活 10 万,平均 3 轮对话,每天 30 万次调用 ≈ $5,850,一个月 17 万刀。
结论:大模型做“兜底”+“冷启动”最划算,日常 90% 流量仍走 Rasa,小模型成本只有 LLM 的 1/50。未来路线:Rasa 负责高频意图,LLM 负责长尾与情绪安抚,混合路由,成本可控,体验再上一个台阶。
踩坑一年,最大的感受是:客服 AI 拼的不是算法有多炫,而是状态管理、集成细节和成本算得精不精。把超时、灰度、压测这些“脏活”做扎实,模型差一点用户也感知不到;反过来,状态一丢,再好的 LLM 也救不了场。希望这份避坑笔记能帮你少熬几个通宵,项目早日上线,安稳睡个好觉。