PHP+ThinkPHP在线客服系统接入AI知识库:架构设计与实现
简介这份资源面向使用 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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 里、回答可用率人工判断答案是否可接受、转人工率不该转的转了多少。这三个数一摆方案的说服力就出来了。测试类别样本数检索命中率回答可用率说明高频 FAQ30不低于 90%不低于 85%知识库覆盖充分长尾问题30不低于 70%不低于 60%允许转人工兜底编号查询20不低于 85%不低于 80%关键词路起作用多轮追问20不低于 75%不低于 70%看上下文管理答辩演示我建议按“正常问答 → 低分转人工 → 改文档再问 → 模型超时降级”四步走每步都有预期结果评委问什么都能现场复现。演示脚本提前写好别现场手敲问题。有个小技巧把测试集里最典型的三个问题做成快捷按钮演示时点按钮比打字稳也显得系统完整。从那以后我每次带项目验收前都强制自己先跑一遍测试集再上台因为现场翻车的代价远大于提前半小时跑数据。希望这套结构和实现细节能帮到你照着改改就能变成你自己的方案。本文还有配套的精品资源点击获取