微信在线AI客服系统源码:PHP实现消息接入与AI应答

发布时间:2026/10/7 10:13:44
微信在线AI客服系统源码:PHP实现消息接入与AI应答 简介这套微信在线AI客服系统源码基于PHP开发面向需要搭建企业级智能客服平台的中高级开发者与运维人员解决7×24小时客户咨询响应、AI与人工协同服务的问题。系统深度集成企业微信客服支持文本对话、图片分析与视频分析等多种交互方式并内置对话管理、人工转接、咨询提醒等高级功能模块划分清晰便于按业务场景二次开发。资源包共38个文件以31个PHP源码文件为主体另含说明文档、配置示例、前端页面与版本控制辅助文件压缩包约20.57MB结构规整、开箱即用。目前已有65人学习下载。读者可获得一套可直接部署运行的完整客服系统框架理解AI服务、微信交互与对话流程管理的模块化设计思路并借助配置文件灵活调整回复模板与对话策略快速落地符合自身业务需求的智能客服方案。1. 微信在线 AI 客服系统源码从“能跑”到“能用”差在哪很多团队拿到一套微信在线 AI 客服系统源码第一反应是“传上去、配个库、跑起来”结果发现消息能收不能回、AI 答非所问、多客服抢单、会话丢历史。这套基于 PHP 的微信在线 AI 客服系统源码解决的不是“有没有客服入口”而是把微信消息接入、AI 自动应答、人工坐席接管、会话持久化这几件事串成一条可运维的链路。它适合三类人想给公众号或小程序加智能客服的中小团队、需要二次开发客服中台的 PHP 工程师、以及拿它当课程设计或交付底座的开发者。源码本身是工程骨架真正决定能不能上线的是你对微信消息协议、AI 接口调用和会话状态管理的理解。下面按“资源是什么 → 怎么用 → 坑在哪”的顺序拆开讲每一步都落到能抄的配置和代码上。2. 消息接入层PHP 怎么接住微信推送并转成 AI 可用的会话2.1 微信服务器配置与 URL 校验的 PHP 实现微信公众平台的服务器配置是整个系统的入口。你在后台填的 URL 必须能在 1 秒内完成signature校验并原样返回echostr否则微信会判定接入失败。常见做法是在入口文件里单独写一个校验分支不要和业务逻辑混在一起。?php // wechat_entry.php 微信消息统一入口 define(TOKEN, your_wechat_token); // 与公众平台后台填写的 Token 一致 $signature $_GET[signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; $echostr $_GET[echostr] ?? ; // 1. 排序后拼接再 sha1这是微信规定的校验算法 $tmpArr [$timestamp, $nonce, TOKEN]; sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $calcSign sha1($tmpStr); // 2. 校验通过且是首次接入直接返回 echostr if ($calcSign $signature $echostr ! ) { echo $echostr; exit; } // 3. 校验不通过直接拒绝避免伪造请求进入业务层 if ($calcSign ! $signature) { http_response_code(403); exit(invalid signature); } // 4. 校验通过后交给消息处理器 require __DIR__ . /handler.php;这段代码的逻辑说明sort($tmpArr, SORT_STRING)必须用字符串排序用默认排序在部分时间戳下会得到错误顺序sha1是微信公众平台早期就固定的算法不要换成md5。参数上TOKEN要和后台完全一致大小写敏感。校验通过后不要在这里做数据库查询入口文件越轻越好否则微信 5 秒超时会导致重试用户会收到重复回复。2.2 把 XML 消息转成统一会话结构微信推送过来的是 XMLAI 接口要的是结构化文本。中间需要一个转换层把不同消息类型text、image、event统一成内部会话对象。这一步做不好后面 AI 拿到的就是一堆带标签的字符串意图识别直接崩。?php // handler.php 消息解析与统一封装 $raw file_get_contents(php://input); libxml_disable_entity_loader(true); // 防止 XXEPHP 7 以下必须加 $xml simplexml_load_string($raw, SimpleXMLElement, LIBXML_NOCDATA); if ($xml false) { exit(); } $msg [ openid (string)$xml-FromUserName, // 用户唯一标识 official (string)$xml-ToUserName, // 公众号原始 ID type (string)$xml-MsgType, // text / image / event content (string)($xml-Content ?? ), // 文本内容 event (string)($xml-Event ?? ), // 事件类型 time (int)$xml-CreateTime, ]; // 事件消息没有 Content统一补空串避免下游报 undefined if ($msg[type] event) { $msg[content] ; } // 交给会话管理器按 openid 维度维护上下文 require __DIR__ . /session_manager.php; $reply handle_message($msg); echo build_reply_xml($msg, $reply);逻辑说明LIBXML_NOCDATA会把 CDATA 内容直接取出来否则Content会带一层 CDATA 包装。libxml_disable_entity_loader(true)是防 XXE 的常规操作PHP 8 之后默认关闭外部实体但老项目迁移过来时这行不能省。参数上openid是会话主键所有上下文、历史、坐席绑定都围绕它展开。build_reply_xml负责把 AI 返回的文本重新包成微信要求的 XML 格式注意CreateTime要用当前时间戳不要复用用户消息的时间。2.3 会话上下文存储用 Redis 还是 MySQLAI 客服和普通问答最大的区别是“记得住上一句”。用户问“你们发货吗”AI 答“发”用户接着问“几天到”如果上下文丢了AI 就不知道“几天到”指的是发货时效。上下文存储有两种常见做法Redis 存最近 N 轮MySQL 存完整会话归档。存储方案适用场景过期策略注意点Redis List最近 510 轮上下文按 openid 设 30 分钟 TTL内存成本高需限制单用户长度MySQL 表完整会话归档与审计按天分区或定期归档查询慢不适合实时拼上下文Redis MySQL实时用 Redis归档落 MySQLRedis 过期后从 MySQL 回捞写入要异步避免阻塞回复我一般会这样组合Redis 存chat:ctx:{openid}的 List只保留最近 6 轮每轮存role和content的 JSONMySQL 的chat_log表异步写入完整记录。这样 AI 调用时只读 Redis响应快客服后台查历史时读 MySQL数据全。参数上TTL 设 30 分钟是经验值太短用户中途离开再回来上下文就断了太长内存扛不住。3. AI 应答与人工接管PHP 侧怎么调度模型和坐席3.1 AI 接口调用的超时、重试与降级AI 接口不是永远可用的。网络抖动、额度耗尽、返回格式异常都会发生。PHP 侧调用 AI 接口时必须设超时和降级否则用户会一直卡在“正在输入”。?php // ai_client.php AI 接口调用封装 function call_ai(array $messages, int $timeout 8): string { $payload json_encode([ model your_model_name, messages $messages, stream false, ], JSON_UNESCAPED_UNICODE); $ch curl_init(https://your-ai-endpoint/v1/chat/completions); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS $payload, CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT $timeout, // 总超时必须小于微信 5 秒重试窗口 CURLOPT_CONNECTTIMEOUT 3, // 连接超时单独设 CURLOPT_HTTPHEADER [ Content-Type: application/json, Authorization: Bearer . AI_API_KEY, ], ]); $resp curl_exec($ch); $err curl_error($ch); curl_close($ch); // 降级接口失败时返回兜底话术不让用户空等 if ($resp false || $err) { return 当前咨询较多请稍后再试或回复“人工”转接客服。; } $data json_decode($resp, true); return $data[choices][0][message][content] ?? 抱歉我暂时没理解请换个说法。; }逻辑说明CURLOPT_TIMEOUT设 8 秒是上限实际要配合微信 5 秒重试机制建议在业务层用fastcgi_finish_request()先返回“正在思考”再异步补发。参数上stream设 false 是为了简化解析如果要做打字机效果再开流式。降级话术里带“回复人工”是给用户出口也是给系统转坐席的触发词。注意Authorization头不要写死在代码里用环境变量或配置文件。3.2 人工坐席接管的状态机设计AI 和人工的切换不能靠“感觉”要有明确状态。常见状态有ai_servingAI 服务中、waiting_human用户请求人工、human_serving人工服务中、closed会话结束。状态存在 Redis 的chat:state:{openid}里每次消息进来先读状态再决定路由。?php // session_manager.php 状态路由核心逻辑 function handle_message(array $msg): string { $openid $msg[openid]; $state redis()-get(chat:state:{$openid}) ?: ai_serving; // 用户主动请求人工或 AI 连续两次未命中转 waiting_human if ($msg[content] 人工 || $msg[content] 转人工) { redis()-setex(chat:state:{$openid}, 1800, waiting_human); return 已为您转接人工客服请稍候。; } if ($state waiting_human || $state human_serving) { // 坐席未接入时先排队已接入则消息进坐席队列 push_to_agent_queue($openid, $msg[content]); return $state waiting_human ? 客服正在赶来您前面还有少量排队。 : ; } // 默认走 AI $ctx load_context($openid); $ctx[] [role user, content $msg[content]]; $answer call_ai($ctx); save_context($openid, $ctx, $answer); return $answer; }逻辑说明状态机的好处是任何时刻系统都知道“这条消息该谁回”。waiting_human和human_serving要分开前者是排队后者是已接入坐席端拉取队列时只拉waiting_human。参数上setex的 1800 秒是会话空闲过期时间超过这个时间没消息就自动回到ai_serving避免坐席被长期占用。push_to_agent_queue建议用 Redis List 的LPUSH坐席端BRPOP阻塞获取比轮询省资源。3.3 多坐席分配与消息去重多个坐席同时在线时最容易出的问题是“同一个用户被两个坐席接待”或“消息重复推送”。解决思路是给每个 openid 加一把分布式锁坐席接入时先抢锁。?php // agent_assign.php 坐席抢单 function assign_agent(string $openid, int $agentId): bool { $lockKey chat:lock:{$openid}; // SET NX EX 是原子操作抢到锁才能接入 $ok redis()-set($lockKey, $agentId, [nx, ex 300]); if (!$ok) { return false; // 已被其他坐席接入 } redis()-setex(chat:state:{$openid}, 1800, human_serving); redis()-set(chat:agent:{$openid}, $agentId); return true; }逻辑说明SET NX EX保证只有一个坐席能抢到锁的 300 秒是接入确认窗口坐席端要在这段时间内点“接入”否则锁释放。参数上agentId存进锁的值里方便排查是谁接的。消息去重靠微信MsgId在入口处用 Redis 的SETNX msg:{MsgId}判断重复推送直接返回空串避免用户收到两条一样的回复。4. 避坑与排查这套源码最容易翻车的五个地方4.1 现象用户发消息没反应日志里也没有记录原因微信服务器配置的 URL 校验没通过或者入口文件被框架路由拦截了。常见于把入口文件放在 ThinkPHP、Laravel 的控制器里框架先解析了请求echostr被吞掉。解决把微信入口独立成public/wechat.php绕过框架路由在 Nginx 里加一条location /wechat.php { fastcgi_pass ...; }确保直达 PHP。校验时用error_log把signature、timestamp、nonce打出来和微信后台的 Token 逐字比对。4.2 现象AI 回复重复用户收到两条一样的话原因微信 5 秒超时重试。AI 接口响应超过 5 秒微信认为你没收到重推一次你的系统又处理了一遍。解决入口处用MsgId做幂等SETNX msg:{MsgId} 1 EX 60已存在直接exit()。同时把 AI 调用改成异步先返回“正在思考”用fastcgi_finish_request()结束请求再在后台进程里调 AI 并通过客服消息接口补发。4.3 现象上下文串了A 用户的对话出现在 B 用户窗口原因会话 key 用了公众号 ID 或坐席 ID而不是openid。或者 Redis 连接复用时 key 拼接少了分隔符。解决所有会话 key 统一格式chat:ctx:{openid}openid来自FromUserName不要用ToUserName。在load_context和save_context里加断言openid为空直接抛异常别让它静默写入。4.4 现象人工坐席接入后AI 还在自动回复原因状态更新和消息处理有竞态。坐席抢锁成功但状态还没写进 Redis用户消息已经进来走了 AI 分支。解决抢锁和状态更新放在一个 Lua 脚本里原子执行或者先写状态再抢锁抢锁失败回滚状态。坐席端接入后前端要立即刷新会话列表避免坐席以为没接上又点一次。4.5 现象XML 解析报错Content取出来是空原因微信推送的 XML 里Content包在 CDATA 里simplexml_load_string没加LIBXML_NOCDATA取出来是对象而不是字符串。解决解析时固定加LIBXML_NOCDATA并且对Content做(string)强制转换。图片消息没有Content要用$xml-PicUrl事件消息用$xml-Event分支判断要写全别默认所有消息都有文本。5. 进阶把 AI 客服接进小程序登录与消息推送链路微信生态里公众号客服只是入口之一小程序登录获取手机号、模板消息推送、企业微信互通才是完整链路。这套 PHP 源码的扩展点在于把openid换成小程序的openid或unionid会话层几乎不用改。小程序登录获取手机号走的是getPhoneNumber接口前端拿到code后传给 PHP 后端后端用code换手机号再和openid绑定写入用户表。?php // miniprogram_login.php 小程序手机号绑定 function bind_phone(string $code, string $openid): array { // 1. 用 code 换 access_token小程序侧 $tokenUrl https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credential . appid . APPID . secret . SECRET; $token json_decode(file_get_contents($tokenUrl), true)[access_token] ?? ; // 2. 用 access_token code 换手机号 $phoneUrl https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token . $token; $ch curl_init($phoneUrl); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS json_encode([code $code]), CURLOPT_RETURNTRANSFER true, CURLOPT_HTTPHEADER [Content-Type: application/json], ]); $resp json_decode(curl_exec($ch), true); curl_close($ch); $phone $resp[phone_info][phoneNumber] ?? ; if ($phone ) { return [ok false, msg 手机号获取失败]; } // 3. 绑定到用户表openid 作为主键 db()-update(user, [phone $phone], [openid $openid]); return [ok true, phone $phone]; }逻辑说明access_token要缓存不能每次请求都换微信有调用频率限制常见做法是存 Redis 设 7000 秒过期。参数上code是一次性的用完即废前端要确保每次点击都重新获取。绑定成功后客服系统就能用手机号做用户识别坐席端显示“138****1234”而不是一串 openid体验直接上一个台阶。验证这套链路是否通我一般会走一遍小程序点授权 → 后端收到 code → 换手机号成功 → 数据库 user 表有记录 → 公众号发消息 → 客服系统能按手机号查到历史会话。任何一步断了先看 Redis 里access_token有没有、openid和手机号有没有绑上、消息入口的MsgId幂等有没有误杀。从那以后我每次部署这套源码都强制先跑一遍“发消息 → 收回复 → 转人工 → 坐席接入 → 结束会话”的完整闭环再去看 AI 回答质量。希望帮到你。本文还有配套的精品资源点击获取