拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于TP5.1的多商户在线客服系统架构与机器人自动聊天实现解析

基于TP5.1的多商户在线客服系统架构与机器人自动聊天实现解析

先说结论:之前给一个多商户平台搭在线客服系统,我把核心框架压在TP5.1上,跑了一年多,单机扛住了几百个商户的并发会话,机器人大档口兜掉了大约六成重复咨询。如果你正在找一套能二次开发的在线客服系统源码,又是PHP技术栈,那这篇可以把选型、架构和机器人实现拆透给你看,省得你从零趟雷。

这套系统本质上是"一个平台、多个商户、各自客服"的SaaS化客服底座。每个商户有自己的客服团队、自己的欢迎语、自己的机器人开关和知识库;访客从任意一个商户的商城页进来,都能直接发起会话,机器人先接待,答不了再转人工。能做到这一点,靠的是TP5.1的程序框架加一套精心设计的会话分配与消息推送机制。下面我把整个项目的核心逻辑和实际踩坑过程一块儿整理出来。

1. 项目定位与整体架构:TP5.1搭多商户客服,骨架应该长什么样

1.1 先搞清楚这个源码要解决什么问题

很多业务方嘴上说"要一套客服系统",实际上需求差异非常大。单商户客服系统,比如一个官网带一个在线客服,那太简单了,一个表单提交加个邮件通知都能糊弄;难点在于"多商户"这三个字。平台方(比如商城平台、招商园区、云市场)需要给几十上百个入驻商户提供客服能力,每个商户是独立品牌,客服团队不互通,数据更不能混。

我当时整理的需求清单是这样的:

  • 平台管理员:管商户开通、续费、全局配置,能看所有商户的会话统计。
  • 商户管理员:配置本店客服人员、机器人值班时间、自动回复话术、无应答转人工的等待时长。
  • 一线客服:接待属于自己商户的访客,支持同时多会话、快捷回复、转接给本店其他客服。
  • 访客/买家端:在商户页面发起咨询,自动接入机器人,机器人无法识别时无缝转人工。

这套业务模型放在电商平台里是刚需。买家问的几乎全是"有没有货""什么时候发货""怎么退货",这些重复问题不交给机器人,人工客服再翻一倍也不够用。所以源码里"可开机器人自动聊天"不是后期迭代功能,而是从架构上就定好的核心模块。

1.2 为什么选TP5.1而不是别的框架

项目定在PHP技术栈之后,我在ThinkPHP 5.0、5.1、Laravel之间做了一个很现实的取舍。

TP5.1的核心优势是它刚好踩中了重量和灵活性的平衡点。比5.0多了更规范的中间件、依赖注入、Facade机制,做API接口开发很顺手;比Laravel更轻,部署要求低,虚拟主机都能跑,这对很多做二次开发的用户来说非常关键。Laravel的生态是好,但上了容器、队列、Eloquent那一套以后,很多中小企业运维玩不转。我见过的客服系统二次开发用户,用空间商环境的占很大比例,TP5.1几乎是下限要求。

源码的运行时架构大概是这样的:

Nginx 接收 HTTP 和 WebSocket 反向代理 | +---> PHP-FPM 跑 TP5.1 应用层 | 处理会话创建、消息落地、机器人逻辑、客服分派 | +---> Workerman GatewayWorker 维护客服与访客的长连接通道 | +---> MySQL 存储商户、客服、访客、会话、消息、机器人知识库 | +---> Redis 维持在线状态、消息队列、会话上下文、未读计数

TP5.1部分主要写业务逻辑和HTTP接口,包括商户后台的配置接口、服务端回调接口等;实时收发消息则由Workerman承担,两者之间通过Redis做中间数据交换。实际做过之后你会发现,把"业务逻辑"和"长连接状态"分离是这套系统能稳定跑的基石。

1.3 源码目录结构解读

如果有人拿这套源码做二次开发,我建议先对目录结构有一个全局认识:

app/ index/ 访客端接口:创建会话、发送消息、接收消息 merchant/ 商户后台:客服管理、机器人配置、会话列表 platform/ 平台后台:商户管理、系统参数 api/ 内部API:客服分派、转接处理 command/ 常驻任务:超时未回复提醒、离线消息推送 extend/ Cus/ 机器人解析、关键词库、相似度匹配、第三方AI网关 Cus/Workerman/ 长连接核心代码 public/ index.php TP5.1入口

核心并不在控制器里,而是在extend下的机器人解析和消息转发逻辑中,这个我们后面重点讲。

2. 多商户数据隔离与客服分派:系统最容易被低估的设计环节

2.1 商户维度的数据隔离怎么做才不会有安全隐患

