限时福利领取


背景痛点:传统客服系统到底卡在哪?

去年“618”大促,公司老客服系统直接“罢工”——高峰期 3k 并发就把单体式应用打挂,重启后用户会话全丢,客服小姐姐只能手动让用户再描述一遍问题,场面一度尴尬。复盘发现两大硬伤:

  1. 并发模型老旧:Tomcat 8 默认 200 工作线程,阻塞 I/O 一打满就排队,CPU 空转却吞吐上不去。
  2. 意图识别靠“关键字”:用户一句“我付不了款”被拆成“付/不了/款”,结果命中“退款”流程,答非所问,体验翻车。

痛定思痛,我们决定用 Java 技术栈重写一套“能扛大流量、还能听懂人话”的智能客服系统,并把 AI 能力作为“一等公民”嵌入开发流程,而不是事后打补丁。


技术选型:Spring Cloud 胜出的三个理由

社区主流 Java 微服务框架基本就是 Spring Boot 与 Quarkus 二选一。团队先做了 48h 的 Spike(快速原型),结论如下:

维度 Spring Boot 3.x Quarkus 3.x
冷启动 ~2.5 s ~0.9 s
内存占用(空载) ~180 MB ~90 MB
生态成熟度 全家桶(Spring Cloud、Data、Security)文档&问答海量 部分组件还在 1.0 边缘
开发者熟悉度 10 人团队 9 人用过 仅 2 人写过 Demo
GraalVM 原生镜像支持 支持,但配置复杂 官方一键搞定

虽然 Quarkus 在“启动速度 + 内存”上很香,但智能客服更关注高峰期水平扩容而不是每天重启。Spring Cloud 2023.x 配套的 spring-cloud-kubernetes 能直接对接 K8s Service 发现,Sidecar 方案也成熟,最终拍板:Spring Cloud 2023.x + JDK 17 + WebFlux 作为底座,让 AI 模块以独立微服务形式插拔。


核心实现:让 AI 真正“长”在系统里

1. 实时通信:WebSocket 双工通道

客服场景既要用户说,也要机器人秒回,HTTP/2 推送都不够直接。我们采用 spring-boot-starter-websocket + Reactive 模式,一条 TCP 通道全双工,省去重复 TLS 握手。

关键配置(节选):

@Configuration
@EnableWebSocketMessageBroker
public class WsConfig implements StompEndpointRegistryConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat")          // 用户长连接入口
                .setAllowedOriginPatterns("*")
                .withSockJS();                 // 降级选项
    }
}

上线后单 Pod 8C16G 可稳定保持 4w 并发 WebSocket,连接回收间隔 45s,内存无明显锯齿。

2. NLP 意图识别:把“模型”当服务

AI 模块独立成一个 intent-service,对外暴露 grpc 接口,内部跑 BERT+FC 做 21 分类(订单、物流、退款、营销…)。训练平台用 Python,推理用 ONNX Runtime Java API,一次 warm-up 后 QPS 稳定在 1.2k,P99 延迟 28 ms。

调用侧代码(屏蔽业务异常):

@Autowired
private IntentGrpc.IntentBlockingStub intentStub;

public Mono<IntentReply> predict(String text) {
    return Mono.fromCallable(() ->
            intentStub.predict(IntentRequest.newBuilder().setText(text).build())
    ).subscribeOn(Schedulers.boundedElastic()); // 避免阻塞 Netty 事件线程
}

3. 会话状态管理:Redis + Protobuf 压缩

会话状态既要高并发读写,又要支持断线重连后恢复。我们选型 Redis Hash 存储,Key 设计 chat:{userId},TTL 30 min,字段包括:

  • intent:当前意图
  • node:对话状态机节点
  • vars:上下文变量(Map<String,String>)

为了减少 30% 网络流量,把 vars 用 Protobuf 序列化后再做 LZ4 压缩,100 个字段典型会话从 8 KB → 1.9 KB。


代码示例:拿来即用的三个片段

1. 消息路由控制器(REST 风格,兼容 WebSocket)

@RestController
@RequestMapping("/api/chat")
public class ChatController {
    private final SimpMessagingTemplate template; // 注入 WebSocket 模板
    private final ChatService chatService;

