背景痛点:促销高峰期的“双重暴击”
去年双十一,我们团队的电商 chatbot 在 0 点前 5 分钟迎来流量洪峰,QPS 从 2 k 瞬间飙到 8 k,结果出现两个“名场面”:
- 意图识别平均耗时从 120 ms 涨到 600 ms,直接把整体链路 TP99 拖到 2.3 s,用户疯狂点“人工客服”。
- 推荐接口 RT 抖动,导致“猜你喜欢”商品卡片返回超时,转化率比平时掉了 18%。
事后复盘,根因集中在三点:
- 规则引擎在高并发下线性匹配,CPU 占用飙升,意图识别准确率从 94% 跌到 78%。
- 对话状态与商品推荐耦合在同一个同步线程,任何一方慢就等于全局慢。
- 库存扣减逻辑放在对话服务里,分布式锁竞争激烈,线程阻塞把 GC 压力也带崩。
一句话:旧架构把“对话、推荐、交易”三件事绑在一起,促销期流量一冲,全线雪崩。
技术对比:规则引擎 vs 轻量 BERT
我们先用 20 W 条历史语料做基准测试,硬件 4C8G,对比结果如下:
| 方案 | 平均 QPS | 平均 RT | 准确率 | CPU 峰值 |
|---|---|---|---|---|
| 规则引擎(正则+关键词) | 1 800 | 120 ms | 78 % | 85 % |
| BERT-mini + ONNX | 5 200 | 28 ms | 93 % | 45 % |
规则引擎在并发高时 RT 线性增长,而 BERT-mini(4 层 encoder,参数量 16 M)经 ONNX Runtime 加速后,QPS 提升 3×,准确率反而提高 15 个百分点,直接决定我们换赛道。
核心实现:事件驱动把“聊、推、买”拆成三条线
1. 总体架构
┌──────────┐ Kafka topic-partition ┌──────────────┐ │ 网关服务 │──event──► dialog-core.0~N ──►│ 对话状态机 │ └──────────┘ └─────┬────────┘ │ │ ▼ 推荐事件 ▼ ┌──────────┐ Kafka ┌─────────────────┐ ┌────────────┐ │ 推荐服务 │◄────────►│ 商品缓存(Redis) │ │ 订单服务 │ └──────────┘ └─────────────────┘ └────────────┘核心思想:把每一次用户发言封装成DialogEvent,用 Spring Cloud Stream 按userId%partition做分区,保证同一用户顺序消费,横向扩展无锁。
2. Spring Cloud Stream 分区配置
spring.cloud.stream.bindings.dialog-in: destination: dialog-core group: chatbot-consumer consumer.concurrency: 8 partitioned: true生产端指定 partitionKeyExpression:
Message<DialogEvent> msg = MessageBuilder .withPayload(event) .setHeader("partitionKey", userId) .build();3. 轻量化 BERT 部署
- 训练:用 4 层 BERT + BiLSTM 输出 32 类意图,蒸馏后 16 M。
- 转换:PyTorch → ONNX,开启动态轴 batch。
- 推理:ONNX Runtime 1.16,开 CPU 指令集 AVX512,线程数 = 物理核数 - 1。
- 预热:容器启动时加载模型,随机输入 warmup 100 次,避免首请求冷启动。
4. 商品推荐热数据缓存
- 缓存键:
rec:{categoryId}:{hour},小时级切片,TTL 900 s。 - 更新策略:CDC 监听 MySQL binlog,变化即推,本地 Caffeine 做二级缓存,命中率 96%。
- 降级:缓存穿透 → 返回“热销榜”默认 20 条,保证不空窗。
代码示例:Kafka 侧三板斧
以下片段均跑在 Spring Cloud Stream 3.2.x,已加详细注释。
1. 消息幂等性处理
@StreamListener("dialog-in") public void handle(DialogEvent event, @Header(name = KafkaHeaders.RECEIVED_PARTITION, required = false) Integer partition, @Header(name = KafkaHeaders.RECEIVED_OFFSET, required = false) Long offset) { String idemKey = "dialog:" + event.getUserId() + ":" + partition + ":" + offset; Boolean absent = redis.set(idemKey, "1", "NX", "EX", 3600); if (Boolean.FALSE.equals(absent)) { log.warn("重复消息丢弃, key={}", idemKey); return; } // 真正业务 }2. 对话状态机(Sprint StateMachine)
enum State { IDLE, AWAIT_CATEGORY, AWAIT_CONFIRM } enum Event { MSG, RECV_CATEGORY, RECV_CONFIRM } @Configuration public class DialogStateConfig extends StateMachineConfigurerAdapter<State, Event> { @Override public void configure(StateMachineTransitionConfigurer<State, Event> t) throws Exception { t.withExternal() .source(IDLE).target(AWAIT_CATEGORY) .event(MSG) .and() .withExternal() .source(AWAIT_CATEGORY).target(AWAIT_CONFIRM) .Event(RECV_CATEGORY); } }3. 超时补偿机制
@Scheduled(fixedDelay = 30_000) public void compensateTimeout() { Set<String> timeoutKeys = redis.keys("dialog:*:expireAt"); long now = Instant.now().toEpochMilli(); timeoutKeys.forEach(k -> { long expire = Long.parseLong(redis.get(k)); if (now > expire) { String userId = k.split(":")[1]; // 发送兜底“还在吗?”模板 template.send("dialog-core", userId, new DialogEvent(userId, "TIMEOUT_MSG")); redis.del(k); } }); }性能优化:压测报告一览
用 JMeter 200 线程循环压 15 min,对比优化前后:
| 指标 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| TP99 | 2 300 ms | 480 ms | ↓79 % |
| 平均 CPU | 78 % | 42 % | ↓46 % |
| 错误率 | 3.6 % | 0.12 % | ↓97 % |
关键优化点:
- 意图识别 RT 从 600 ms 降到 28 ms,直接砍掉最慢一环。
- 推荐缓存命中后 RT 10 ms 以内,整体对话链路稳定在 400 ms 左右。
- 库存扣减拆到独立服务,用 Redis + Lua 脚本保证原子性,分布式锁竞争下降 80 %。
避坑指南:三个隐形炸弹
1. 对话上下文存储的序列化陷阱
最早用 Java 原生序列化,版本一升级就反序列化失败。后来统一改 JSON + Jackson 的@JsonTypeInfo,再配redis.set(json, EX=1800),可平滑升级、可跨语言。
2. 机器学习模型冷启动
容器刚启动首请求会触发模型加载,RT 飙到 3 s。解决方式:
- Dockerfile 里加
RUN python warmup.py把模型提前下好。 - 启动脚本里用
curl localhost:8080/warmup调一次,Kubernetes readiness 探针等返回 200 再注册服务。
3. 分布式锁在库存扣减中的正确使用
错误姿势:setnx + expire 分两步,极端情况 master 宕机没来得及 expire,锁永驻。
正确姿势:Redis 2.6+ 用一条 Lua 脚本保证原子性,或者直接用 Redisson 的RLock.tryLock(100, 10, TimeUnit.SECONDS),看门狗自动续期,代码简洁又安全。
延伸思考:把意图识别搬到端侧?
目前模型 16 M、ONNX Runtime 压缩后 6 M,ARM 端侧推理框架已能跑到 30 ms。如果能把意图识别下沉到 App 端:
- 好处:省一次后端 RPC,离线也能识别,用户弱网环境体验提升。
- 挑战:模型更新热下发、端侧兼容、隐私合规。
下一版我们准备用 TensorFlow Lite 做 A/B,对比后端与端侧在准确率、耗电、包体积上的综合收益,欢迎一起蹲结果。
写在最后:动手把 AI 装进“耳朵+嘴巴”
把对话、推荐、交易拆成事件流后,系统吞吐量直接翻 3 倍,转化率提升 45 %,双十一再也不是“救火现场”。如果你也想亲手搭一套实时、低延迟、可扩展的语音交互系统,可以看看这个动手实验——从0打造个人豆包实时通话AI。我跟着教程把 ASR、LLM、TTS 串成一条完整链路,本地跑通只花了两个晚上,连前端 Web 页面都打包好了,小白也能顺利体验。祝你玩得开心,早日让 AI 开口说话!