简介:这是一份基于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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;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> <CreateTime>1735689600</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填公众号原始 ID(gh_开头),FromUserName填任意 openid,CreateTime用当前 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 参数对照表:一个都别填错
公众号后台基本配置里那几项,和这套源码的配置项是一一对应的。我做过一张对照表,每次部署都照着填:
| 公众平台字段 | 源码配置项 | 说明 |
|---|---|---|
| AppID | wechat.appid | 公众号的唯一标识,wx开头 |
| AppSecret | wechat.appsecret | 与 AppID 成对,注意不要提交到 Git |
| Token | wechat.token | 你自定义的英文字符串,签名校验共用 |
| EncodingAESKey | wechat.encodingAESKey | 43 位密钥,安全模式下必填 |
| 消息加解密方式 | wechat.aes_mode | 明文/兼容/安全三选一 |
| 服务器地址(URL) | 回调路由 | 公众号后台填https://域名/wx/callback |
最容易填错的是 AppSecret 和 Token 混淆。AppSecret 是微信分配的一串随机字符,Token 是你自己起的任意字符串,两者用途完全不同——AppSecret 用于换取access_token,Token 只用于验证签名。曾经见过有人把 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_source='fallback'的记录拉出来,把高频的 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']]; }几个参数我这里单独解释。temperature=0.3是有意的——客服场景要的是稳定、准确的回复,不是创造性发挥,温度越低输出越保守。如果你的客服语气经常飘,检查是不是有人把 temperature 改高了。max_tokens=512控制在 512,是因为客服回复不需要长文,而且 token 数直接关系到接口响应时间,生成 500 个 token 和生成 2000 个 token 的耗时差距可能超过一秒。array_slice($context, -5)只取最近 5 轮上下文,这是控制 prompt 长度和成本的关键——不是所有历史都要带上,3 轮之前的对话对当前问题的参考价值已经很低,带上反而会干扰模型。
返回结构用['ok' => bool, 'msg' => string]统一包一层,调用方拿到ok=false时走兜底话术。这个封装模式在 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?key=xxx'; $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字段。你的代码没做消息类型过滤,把事件消息当成文本消息丢给 AI,AI 回了句“你好,有什么可以帮您”,但用户根本还没开口说话。
原因:入口只判断了MsgType是不是text,没排除event。关注事件、菜单点击事件、扫码事件都属于event,它们没有Content。
解决:入口处先判断MsgType——只对text消息走完整 AI 链路,event消息单独写一个分支,比如subscribe就回复欢迎语或推送菜单。这样既不会让 AI 答非所问,也避免把事件消息误写入聊天日志污染统计数据。
5.5 现象:小程序端接入时,获取不到用户手机号
在小程序里想拿用户手机号做身份绑定,但接口返回用户拒绝授权,或者根本弹不出授权框。
原因:微信小程序登录获取手机号有两个硬前提。第一,必须是用户主动点击按钮触发的授权,不能是页面加载时静默调用;第二,按钮必须用微信官方开放的<button open-type="getPhoneNumber">组件,不是普通<button>。如果你用服务端 API 直接调phonenumber.getPhoneNumber,没经过用户点按,微信会直接拒绝。
解决:前端把open-type="getPhoneNumber"放到按钮上,用户在按钮点击事件里授权;拿到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 条测试用例,分四类:高频 FAQ(5 条)、需要多轮上下文的问题(5 条)、直接转人工的请求(5 条)、刁钻越界问题(5 条)。每条用例记录三个指标:是否 5 秒内响应、回复内容是否可接受、是否错误触发了转人工。跑完之后算一个「AI 有效解决率」,低于 70% 就回去调意图识别和 prompt,高于 90% 再考虑放开全量。
这套源码给我的整体印象是“骨架搭得非常稳,肉得自己长”。微信通道、会话管理、签名验证这些硬骨头它都处理好了,你要做的只是接入自己的模型和知识库。从那以后我每次部署微信侧的 AI 客服项目,都会强制走一遍「比对 Token → 本地伪造消息 → 正式环境联调 → 压测超时」这四步,宁可慢十分钟也不让线上翻车。希望帮到你。
本文还有配套的精品资源,点击获取