背景痛点:传统客服系统的三座大山
去年“618”大促,我们团队的老客服系统被流量冲垮,客服小姐姐们一边接电话一边吐槽“系统卡成PPT”。复盘后发现问题集中在三点:
长尾意图识别不准
用户问“我买的面膜过敏能退吗”,系统只匹配到“退货”关键词,却忽略“过敏”隐含投诉意图,导致答非所问,满意度掉到62%。会话状态维护困难
同一个用户先在小程序线,又切到App,对话历史丢失,只能重复描述问题,体验极差。多租户隔离问题
SaaS 模式下给 A、B 两家客户共用集群,A 家搞活动突增 5 倍流量,B 家跟着卡顿,被投诉“殃及池鱼”。
痛定思痛,我们决定用“扣子空间”重构一套高可用智能客服,目标只有一个:99.9% 请求 500 ms 内返回,让客服小姐姐再也不用背锅。
技术选型:中文场景下谁更懂“人话”
我们拉了三款主流方案在同样 10 万条真实语料上做对比,结果如下:
| 维度 | Rasa 2.8 | Dialogflow ES | 扣子空间 |
|---|---|---|---|
| 中文意图准确率 | 87% | 84% | 93% |
| 单实例 QPS | 180 | 220 | 350 |
| 部署复杂度 | 高(需 GPU+PyTorch) | 中(Google 云绑定) | 低(Jar 包直接启动) |
| 按量计费/月 | 1.2 万 | 1.5 万 | 0.8 万 |
结论:扣子空间在中文准确率、成本、部署友好度上全面胜出,于是拍板“就它了”。
核心实现:SpringBoot + 扣子空间 SDK 骨架
1. 项目结构
coobot-service ├─ controller // 入口,接收微信、App、小程序事件 ├─ intent // 意图识别,调用扣子空间 ├─ session // 会话状态,Redis 实现 ├─ async // 异步消息,带退避 └─ multi-tenant // 租户隔离2. 入口控制器(精简异常+日志)
@RestController @RequestMapping("/bot") @RequiredArgsConstructor public class BotController { private final IntentService intentService; private final SessionService sessionService; @PostMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Reply> chat(@Valid @RequestBody ChatRequest req) { try { String tenant = TenantContext.get(); // 线程变量取租户 Long userId = req.getUserId(); String query = req.getQuery(); // 1. 读会话 Session session = sessionService.get(tenant, userId); // 2. 意图识别 Intent intent = intentService.recognize(tenant, query, session); // 3. 回写会话 session.appendTurn(query, intent); sessionService.save(tenant, userId, session); return ResponseEntity.ok(new Reply(intent.getAnswer())); } catch (Exception e) { log.error("chat error, tenant={}, user={}", TenantContext.get(), req.getUserId(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new Reply("系统开小差,已转人工客服,稍等~")); } } }3. 异步消息退避策略(Java 示例)
大促峰值时,如果同步调用扣子空间超时,会拖死线程池。我们用 Spring 的@Async+ 退避重试:
@Service public class IntentService { private final KouziClient kouziClient; @Async("botExecutor") // 线程池隔离 @Retryable(value = {RpcException.class}, maxAttempts = 3, backoff = @Backoff(delay = 200, multiplier = 2)) public CompletableFuture<Intent> recognize(String tenant, String query, Session session) { // 退避参数:首次 200ms,第二次 400ms,第三次 800ms KouziReq req = KouziReq.builder() .tenantKey(tenant) .query(cleanQuery(query)) // 先清洗 .context(session.getLast3Turns()) .build(); KouziResp resp = kouziClient.infer(req); return CompletableFuture.completedFuture( Intent.from(resp)); } /** * query 清洗:去表情、全角转半角、敏感词替换 */ private String cleanQuery(String raw) { return raw.replaceAll("\\[\\w+\\]", "") // 去微信表情 .replaceAll(",", ",") // 全角逗号 .trim(); } }线程池配置:
botExecutor: core-pool-size: 32 max-pool-size: 128 queue-capacity: 2000 keep-alive-seconds: 604. Redis 跨渠道会话状态
会话结构用 Hash 存储,field =channel + ":" + userId,value = 压缩后的 JSON,TTL 24 h。
@Service public class SessionService { private final RedisTemplate<String, String> redis; private static final int SESSION_TTL = 86400; public Session get(String tenant, Long userId) { String key = buildKey(tenant, userId); String val = redis.opsForValue().get(key); if (val == null) { return new Session(); // 新建 } return JsonUtil.fromZip(val, Session.class); } public void save(String tenant, Long userId, Session session) { String key = buildKey(tenant, userId); redis.opsForValue().set(key, JsonUtil.toZip(session), SESSION_TTL, TimeUnit.SECONDS); } private String buildKey(String tenant, Long userId) { return "bot:session:" + tenant + ":" + userId; } }这样用户先在小程序问“怎么退货”,再到 App 输入“面膜过敏”,都能拿到同一 Session,意图上下文不丢。
性能优化:压测报告与冷启动
1. 压测环境
- 4C8G 容器 * 3
- 扣子空间模型服务 2 副本
- 并发梯度:200/500/1000/1500 QPS
| 并发 | TP99(ms) | 错误率 | CPU 使用率 |
|---|---|---|---|
| 200 | 180 | 0% | 35% |
| 500 | 260 | 0% | 55% |
| 1000 | 380 | 0.1% | 78% |
| 1500 | 510 | 0.3% | 90% |
目标 500 ms 内 TP99 在 1000 QPS 仍有余量,满足大促峰值。
2. 冷启动优化
高频意图(退货、查物流、优惠券)提前加载到本地缓存,应用启动时异步预热:
@EventListener(ApplicationReadyEvent.class) public void preload() flushCache() { List<String> hotIntents = List.of("return", "logistics", "coupon"); hotIntents.parallelStream() .forEach(i -> kouziClient.preload(tenant, i)); }实测冷启动后首请求从 1.2 s 降到 180 ms,客服小姐姐再也不用“等 3 秒”。
避坑指南:NLP 误判与多租户隔离
1. query 清洗策略
- 去掉渠道特征符号,如微信“[微笑]”、钉钉“@xxx”。
- 同义词归一,“快递/物流/邮寄”统一映射到“logistics”。
- 数字归一化,“2号”→“二号”,避免模型把“2”当英文。
清洗完再送扣子空间,误判率从 7% 降到 3%。
2. 多租户资源隔离
- 线程池隔离:每个租户独立线程池,核心数 = 租户购买套餐档位。
- 令牌桶限流:Redis + Lua 脚本,按租户 key 做漏斗,超量直接返回“客服忙,请稍等”。
- 模型热更新灰度:新意图模型先给 10% 流量租户试用,无异常再全量。
上线后 A 家搞直播秒杀,B 家业务毫无感知,终于不再“背锅”。
代码规范小结
- 所有示例均带 try-catch,异常落盘 + 返回友好提示。
- 关键性能参数(TTL、退避 delay、线程池大小)全部提取到 yml,并在字段上加注释。
- 统一使用阿里 p3c Checkstyle 模板,CI 阶段强制扫描,0 警告才能合并。
互动环节
当用户意图同时包含“退货”和“投诉”时,你会如何设计优先级仲裁机制?欢迎评论区聊聊你的思路!