多商户系统第一个要命的问题就是串数据。访客明明问的是A店,结果B店客服也看到了,甚至B店客服还能回复A店的客户,这是绝对事故。我做隔离的时候用了三层防护。

第一层是数据库层,所有核心业务表都带merchant_id字段。会话表、消息表、客服表、机器人规则表,全都不例外。加这个字段不单是为了查询,更重要的是让每一个SQL都天然具备商户维度,即便业务代码里有人忘了加条件,也还有底层拦截兜底。

第二层是TP5.1中间件层的全局注入。我写了一个MerchantScope中间件,商户后台的所有控制器请求进去之前,统一解析商户身份并写入当前模块上下文,同时把通用的查询条件加上。

// app/middleware/MerchantScope.php namespace app\middleware; use think\Request; class MerchantScope { public function handle(Request $request, \Closure $next) { // 从商户后台登录态中解析商户ID $merchantId = session('merchant_id'); if (!$merchantId) { return json(['code' => 401, 'msg' => '商户未登录']); } // 绑定到当前请求上下文,后续控制器可直接取用 $request->merchantId = $merchantId; return $next($request); } }

注册到路由规则后,商户下的所有控制器都免不了走这道门。第三层是缓存层的key隔离。Redis里存储会话、客服在线状态时,key一律加上商户ID前缀,比如kf:online:125、session:conv:125:98。很多人一开始图省事直接用客服ID做key,等商户多了以后不停撞前缀,排查起来比改代码还痛苦。

2.2 访客会话怎么分配客服才公平又高效

访客发起会话后,如果机器人判断需要转人工,系统要选择一个客服。这个分配逻辑我用的是"轮询+空闲优先"的组合策略,而不是简单随机。

  • 先把当前商户下所有在线客服拉出来;
  • 排除掉排队数量超过阈值的客服(比如超过8个活跃会话就暂不分配);
  • 在剩余客服里取当前活跃会话数最少的那个人;
  • 如果所有人都在忙,则按轮询顺序压给下一个客服。

每次分派都做一次Redis原子自增计数,避免两个人同时抢到同一个客服。这个并发的坑我实际遇到过:用原生数组计数在并发瞬间会超分,后来改用Redis INCR,流量一大稳了很多。

转接功能也要限制在同一个商户内。客服A想把当前会话转给B,如果B不在同一个merchant_id下,接口直接抛业务异常。跨商户转接除非是平台客服,一律不开放,权限模型上卡死。

2.3 多商户配置的缓存策略

由于每个商户都有独立的机器人开关、欢迎语、工作时间段、自动回复语,这些配置如果每次都查MySQL,接口性能会非常难看。我把商户配置序列化后放Redis,用版本号做到期失效。

详细点说,merchant_config表里存的是商户ID加一条JSON配置,Redis里以cfg:merchant:125为key缓存。商户管理员在后台改一次配置,就把版本号加一,缓存读取时比对版本号,不一致就重新拉库。这套方案比单纯设置过期时间更精准,不至于改了配置之后等十分钟才生效。

3. 机器人自动聊天的实现:从关键词匹配到带上下文的简单语义

3.1 每个商户独立开关机器人,如何落地

标题里的"可开机器人自动聊天",关键词是"可开"。这意味着机器人不是一个全平台统一的功能,每个商户自己决定开不开、什么时候开。

我在商户后台放了这样一组配置:

  • 机器人总开关(on/off);
  • 机器人值班时段(比如工作日9点到21点,其他时间全部转人工);
  • 兜底回复语(当机器人无法识别时发送的话术);
  • 转人工触发条件(机器人连续答不上2次之后转人工);
  • 机器人应答知识库(商户自己维护问题-答案对)。

这些配置放在merchant_config表的JSON字段里,读取时整个结构一起解析。代码里的处理原则是:每个访客会话开始时先判断机器人是否开启;开启则置一个会话状态位robot_mode = 1;人工介入后置为0,并且同类会话不再切回机器人,避免客服刚打完"亲,在的"又被机器人抢话。

3.2 三档匹配规则的设计与实现

机器人聊天要能落地,不能一上来就谈训练模型。工业场景里最稳妥的做法是按成本从低到高排列匹配规则。我用了三档。

第一档是关键词精确匹配。商户在知识库里维护"发货"、"运费"、"退货"这类核心词,每个词绑定标准回复。访客消息进来后,先做一次全等和包含匹配。这一档实现最简单,一秒出结果,命中率最高。

第二档是正则表达式规则。类似于匹配"多(少|久).*发货"这种模式,解决关键词换个说法就没反应的问题。正则规则也是商户后台可配置的,但我加了一条限制:每商户正则总数不超过200条,否则会拖慢消息响应。

