☰
PHP+ThinkPHP客服系统接入AI知识库:关键词匹配与人工转接实战
2026/11/2 17:49:48 网站建设 项目流程

简介:这份资源面向使用 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_HOST=127.0.0.1 # DB_NAME=customer_service # DB_USER=root # DB_PASS=你的密码 # 4. 导入表结构(假设项目带了 sql 文件) mysql -u root -p customer_service < database/install.sql # 5. 启动内置服务器测试 php think run

composer 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 会话状态卡在“等待人工”不动

现象是用户点了转人工,后台客服端看不到。原因是状态更新和队列查询用的字段不一致,比如更新时写了status=2,查询时却按state=2查。这类问题没有捷径,把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 跑,对比命中率和响应时间。我踩过的坑是接口方法签名一开始定得太窄,只返回答案字符串,后来想加“置信度”和“命中来源”就得改所有实现,所以返回值用数组、留好扩展位。

从那以后我每次抽接口都先把返回值结构想清楚再动手,宁可多包一层数组,也不让调用方去猜。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询