
简介这份资源面向使用 PHP 开发在线客服系统的开发者与运维人员重点解决客服系统接入 AI 知识库时的环境配置与部署问题基于 ThinkPHP 框架构建适合具备一定 PHP 基础、希望快速落地智能客服的中级开发者参考。压缩包共 2010 个文件约 68.72MB以 js、html、css 等前端资源为主辅以 xml、json、md、txt 等配置与说明文档另有 131 个 php 业务文件、3 个 sql 数据库脚本及少量 sh 部署脚本整体结构完整覆盖前后端与数据层。资源附带安装教程明确列出需安装 fileinfo、redis 插件并说明在 php.ini 中禁用 pcntl_signal、pcntl_fork 等进程控制函数帮助读者规避常见环境报错。目前已有 138 人学习读者可据此获得一套可直接部署的客服系统源码、AI 知识库接入思路与完整环境配置清单减少从零搭建与调试的时间成本。1. 客服系统接上知识库一个 PHP 项目能解决的真实痛点半夜两点客户在网站上点开客服窗口问“你们这个套餐能不能开票”值班的只有一台没人盯的电脑。传统 PHP 在线客服系统在这种场景下基本抓瞎——要么留言等白天回复要么靠人工客服轮班硬扛。这个项目要干的事很直接在已有的 PHP 在线客服系统里接入一个 AI 知识库让机器人先接住常见问题答不上来再转人工。技术栈是 PHP ThinkPHP 框架适合手里已经有一套客服系统、想低成本加一层智能问答能力的开发者也适合想拿一个完整业务场景练手 ThinkPHP 的人。它不追求大模型自研重点在“知识库怎么管、问题怎么匹配、会话怎么流转”这套工程链路。2. 项目定位与技术选型为什么是 ThinkPHP 而不是别的2.1 这个项目到底在解决什么先把边界划清楚。它不是要做一个通用大模型对话产品而是在一个已经存在的在线客服系统上加一个“知识库问答”的能力层。用户打开聊天窗口消息先进入系统系统拿这句话去知识库里找最接近的答案找到就直接回找不到就标记为“需人工”推给在线客服。整个过程对前端聊天窗口是透明的用户感知不到背后是机器人还是人。这种定位决定了技术选型不会太激进。项目正文里明确是 PHP ThinkPHP说明它面向的是大量已经用 PHP 写业务的中小团队。这类团队的现实是服务器上跑着 PHP 环境数据库是 MySQL前端可能是 jQuery 或者简单的 Vue运维不会为了一个客服功能去上一套 Python 推理服务。所以知识库的匹配逻辑大概率是“关键词 相似度”这种能在 PHP 里直接算的方案而不是调外部大模型 API。常见做法是把知识库条目存进 MySQL用分词或关键词命中做召回再按命中权重排序取 Top1。提示如果你的场景里问题表述非常口语化、同义表达多纯关键词召回会不够用这时候才需要考虑引入外部语义模型但那已经超出这个项目的原始范围。2.2 技术栈拆解与目录结构ThinkPHP 的项目结构有比较固定的约定这个客服系统大概率遵循标准的多应用或单应用布局。下面是我按这类项目常见组织方式还原的目录骨架实际下载后可以对照着看project/ ├── app/ │ ├── controller/ # 控制器层处理 HTTP 请求 │ │ ├── Chat.php # 聊天消息入口 │ │ ├── Knowledge.php # 知识库增删改查 │ │ └── Admin.php # 后台管理 │ ├── model/ # 模型层对应数据表 │ │ ├── KnowledgeModel.php │ │ ├── SessionModel.php │ │ └── MessageModel.php │ ├── service/ # 业务逻辑匹配算法放这里 │ │ └── MatchService.php │ └── middleware/ # 中间件鉴权、日志 ├── config/ │ └── database.php # 数据库连接配置 ├── public/ │ └── index.php # 入口文件 ├── route/ │ └── app.php # 路由定义 └── vendor/ # Composer 依赖这个结构的关键点在于service层。很多 PHP 项目习惯把逻辑全塞控制器里但这个项目如果要把“匹配”这件事做干净就必须单独抽一个服务类。控制器只负责收参数、调服务、返回 JSON匹配逻辑全部在MatchService里这样后面换算法、加缓存都不用动控制器。2.3 数据表设计知识库和会话怎么存知识库的核心表至少要有这几张字段是我按这类系统常见设计补的表名关键字段作用knowledgeid, question, answer, keywords, category_id, status存问答对和关键词categoryid, name, sort知识分类sessionid, user_id, status, created_at一次会话messageid, session_id, role, content, created_at会话里的每条消息knowledge表里的keywords字段很关键它存的不是原始问题而是人工提炼过的关键词串比如“开票,发票,报销凭证”。匹配时先拿用户输入去撞这些关键词命中越多分越高。message表的role字段区分user和bot这样前端渲染时能直接按角色显示左右气泡。3. 核心链路拆解一条消息从进入到返回经历了什么3.1 消息入口与参数校验聊天消息的入口控制器是整个系统的咽喉所有用户输入都从这里进。ThinkPHP 里通常这样写// app/controller/Chat.php public function send() { // 接收前端 POST 过来的参数 $sessionId input(post.session_id/d, 0); $content input(post.content/s, ); // 基础校验会话必须存在内容不能为空 if (!$sessionId || $content ) { return json([code 400, msg 参数不完整]); } // 内容长度限制防止超长文本拖垮匹配 if (mb_strlen($content) 500) { return json([code 400, msg 内容过长]); } // 交给服务层处理控制器不写业务 $result (new \app\service\MatchService())-handle($sessionId, $content); return json([code 0, data $result]); }这段代码的逻辑很直白先收session_id和content/d和/s是 ThinkPHP 的输入过滤标记分别强制转整型和字符串。校验分三层——会话存在性、内容非空、长度上限。长度限制这一步很多人会省但实际跑起来用户粘贴一大段合同文本进来匹配算法会直接卡住所以 500 字的上限是必要的保护。最后把活交给MatchService控制器保持薄。3.2 知识库匹配算法关键词命中加权重排序匹配服务是整个项目最值得看的部分。下面是一个能在 PHP 里直接跑的召回实现// app/service/MatchService.php public function handle(int $sessionId, string $content): array { // 1. 先落库用户消息保证会话记录完整 MessageModel::create([ session_id $sessionId, role user, content $content, ]); // 2. 从知识库取所有启用状态的条目 $items KnowledgeModel::where(status, 1)-select(); $best null; $bestScore 0; foreach ($items as $item) { $score 0; // 关键词字段按逗号拆开逐个撞用户输入 $keywords explode(,, $item[keywords]); foreach ($keywords as $kw) { $kw trim($kw); if ($kw ! mb_strpos($content, $kw) ! false) { $score 2; // 关键词命中权重高 } } // 原始问题做一次相似度兜底 similar_text($content, $item[question], $percent); $score $percent / 100; if ($score $bestScore) { $bestScore $score; $best $item; } } // 3. 分数低于阈值视为没匹配上转人工 if ($bestScore 1.5) { return [type human, msg 正在为您转接人工客服]; } // 4. 命中则落库机器人回复并返回 MessageModel::create([ session_id $sessionId, role bot, content $best[answer], ]); return [type bot, answer $best[answer], score $bestScore]; }逐段说。第一步先落用户消息这是为了会话记录可追溯后面人工接手时能看到用户问过什么。第二步取全量知识库这里没有做分页是因为知识库条目通常不会太大几百到几千条在 PHP 里遍历完全扛得住如果条目上万就得改成先按分类或关键词倒排索引召回。第三步是打分核心关键词命中一次加 2 分similar_text算出的百分比除以 100 作为补充分这样“关键词精确命中”永远优先于“字面相似”。阈值 1.5 是个经验值意味着至少命中一个关键词或者相似度极高才算数低于这个就转人工。注意similar_text是 PHP 内置函数性能一般知识库超过五千条时建议换成先分词再倒排或者加一层 Redis 缓存热门问题的匹配结果。3.3 会话流转机器人和人工怎么交接匹配不上转人工这个“转”不是简单返回一句话就完事。会话表里要有个状态字段来标记当前会话归谁管// 转人工时更新会话状态 SessionModel::where(id, $sessionId)-update([ status 2, // 1机器人接待 2等待人工 3人工接待中 updated_at date(Y-m-d H:i:s), ]);状态机是 1 到 2 到 3。机器人接待时状态是 1匹配失败置为 2 进入等待队列人工客服接入后置为 3。后台客服端轮询或走 WebSocket 拉取状态为 2 的会话列表接单后改成 3。这个设计的好处是职责清晰机器人只管答答不了就挂起人工端只管从队列里捞。常见翻车点是状态没加锁两个客服同时接同一个会话解决办法是在更新时加where(status, 2)条件受影响行数为 0 就说明被别人抢了。4. 跑起来环境准备到第一条机器人回复4.1 环境依赖与安装步骤跑这个项目需要 PHP 7.4 以上ThinkPHP 6 的要求MySQL 5.7 以上Composer 包管理器。步骤按顺序来# 1. 进入项目目录安装依赖 composer install # 2. 复制环境配置文件 cp .env.example .env # 3. 编辑 .env填数据库连接 # DB_HOST127.0.0.1 # DB_NAMEcustomer_service # DB_USERroot # DB_PASS你的密码 # 4. 导入表结构假设项目带了 sql 文件 mysql -u root -p customer_service database/install.sql # 5. 启动内置服务器测试 php think runcomposer install会拉vendor目录如果卡住就换国内镜像源。.env文件是 ThinkPHP 6 的标准配置方式数据库信息填错是最常见的启动失败原因报错一般是“连接被拒绝”或“未知数据库”。php think run默认起在 8000 端口浏览器访问http://127.0.0.1:8000能看到聊天页面就说明入口通了。4.2 知识库初始化与验证系统跑起来后第一件事是往知识库里塞几条测试数据不然机器人永远转人工INSERT INTO knowledge (question, answer, keywords, status) VALUES (怎么开发票, 在个人中心-订单详情页点击申请开票填写抬头后三个工作日内开出。, 开票,发票,报销, 1), (支持哪些付款方式, 目前支持微信、支付宝和对公转账。, 付款,支付,转账, 1);插完数据在聊天窗口输入“我要开发票”如果返回的是开票那条答案说明匹配链路通了。如果还是转人工按这个顺序排查知识库status是不是 1、keywords字段有没有多余空格、用户输入里有没有包含关键词。这一步验证完整个核心闭环就算跑通了。5. 避坑与排查上线前必须过的几道坎5.1 中文匹配失效英文却正常现象是输入“发票”匹配不上输入“invoice”反而能命中。原因是similar_text和mb_strpos对中文的处理依赖字符编码如果数据库连接字符集不是utf8mb4中文在存取过程中会变成乱码关键词自然撞不上。解决是把config/database.php里的charset改成utf8mb4同时确认表字段也是这个字符集。改完要清一次缓存ThinkPHP 的配置缓存不清不生效。5.2 机器人答非所问总是命中同一条现象是无论问什么都返回知识库里第一条。原因是打分逻辑里similar_text对短文本的百分比波动很大两条不相关的问题也可能算出 30% 以上加上关键词没命中时全靠这个分排序就乱了。解决办法是给相似度分设上限比如最高只算 0.5 分确保关键词命中的 2 分始终占主导同时把阈值从 1.5 提到 2.0宁可转人工也不乱答。5.3 会话状态卡在“等待人工”不动现象是用户点了转人工后台客服端看不到。原因是状态更新和队列查询用的字段不一致比如更新时写了status2查询时却按state2查。这类问题没有捷径把SessionModel里所有涉及状态的读写点列出来对一遍。另一个可能是客服端轮询接口被浏览器缓存了 GET 请求加个时间戳参数就能解决。5.4 并发下消息重复入库现象是同一句话在会话记录里出现两次。原因是前端发送按钮没做防抖用户连点两下发了两条请求。后端层面可以在message表加一个基于session_id content 时间窗口的去重判断但更省事的做法是前端发送后立即禁用按钮收到响应再恢复。两边都做最稳。6. 二次开发把匹配层换成可插拔的这个项目最值得动手改的地方是MatchService。现在的实现把“取数据、打分、落库”揉在一个方法里想换算法就得改这个方法容易改出问题。我一般会把它拆成一个接口加多个实现// app/service/matcher/MatcherInterface.php interface MatcherInterface { // 输入用户问题返回匹配到的知识条目或 null public function match(string $content): ?array; } // app/service/matcher/KeywordMatcher.php class KeywordMatcher implements MatcherInterface { public function match(string $content): ?array { // 这里放原来的关键词打分逻辑 // 返回 [answer ..., score ...] 或 null } }然后在MatchService里根据配置决定用哪个实现$matcherClass config(chat.matcher, KeywordMatcher::class); $matcher new $matcherClass(); $hit $matcher-match($content);这样以后要加语义匹配只需要新写一个SemanticMatcher实现同一个接口改一行配置就切换不用动会话流转和落库逻辑。验证方法是准备一组测试问题分别用两个 matcher 跑对比命中率和响应时间。我踩过的坑是接口方法签名一开始定得太窄只返回答案字符串后来想加“置信度”和“命中来源”就得改所有实现所以返回值用数组、留好扩展位。从那以后我每次抽接口都先把返回值结构想清楚再动手宁可多包一层数组也不让调用方去猜。希望帮到你。本文还有配套的精品资源点击获取