背景痛点传统客服系统“三座大山”去年双十一我们老客服系统直接“罢工”——高峰期 3k 并发CPU 飙到 95%用户平均等待 18s 才收到“人工客服请排队”。复盘发现三大硬伤单体服务里“查询-意图-回复”全挤在一个线程池排队效应指数级放大。对话状态放在 JVM 内存重启即丢用户重连后得把“我要退货”再说一遍。关键词正则匹配意图新活动上线一次就要发版准确率 68%客诉率却 20%。痛定思痛老板拍板三个月内重构一套“高并发、不丢话、懂人话”的智能客服。于是有了这篇踩坑笔记。架构设计为什么不是“一把梭哈”单体先画个对比表维度单体微服务扩容粒度整包扩容浪费按需扩“对话服务”或“NLU 服务”发布影响改一句正则全站重启只热更“意图服务”语言混搭全 JavaPython 做模型Java 做事务各取所长故障半径一挂全挂超时降级、快速熔断技术选型Spring Cloud团队最熟生态全Gateway 自带熔断。RabbitMQ可靠队列延迟消息天然支持“超时重试”。Redis轻量级 KV1ms 延迟对话上下文 TTL 自动过期省掉自己写清理线程。系统总览Mermaidgraph TD A[客户端/Web] --|WS| B(Gateway) B -- C[对话服务br/Spring Boot] C --|发布事件| D[(RabbitMQ)] D --|消费| E[意图服务br/Python/BERT] E --|回包| D D -- C C -- F[(Redisbr/对话状态)] C -- G[订单/商品服务br/Feign]核心实现一BERT 意图分类Python需求支持 32 个业务意图150ms 返回准确率≥90%。模型选型BERT-base-Chinese → 蒸馏 微调 3epoch量化 int8推理 90ms→40ms。代码片段含异常兜底# intent_service.py import torch, json, os, logging from transformers import BertTokenizer, BertForSequenceClassification from starlette.applications import Starlette from starlette.responses import JSONResponse import uvicorn MODEL_PATH /model/bert-intent ID2LABEL {0: 退货, 1: 查物流, 2: 修改地址, 31: 人工} try: tokenizer BertTokenizer.from_pretrained(MODEL_PATH) model BertForSequenceClassification.from_pretrained(MODEL_PATH) model.eval() except Exception as e: logging.error(模型加载失败, exc_infoTrue) raise RuntimeError(NLU 无法启动) from e async def predict(sentence: str): try: inputs tokenizer(sentence, return_tensorspt, truncationTrue, max_length64) with torch.no_grad(): logits model(**inputs).logits probs torch.nn.functional.softmax(logits, dim-1) idx int(torch.argmax(probs)) confidence float(probs[0][idx]) return {intent: ID2LABEL.get(idx, 未知), confidence: confidence} except Exception as e: logging.exception(predict error) # 降级返回兜底意图 return {intent: 人工, confidence: 0.0} app Starlette(debugFalse) app.route(/intent, methods[POST]) async def intent_endpoint(request): data await request.json() result await predict(data.get(q, )) return JSONResponse(result) if __name__ __main__: uvicorn.run(app, host0.0.0.0, port7001)部署小贴士Gunicorn 1worker*4thread 足以抗 1k QPS显存只占 1.2G。核心实现二状态机多轮对话Java需求用户说“我要退货”→校验订单→选择退货原因→提交全程 5min 内有效支持超时重试。技术方案Spring StateMachine Redis 持久化 RabbitMQ 延迟队列DLX做“闹钟”。关键代码精简可运行Configuration EnableStateMachine(name csStateMachine) public class CSStateMachineConfig extends StateMachineConfigurerAdapterString, String { public static final String STATE_INIT INIT; public static final String STATE_AWAIT_ORDER AWAIT_ORDER; public static final String STATE_AWAIT_REASON AWAIT_REASON; public static final String EVENT_REASON_OK REASON_OK; Override public void configure(StateMachineStateConfigurerString, String states) throws Exception { states.withStates() .initial(STATE_INIT) .states(Set.of(STATE_AWAIT_ORDER, STATE_AWAIT_REASON, CONFIRM)); } Override public void configure(StateMachineTransitionConfigurerString, String transitions) throws Exception { transitions.withExternal().source(STATE_INIT).target(STATE_AWAIT_ORDER).event(ASK_ORDER) .and() .withExternal().source(STATE_AWAIT_ORDER).target(STATE_AWAIT_REASON).event(ORDER_OK) .and() .withExternal().source(STATE_AWAIT_REASON).target(CONFIRM).event(EEVENT_REASON_OK); } } Service public class DialogueService { Autowired private StateMachineFactoryString,String factory; Autowired private StringRedisTemplate redis; private static final String PREFIX dialog:; private static final int TTL_SEC 300; // 5min // 每次消息入口 public String handle(String userId, String text){ String key PREFIX userId; String stateStr redis.opsForValue().get(key); StateMachineString,String sm; if(stateStrnull){ sm factory.getStateMachine(userId); sm.start(); }else{ sm restore(userId, stateStr); } // 省略调意图服务拿 intent sm.sendEvent(convertIntent2Event(text)); persist(sm, key); return generateReply(sm); } private void persist(StateMachineString,String sm, String key){ // 序列化状态到 JSON String json StateJsonUtil.serialize(sm); redis.opsForValue().set(key, json, TTL_SEC, TimeUnit.SECONDS); } private StateMachineString,String restore(String userId, String json){ StateMachineString,String sm factory.getStateMachine(userId); StateJsonUtil.deserialize(sm, json); return sm; 疏漏点状态机 restore 后旧实例没关会内存泄漏记得 stop()。 }超时重试RabbitMQ 延迟队列 5min 后投递“TIMEOUT”事件状态机捕获后自动清除 Redis key 并提示“会话已过期”。性能优化压测与缓存JMeter 线程组 500Ramp-up 30s循环 20 次测得老系统平均 QPS 210RT 2.3s错误率 18%新系统平均 QPS 830RT 280ms错误率 1%吞吐量提升 ≈ (830-210)/210 ≈ 300%达成目标。Redis 对话缓存 TTL 策略正常流程300s 固定过期用户主动结束/取消立即 del节省内存大促预热把 TTL 调到 600s防止集中重连打爆 DB内存占用峰值 8G约 80w 进行中的对话成本可接受。避坑指南敏感数据 幂等日志脱敏正则匹配手机号、身份证、订单号统一替换为$$1****5678。使用 LogbackMaskingPatternLayout业务代码零侵入。幂等性对话服务对外接口全部带Idempotency-Key网关层做 15min 去重表。用户重试点击只返回第一次结果避免生成重复工单。小坑Spring Cloud 2021 版默认关闭hystrix开启resilience4j后一定记得配timeoutDuration否则 Feign 默认 1min会把整个链路拖垮。生产建议监控与可观测业务指标意图识别准确率、任务完成率、平均轮次通过 Micrometer Prometheus 15s 采集。系统指标QPS、RT、线程池队列长度Grafana 大盘一目了然。模型指标Python 侧暴露/metrics统计推理耗时、GPU 利用率低于 80% 自动扩容 Pod。告警准确率跌 5% 或 RTP99 1s 持续 3min立即飞书 电话。开放问题如何平衡模型精度与推理延迟的关系我们在 INT8 量化后掉点 1.2%但延迟腰斩若用知识蒸馏到 ALBERT 可再提速 30%却掉点 2.8%。你的业务愿意牺牲多少准确率换速度欢迎留言一起探讨。