全渠道客服系统架构实战|用Golang重构后,我们省下了50%的客服沟通时间
演示网站:gofly.v1kf.com我的微信:llike620
最近在重构公司的客服系统,有些技术思考想和大家聊聊。我们之前用的某SaaS客服工具,随着业务量上来,定制需求越来越多,API限制让人头疼,数据安全团队也三天两头来敲打。一咬牙,决定自己搞一套能独立部署的全渠道一站式方案。
折腾了半年,结果挺惊喜:新系统上线后,客服平均会话处理时间直接砍半。今天不聊业务价值,主要从后端开发角度,说说这套基于Golang的高性能客服系统——唯一客服系统(gofly.dev)——的技术实现和源码层面的设计思考。
为什么选择Golang重构?
老系统是PHP+Node.js混合架构,历史包袱重。渠道一多(微信、网页、APP、邮件),消息同步的延迟问题就冒出来了,客服经常抱怨“客户说了好几句,我这边才弹出来”。
Golang的并发模型在这里优势太明显。我们用goroutine处理各渠道的消息接入,channel做消息路由,一个简单的消息中转服务,单机就能扛住上万并发连接。内存占用只有老系统的1/3左右,部署也从原来的8台服务器缩到了3台。
核心架构:如何做到“全渠道”而不乱?
全渠道最怕什么?数据散落各处,客服要切N个后台。我们的设计核心是一个统一消息总线。所有渠道接入层收到的消息,都转换成内部协议格式,塞进消息总线。客服端通过WebSocket连接中心调度服务,实时收取分配的消息。
go // 简化的消息总线核心代码 type MessageBus struct { channels map[string]chan *Message mu sync.RWMutex }
func (mb *MessageBus) Route(msg *Message) { mb.mu.RLock() ch, ok := mb.channels[msg.ChannelID] mb.mu.RUnlock()
if ok {
select {
case ch <- msg:
// 成功投递
case <-time.After(100 * time.Millisecond):
// 超时处理,写入持久化队列
enqueueToRedis(msg)
}
}
}
这个设计让新增渠道变得很简单:只需实现对应的接入层适配器,把消息格式转成统一的Protocol Buffer格式,往总线里丢就行。我们目前接了12个渠道,代码结构依然清晰。
省下50%沟通时间的秘密:智能体引擎
客服时间主要花在哪?重复问题解答、用户信息查询、工单流转。我们内置了一个可编程的客服智能体引擎,不是简单的关键词回复,而是真正能调用业务API的“副驾驶”。
比如用户问“我的订单123456发货没?”,传统客服要切到订单系统查。现在智能体自动匹配意图,调用订单查询接口,把结果结构化地呈现给客服,客服点一下“发送”就行。源码里我们设计了一个简单的DSL来描述这类动作:
go // 智能体动作定义示例 { “intent”: “查询订单状态”, “patterns”: [“订单.*状态”, “发货了没”, “到哪了”], “action”: { “type”: “http”, “endpoint”: “{{.Config.OrderAPI}}/v1/orders/{{.ExtractOrderID}}”, “method”: “GET”, “response_template”: “订单{{.order_id}}当前状态:{{.status}},物流单号:{{.tracking_number}}” } }
更实用的是对话记忆。我们给每个会话维护了一个上下文KV存储,智能体可以记住用户之前说过什么。比如用户先问“你们支持七天无理由吗?”,过几分钟又问“那退货邮费谁出?”,智能体会结合上下文,知道“那”指的是七天无理由退货,直接给出精准答案。
性能压测数据
说实话,最初选择Golang就是冲着性能来的。但实际结果还是有点超出预期:
- 消息转发延迟:P99 < 50ms(旧系统P99在200ms左右)
- 单机连接数:实测稳定支持1.2W+ WebSocket长连接
- 内存占用:日常活跃约800MB(旧系统同等负载下2.5GB)
- 启动时间:冷启动3秒内完成服务注册和路由加载
这些数据怎么来的?我们开源了压测工具和脚本(在项目gofly.dev的benchmark目录),欢迎各位拿自己的环境试试。
独立部署的“坑”与解决方案
既然选择独立部署,有些坑必须提前填平:
会话持久化:我们放弃了MySQL存消息,改用MongoDB分片存储。单条消息虽然小,但量大了之后关系型数据库的索引维护成本太高。
客服状态同步:客服登录、分配、离线状态用Redis集群存,但单纯依赖Redis订阅发布有状态不一致风险。我们加了一层Gossip协议做状态同步,确保调度服务即使重启也不会把消息分给已离线的客服。
消息可达性保证:用了类似Kafka的ACK机制,消息被客服端确认收到后才从待发送队列移除。如果客服端30秒没ACK,会自动重分配给其他在线客服。
关于源码和二次开发
项目完全开源(Apache 2.0协议),代码结构我们刻意保持扁平:
├── core/ # 核心总线、路由 ├── adapters/ # 各渠道适配器 ├── agent/ # 智能体引擎 ├── scheduler/ # 客服调度算法 └── web/ # 管理后台前端
每个目录都有详细的README.md,包含架构图和核心流程说明。我们特别重视文档,因为知道很多团队需要二次开发。
举个例子,如果你想加一个“抖音客服”渠道: 1. 在adapters/下新建douyin/ 2. 实现MessageReceiver接口(就三个方法) 3. 在配置里注册这个适配器 4. 重启服务,后台管理页面自动出现抖音渠道配置选项
整个流程,熟练的话半小时搞定。
最后聊聊技术选型
有朋友问为什么不用Erlang(确实适合IM),或者Java(生态丰富)。说到底还是权衡:我们要的是高性能+快速迭代+易部署。Golang静态编译,一个二进制文件扔服务器就能跑,依赖问题少。对于中小团队,这种简洁性带来的运维成本降低,比极致的性能优化更实在。
这套系统已经在十几家企业生产环境跑着,每天处理百万级消息。代码在GitHub上,搜索“gofly.dev”就能找到。欢迎Star,更欢迎提PR——我们特别缺IM协议优化和分布式事务方面的贡献。
做技术产品,最终还是要解决实际问题。如果你也在为客服系统头疼,不妨试试我们的方案。至少,50%的客服时间节省,对业务团队来说是真金白银的价值。
下次聊聊这套系统里的WebSocket连接保活和断线重连设计,那又是另一个充满坑的故事了。