    @PostMapping("/send")
    public Mono<Void> send(@RequestBody ChatRequest req) {
        return chatService.reply(req.getUserId(), req.getText())
                         .doOnNext(reply -> template.convertAndSendToUser(
                                 req.getUserId(), "/queue/reply", reply));
    }
}

2. 自定义限流注解(基于 Redis 令牌桶)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int qps() default 10;
}

@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate redis;

    @Around("@annotation(limit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit limit) throws Throwable {
        String key = "rate:" + pjp.getSignature().toShortString();
        long refill = 1000 / limit.qps();
        Boolean ok = redis.opsForValue()
                          .setIfAbsent(key, "1", Duration.ofMillis(refill));
        if (Boolean.TRUE.equals(ok)) {
            return pjp.proceed();
        }
        throw new RateLimitException("Too many requests");
    }
}

3. 对话状态机(Spring StateMachine 简化版)

@Component
public class DialogStateMachine {
    enum State { START, COLLECT_INFO, HANDOVER, END }
    enum Event { INTENT_OK, INTENT_UNKNOWN, AGENT_JOIN }

    private final StateMachine<State, Event> sm;

    public DialogStateMachine() {
        Builder<State, Event> builder = StateMachineBuilder.builder();
        builder.externalTransition()
               .source(State.START).target(State.COLLECT_INFO)
               .event(Event.INTENT_OK);
        builder.externalTransition()
               .source(State.START).target(State.HANDOVER)
               .event(Event.INTENT_UNKNOWN);
        sm = builder.newStateMachine(State.START);
        sm.start();
    }

    public State sendEvent(Event e) {
        sm.sendEvent(e);
        return sm.getState().getId();
    }
}

性能优化:把“小水管”换成“四车道”

1. 线程池配置建议

  • Netty WebSocket IO 线程数 = CPU 核心数(8)
  • 业务线程池(boundedElastic)= 核心数 * 2,队列 8k,拒绝策略 callerRuns,防止用户端雪崩
  • AI 推理线程单独一个 fixed 池,大小 4,避免抢占业务线程

2. 对话上下文压缩算法

上文提到的 Protobuf+LZ4 已够用,但实测发现重复 Key 很多(商品名、地址)。于是再加一步 字典编码:把出现频率 Top 200 的字符串预编为 2 Byte 索引,整体压缩率再降 18%,CPU 消耗只增 2%,值得。


避坑指南:上线不踩雷的 checklist

1. WebSocket 连接数监控

Tomcat 默认最大连接 8k,K8s 滚动发布时旧 Pod 先被摘掉,但连接还在,新 Pod 瞬间 1w+ 连接直接 Too many open files。记得调大 ulimit nofile=65535,并在 actuator 暴露自定义指标:

@ReadOperation
public Map<String, Object> wsMetrics() {
    return Map.of("active", wsHolder.getActiveCount(),
                  "max", wsHolder.getMaxCount());
}
`

配合 Prometheus + Grafana,告警阈值 80% 即扩容。

2. 机器学习模型冷启动

intent-service 第一次加载 BERT 模型要 4 s,期间调用方超时熔断,直接 502。解决思路:

  1. 容器启动时加 postStart 钩子,先跑一条预热样本,把模型占满 GPU
  2. 提供 /ready 探针,模型未 warm-up 完成前不注册到 K8s Service
  3. 调用方开启 retry+exponential back-off,最大 3 次,总耗时 < 1 s 用户无感

总结与展望:AI 不是终点,而是持续迭代起点

系统上线三个月,核心指标对比老系统:

  • 峰值并发:3k → 18k
  • 意图识别准确率:68% → 91%
  • 平均响应时延:1.2 s → 320 ms

但最深刻的体会是:AI 模块必须纳入 CI/CD。我们给对话流程加了 Feature Flag,通过 A/B 测试把“新模型 vs 旧模型”灰度到 5% 用户,观察 24h 准确率、满意度评分,再全量。下一步准备把强化学习加进来,让模型自己从客服点击“答案有用/无用”的反馈里持续更新。

如果你也在用 Java 栈做智能客服,希望上面的代码和坑点能帮你少走一些弯路。微服务 + AI 不是银弹,但只要把“高并发”和“持续迭代”同时写进架构第一行注释,就已经赢了一半。

限时福利领取


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