简介这是一份基于PHP开发的2026版微信在线AI客服系统开源源码面向需要快速搭建智能客服能力的企业团队及中高级PHP开发者。系统内置上下文理解、AI参数配置、产品知识库、常见问题FAQ与促销推荐并支持图片内容识别、视频分析等多媒体交互人工客服侧提供自定义关键词触发转接、一键介入与用户ID映射可满足企业微信场景下7×24小时客服运营需求。包体共43个文件以31个PHP脚本为核心辅以HTML页面、MD说明、TXT配置与声明等压缩包大小20.58MB目录按includes、public、logs、conversations等模块划分便于定位与二次开发。已有145人学习下载。源码附带系统功能介绍、配置示例及资源索引部署门槛低既可直接用于企业微信客服快速落地也可作为PHP学习AI对话、多媒体处理与客服工作流的完整工程。1. 微信AI客服系统开源源码我拆完之后的结论是——值得下把微信公众号或小程序收到的用户消息自动转成 AI 请求再把 AI 回复按微信接口规范推回给用户中间夹着会话管理、上下文记忆、人工转接——这套微信 AI 客服系统开源源码干的就是这件事。它的价值不在模型本身多强而是“微信消息通道 客服逻辑”已经现成你不用从零啃消息签名、XML 解析、5 秒被动回复超时这些微信接口里最磨人的细节。适合两类人一类是公众号粉丝量上来后客服忙不过来的运营者另一类是想搞懂微信服务器回调到底怎么调通的开发者。我本地完整跑了一遍结论是代码能直接部署坑主要集中在微信侧配置下面按我的拆解顺序讲。2. 技术选型与整体架构为什么这套源码用 PHP 而不是 Java 服务2.1 消息入口的两种落地方式公众号回调 vs 小程序客服微信生态里做 AI 客服消息入口有两条路线。第一条是公众号开发模式用户在公众号对话框发消息微信服务器把消息 POST 到你在公众平台配置的服务器 URL你的程序解析 XML、返回响应。第二条是小程序客服消息用户在小程序里进入会话消息通过客服消息接口下发。这里有个前提容易被忽略小程序要拿到用户手机号必须走“微信小程序登录获取手机号”的授权弹窗用户点过同意按钮后服务端才能用 code 换手机号不是静默能拿到的。所以很多开源客服系统干脆不做手机号强绑定直接用 openid 当用户主键这套源码也是这个选择。这套源码走的是公众号回调路线理由很实际。公众号客服的接入门槛比小程序低注册一个测试号就能开始调不要求认证服务号而小程序客服需要先有小程序主体、配客服组件还需要用户在小程序前台触发会话链路更长。部署成本上 PHP 也有天然优势——一套 Nginx PHP-FPM 或者虚拟主机就能跑不依赖 Maven 仓库、JVM 调优这些重型工具。对比一下如果走 spring boot mybatis 那套 Java 方案光是打包、配 Tomcat、处理 Maven 依赖就能劝退一半想做客服系统的小团队。PHP 在这里的定位是“快速把通道跑通”不是高并发——单公众号回调的 QPS 本来就不高PHP-FPM 完全扛得住。硬要说它的边界就是当你的客服消息量达到每秒几千条、需要异步削峰时PHP 的同步模型会吃力那时候才需要考虑 Swoole 常驻内存或者换 Java 网关。对绝大多数中小业务这个选型是合理的。2.2 从用户发消息到 AI 回复一次完整请求的状态机把一次消息请求的完整链路拆开看一共七步微信服务器 POST 一条 XML 到你的回调地址程序用 token、timestamp、nonce 做 SHA1 签名校验校验不过直接拒绝校验通过后解析 XML取MsgType、Content、FromUserName字段用FromUserName即 openid查会话表看有没有未完结的上下文有上下文就带着历史消息调 AI 接口没有就新建一条会话记录拿到 AI 回复后封装成被动回复 XML在 5 秒内返回给微信把双方消息写入日志表更新会话表的last_active_at这个流程里最容易被忽略的是第 4 步。很多初版代码拿到消息就直接怼给大模型不带任何状态结果用户问“那退款呢”的时候AI 根本不知道“那”指代的是上一单没发货的订单。这套源码里我比较认可的设计是把会话状态拆成了独立字段scene标记当前是 AI 接待还是已转人工context_json存最近 N 轮对话摘要而不是每次都把所有历史重新发给模型。这个设计直接决定了你后续接大模型时 prompt 不会越拼越长token 消耗可控。会话表里还有一个容易被忽略的细节scene字段必须要能表达“等待人工”、“AI 接待中”、“会话已结束”三种以上状态而不是简单的 0/1 布尔值——因为用户转人工之后AI 不应该再自动答一句“已为您转接”否则用户会收到两条声音打架的回复。2.3 数据库表结构与关键配置项这套源码的建表语句很精简核心就三张表用户表、会话表、日志表。用户表和会话表可以合一但拆开更清晰。建表 SQL 如下CREATE TABLE wx_user ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL, nickname varchar(64) DEFAULT , scene tinyint(1) NOT NULL DEFAULT 0 COMMENT 0-AI接待 1-转人工 2-会话结束, context_json text COMMENT 最近对话上下文,JSON数组, last_active_at int(11) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chat_log ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL, direction tinyint(1) NOT NULL COMMENT 1-用户消息 2-AI回复 3-人工回复, content text, reply_source varchar(16) DEFAULT COMMENT ai/manual/fallback, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_openid_time (openid, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;direction和reply_source两个字段是我建议你必须保留的。direction区分消息方向reply_source标记这条回复来自 AI、人工还是兜底话术。有了这两个字段你后续统计“AI 接管率”“转人工率”“兜底率”的时候直接一条 SQL 就查出来了不用翻代码猜。这也是我判断一个开源客服系统做没做好的第一眼标准——日志表里有没有回复来源标记。配置项集中在/config/config.php格式如下return [ wechat [ appid wx1234567890abcdef, appsecret your_app_secret, token your_custom_token, encodingAESKey , aes_mode safe // safe兼容模式 / raw明文模式 ], ai [ api_url https://api.example.com/v1/chat/completions, api_key sk-xxx, model qwen-plus, max_tokens 512, timeout 3.5, ], db [ host 127.0.0.1, port 3306, name wx_kefu, user root, pass ] ];ai.timeout这个参数我单独圈一下它被设成 3.5 秒是刻意留出余量。微信对被动回复的硬性要求是 5 秒内返回超过 5 秒微信会重试并告诉用户“该公众号暂时无法提供服务”。这里 3.5 秒是 AI 调用预算剩下 1.5 秒留给网络传输、框架启动、数据库查询。如果你把 timeout 设成 4.9 秒那线上一定会偶发超时因为任何一次网络抖动都会撞上 5 秒红线。3. 部署与接入从源码落地到微信公众平台联调3.1 本地调试没有公网怎么调通回调微信回调要求一个公网可访问的 URL但本地开发时没有公网 IP最痛苦的就在这里。常见做法是先用微信公众平台的“接口调试工具”验证签名或者直接本地写脚本模拟微信 POST。验证签名这段是必考代码不长但一次都不能错// verify_signature.php ?php $token your_custom_token; $signature $_GET[signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; $tmpArr array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); // 必须按字典序排序 $tmpStr sha1(implode($tmpArr)); if ($tmpStr $signature) { // 首次接入时微信会带 echostr 参数直接原样返回 if (isset($_GET[echostr])) { echo $_GET[echostr]; exit; } echo signature check passed; } else { exit(invalid signature); }这里最容易翻车的是sort($tmpArr, SORT_STRING)这行。PHP 的sort()默认按数字排序但微信要求字典序字符串排序不显式传SORT_STRING的话timestamp和nonce在个别组合下会排错顺序导致签名对不上。另外一个坑是echostr的处理——首次接入时微信带的是 GET 请求你需要原样返回echostr参数不要包任何 XML不要加空格。验证完签名本地伪造消息用 curl 往本地路由 POST 一条标准 XML 即可// simulate_request.php ?php $xml XML xml ToUserName![CDATA[gh_xxxx]]/ToUserName FromUserName![CDATA[oXXXX_openid]]/FromUserName CreateTime1735689600/CreateTime MsgType![CDATA[text]]/MsgType Content![CDATA[你好我想查一下订单]]/Content /xml XML; $ch curl_init(http://127.0.0.1:8080/index.php); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: text/xml]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $resp curl_exec($ch); curl_close($ch); echo $resp;ToUserName填公众号原始 IDgh_开头FromUserName填任意 openidCreateTime用当前 Unix 时间戳。这条模拟请求能覆盖你 90% 的本地调试场景——解析、意图识别、AI 调用、被动回复封装全链路都能在不出网的情况下跑通。本地调试还有一个容易忽略的技巧php伪造微信浏览器头信息。微信内置浏览器的 UA 带MicroMessenger标识很多客服 H5 页面会判断这个 UA 决定是否放行。调试时我习惯在 curl 里加这个请求头curl -H User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) MicroMessenger/8.0.49 http://127.0.0.1:8080/h5_support.php不加这个 UA你永远不知道用户在微信里打开的页面和你浏览器里看到的是不是同一个。这套源码里如果是 H5 形态的客服工作台这个技巧能帮你省掉大量“用户说打不开我这儿明明是好的”的撕扯时间。3.2 正式环境三项硬要求本地调通之后上正式环境微信侧有三项硬要求缺一个公众号后台就报错。第一是域名。回调 URL 必须是公网可解析的域名微信不认 IP 直连个别地区或特殊接口例外但公众号回调基本都要求域名。域名必须 ICP 备案大陆服务器尤其严格备案没下来之前回调地址填了也白填。第二是 HTTPS。微信要求回调地址必须 HTTPS证书要完整链不能用自签名证书。买一个域名证书不贵一年几十块配好 Nginx 之后记得用curl -I https://你的域名/wx/callback检查一遍证书链。第三是 IP 白名单。在公众号后台“基本配置”里你需要把服务器出口 IP 加进白名单否则调用getAccessToken等 API 时会报40164错误。注意这里加的是服务器出口 IP不是你本机 IP。如果你用云函数或负载均衡出口 IP 可能不止一个都要加。3.3 参数对照表一个都别填错公众号后台基本配置里那几项和这套源码的配置项是一一对应的。我做过一张对照表每次部署都照着填公众平台字段源码配置项说明AppIDwechat.appid公众号的唯一标识wx开头AppSecretwechat.appsecret与 AppID 成对注意不要提交到 GitTokenwechat.token你自定义的英文字符串签名校验共用EncodingAESKeywechat.encodingAESKey43 位密钥安全模式下必填消息加解密方式wechat.aes_mode明文/兼容/安全三选一服务器地址(URL)回调路由公众号后台填https://域名/wx/callback最容易填错的是 AppSecret 和 Token 混淆。AppSecret 是微信分配的一串随机字符Token 是你自己起的任意字符串两者用途完全不同——AppSecret 用于换取access_tokenToken 只用于验证签名。曾经见过有人把 Token 直接粘贴到appsecret字段里结果getAccessToken一直报40125错误排查了半天才发现是配置项张冠李戴。4. AI 回复引擎把大模型接进客服系统的核心代码4.1 意图识别与关键词兜底不急着上模型很多人拿到这套源码后的第一反应是“直接调大模型”但我的建议是先在前面加一层轻量意图识别。原因很简单客服场景里用户问的问题高度集中于订单、退款、物流、人工这几个类目用正则加关键词就能覆盖八成没必要把这些请求全部丢给大模型烧 token。而且意图识别层是天然的“闸门”——识别成“转人工”的请求根本不会进 AI 链路直接进人工队列避免 AI 跟人工抢活也避免用户着急时被 AI 兜圈。// intent.php ?php $intents [ order [订单, 物流, 快递, 发货, 到哪了], refund [退款, 退货, 换货, 不想要了], manual [人工, 转人工, 投诉, 真人, 客服电话], greeting [你好, 您好, 在吗, hi, hello], ]; function matchIntent(string $text): string { global $intents; $text mb_strtolower($text, UTF-8); $scores []; foreach ($intents as $name $keywords) { $scores[$name] 0; foreach ($keywords as $kw) { if (mb_strpos($text, $kw) ! false) { $scores[$name]; } } } arsort($scores); return $scores[key($scores)] 0 ? key($scores) : unknown; }这段代码的逻辑是遍历每个意图的关键词数组命中一个累加一分最后取最高分。mb_strtolower是为了把英文关键词统一成小写mb_strpos用多字节版本是为了正确处理中文——用普通的strpos在 UTF-8 中文场景下会出错。兜底策略任何意图得分都不超过 0返回unknown此时才把消息交给大模型处理。这套源码里我看到的处理方式是“意图优先、AI 兜底”order、refund这类高频问题直接匹配预设话术或知识库条目unknown才走大模型。这样做的好处一是省钱二是响应快——知识库命中是毫秒级大模型至少一到两秒。线上运营一段时间后你可以把日志表里reply_sourcefallback的记录拉出来把高频的 fallback 问题沉淀成新的意图持续降低大模型调用量。4.2 大模型 API 封装与超时策略真正调用大模型的部分封装在一个独立的函数里方便切换不同供应商// ai_client.php ?php function askAI(string $openid, string $question, array $context): array { $cfg $GLOBALS[config][ai]; $messages []; $messages[] [ role system, content 你是商城客服助手。回答简洁不超过80字。不知道的事情不要编引导用户转人工。 ]; // 把最近5轮上下文塞进去 foreach (array_slice($context, -5) as $turn) { $messages[] $turn; } $messages[] [role user, content $question]; $ch curl_init($cfg[api_url]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ Content-Type: application/json, Authorization: Bearer . $cfg[api_key], ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ model $cfg[model], messages $messages, max_tokens $cfg[max_tokens], temperature 0.3, ])); curl_setopt($ch, CURLOPT_TIMEOUT, $cfg[timeout]); $resp curl_exec($ch); $errno curl_errno($ch); curl_close($ch); if ($errno ! 0) { return [ok false, msg timeout or network error]; } $data json_decode($resp, true); if (!isset($data[choices][0][message][content])) { return [ok false, msg bad response]; } return [ok true, msg $data[choices][0][message][content]]; }几个参数我这里单独解释。temperature0.3是有意的——客服场景要的是稳定、准确的回复不是创造性发挥温度越低输出越保守。如果你的客服语气经常飘检查是不是有人把 temperature 改高了。max_tokens512控制在 512是因为客服回复不需要长文而且 token 数直接关系到接口响应时间生成 500 个 token 和生成 2000 个 token 的耗时差距可能超过一秒。array_slice($context, -5)只取最近 5 轮上下文这是控制 prompt 长度和成本的关键——不是所有历史都要带上3 轮之前的对话对当前问题的参考价值已经很低带上反而会干扰模型。返回结构用[ok bool, msg string]统一包一层调用方拿到okfalse时走兜底话术。这个封装模式在 PHP 项目里很实用避免了到处写try/catch处理 cURL 错误。4.3 人工客服转接逻辑别让 AI 和人工抢话转人工是客服系统里最容易做砸的一环。常见错误是 AI 识别到“转人工”后既回了一句“已为您转接”又继续在后续消息里抢答。这套源码的解决方式是把转人工做成“状态切换”而不是“一次性回复”。// manual_transfer.php ?php if ($intent manual) { // 1. 更新会话状态scene 从 0 变成 1 $db-update(wx_user, [scene 1], [openid $openid]); // 2. 推送通知到人工客服工作台企微群机器人 $webhook https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxx; $payload json_encode([ msgtype text, text [content 用户 {$openid} 请求转人工\n最后消息 . $question] ]); $ch curl_init($webhook); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: application/json]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_exec($ch); curl_close($ch); // 3. 给用户一个明确反馈 return 已为您转接人工客服请稍候人工接入后您可以直接描述问题。; }转人工这里还有一个细节状态切换之后后续所有用户消息都应该走“人工回复”分支而不是 AI 自动回复。实现上主入口判断scene字段如果已经是 1就直接把消息写入日志并转发到人工工作台不再调用askAI。条件判断要放在意图识别之前否则用户转人工之后又发一句“在吗”AI 又跳出来答一句“我在”人工客服和 AI 同时在回复体验很差。这套源码默认用企业微信群机器人做人工通知好处是客服不用登录额外工作台在企微群里就能回复坏处是企微群机器人只支持主动推送不支持双向接收所以人工在群里的回复内容需要人工手动登记。如果要做到完全双向得接客服工作台或企业微信的「微信客服」API那是下一步的升级方向。5. 避坑指南微信接口调不通九成原因都在这里5.1 现象一直报invalid signature本地验证却是对的本地用verify_signature.php测签名能通过一上正式环境就报invalid signature页面显示“该公众号暂时无法提供服务”。原因九成是公众号后台的 Token 和源码配置的 Token 不一致或者修改过 Token 后没有重新提交服务器配置。微信公众平台每次修改 Token、URL、EncodingAESKey 都需要点击“提交”并等待生效很多人改了配置忘了点提交。解决先在公众号后台把 Token 复制出来对比/config/config.php里的wechat.token保证逐字符一致。然后重新点一次“提交”微信会立刻向你的服务器地址发一条验证请求。如果提交后仍然报错打开 Nginx 的 access log 看这条 GET 请求有没有到达你的服务器——没到达说明 URL 或端口不对到达了但报错说明签名验证代码有问题检查排序是否为SORT_STRING。5.2 现象用户收到两条一样的回复或者回复顺序错乱AI 回复会在用户发出消息后两秒左右到达但偶尔会收到两条内容相同的回复。原因微信服务器在 5 秒内没收到你的被动回复响应时会重试发送同一条消息。如果你的代码里 AI 调用耗时接近 5 秒微信已经重试了你的程序处理了两次请求于是回复发了两遍。另一个常见场景是异步回复路径没做好幂等——同一个MsgId处理了两次。解决在入口处记录MsgId同一个MsgId只处理一次。另外检查 AI 调用的超时配置ai.timeout必须小于 5 秒建议 3.5 秒以内。如果 AI 偶尔超时返回兜底话术“正在为您查询请稍候”而不是静默结束——至少用户有反馈。5.3 现象从明文模式切到安全模式后一堆乱码或“解密失败”本地明文模式跑得好好的生产环境为了安全切到兼容模式或安全模式结果消息全乱码。原因安全模式下微信推送的 XML 里Encrypt字段是密文需要 AES 解密才能拿到原文。很多代码只写了明文解析的分支没处理密文分支或者 EncodingAESKey 填错了解密自然失败。解决切换模式前先确认encodingAESKey是 43 位且和公众号后台一致。兼容模式下收到的消息是「密文 明文」并存优先解析密文。安全模式下收到的 XML 直接是密文解密用的aes_key是EncodingAESKey 之后做 base64 decode不是直接原字符串。这个细节最容易掉坑里。5.4 现象AI 回复了“你好”但用户其实刚关注公众号用户首次关注公众号时微信会推送一条event类型的消息subscribe事件没有Content字段。你的代码没做消息类型过滤把事件消息当成文本消息丢给 AIAI 回了句“你好有什么可以帮您”但用户根本还没开口说话。原因入口只判断了MsgType是不是text没排除event。关注事件、菜单点击事件、扫码事件都属于event它们没有Content。解决入口处先判断MsgType——只对text消息走完整 AI 链路event消息单独写一个分支比如subscribe就回复欢迎语或推送菜单。这样既不会让 AI 答非所问也避免把事件消息误写入聊天日志污染统计数据。5.5 现象小程序端接入时获取不到用户手机号在小程序里想拿用户手机号做身份绑定但接口返回用户拒绝授权或者根本弹不出授权框。原因微信小程序登录获取手机号有两个硬前提。第一必须是用户主动点击按钮触发的授权不能是页面加载时静默调用第二按钮必须用微信官方开放的button open-typegetPhoneNumber组件不是普通button。如果你用服务端 API 直接调phonenumber.getPhoneNumber没经过用户点按微信会直接拒绝。解决前端把open-typegetPhoneNumber放到按钮上用户在按钮点击事件里授权拿到code之后传给后端后端用phoneCode换手机号。如果这套源码的小程序端没有实现这个交互你需要自己补一个授权按钮页面。基于 openid 已经能完成客服功能手机号只是锦上添花的身份冗余优先级可以往后放。6. 进阶让 AI 客服会多轮对话并学会用知识库兜底把基础链路跑通之后下一步值得做的是两件事多轮对话的上下文裁剪和知识库检索兜底。先说上下文裁剪。前面讲过array_slice($context, -5)只取最近 5 轮但这是一个静态的窗口。更好的做法是按 token 数估算动态窗口比如系统 prompt 占 200 token知识库命中内容占 300 token那么留给对话历史的只有 500 token按 1024 上限算。可以写一段脚本遍历 context 数组每轮消息按“中文字符数 × 1.5 4”估算 token 数从最近一轮往前累加超过预算就截断。这样能保证每个请求的 prompt 长度稳定不会因为对话轮数多了之后触发模型输入上限报错。知识库兜底是另一个独立模块。你肯定不希望用户问“你们发什么快递”这种 FAQ 也绕一圈大模型知识库命中的答案应该直接返回。做法是先建一张faq表字段就三列question、answer、keywords然后写一个检索函数function searchFaq(string $question): ?string { $keywords preg_split(/[。?! ]/, $question); $rows $db-query(SELECT * FROM faq ORDER BY CHAR_LENGTH(keywords)); $best null; $bestScore 0; foreach ($rows as $row) { $score 0; $kwList explode(,, $row[keywords]); foreach ($kwList as $kw) { if (mb_strpos($question, $kw) ! false) { $score; } } $score $score / count($kwList); // 命中率 if ($score $bestScore) { $bestScore $score; $best $row; } } return ($bestScore 0.5) ? $best[answer] : null; }命中率阈值0.5意思是问题里至少一半关键词命中才返回答案低于阈值就返回null再走大模型。这套简单的“关键词命中率检索”方案在小规模知识库几百条以内下效果够用条目过千时再考虑向量化检索用text2vec之类的模型把问题和知识库条目都转成向量算余弦相似度。不要一上来就上向量库先用关键词方案撑住量级成本最低也最容易排查问题。验证整个系统是否真的好用我的习惯是准备一组 20 条测试用例分四类高频 FAQ5 条、需要多轮上下文的问题5 条、直接转人工的请求5 条、刁钻越界问题5 条。每条用例记录三个指标是否 5 秒内响应、回复内容是否可接受、是否错误触发了转人工。跑完之后算一个「AI 有效解决率」低于 70% 就回去调意图识别和 prompt高于 90% 再考虑放开全量。这套源码给我的整体印象是“骨架搭得非常稳肉得自己长”。微信通道、会话管理、签名验证这些硬骨头它都处理好了你要做的只是接入自己的模型和知识库。从那以后我每次部署微信侧的 AI 客服项目都会强制走一遍「比对 Token → 本地伪造消息 → 正式环境联调 → 压测超时」这四步宁可慢十分钟也不让线上翻车。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?