第三档是相似度匹配。我实现了一个基于编辑距离的宽松匹配:把访客消息和知识库问题做短文本相似度计算,超过阈值就返回该问题的答案。

// extend/Cus/Similarity.php namespace Cus; class Similarity { // 简单编辑距离计算 public static function distance($str1, $str2) { $len1 = mb_strlen($str1); $len2 = mb_strlen($str2); $matrix = []; for ($i = 0; $i <= $len1; $i++) { for ($j = 0; $j <= $len2; $j++) { if ($i == 0) $matrix[$i][$j] = $j; elseif ($j == 0) $matrix[$i][$j] = $i; else { $cost = ($str1[$i-1] === $str2[$j-1]) ? 0 : 1; $matrix[$i][$j] = min( $matrix[$i-1][$j] + 1, $matrix[$i][$j-1] + 1, $matrix[$i-1][$j-1] + $cost ); } } } return $matrix[$len1][$len2]; } }

注意,PHP里直接操作字符串下标对多字节中文不友好。上述代码为了演示逻辑用的是简化版,真正落地的版本要先按mb_str_split把中文切成数组再计算。如果你拿这套代码去跑,一定记得把中文分词这一步加上。

这三档规则按顺序执行,上一档命中就不再走下一档。实际数据里大概20%的问题被第一档命中,第三档能再挽回15%,剩下的进入兜底语和转人工流程。整个匹配过程控制在100毫秒以内,访客端基本无感知。

3.3 对话上下文:让机器人记住上一句话

多商户客服机器人如果只想做"一问一答"其实比较简单,但访客常见的问题是先说"我想问个事",然后才问具体内容。没有上下文,这种会话机器人完全接不住。

我在Redis里给每个会话维护了一个长度为10的消息队列ctx:conv:{id},存最近的访客消息和机器人回复。当新消息在第二、三档都没有命中时,会尝试做一次"结合上文的意图猜测"。比如访客上一句是"我有个订单",当前句是"发货了吗",系统会把"订单"和"发货"组合起来命中"查发货"规则。

这种做法不需要引入太大的模型,只靠简单的拼接缓存就解决了相当大比例的多轮对话场景。大家在做客服机器人时,不要一上来就上大模型,先用规则把八成常见问答覆盖掉,再考虑更高级的引擎。

3.4 机器人转人工的判定时序

机器人答不上来时什么时候转人工?最简单的是答不上就立刻转,但这样会把客服逼疯。我设了两条路线:

  • 兜底次数达到2次,自动转人工,同时给访客发送"正在为您转接人工客服";
  • 对话总轮次超过8轮,且至少出现一次兜底语,自动提示转人工。

这两者的差别在于,有些访客就是一直重复问同一个问题,此时必须尽早转人工,别让机器人在原地打转。转人工动作触发之后,走的就是2.2节里的分派逻辑了。

4. 消息可靠性与核心表结构设计:会话不丢是第一原则

4.1 核心表结构和每张表的用途

客服系统的命根子是消息,消息丢了谁都不会再信任这个系统。我先说表结构,再说消息怎么防丢。

一张最精简的库表设计大概这样:

merchant 商户表:商户名称、套餐等级、机器人开关、配置版本号 merchant_config 商户配置表:JSON配置、更新时间 agent 客服表:商户ID、客服姓名、账号、在线状态、接待上限 visitor 访客身份表:来源商户、访客标识、最近一次IP、UA session 会话表:会话ID、商户ID、客服ID、访客ID、状态、开始时间、结束时间 message 消息表:会话ID、发送者类型(访客/客服/机器人)、内容、类型、创建时间、消息序号 robot_rule 机器人规则表:商户ID、类型(关键词/正则/相似度)、匹配规则、回复内容、命中次数

会话状态字段我直接用了整型枚举:1等待分配,2机器人服务中,3人工服务中,4已结束,5用户主动关闭,6超时关闭。每次状态流转都有update_time记录,方便后台统计客服工作量。

4.2 消息不丢的方案:先落库,再推送

很多人做WebSocket客服系统时喜欢消息先推再存,这样访客端体验好,但一旦推送通道出问题,消息就永远丢了。我做过一个反面案例之后,定了铁律:任何消息必须先落MySQL,再走长连接推送。

流程上分四步:

  1. 发送方调用HTTP接口POST /api/message/send,消息写入message表,同时生成一个单调递增的消息序号seq;
  2. 写入成功后,把消息推送到Redis消息队列msg:conv:{id};
  3. Workerman的长连接进程从Redis队列里拿到消息,推送给接收方客户端;
  4. 接收方客户端在本地记录已收到的最大seq,重连时靠GET /api/message/pull?since_seq={max_seq}增量拉取。

