最近在做一个AI智能客服系统的重构项目之前的老系统在高并发下经常卡顿对话也容易“失忆”用户体验很不好。借着这次机会把整个从架构设计到性能调优的实战过程梳理一下希望能给有类似需求的同学一些参考。1. 背景痛点传统客服系统为什么“力不从心”我们之前的系统说白了就是个“增强版FAQ”。用户问题过来去数据库里做模糊匹配匹配上了就返回预设答案。这套逻辑在初期用户量少的时候还行但随着业务发展问题就全暴露出来了并发处理能力弱用的是同步阻塞的HTTP短连接每个请求都独占一个线程去查库。用户稍微一多线程池瞬间被打满新来的请求只能排队响应时间直线上升。上下文“金鱼记忆”系统根本记不住对话历史。用户问“我的订单怎么样了”系统能回答。但用户接着问“那能退货吗”系统就懵了因为它不知道“那”指的是上一个订单。多轮对话更是奢望。扩展性差所有逻辑都揉在一个单体应用里NLP意图识别模块一升级整个服务都得重启。想加个新渠道比如小程序客服代码改动量巨大。2. 技术选型没有银弹只有合适针对这些问题我们重新评估了技术栈核心思路是异步、无状态、可扩展。框架选型Spring Boot vs Quarkus我们最终选择了Spring Boot。原因很简单团队熟悉生态丰富各种中间件Redis, Kafka, Netty的集成 Starter 非常成熟能快速搭建原型。Quarkus 的启动速度和内存占用确实诱人但考虑到现有团队的技术栈和快速上线的压力Spring Boot 是更稳妥的选择。不过我们在容器镜像构建时用了分层优化和 GraalVM 的探索这是后话。缓存方案Redis vs Memcached高频问答对、用户对话上下文、限流计数器都需要缓存。我们选了Redis。除了基础的 KV我们还需要 List 来管理消息队列做异步处理需要 Sorted Set 来做对话超时管理需要 Pub/Sub 来做分布式节点间的通知。Memcached 功能相对单一在数据结构支持上不如 Redis 全面。NLP引擎规则匹配 vs 深度学习模型这是一个混合策略。对于明确、固定的业务点如“查余额”、“联系人工”我们保留了高效的规则匹配Aho-Corasick算法毫秒级响应。对于复杂的、开放的语义理解如“我刚买的手机屏幕碎了怎么办”我们接入了深度学习模型基于BERT微调。模型部署我们用了 TensorFlow Serving通过 gRPC 调用与业务服务解耦。3. 核心实现构建高并发的对话引擎3.1 通信层用Netty扛住海量连接直接用 Spring Boot 内置的 Tomcat 做 WebSocket 支持在连接数上万时性能下降明显。我们引入了Netty来独立部署 WebSocket 服务。核心是设计一个轻量级的协议在 WebSocket 帧之上定义自己的消息头包含消息ID、类型如心跳、用户消息、系统通知、序列化方式等。Netty 的ChannelGroup用来管理所有连接但要注意广播消息时的性能问题。3.2 对话管理状态机让流程清晰可控多轮对话的核心是状态管理。我们为每个对话会话Session设计了一个状态机State Machine。比如一个“退货申请”流程状态可能包括WAIT_FOR_ORDER_ID-WAIT_FOR_REASON-WAIT_FOR_CONFIRM-COMPLETED。 每个状态都绑定一个处理器Processor负责处理用户在当前状态下的输入并决定下一个状态是什么。状态转移图以简化的退货流程为例[用户发起退货] | v [状态: 询问订单号] --(用户提供订单号)-- [状态: 询问退货原因] | | | v | [状态: 确认信息] --(用户确认)-- [状态: 完成] | | | --(用户取消)-- [状态: 取消] | --(超时或未知输入)-- [状态: 结束/转人工]状态和上下文我们序列化成 JSON 后存入 RedisKey 为session:{sessionId}并设置过期时间如30分钟。这样业务服务本身是无状态的可以任意水平扩展。3.3 异步流水线别让慢查询拖垮系统用户消息处理的链路很长协议解析 - 风控检查 - NLP意图识别 - 业务逻辑处理 - 回复生成 - 推送。如果全部同步执行最慢的环节通常是NLP模型调用会阻塞整个链路。我们设计了异步处理流水线核心是Disruptor或Spring Reactor的队列。每个环节都是一个Processor通过事件驱动串联起来。这里的关键是背压Backpressure控制防止下游处理不过来导致内存溢出。一个简化的背压控制示例使用 Reactor// 定义一个处理阶段限制并发数设置缓冲队列大小 Sinks.ManyMessageEvent sink Sinks.many().unicast().onBackpressureBuffer(1000); FluxMessageEvent flux sink.asFlux(); flux .onBackpressureDrop(event - { // 背压时丢弃策略记录日志并返回友好提示 log.warn(System busy, dropped event: {}, event.getId()); sendBusyResponse(event.getSessionId()); }) .parallel(4) // 并行度根据CPU核心数调整 .runOn(Schedulers.boundedElastic()) // 指定调度器隔离阻塞操作 .flatMap(this::processEvent) // 实际处理函数 .sequential() .subscribe();4. 代码示例配置与容错4.1 关键Spring Bean配置我们把对话状态管理器、NLP客户端等都配置成Bean方便管理和注入。Configuration public class AIConfiguration { Bean ConditionalOnMissingBean public RedisTemplateString, DialogSession dialogSessionRedisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplateString, DialogSession template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // 使用Jackson2JsonRedisSerializer避免Java原生序列化的兼容性问题 Jackson2JsonRedisSerializerDialogSession serializer new Jackson2JsonRedisSerializer(DialogSession.class); ObjectMapper om new ObjectMapper(); om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); om.registerModule(new JavaTimeModule()); serializer.setObjectMapper(om); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); return template; } Bean public NlpServiceClient nlpServiceClient(Value(${nlp.service.url}) String serviceUrl) { // 使用带连接池、超时、重试的HTTP客户端 OkHttpClient okHttpClient new OkHttpClient.Builder() .connectTimeout(Duration.ofSeconds(2)) .readTimeout(Duration.ofSeconds(5)) .writeTimeout(Duration.ofSeconds(3)) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) .build(); return new HttpNlpServiceClient(serviceUrl, okHttpClient); } }4.2 对话超时与重试机制网络调用失败是常态必须有重试但也要防止无限重试。Service public class DialogEngine { private final NlpServiceClient nlpClient; private final RetryTemplate retryTemplate; public DialogEngine(NlpServiceClient nlpClient) { this.nlpClient nlpClient; // 配置重试模板 this.retryTemplate RetryTemplate.builder() .maxAttempts(3) // 最多重试3次含首次 .exponentialBackoff(100, 2, 1000) // 初始间隔100ms倍数增长最大间隔1s .retryOn(RemoteAccessException.class) // 只对网络异常重试 .notRetryOn(NlpServiceException.class) // 业务逻辑错误不重试 .withListener(new RetryListener() { Override public T, E extends Throwable void onError(RetryContext context, RetryCallbackT, E callback, Throwable throwable) { log.warn(NLP service call failed, retry count: {}, context.getRetryCount(), throwable); } }) .build(); } public Intent recognizeIntent(String userInput, String sessionId) throws ServiceException { try { // 使用重试模板包裹可能失败的调用 return retryTemplate.execute(context - { // 这里可能会抛出RemoteAccessException return nlpClient.recognize(userInput, sessionId); }); } catch (RetryException e) { // 重试耗尽后抛出业务异常触发降级逻辑 throw new ServiceException(NLP service unavailable after retries, e); } catch (NlpServiceException e) { // 业务异常直接抛出 throw e; } } }5. 性能优化从压测数据到JVM调优系统搭起来后我们用 JMeter 做了全面的压测。关键指标吞吐量TPS从最初的 200 req/s 优化到了 1200 req/s。平均响应时间ART从 800ms 降低到 150ms 以内P95。错误率在 4 倍于日常峰值的压力下错误率低于 0.1%。GC调优 压测时发现 Full GC 比较频繁。我们用的是 G1GC调整了以下参数基于8核16G容器环境-XX:UseG1GC -Xms8g -Xmx8g // 堆内存固定避免动态调整开销 -XX:MaxGCPauseMillis200 // 目标暂停时间 -XX:InitiatingHeapOccupancyPercent35 // 更早启动并发标记 -XX:ConcGCThreads4 // 并发GC线程数 -XX:ParallelGCThreads8 // 并行GC线程数调整后Young GC 频率略有增加但每次时间很短Full GC 基本消失。线程池计算 对于处理计算密集型任务如规则匹配的线程池大小不宜超过 CPU 核心数。对于 I/O 密集型任务如调用外部 NLP 服务可以设置大一些。 我们参考了线程数 CPU核心数 * (1 平均等待时间 / 平均计算时间)这个公式。例如调用 NLP 服务平均耗时 50ms其中等待网络响应约 48ms计算时间约 2ms那么对于 8 核机器理论线程数约为8 * (1 48/2) 200。但实际我们会设置一个上限如 100并通过队列和拒绝策略来控制。6. 避坑指南那些我们踩过的“坑”对话上下文序列化陷阱 最初我们用 Java 自带的ObjectOutputStream序列化DialogSession对象到 Redis。结果一升级服务增加了一个字段老数据就反序列化失败了。务必使用跨语言、向前向后兼容的序列化方式如 JSONJackson/Protobuf。我们换成了 Jackson并忽略未知字段FAIL_ON_UNKNOWN_PROPERTIES false。NLP模型冷启动延迟 深度学习模型服务刚启动时第一次推理特别慢可能好几秒。解决办法是预热Warm-up。在服务启动后或定时任务中用一些典型的 query 去循环调用自己的服务接口直到响应时间稳定到正常水平。分布式会话一致性 用户请求可能被负载均衡到不同实例。如果会话状态只在本地内存就乱套了。我们的方案是将会话状态集中存储Redis。但这里又有个细节多个实例可能同时修改同一个会话。我们使用 Redis 的WATCH事务或者直接用Redisson的分布式锁来保证短时间内的操作原子性避免状态覆盖。7. 延伸思考如何优雅降级第三方 NLP 服务不可能 100% 可靠。当它挂掉时智能客服不能跟着全挂。我们的降级策略是分层的快速失败与本地兜底重试机制如前面代码所示快速耗尽后立即触发降级。降级到本地的规则引擎和高频问答缓存。虽然智能性下降但核心业务流程如查订单、转人工依然可用。降级开关与流量染色在配置中心如 Nacos设置一个全局降级开关。一旦打开所有新请求直接走本地兜底逻辑。同时可以对部分用户如内部测试用户的请求进行“染色”即使降级开关打开也继续走 NLP 服务用于验证服务恢复情况。异步修复与补偿在降级期间可以将用户的问题异步存储到消息队列如 Kafka。待 NLP 服务恢复后再消费这些消息进行意图识别并通过其他渠道如站内信、短信将更准确的答案补充推送给用户。写在最后这次重构让我深刻体会到一个高可用的 AI 智能客服系统“智能”只是亮点而“稳定”和“高效”才是基石。技术选型要务实架构设计要面向失败每一行代码都要考虑异常和边界情况。从同步到异步从有状态到无状态从单体到分布式每一步都是对系统韧性的提升。目前系统已经平稳运行了几个月扛住了几次促销活动的流量冲击。下一步我们计划在对话质量评估和持续学习闭环上做更多探索比如根据用户对回答的“点赞/点踩”行为自动优化知识库和模型。路还很长但方向越来越清晰了。