简介:这份资源面向使用 PHP 开发在线客服系统的开发者,尤其是基于 ThinkPHP 框架搭建运营级客服平台的技术人员,重点解决客服系统接入 AI 知识库时的环境配置与部署问题。压缩包共 2010 个文件,约 68.72MB,以 613 个 js、368 个 html、258 个 css 等前端资源为主,另有 131 个 php 业务文件、3 个 sql 数据库脚本及 json、xml 配置数据,并附安装教程文档,覆盖插件安装、php.ini 禁用函数调整等关键环节。已有 138 人学习关注。读者可从中获得一套可直接部署的客服系统源码,理解 fileinfo、redis 扩展的配置要点,掌握 pcntl 系列函数的禁用处理方式,并借助 AI 知识库接入思路实现智能问答与快速响应,适合需要落地企业级客服场景的中级 PHP 开发者参考。
1. 需求先立住:为什么客服系统要接知识库,而不是堆话术
做过在线客服的都知道,最耗人力的不是接待量,而是重复问题。用户问“订单到哪了”“退款多久到账”“发票怎么开”,十个人问八遍,客服复制粘贴到手软。传统做法是配一堆快捷话术,但话术是死的,用户换个问法就匹配不上,最后还得人工兜底。这套基于 PHP 和 ThinkPHP 的在线客服接入 AI 知识库方案,解决的就是“让机器先接住高频问题,接不住的再转人工”这件事。它适合两类人:一是要交毕设或课程设计的学生,需要一套结构完整、能跑通、能答辩的系统;二是中小团队里要快速搭一套客服后台的开发者,不想从零造轮子。核心思路不复杂——把企业自己的文档、FAQ、产品说明灌进知识库,用户提问时先检索再生成回答,PHP 侧只负责会话、权限和转人工的调度。
2. 总体架构设计:四层结构与知识库检索链路
2.1 分层职责划分
这套系统我一般拆成四层,每层职责单一,出问题好定位。接入层负责 HTTP 入口、会话鉴权和限流,用 ThinkPHP 的路由和中间件就能覆盖;业务层管会话生命周期、消息落库、转人工判定;知识层是核心,负责文档切片、向量化、检索召回;模型层对接大模型接口,把检索到的片段拼成提示词再生成回答。四层之间用接口解耦,知识层和模型层可以独立替换,比如换一个向量库或换一个模型供应商,业务层代码基本不动。
分层的好处是答辩时能讲清楚“为什么这么设计”。很多同学一上来就把检索和生成写在一个控制器里,代码能跑,但一被问“知识库更新了怎么办”“模型超时怎么降级”就答不上来。分层之后,每个问题都有明确的归属层。
2.2 一次问答的完整链路
用户发一条消息,链路是这样的:接入层校验会话 token,业务层把消息写入消息表并标记状态为“待处理”,然后调用知识层检索。知识层把问题向量化,在向量库里做相似度搜索,取回 Top-K 片段,同时用关键词检索做一路补充,两路结果合并去重后按分数排序。模型层拿到片段和原始问题,拼成提示词调用大模型,返回答案后业务层落库、推给前端。如果检索最高分低于阈值,或者模型返回“无法回答”,就触发转人工,把会话标记为“待人工接入”,推送到客服工作台。
这条链路里有两个关键参数:Top-K 和相似度阈值。Top-K 一般取 3 到 5,取太多提示词会超长,取太少召回不够;阈值通常设在 0.75 左右,低于这个分数说明知识库里没有相关内容,硬答不如不答。这两个值不是拍脑袋定的,要在验收阶段用测试集调。
2.3 技术选型说明
PHP 侧用 ThinkPHP 6,主要是路由、中间件、ORM 都成熟,学生上手快,团队维护成本低。向量检索不一定非要上重型向量数据库,数据量在几万条以内,用 MySQL 存向量加 PHP 侧算余弦相似度也能跑,但性能一般;更稳妥的做法是接一个轻量向量库,或者用支持向量检索的扩展。模型接口走 HTTP,封装成一个服务类,超时和重试在类里统一处理。会话存储用 Redis,消息历史落 MySQL,这样既能快速读写,又能持久化。
提示:选型时别追求“最新最热”,要看你答辩时能不能讲清楚每个组件为什么存在。向量库不是必须的,但“为什么需要它”必须能答。
3. 知识库模块实现:文档切片、向量化与检索召回
3.1 文档入库与切片策略
知识库的质量直接决定回答质量。企业文档通常是 Word、PDF、Markdown,先统一转成纯文本,再按语义切块。切片不能按固定字数硬切,会把一句话切断。我一般按段落切,单块控制在 300 到 500 字,块之间留 50 字左右重叠,避免上下文丢失。每块带上来源文档名和章节标题,检索命中后能追溯出处,答辩时这也是加分项。
// 文档切片服务:按段落聚合,控制块大小并保留重叠 class ChunkService { public function split(string $text, int $maxLen = 400, int $overlap = 50): array { $paragraphs = preg_split('/\n\s*\n/u', trim($text)); $chunks = []; $buffer = ''; foreach ($paragraphs as $p) { $p = trim($p); if ($p === '') continue; // 单段超长时先按句号切,避免整段塞进一个块 if (mb_strlen($p) > $maxLen) { $sentences = preg_split('/(?<=[。!?])/u', $p); foreach ($sentences as $s) { if (mb_strlen($buffer . $s) > $maxLen && $buffer !== '') { $chunks[] = $buffer; $buffer = mb_substr($buffer, -$overlap) . $s; } else { $buffer .= $s; } } } else { if (mb_strlen($buffer . $p) > $maxLen && $buffer !== '') { $chunks[] = $buffer; $buffer = mb_substr($buffer, -$overlap) . $p; } else { $buffer .= $p; } } } if ($buffer !== '') $chunks[] = $buffer; return $chunks; } }这段代码的关键在$maxLen和$overlap两个参数。$maxLen控制单块上限,太大检索粒度粗,太小语义不完整;$overlap是块间重叠字数,保证跨块的问题不会因为切断而丢上下文。实际调的时候,FAQ 类文档可以切小一点,产品手册切大一点,按文档类型配不同参数。
3.2 向量化与存储
切片完成后逐块调向量化接口,拿到向量后存库。表结构至少要有块 ID、文档 ID、块内容、向量、来源标题、创建时间。向量字段用文本存 JSON 数组即可,检索时在 PHP 侧反序列化算相似度;数据量大了再换向量库。这里有个血泪经验:向量化接口要加批量处理和失败重试,几百个块逐条调很容易超时,批量一次传 16 到 32 条,失败的单条重试两次,还失败就记日志跳过,别让一个坏块卡住整个入库流程。
CREATE TABLE kb_chunk ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, doc_id INT UNSIGNED NOT NULL, content TEXT NOT NULL, embedding TEXT NOT NULL COMMENT 'JSON 数组形式的向量', source_title VARCHAR(255) DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_doc (doc_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.3 混合检索与重排
纯向量检索对专有名词和编号不敏感,比如“订单号 A1234”这种,向量相似度可能召回不到。所以检索要做两路:向量路取 Top-10,关键词路用 MySQL 全文索引或 LIKE 取 Top-10,合并后按加权分数排序,向量分权重 0.7,关键词分权重 0.3,去重后取前 5 条送进模型。这个权重不是固定的,业务问题偏口语就调高向量权重,偏编号查询就调高关键词权重。验收阶段用一批真实问题跑一遍,看召回率再定。
4. 会话与转人工模块:状态机、上下文与降级策略
4.1 会话状态机设计
会话不能只有“进行中”一个状态,否则转人工、超时、关闭全混在一起。我一般定义五个状态:bot(机器接待)、waiting(等待人工)、human(人工接待)、closed(已关闭)、timeout(超时关闭)。状态流转有明确规则:机器接待中用户点“转人工”或触发低分兜底,进入waiting;客服接入变human;用户主动结束或长时间无消息变closed或timeout。每次流转记一条日志,答辩时拿状态图讲,比讲代码逻辑清楚得多。
// 会话状态流转:集中管理合法迁移,避免控制器里到处 if class SessionStateMachine { private const ALLOWED = [ 'bot' => ['waiting', 'closed', 'timeout'], 'waiting' => ['human', 'closed', 'timeout'], 'human' => ['closed', 'timeout'], ]; public function transit(array $session, string $to): array { $from = $session['status']; if (!in_array($to, self::ALLOWED[$from] ?? [], true)) { throw new \RuntimeException("非法状态流转: {$from} -> {$to}"); } $session['status'] = $to; $session['updated_at'] = date('Y-m-d H:i:s'); return $session; } }把合法迁移集中在一处,好处是新增状态时只改一个地方,也方便写单元测试。很多同学把状态判断散落在各个方法里,最后自己都说不清哪些流转是允许的,一测就翻车。
4.2 多轮上下文管理
用户不会一句话把问题说全,经常是“退款”之后跟一句“多久到账”。上下文管理要做两件事:一是把最近 N 轮对话拼进提示词,N 一般取 3 到 5,太多会稀释当前问题;二是做指代消解,把“它”“这个”替换成上一轮的主语。简单做法是把最近几轮用户消息和机器回答一起送进模型,让模型自己理解;更稳的做法是在检索前用一轮小模型调用改写问题,把上下文补全再检索。前者省成本,后者召回准,按预算选。
4.3 转人工触发与降级
转人工的触发条件要写死,不能靠模型“感觉”。我一般设三条:检索最高分低于阈值、模型返回无法回答标记、用户连续两次点转人工。触发后会话进waiting,同时给客服工作台推一条待接入通知。模型接口超时或报错时走降级:直接返回检索到的原文片段加一句“以下内容供参考”,并提示可转人工。降级逻辑一定要有,答辩时被问“模型挂了怎么办”能答上来,就是实打实的工程思维。
注意:转人工阈值和模型超时时间要写进配置文件,别硬编码在代码里。验收时调参全靠改配置,硬编码会让你改到怀疑人生。
5. 避坑与常见问题排查:上线前最容易翻车的五件事
5.1 检索有结果但回答答非所问
现象是用户问 A,机器答了相关的 B。原因通常是切片粒度过大,一个块里混了多个主题,检索命中块但模型抓错了重点。解决办法是把切片调小,或者在块里保留章节标题作为上下文前缀,让模型知道这段属于哪个主题。另一个原因是 Top-K 取太大,噪声片段挤掉了正确片段,把 K 降到 3 再试。
5.2 向量入库慢到超时
现象是导入几百篇文档时脚本跑一半挂掉。原因是逐条调向量化接口,网络往返次数太多。解决是改批量接口,一次传 16 到 32 条,配合失败重试和断点续传,记录已处理的块 ID,重跑时跳过。入库脚本要能重复执行,别做成一次性的。
5.3 会话状态错乱导致重复转人工
现象是用户点一次转人工,工作台出现多条待接入。原因是前端重复提交或后端没做幂等。解决是在状态流转前加锁,用 Redis 对会话 ID 加短锁,流转成功后释放;同时前端按钮点击后置灰。状态机里waiting只能从bot进入,重复请求会被非法流转拦截,这也是集中管理状态的好处。
5.4 模型返回内容带 Markdown 或多余前缀
现象是前端展示一堆**和“根据提供的信息”。原因是提示词没约束输出格式。解决是在提示词里明确“只输出答案正文,不要解释,不要 Markdown”,并在后端做一次清洗,去掉常见前缀。清洗规则要可配置,别写死一堆正则。
5.5 知识库更新后旧答案还在
现象是文档改了,机器还答旧内容。原因是向量库没同步更新,或者缓存没失效。解决是文档更新时先删旧块再重新切片入库,用文档 ID 做关联删除;检索结果加一层缓存时,缓存 key 带上知识库版本号,版本变了缓存自然失效。这个坑很隐蔽,验收时一定要测“改文档再问”的场景。
6. 验收与答辩:用测试集和演示脚本把方案讲圆
验收不是跑通一个问答就完事,要拿数据说话。我一般准备一个 50 到 100 条问题的测试集,覆盖高频问题、长尾问题、带编号的查询、多轮追问四类,每条标注标准答案或应命中的文档块。跑一遍系统,统计三个指标:检索命中率(正确块是否在 Top-5 里)、回答可用率(人工判断答案是否可接受)、转人工率(不该转的转了多少)。这三个数一摆,方案的说服力就出来了。
| 测试类别 | 样本数 | 检索命中率 | 回答可用率 | 说明 |
|---|---|---|---|---|
| 高频 FAQ | 30 | 不低于 90% | 不低于 85% | 知识库覆盖充分 |
| 长尾问题 | 30 | 不低于 70% | 不低于 60% | 允许转人工兜底 |
| 编号查询 | 20 | 不低于 85% | 不低于 80% | 关键词路起作用 |
| 多轮追问 | 20 | 不低于 75% | 不低于 70% | 看上下文管理 |
答辩演示我建议按“正常问答 → 低分转人工 → 改文档再问 → 模型超时降级”四步走,每步都有预期结果,评委问什么都能现场复现。演示脚本提前写好,别现场手敲问题。有个小技巧:把测试集里最典型的三个问题做成快捷按钮,演示时点按钮比打字稳,也显得系统完整。
从那以后我每次带项目,验收前都强制自己先跑一遍测试集再上台,因为现场翻车的代价远大于提前半小时跑数据。希望这套结构和实现细节能帮到你,照着改改就能变成你自己的方案。
本文还有配套的精品资源,点击获取