痛点分析:上线前夜的三连暴击
第一次把智能客服推到预生产环境时,我们踩的坑比需求文档的页码还多。总结下来,最痛的其实就三刀:
意图识别延迟飙高
高峰期平均响应 800 ms,P99 直接到 2.3 s,用户以为机器人“掉线”,疯狂重发,结果雪崩。会话状态说没就没
多轮查询“订单→修改地址→确认”走到第三步突然失忆,用户原地爆炸,客服同学人工接盘接到手软。峰值流量应对无力
618 零点 5 k TPS 洪峰一来,单体服务直接 OOM,K8s 重启速度赶不上崩溃速度,SLA 血崩。
这三刀刀刀致命,逼得我们不得不把“能跑”的 Demo 重构成“能扛”的平台。
技术选型:为什么把 Dialogflow 请下牌桌
前期调研时,我们把 Rasa、Dialogflow、Luis 放在同一赛道,用 5 万条真实中文语料做盲测,结果如下:
| 指标 | Rasa 3.2 | Dialogflow ES | Luis |
|---|---|---|---|
| 中文准确率 | 91.4 % | 86.7 % | 84.2 % |
| 平均延迟 | 120 ms | 280 ms | 320 ms |
| 免费额度后成本 | 0.008$/次 | 0.02$/次 | 0.025$/次 |
| 私有部署 |
钱还是小事,大促峰值 8 k TPS 时,按量计费直接上天;再加上 Dialogflow 对上下文槽位有长度限制,多轮对话一复杂就“失忆”。综合准确率、成本、可控性,我们拍板自研:用 Python 做 NLU,Go 做高并发对话引擎,全部握在自己手里。
核心实现:微服务 + 状态机 + 消息队列
1. 微服务骨架:Go Gin 版
// main.go package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.New() r.Use(gin.Recovery()) // 防 panic 退出 r.POST("/chat", handleChat) r.Run(":8080") } func handleChat(c *gin.Context) { var req ChatReq if err := c.ShouldBindJSON(&req); err != nilstab { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "bad json"}) return } // 省略业务逻辑 c.JSON(http.StatusOK, gin.H{"reply": "pong"}) }2. 分布式会话:Redis + Lua 保证原子续期
// session.go const luaRefresh = ` local key = KEYS[1] local ttl = ARGV[1] local ok = redis.call("SETEX", key, ttl, redis.call("GET", key)) if ok then return 1 else return 0 end ` func RefreshTTL(pool *redis.Pool, sid string, ttl int) error { conn := pool.Get() defer conn.Close() res, err := redis.Int(script.Do(conn, sid, ttl)) if err != nil || res == 0 { return fmt.Errorf("refresh ttl fail, sid=%s", sid) } return nil }- 用
SETEX保证“读-改-写”原子性,Lua 脚本把 TTL 续期做成一行事务,杜绝并发竞争。 - 连接池外层包指数退避重试,最多 3 次,防止 Redis 抖动时雪崩。
3. 异步任务队列:Celery → Kafka 的演进
早期 Celery+RabbitMQ 在 2 k TPS 时还算优雅,但 celery worker 的 ACK 机制在重启场景下容易丢任务。大促前压测直接跪,最终换成 Kafka:
- 分区数 = 3 × 目标 TPS ÷ 1000,保证单分区 1 k 以内。
- 生产端异步刷盘,ack=1,平衡可靠与性能。
- 消费端用 sarama-go,开启
BalanceStrategySticky,重平衡时间从 30 s 降到 5 s。
性能优化:把 5 k TPS 压到 2 ms
1. 负载测试方案
Locust 脚本模拟 30 万并发连接,阶梯式压到 8 k TPS,关键指标:
- CPU 使用率 65 % 时,P99 延迟 18 ms。
- 90 % 响应 < 12 ms,满足 SLA 500 ms 绰绰有余。
- 内存占用在 GOGC=100 时 4.2 G,调到 200 后降到 3.1 G,GC 次数减半,CPU 降 8 %。
2. Go GC 调优实战
import _ "net/http/pprof" func main() { go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }() // 业务代码 }- 镜像里内置 pprof,压测时随时
go tool pprof heap,发现标记阶段占 30 % CPU。 - 调大
GOGC=200,让 GC 触发阈值从 100 % 提到 200 %,延迟降低 12 %,内存换时间。 - 注意:若容器内存上限 4 G,GOGC 别盲目拉满,需留 25 % headroom 防止 OOM Kill。
避坑指南:多轮对话与敏感词
1. 上下文丢失 3 种修复方案
- 槽位快照:每轮把 Redis Hash 全量序列化后
HSET到slot:{sid},后端重启可恢复。 - 消息幂等:前端生成 uuid,重复请求直接返回缓存结果,防止用户狂点导致状态漂移。
- 版本号机制:给会话加 ver 字段,后端只接受 ver+1,拒绝乱序,解决异步通道回包顺序错乱。
2. 敏感词 DFA 实现注意
# dfa.py 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, n = [], 0, len(text) while i < n: ch, j = text[i], i node = self.root while j < n and ch in node: if node.get('end'): res.append('*' * (j - i + 1)) i = j + 1 break j += 1 ch = node.get(text[j], None) else: res.append(text[i]) i += 1 return ''.join(res)- 敏感词库 1.2 万条,初始化放内存,占 3 M,QPS 5 k 时 CPU 0.3 核。
- 一定用 Unicode 码点遍历,防止 emoji 截断误判。
- 热更新:监听配置中心变更,双缓冲切换,reload 时无锁,新老词库 1 s 内完成替换。
代码规范:错误处理与性能注释
// redis.go func GetPool() *redis.Pool { return &redis.Pool{ MaxIdle: 50, MaxActive: 1000, Dial: func() (redis.Conn, error) { // 最多重试 3 次,指数退避 var c redis.Conn var err error for i := 0; i < 3; i++ { c, err = redis.Dial("tcp", "redis:6379") if err == nil { return c, nil } time.Sleep(time.Duration(1<<i) * time.Second) } return nil, fmt.Errorf("redis unreachable after 3 retries: %w", err) }, } }- 所有 IO 出错都要带
fmt.Errorf("...: %w", err),方便errors.Is统一判责。 - 关键路径加
// PERF: xxx注释,提醒后人别乱改;如// PERF: goroutine leak check here,review 时一眼看到。
延伸思考:冷启动的小样本学习
平台上线新行业时,标注数据往往 < 200 条,传统 fine-tune 会严重过拟合。我们试了两种思路:
- Prompt-based 抽取:用中文 GPT-3 做意图生成,再人工快速审核,把 200 条扩到 2 k,F1 提升 11 %。
- 元学习 + 原型网络:在 20 个旧行业元训练,新行业 50 条样本就能达到 85 % 准确率,训练时间 3 分钟。
但小样本的 bad case 不可控,线上一旦漂移,用户体感“答非所问”。目前做法是“小模型灰度 + 实时置信度熔断”,置信度 < 0.8 自动转人工,后续再回流标注。冷启动这条路,欢迎一起聊聊你们的骚操作。
整套平台上线半年,目前稳定扛 6 k TPS,大促零事故。代码还在持续迭代,如果你也在踩智能客服的坑,欢迎留言交换经验,一起把机器人调教得更像人。