这套设计最本质的保障是:就算WebSocket断连、Workerman重启、客户端闪退,消息也已经安全地躺在MySQL里。访客刷新页面后从断点seq继续拉,一条都不丢。

4.3 已读未读和离线消息的处理

已读未读我采用的是"会话维度计数"而不是"每条消息维度打标记",因为客服消息列表里只需要看到未读数,没必要为每条消息维护状态位。具体做法是:session表中存字段visitor_last_read_seq和agent_last_read_seq,每次拉消息时把对方的已读seq带上,前端自行计算未读数量。

离线消息主要靠客户端主动拉取,前面说的增量拉取接口天然支持这种情况。要注意的是,增量拉取接口在bing法查询时一定要带商户ID过滤,并且对since_seq做校验,避免访客伪造seq拉取到不属于自己会话的消息。这是安全边界,不能省。

5. 部署上线阶段我踩过的坑:如果你要做二次开发,这几条一定看

5.1 Nginx代理WebSocket超时设置

把Workerman跑起来并不难,难在让Nginx把长连接安稳地代理过去。默认Nginx配置里proxy_read_timeout是60秒,而WebSocket连接通常会空闲超过60秒(访客可能在打字、可能在看商品),超时后Nginx会主动断开。访客端WebSocket一断,系统就表现成"一会儿能用一会儿掉线"。

正确处理是在Nginx的location配置里显式声明:

proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s;

这条配置我不止在一台服务器上吃过亏。改成3600秒后,长连接可以稳稳保持,配合心跳机制(每60秒发一个ping帧)基本不会被人为断开。

5.2 TP5.1的Session和长连接之间闹过笑话

上线第一天我发现一个诡异问题:客服后台登录后,页面接口一切正常,但打开WebSocket推送时提示未登录。排查了半天,发现是TP5.1的Session默认使用文件存储,WebSocket网关进程根本无法读取PHP-FPM生成的Session文件,因为文件路径和用户身份根本没传递给Gateway。

客户身份验证必须走带签名的Token。访客和客服初始化长连接前,HTTP接口给前端签发一个短期Ticket,里面包含会话ID和用户身份,签名用HMAC。Gateway拿到Ticket后做验签,再建立长连接。千万不要在长连接通道里依赖传统Session,那是把火车轮子装到自行车上,必然翻车。

5.3 Redis连接数被打爆的那次事故

前面设计里大量用Redis做在线状态、消息队列、机器人上下文,如果每一条消息、每一个轮询都走一次Redis连接,瞬时并发一高,Redis连接数能冲到几千。默认的Redis maxclients是10000,看着够,但加上内置的慢查询和持久化fork,很容易把内存和CPU打满。

我的补救措施是两层:

  • 应用层使用TP5.1的Redis连接池,也就是长连接模式,不要让每个请求都connect;
  • Workerman进程内部单独复用Redis连接,避免每个GatewayWorker连接都开一个Redis连接。

实际上线之后,单机3000并发在线,Redis连接稳定在200左右,这就对了。如果你看到Redis连接数随访客数线性上涨,先检查是不是在循环体里new了Redis客户端。

5.4 商户配置缓存雪崩风险

刚开始配置缓存过期时间统一设为600秒,结果半夜定时任务把几百个商户的配置同时更新,Redis里的key一起失效,数据库压力瞬间飙上去。虽然不至于宕机,但接口耗时从30毫秒涨到1秒多,体验差一大截。

后来改成每个商户的配置key加上随机的过期基准,60秒到300秒之间随机,再配合版本号机制。更新配置时主动删缓存而非等它过期,这样数据库压力就平缓了。做多商户系统,凡是缓存穿透和雪崩的问题都要按这个思路处理:随机过期时间+主动失效。

写在最后的个人体会

做多商户在线客服系统,技术难点从来不在"能发消息",而在"消息不乱、数据不串、机器人能接得住、少了不丢"。TP5.1这套组合在中小规模场景下非常务实:开发效率高,部署门槛低,出了问题社区里能搜到答案;配合Workerman和Redis,单机能支撑的在线会话量对于绝大多数商户平台来说是绰绰有余的。

最后给你一个建议:拿到源码后先不要改功能,先把商户之间的隔离逻辑走一遍,用一个商户开机器人、另一个商户关机器人交叉测试,确认数据不串之后再动UI。这套系统的地基是数据隔离,地基稳了,其他都好说。

返回列表