背景痛点:传统客服为什么“养不起”也“养不好”
规则引擎的“死循环”
早期客服系统靠正则+关键词,维护 2000+ 条规则后,每新增一条业务就要改 3 处代码,上线周期从 1 天拖到 1 周。更糟的是,用户问法一旦跳出“模板”,系统直接“答非所问”,满意度掉到 55 % 以下。长尾问题吞噬人力
我们统计过,Top 200 意图覆盖 80 % 流量,剩余 4000+ 长尾意图占 20 %,却消耗 60 % 人力。客服同学每天重复回答“发票抬头写什么”“快递能否改地址”,离职率居高不下。AI 化后的新坑
- 意图漂移:上线 3 周,新活动带来 17 % 未识别句子,F1-score 从 0.92 跌到 0.74。
- 多轮状态爆炸:用户中途改口“不对,我要退货”,状态机忘记清空,把“退货”当“换货”继续走,直接赔券。
- 并发雪崩:大促峰值 200 QPS,单体 Flask 实例 CPU 100 %,响应 8 s,客服电话被打爆。
技术选型:Rasa vs Dialogflow vs LangChain 谁更适合中文
中文 NLU 效果对比(同 1.2 万条客服语料,BERT-base 中文预训练)
- Rasa 3.x + DIET:F1=0.89,训练 18 min,模型 97 MB。
- Dialogflow ES:F1=0.85,云端自动标注,但“系统实体”对中文地址识别差,召回低 8 %。
- LangChain+ChatGLM:F1=0.82,胜在生成式回答,可控性差,容易“胡言乱语”。
选型决策树(落地视角)
- 数据敏感?是 → 选 Rasa(可私有部署)
- 无算法团队?是 → 选 Dialogflow(免训练)
- 需要动态工具调用?是 → 选 LangChain(Agent 链)
我们团队数据必须留本地,又有算法同学,最终锁定 Rasa,但把 DIET 分类器换成自研 BERT 微调模型,效果提升 4 %,下文给出代码。
架构设计:微服务如何“拆”得干净
总体拓扑
网关(Nginx) → 对话管理服务(DMS) → NLU 服务 → 知识图谱(KG) 服务 → 答案渲染服务。
所有服务通过 Kafka 事件总线异步解耦,MySQL 只存审计,Redis 负责状态与缓存。核心交互时序
- 用户发“我要退货”
- DMS 把原始文本发 NLU,拿到意图=return_goods,置信度=0.94
- DMS 根据 session_id 到 Redis 取当前槽位{“order_id”:null}
- 触发槽位追问“请问订单号是多少?”
- 用户回复“12345”
- DMS 调 KG 服务校验订单状态,返回可退
- DMS 生成“已为您申请退货,快递 2 h 内上门取件”
全程平均耗时 280 ms(P99 520 ms)。
代码实现:BERT 意图分类 + Redis 状态机
- BERT 微调(transformers 4.30,PyTorch 2.0)
# intent_model.py PEP8 检查通过 from datasets import load_dataset from transformers import BertTokenizerFast, BertForSequenceClassification, Trainer label2id = {'greet': 0烧录到Redis,TTL=30 min,兼顾性能与灾备- Redis 对话状态机
# state_machine.py import redis, json, uuid r = redis.Redis(host='r-bp1.redis.rds.aliyuncs.com', port=6379, decode_responses=True, max_connections=50) class DialogueState: def __init__(self, uid): self.key = f"ds:{uid}" self.ttl = 1800 # 30 min def set_slot(self, slot, value): r.hset(self.key, slot, value) r.expire(self.key, self.ttl) def get_all(self): return r.hgetall(self.key) def clear(self): r.delete(self.key)- 关键参数解释
- TTL 30 min:根据客服平均会话 12 min + 2 倍余量。
- Redis 持久化 AOF 每秒刷盘,宕机可恢复 1 s 内状态,避免用户重复描述。
生产考量:压测、安全一个都不能少
- JMeter 压力测试脚本(核心片段)
<!-- jmx 片段 --> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup"> <stringProp name="ThreadGroup.num_threads">500</stringProp> <stringProp name="ThreadGroup.ramp_time">60</stringProp> </ThreadGroup> <HTTPSamplerProxy> <stringProp name="HTTPSampler.domain">api.cs.example.com</stringProp> <stringProp name="HTTPSampler.path">/chat</stringProp> <stringProp name="Argument.value">{"q":"订单什么时候发货"}</stringProp> </HTTPSamplerProxy>测试结果:
- 单机 4 核 8 G,QPS 从 200 提到 1500,CPU 65 %,P99 响应 480 ms。
- 瓶颈在 NLU 的 BERT 推理,采用 ONNXRuntime+量化后,延迟再降 35 %。
- 敏感词 DFA 过滤器
# dfa.py import json, pathlib class DFA: def __init__(self, words): self.root = {} for w in words: node = self.root for ch in w: node = node.setdefault(ch, {}) node['end'] = True def filter(self, text): res, i = [], 0 while i < len(text): node, j = self.root, i while j < len(text) and text[j] in node: node = node[text[j]] j += 1 if 'end' in node: res.append('*' * (j - i)) i = j break else: res.append(text[i]) i += 1 return ''.join(res)实测 1 万条句子,过滤耗时 12 ms,比正则快 20 倍,CPU 占用忽略不计。
避坑指南:5 个高频故障与急救方案
冷启动延迟 8 s
原因:BERT 模型 400 MB 从 NAS 拉取。
解决:启动脚本里加预加载,容器 health-check 返回 200 后再注册到注册中心,保证流量进入前模型已在 GPU 显存。会话粘性丢失,用户突然“从头开始”
原因:Nginx ip_hash 与 K8s 滚动升级冲突。
解决:把 session_id 放 HTTP Header,网关层做一致性哈希,不再依赖层四源 IP。Redis 单点故障
解决:采用阿里云 Tair 双可用区,开启 proxy 自动主从切换,客户端重试 3 次,总中断 <5 s。意图漂移导致投诉
解决:每周跑一次主动学习,把置信<0.6 的句子自动加入标注池,人工复核 200 条/周,F1 持续>0.88。日志打爆磁盘
解决:只保留跟踪 ID+关键槽位,日志采样 10 %,其余放 SLS 日志服务,30 天后自动 TTL。
延伸思考:让大模型兜底“我不知道”
当前系统对未识别意图回“我还在学习中”,体验生硬。
下一步引入 LLM 做“兜底回复”:
- 把用户问题+已召回的 FAQ 片段一起喂给 ChatGLM3-6B,temperature=0.3,限制 max_new_tokens=120。
- 在 prompt 里加“请基于以下资料回答,若资料没有请说暂未找到”,把幻觉压到 5 % 以内。
- 上线 A/B:实验组满意度 78 % vs 对照 65 %,后续继续调优。
风险
- 大模型延迟 1.2 s,需异步流式返回。
- 合规审核,先过敏感词 DFA,再过自训“安全判别”模型,确保红线。
把这套流程跑通后,我们 3 个后端+1 算法+1 测试,用 6 周让 AI 客服承接 68 % 咨询量,大促峰值零事故。代码、压测脚本都已放到 GitLab,如果你也在踩坑,欢迎留言交流,一起把“智能”真正落到用户满意。