智能客服业务流程图实战:从设计到高并发优化的全链路解析
把“流程图”真正跑起来,而不是挂在墙上吃灰——一次踩坑后的复盘笔记。
1. 背景:为什么老流程图撑不住高并发?
去年双十一,公司智能客服峰值 QPS 飙到 2 k,老系统直接“罢工”:
- 节点耦合:一个“转人工”按钮逻辑散落在 4 个服务,改一行代码要发版 3 个应用。
- 状态同步延迟:MySQL 行锁扛不住,用户明明已评价,后台仍提示“待评价”,刷新才变。
- 扩展痛苦:新加“发红包补偿”节点,要在 5 张表加字段,上线窗口 3 小时,心惊胆战。
一句话:流程图只解决了“看得见”,没解决“跑得稳”。
2. 技术选型:规则引擎、状态机还是工作流?
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 规则引擎 Drools | 热更新规则 | 学习曲线陡,调试困难 | 规则少,没必要 |
| 状态机 Spring StateMachine | 事件驱动,轻量 | 集群状态同步需自实现 | 半成品 |
| 工作流引擎 Activiti | 可视化强 | 重,节点粒度粗,高并发下锁表 | 过度设计 |
最终拍板:自研轻量级事件驱动状态机——只保留“事件发布 + 状态快照 + 幂等消费”三板斧,Redis 当“总线”,MySQL 仅做归档。
3. 核心实现:把流程图拆成可执行代码
3.1 PlantUML 模板:一张图=一份 yml 配置
用 PlantUML 画流程,再解析成节点 JSON,图与代码同源,再也不怕“图是图,码是码”。
@startuml title 智能客服主流程 [*] --> Consult Consult --> Evaluate : 已解决 Consult --> Transfer : 未解决 Evaluate --> Close : 好评 Evaluate --> Compensate : 差评 Compensate --> Close : 红包发出 Transfer --> Human : 排队成功 Human --> Close : 会话结束 @enduml解析脚本(Python 片段):
# plantuml_parser.py import re, yaml def to_nodes(puml: str): """把 plantuml 转成节点列表,时间复杂度 O(n)""" nodes, edges = set(), [] for line in puml.splitlines(): m = re.search(r'(\w+) --> (\w+) :? ?(.+)?', line) if M: src, dst, evt = M.groups() nodes.update([src, dst]) edges.append(dict(src=src, dst=dst, event=evt or '')) return dict(nodes=list(nodes), edges=edges) if __name__ == '__main__': print(yaml.dump(to_nodes(open('flow.puml').read())))跑完直接得到flow.yml,Spring 启动时灌进状态机,零人工硬编码。
3.2 Redis 分布式锁:保证幂等
以“发红包”节点为例,用户狂点“补偿”按钮也只发一次。
// RedisLock.java public class RedisLock { private static final String LOCK_PREFIX = "cs:lock:"; private static final long TTL_MS = 5_000; @Autowired private StringRedisTemplate redis; /** * 非阻塞获取锁,时间复杂度 O(1) * @return 唯一 token,释放锁需携带 */ public Optional<String> tryLock(String bizId) { String token = UUID.randomUUID().toString(); Boolean ok = redis.opsForValue() .setIfAbsent(LOCK_PREFIX + bizId, token, TTL_MS, TimeUnit.MILLISECONDS); return Optional.ofNullable(ok && ok ? token : null); } public void unlock(String bizId, String token) { // lua 脚本保证原子性 String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; redis.execute(new DefaultRedisScript<>(lua, Long.class), Collections.singletonList(LOCK_PREFIX + bizId), token); } }业务侧调用:
// CompensateService.java public void compensate(Long userId) { String lockId = "compensate:" + userId; Optional<String> token = redisLock.tryLock(lockId); if (!token.isPresent()) { throw new BizException("操作进行中,请勿重复提交"); } try { doSendCoupon(userId); // 真正业务 } finally { redisLock.unlock(lockId, token.get()); } }3.3 节点超时重试 + 异常处理
利用 Redis 的 zset 做延时队列,score=到期时间戳,worker 轮询扫描。
# retry_worker.py import time, json, redis r = redis.Redis() DELAY_QUEUE = 'cs:retry' def push_retry(node, delay_s=60): """节点失败时放入延时队列,O(log n)""" score = time.time() + delay_s r.zadd(DELAY_QUEUE, {json.dumps(node): score}) def poll(): while True: now = time.time() tasks = r.zrangebyscore(DELAY_QUEUE, 0, now, start=0, num=10) for t in tasks: task = json.loads(t) try: call_node(task) # 重试 r.zrem(DELAY_QUEUE, t) # 成功移除 except Exception as e: logger.warning("重试仍失败", exc_info=e) push_retry(task, delay_s=300) # 指数退避 time.sleep(1)4. 性能优化:让 QPS 从 500 → 3000+
- 压测环境:4C8G 容器 * 10,Redis 6.2 集群,MySQL 8.0 主从。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | 520 | 3200 |
| P99 延迟 | 480 ms | 95 ms |
| 内存/请求 | 12 kB | 3 kB |
- 内存瘦身技巧
- 节点上下文只存 diff:快照=基线 + 增量,序列化用 Kryo,比 JSON 省 60% 空间。
- 关闭 Redis 的 KEYSPACE 通知,减少 8% CPU。
- 共用连接池:lettuce 6 共享 io-thread,连接数从 200 降到 20。
5. 避坑指南:生产血泪总结
时钟漂移
集群节点 NTP 不同步,导致 zset 队列“提前”或“延后”消费。解决方案:所有时间戳用 RedisTIME返回的秒表,误差 < 5 ms。死锁 TTL
早期 TTL 设 30 s,GC 抖动导致业务 35 s 才完成,锁被误释放。经验值:业务最大耗时 × 2 + 1 s,目前 5 s 稳如老狗。灰度兼容
流程图加节点后,旧实例无新代码。采用“版本号”字段:- 新节点默认
skipWhenMissing=true,旧实例直接跳过; - 灰度流量 < 5% 时观察无异常,再全量切流。
- 新节点默认
6. 延伸思考:让 LLM 动态改流程?
现在新增节点仍需发版。能否让大模型直接改 yml?思路:
- 线上收集“未解决”会话 → 打标签 → 自动提示“新增补偿节点”。
- 运营在后台点“确认”,LLM 生成 PlantUML 片段 → PR 到 Git → CI 自动压测 → 审批合并。
- 全流程 15 min 内完成,真正做到“对话即需求,需求即流程”。
(已排期 PoC,后续再开坑分享。)
7. 结语
流程图不是壁画,而是可运行、可回滚、可灰度的代码资产。把事件驱动、幂等锁、延时队列这些“小零件”拼好,就能让客服系统在峰值流量面前稳如磐石。希望这套思路能帮你省下几个通宵,也欢迎一起交流 LLM 自动调流程的后续实践——坑还热乎,等你来踩。