资讯动态

原生PHP搭建AI智能客服后台:规则引擎与知识库实战指南

发布时间:2026/10/14 3:56:30 来源:尧图企业网站定制
接到一个很有意思的项目需求用原生 PHP 搭建一个 AI 智能客服管理系统的后台。这东西放在两三年前大家第一反应肯定是上 Python或者抱各种大模型平台的大腿。但实际做下来你会发现原生 PHP 在特定场景下反而有它不可替代的优势——部署简单、成本低、跟现有业务系统同构。这篇文章我就把这套系统的设计思路、核心代码、避坑经验全部摊开讲代码可以直接拿去改部署步骤也给你写清楚。我做过几版类似的客服系统踩过不少坑从最开始的单文件跑通到后来拆出相对清晰的分层整个过程里最深的体感是AI 客服这个AI不一定是大模型规则引擎加合理的知识库设计在绝大多数中低并发业务场景里完全够用而且可控性极强。如果你后续确实需要接入真正的 NLP 服务或大模型接口这套系统的分层设计也能让你平滑切换不至于推翻重来。1. 项目整体设计与技术选型思路1.1 为什么用 PHP 来实现 AI 客服后台先回答一个很多人会问的问题都 2025 年了为什么还用 PHP 写 AI 客服我的答案很现实这套系统的核心价值不在于 AI 算法而在于业务落地。客服后台要解决的绝大多数问题是访客消息接入、知识库命中、会话管理、转人工、数据统计。这些恰好是 PHP 最擅长的应用层业务逻辑。我见过不少团队把客服系统搞得极度复杂微服务、消息队列、向量数据库一套组合拳下来光运维就要养两个人。实际上对于日活几千、并发几十的业务体量一台 2 核 4G 的服务器跑原生 PHP 加 MySQL绰绰有余。成本一个月几十块部署只需要一个 Nginx 加 PHP-FPM没有任何额外的运行时依赖。另外还有一点很实际PHP 的部署生态跟虚拟主机、宝塔面板这类环境极度兼容。很多企业已有的业务系统就是 PHP 写的客服后台直接部署在同一台机器上数据互通、会话关联都方便。你让非技术出身的运维去折腾 Python 虚拟环境和 Gunicorn人家真的会崩溃。注意我这里说的是原生 PHP不依赖 Laravel、ThinkPHP 这类框架。原生 PHP 对于这个体量的项目完全够用而且代码透明、可控性强出问题直接改不用去翻框架源码。如果你公司内部已有成熟的框架规范那用框架改造也是顺手的事核心逻辑不变。1.2 系统功能规划与技术架构拆解这套系统的核心功能我划分成四大模块模块核心功能技术要点消息接入层接收访客消息、发送回复轮询替代 WebSocket降低实现复杂度机器人引擎关键词匹配、知识库检索规则引擎 加权评分可平滑升级为大模型接口管理后台会话监控、转人工、知识库维护前后端分离PHP 提供 JSON API数据存储层会话记录、消息记录、知识条目MySQL 三张核心表索引优化到位架构上我刻意不做消息队列不做 Redis 缓存——除非后续量级真的大到撑不住。原因很简单架构的复杂度应该跟着业务量走而不是跟着技术潮流走。多引入一个组件就是多一个故障点。通信方案上我选择了前端轮询而不是 WebSocket。主要是考虑到部署和运维成本。轮询 2 秒一次对于客服这个场景完全够用。后面如果要做实时性更高的功能再引入 Swoole 或者直接上 GatewayWorker。一开始就上长连接各种断线重连、心跳保活的问题会把你折磨得怀疑人生。2. 核心模型拆解AI 回复的底层逻辑2.1 意图识别不靠大模型靠特征匹配很多人一听AI 客服就以为必须接大模型 API这是最大的误解。这一版系统的核心是一个基于评分机制的规则引擎它比传统的 if-else 硬编码要灵活得多。实现的逻辑是这样的知识库里的每一条问答都配置多个触发关键词每个关键词带一个权重值。用户提问时系统把问题拆成词元再去知识库中匹配按命中的关键词权重求和排序得分最高且超过设定阈值的条目作为回复。举个例子知识库里有这么一条问题如何修改收货地址 关键词改地址权重 10、修改收货权重 8、地址错了权重 6、换地址权重 5用户问我想换个收快递的地址分词后得到换收快递地址。命中换地址权重 5和地址收快递这些相关词总分 11超过阈值 8命中这条问答。这个方案最大的优点就是可控性和可解释性。每一条回复为什么触发后台一查权重清清楚楚不会出现大模型那种看着合理但实际胡说八道的情况。在客服这个场景里答错一次可能就要丢一个客户稳定性比花哨更重要。我的建议是规则引擎做底层保底后期如果要接大模型让规则引擎先做一次初筛。高置信度的直接返回低置信度的再甩给大模型兜底。这样既享受了大模型的泛化能力又保住了规则引擎的稳定性。2.2 知识库设计AI 客服的弹药库如果说引擎是大脑那知识库就是弹药库。这个系统的知识库设计成多级分类 标准问题 扩展问法的结构。每条知识包含三块信息标准问题比如你们发货用哪家快递扩展问法把发什么快递物流是谁送的走什么物流这类变体都挂在这条知识下标准答案一段完整的回复文本这个结构参考了客服行业沉淀下来的 FAQ 管理方法论。扩展问法不是随便填的得从真实客服聊天记录里提炼。我见过不少团队知识库上线后命中率低问题就出在扩展问法都是开发自己猜的跟用户真实表达方式严重脱节。知识库的维护功能必须做进管理后台让运营人员可以随时增删改。这条看着简单实际上特别容易被开发忽略——代码写完了才发现知识只能写死在 PHP 数组里运营每次改答案都得找开发效率低到令人发指。2.3 转人工的触发机制规则引擎再好也得有兜底方案。这套系统里我设计了三个转人工触发条件无答案没有匹配到任何高置信度条目自动转人工用户明确要求消息中命中人工客服转人工等关键词立即转人工连续未命中连续两轮消息都未命中知识库主动引导转人工每个会话维护一个is_human状态字段。当这个字段为 1 时后续所有访客消息都直接进入人工客服工作台机器人不再参与回复。这个状态机是整个会话管理的核心必须理清楚。3. 数据库设计与核心表结构3.1 会话与消息表撑起整个客服流程数据库设计上我坚持核心表尽量少字段尽量精简的原则。整张库就三张核心表cs_sessions、cs_messages、cs_knowledge外加一张管理员表。cs_sessions会话表的设计如下CREATE TABLE cs_sessions ( id int(11) NOT NULL AUTO_INCREMENT, visitor_id varchar(64) NOT NULL COMMENT 访客唯一标识, customer_name varchar(50) DEFAULT NULL COMMENT 访客名称, is_human tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否转人工 0机器人 1人工, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待处理 1进行中 2已结束, last_message text COMMENT 最后一条消息内容, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_visitor (visitor_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;is_human和status这两个字段是查询高频字段必须建索引。visitor_id用于识别同一个访客的历史会话这块要跟前端埋点配合访客第一次进入页面时由后端生成一个唯一 ID写入前端 Cookie后续所有请求都带上这个 ID。cs_messages消息表我更关注查询效率CREATE TABLE cs_messages ( id int(11) NOT NULL AUTO_INCREMENT, session_id int(11) NOT NULL COMMENT 所属会话ID, sender_type tinyint(1) NOT NULL COMMENT 0访客 1机器人 2人工客服, content text NOT NULL COMMENT 消息内容, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_session (session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实际项目里有个要注意的细节消息内容字段类型我用的是text而不是varchar。原因很简单访客可能会贴一段很长的报错信息varchar(255)根本不够用。早期版本吃过这个亏消息直接被 MySQL 截断客服要看完整信息都看不到。3.2 知识库表支撑意图匹配的结构cs_knowledge表的结构设计为多级分类与触发词关联CREATE TABLE cs_knowledge ( id int(11) NOT NULL AUTO_INCREMENT, category_id int(11) DEFAULT NULL COMMENT 分类ID, standard_question varchar(255) NOT NULL COMMENT 标准问题, answer text NOT NULL COMMENT 标准答案, keywords text NOT NULL COMMENT 触发关键词集(JSON数组), hit_count int(11) NOT NULL DEFAULT 0 COMMENT 命中次数用于后续优化, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 0禁用 1启用, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;keywords字段我存的是 JSON 格式的数组每项包含关键词和权重。这比另外建一张关联表在实际操作中要灵活得多——后台直接一整块 JSON 提交不涉及 JOIN 查询性能更好。反正每次匹配就是把整条记录读出来内存里做计算数据库不需要智能。特别的优化点在于hit_count字段。每命中一次就UPDATE cs_knowledge SET hit_count hit_count 1 WHERE id ?。这个数据积累下来配合对话记录就是后续优化知识库最有价值的数据依据。哪些问答被问得最多哪些答案命中率高但客户还是不满意一目了然。3.3 管理员表与会话关联管理员表非常简单字段只有id、username、password_hash、real_name、create_time。密码用password_hash()函数生成明文绝对不进数据库。登录态用 PHP Session 维护。这里有个我在项目里踩过的坑管理员处理会话时需要在cs_sessions表里加一个assignee_id字段记录当前是哪位客服在接管。不加这个字段多客服同时在线时一个访客的消息会被多个客服看到然后抢着回复或者以为别人会回复结果都没人处理。ALTER TABLE cs_sessions ADD COLUMN assignee_id int(11) DEFAULT NULL COMMENT 当前客服ID;4. 核心代码实现机器人回复引擎4.1 消息接入入口的封装所有的外部请求访客发消息、轮询新回复统一走一个入口文件api.php通过action参数区分是收消息还是拉消息。统一入口的好处是方便做鉴权、日志和参数校验只暴露一个文件给外部访问。?php // api.php - 前端统一接口入口 require_once config.php; require_once db.php; require_once bot_engine.php; require_once session.php; $action $_GET[action] ?? ; $allowed_actions [send_message, poll_messages, create_session]; if (!in_array($action, $allowed_actions)) { json_response([code 400, msg 无效的action]); } // 会话鉴权所有请求必须携带有效的visitor_id $visitor_id $_POST[visitor_id] ?? ; if (empty($visitor_id) || !preg_match(/^[a-zA-Z0-9]{8,32}$/, $visitor_id)) { json_response([code 401, msg 无效的访客ID]); } try { switch ($action) { case create_session: $result create_session($visitor_id); break; case send_message: $result handle_send_message($visitor_id); break; case poll_messages: $result handle_poll_messages($visitor_id); break; } json_response([code 0, data $result]); } catch (Exception $e) { error_log([客服系统] . $e-getMessage()); json_response([code 500, msg 服务器开小差了请稍后再试]); }为什么 session 状态放在服务端而不是前端传过来因为服务端必须掌握会话的最终控制权。如果前端拿着is_human 0就能自己决定是机器人回还是人工回那整个转人工机制就形同虚设了。访客端永远只传visitor_id服务端根据 session 状态判断走哪条逻辑这是安全底线。4.2 机器人回复引擎的完整实现接下来是整个系统的核心bot_engine.php。这里实现了分词、知识匹配、权重评分和转人工判断的完整逻辑。?php // bot_engine.php - 机器人回复引擎 /** * 简单的分词函数 * 基于常用中文停用词表过滤再做二元词组组合 * 真正的分词方案需要引入第三方库这里用字符串切割 二元组合 */ function simple_tokenize($text) { $stopwords [的, 了, 吗, 呢, 啊, 在, 是, 我, 你, 他, 什么, 怎么, 如何, 哪些, 可以]; $clean preg_replace(/[^\x{4e00}-\x{9fa5}a-zA-Z0-9]/u, , $text); // 连续提取二字词、三字词作为候选特征 $tokens []; $len mb_strlen($clean, UTF-8); for ($i 0; $i $len - 1; $i) { $tokens[] mb_substr($clean, $i, 2, UTF-8); } for ($i 0; $i $len - 2; $i) { $tokens[] mb_substr($clean, $i, 3, UTF-8); } // 过滤停用词去重 return array_values(array_unique(array_filter($tokens, function($w) use ($stopwords) { return !in_array($w, $stopwords) mb_strlen($w, UTF-8) 2; }))); } /** * 核心评分算法 * 遍历知识库对每条知识的keywords做命中累加 */ function match_knowledge($tokens, $min_score 8) { $pdo get_db_connection(); $sql SELECT * FROM cs_knowledge WHERE status 1 ORDER BY hit_count DESC LIMIT 200; $stmt $pdo-query($sql); $best_score 0; $best_item null; $match_details []; while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { $keywords json_decode($row[keywords], true); if (!is_array($keywords)) continue; $score 0; $hit_words []; foreach ($keywords as $kw) { $word $kw[word] ?? ; $weight $kw[weight] ?? 1; if (empty($word)) continue; // 判断关键词是否在原文或分词结果中出现 foreach ($tokens as $token) { if (mb_strpos($token, $word) ! false || mb_strpos($word, $token) ! false) { $score $weight; $hit_words[] $word; break; } } } $details [ id $row[id], standard_question $row[standard_question], score $score, hit_words $hit_words ]; $match_details[] $details; if ($score $best_score) { $best_score $score; $best_item $row; } } // 记录匹配详情到日志便于后续优化知识库 if ($best_score $min_score) { // 更新命中次数 $update $pdo-prepare(UPDATE cs_knowledge SET hit_count hit_count 1 WHERE id ?); $update-execute([$best_item[id]]); return [ found true, score $best_score, answer $best_item[answer], knowledge_id $best_item[id] ]; } return [found false, score $best_score, details $match_details]; } /** * 检测是否要求转人工 */ function is_human_request($text) { $patterns [人工, 转人工, 人工客服, 真人, 在线客服, 活人, 客服人员]; foreach ($patterns as $p) { if (mb_strpos($text, $p) ! false) { return true; } } return false; } /** * 主回复逻辑 */ function get_bot_reply($session_id, $user_message) { $pdo get_db_connection(); // 1. 查询会话状态 $stmt $pdo-prepare(SELECT * FROM cs_sessions WHERE id ?); $stmt-execute([$session_id]); $session $stmt-fetch(PDO::FETCH_ASSOC); if (!$session) { throw new Exception(会话不存在); } // 2. 如果已经是人工接待状态直接返回提示不走机器人逻辑 if ($session[is_human] 1) { return [type human, reply ]; } // 3. 检查是否要求转人工 if (is_human_request($user_message)) { // 将会话标记为转人工 $update $pdo-prepare(UPDATE cs_sessions SET is_human 1, status 1 WHERE id ?); $update-execute([$session_id]); return [ type transfer_human, reply 您好正在为您转接人工客服请稍候... ]; } // 4. 分词并进行知识库匹配 $tokens simple_tokenize($user_message); $result match_knowledge($tokens); if ($result[found]) { return [ type answer, reply $result[answer], score $result[score] ]; } // 5. 未命中知识库检查连续未命中次数连续2次就转人工 $fail_count $session[fail_count] ?? 0; $new_fail_count $fail_count 1; if ($new_fail_count 2) { $update $pdo-prepare(UPDATE cs_sessions SET is_human 1, fail_count ? WHERE id ?); $update-execute([$new_fail_count, $session_id]); return [ type transfer_human, reply 连续两次没有理解您的意思正在为您转接人工客服... ]; } $update $pdo-prepare(UPDATE cs_sessions SET fail_count ? WHERE id ?); $update-execute([$new_fail_count, $session_id]); return [ type fallback, reply 抱歉我暂时没有理解您的问题。您可以换个方式描述或者说人工转接客服为您服务。 ]; }这个引擎我有几点想特别说明关于简单分词方案如果你只依赖 PHP 内置函数对中文的支持确实是短板。上面的simple_tokenize用连续提取二字词 三字词 停用词过滤的方式做了一个轻量分词原理其实就是枚举所有子串作为候选特征再跟知识库关键词做子串包含判断。这个方法在关键词是专业名词或独特词汇时命中率不错比如发货退款但遇到你们“他们”这种代词还是要靠停用词过滤。如果你需要更专业的分词引入一个分词扩展比如scws或者直接调第三方分词 API替换掉simple_tokenize就可以了。注意match_knowledge里的关键词匹配我用了双向mb_strpos判断。为什么要双向因为用户打的字可能跟知识库关键词有细微差异比如发贷错别字和发货。单向包含能命中一部分双向能提高容错率。但也别指望完全靠这个解决错别字问题——更彻底的方式是用编辑距离算法做模糊匹配这块可以根据业务需要扩展。4.3 消息收发完整流程串联下面这条链路是整个系统的业务主线访客在网页端输入消息前端通过api.php?actionsend_messagePOST 提交后端保存访客消息到cs_messages然后调用get_bot_reply()机器人引擎返回回复内容后端再把机器人回复保存到cs_messages前端通过api.php?actionpoll_messages轮询拉取新消息间隔 2 秒管理员在后台看到会话列表点击进入人工接管发送的消息在sender_type标记为 2handle_send_message和handle_poll_messages是会话的收口直接决定整个流程能否跑通?php // session.php - 会话管理逻辑 function create_session($visitor_id) { $pdo get_db_connection(); // 先查这个访客有没有进行中的会话 $stmt $pdo-prepare(SELECT * FROM cs_sessions WHERE visitor_id ? AND status IN (0, 1) ORDER BY id DESC LIMIT 1); $stmt-execute([$visitor_id]); $session $stmt-fetch(PDO::FETCH_ASSOC); if ($session) { return [session_id (int)$session[id], is_new false]; } // 创建新会话 $stmt $pdo-prepare(INSERT INTO cs_sessions (visitor_id, create_time, update_time) VALUES (?, NOW(), NOW())); $stmt-execute([$visitor_id]); $new_id (int)$pdo-lastInsertId(); // 写入一条欢迎语 $stmt $pdo-prepare(INSERT INTO cs_messages (session_id, sender_type, content, create_time) VALUES (?, 1, ?, NOW())); $welcome_msg 您好我是智能客服小研。请问有什么可以帮您您可以咨询物流、退换货、订单等问题。; $stmt-execute([$new_id, $welcome_msg]); return [session_id $new_id, is_new true]; } function handle_send_message($visitor_id) { $pdo get_db_connection(); $content trim($_POST[content] ?? ); if (empty($content)) { throw new Exception(消息内容不能为空); } if (mb_strlen($content, UTF-8) 500) { throw new Exception(消息长度不能超过500字); } // 找到该访客的进行中会话没有就创建一个 $session get_active_session($visitor_id); if (!$session) { $session_info create_session($visitor_id); $session get_active_session($visitor_id); } $session_id (int)$session[id]; // 1. 保存访客消息 $stmt $pdo-prepare(INSERT INTO cs_messages (session_id, sender_type, content, create_time) VALUES (?, 0, ?, NOW())); $stmt-execute([$session_id, $content]); // 2. 更新会话的last_message和update_time $stmt $pdo-prepare(UPDATE cs_sessions SET last_message ?, update_time NOW() WHERE id ?); $stmt-execute([mb_substr($content, 0, 100), $session_id]); // 3. 判断是机器人回还是人工回 if ($session[is_human] 1) { // 人工接管中机器人不回复等客服来 return [session_id $session_id, need_human true]; } // 4. 机器人回复 $bot_result get_bot_reply($session_id, $content); if (!empty($bot_result[reply])) { // 机器人回复存库 $stmt $pdo-prepare(INSERT INTO cs_messages (session_id, sender_type, content, create_time) VALUES (?, 1, ?, NOW())); $stmt-execute([$session_id, $bot_result[reply]]); } return [ session_id $session_id, reply $bot_result[reply] ?? , type $bot_result[type] ?? answer ]; } function handle_poll_messages($visitor_id) { $pdo get_db_connection(); $session get_active_session($visitor_id); if (!$session) { return [messages [], is_human 0]; } // 获取从这个会话开始的所有消息限制最近的50条 $stmt $pdo-prepare(SELECT sender_type, content, create_time FROM cs_messages WHERE session_id ? ORDER BY id ASC LIMIT 50); $stmt-execute([(int)$session[id]]); $messages $stmt-fetchAll(PDO::FETCH_ASSOC); return [ messages $messages, is_human (int)$session[is_human] ]; }特别注意handle_send_message里的一个业务判断当机器人返回类型为transfer_human时前端要立即切换到人工客服在线的 UI 状态并提示访客已转接人工请稍候。5. 管理后台的搭建与关键功能5.1 会话监控与人工接管流程管理后台我用的是最简单的 PHP 原生页面加 AJAX 轮询。页面布局分三块左侧会话列表、右侧对话窗口、顶部是当前客服信息和退出按钮。没有引入前端框架直接原生 JavaScript 加fetchAPI 轮询接口每 2 秒刷新一次会话列表和当前对话内容。人工接管的核心逻辑?php // admin_api.php - 管理后台接口需要登录态 function admin_takeover_session() { check_admin_login(); // 未登录直接返回401 $session_id intval($_POST[session_id] ?? 0); $admin_id $_SESSION[admin_id]; $pdo get_db_connection(); // 原子更新防止两个客服同时接管同一个会话 $sql UPDATE cs_sessions SET is_human 1, status 1, assignee_id ? WHERE id ? AND (assignee_id IS NULL OR assignee_id ?); $stmt $pdo-prepare($sql); $stmt-execute([$admin_id, $session_id, $admin_id]); $affected $stmt-rowCount(); if ($affected 0) { // 检查是否被其他客服接管 $check $pdo-prepare(SELECT assignee_id FROM cs_sessions WHERE id ?); $check-execute([$session_id]); $row $check-fetch(PDO::FETCH_ASSOC); if ($row $row[assignee_id] ! $admin_id) { return [code 1, msg 该会话已被其他客服接管]; } } return [code 0, msg 接管成功]; } function admin_send_message() { check_admin_login(); $session_id intval($_POST[session_id] ?? 0); $content trim($_POST[content] ?? ); $admin_id $_SESSION[admin_id]; if (empty($content)) { return [code 1, msg 消息不能为空]; } $pdo get_db_connection(); // 验证这个会话确实由当前客服接管 $stmt $pdo-prepare(SELECT id FROM cs_sessions WHERE id ? AND assignee_id ?); $stmt-execute([$session_id, $admin_id]); if (!$stmt-fetch()) { return [code 1, msg 您未接管该会话无法发送]; } // 保存客服消息 $stmt $pdo-prepare(INSERT INTO cs_messages (session_id, sender_type, content, create_time) VALUES (?, 2, ?, NOW())); $stmt-execute([$session_id, $content]); $pdo-prepare(UPDATE cs_sessions SET last_message ?, update_time NOW() WHERE id ?) -execute([mb_substr($content, 0, 100), $session_id]); return [code 0, msg 发送成功]; }这里用了原子 UPDATE 加行数判断来防止两个客服同时接管同一个会话。这种乐观锁的思路在并发场景下比先 SELECT 再 UPDATE 要可靠得多。注意一个细节客服接管时会往cs_sessions写入is_human 1。这时如果访客刚好发来一条新消息handle_send_message会发现已经是人工接管状态不会让机器人回复直接等客服来答。这个联动逻辑就是前面反复强调的服务端控制会话状态的意义。5.2 知识库管理功能运营人员的操作台知识库管理页面尽量做得傻瓜化因为使用者大概率不是程序员。界面上就三个操作新增问答、编辑问答、禁用问答。但是背后有几个小的隐藏功能点值得提一下。批量导入是必须的。知识库冷启动的时候几百条问答靠手工录入会录到怀疑人生。我选择支持 CSV 导入列格式固定为标准问题、标准答案、关键词1:权重1|关键词2:权重2|...导入时按行解析校验必填字段给出导入报告。写了两个多小时省了后续几十个小时的重复劳动。命中调试功能是我后来加上的。后台提供一个输入框输入任意语句系统会模拟整个匹配流程返回命中了哪些知识、得分多少、命中了哪些关键词。这个功能极其有用——知识库维护者可以实时验证自己的关键词配得对不对而不需要真的拿一个访客页面去测。上线一段时间后强烈建议定期看hit_count排名。命中次数最多的问答就是你的知识库 MVP。如果这些问答仍然产生大量转人工不是客户难搞是你的答案质量不行——需要人工客服打磨更好的回复然后更新知识库答案。5.3 统计报表用数据驱动知识库迭代报表功能别做太重三个核心指标就够用指标计算方式价值机器人解决率未转人工的会话数 / 总会话数衡量知识库整体质量平均响应时长SUM(机器人回复时间间隔) / 回复次数衡量体验找出慢接口知识命中 TOP10按hit_count排序指导优化重点统计脚本放在 cron 里每天凌晨跑一次汇总昨天的数据。报表页面不需要实时计算性能没有压力。6. 部署上线全流程6.1 环境准备与目录结构到部署环节了。这套系统对运行环境的要求极低PHP 7.4 以上、MySQL 5.7 以上、Nginx 或 Apache。不需要 Composer 安装依赖不需要 Redis不需要任何扩展库这也是为什么我说它适合低成本落地。生产环境目录结构建议这样组织/var/www/ai-chat/ ├── api.php # 前端访客接口 ├── admin_api.php # 后台管理员接口 ├── config.php # 配置文件 ├── db.php # PDO 数据库连接 ├── bot_engine.php # 机器人引擎 ├── session.php # 会话管理逻辑 ├── admin/ │ ├── index.html # 管理后台页面 │ ├── login.html # 登录页面 │ └── assets/ │ ├── style.css │ └── admin.js └── sql/ └── init.sql # 建表脚本6.2 部署步骤详解部署我拆成六步照着做就行第一步初始化数据库把上面的建表 SQL 保存为init.sql执行mysql -u root -p /var/www/ai-chat/sql/init.sql第二步导入初始知识库找运营要一份 FAQ 文档整理成 CSV在管理后台上传导入。没有初始数据的话机器人就是个空壳问什么都命中不了。第三步配置 config.php?php // config.php define(DB_HOST, 127.0.0.1); define(DB_PORT, 3306); define(DB_NAME, ai_chat); define(DB_USER, ai_chat_user); define(DB_PASS, 你的强密码); define(CHAT_APP_KEY, 前端接口调用凭证建议生成一个随机的32位字符串); define(DEBUG_MODE, false);注意CHAT_APP_KEY记得生成一个随机长字符串前端每次请求带上这个 key后端校验通过才允许调用。不然任何第三方都能往你的客服系统灌垃圾消息会话表很快就会被垃圾数据撑爆。第四步配置 Nginx 站点server { listen 80; server_name chat.example.com; root /var/www/ai-chat; index index.html; location / { try_files $uri $uri/ 404; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }第五步设置目录权限chown -R www-data:www-data /var/www/ai-chat chmod -R 755 /var/www/ai-chat mkdir -p /var/log/ai-chat chown www-data:www-data /var/log/ai-chat第六步验证上线访问http://chat.example.com/api.php?actioncreate_sessionvisitor_idtest001看接口返回是否正常。然后通过前端页面发一条你们发什么快递确认机器人能匹配上。6.3 上线前的自检清单这个清单是我自己每次上新项目都会过一遍的数据库连接串、前端 app_key 是否正确时区配置是否统一为Asia/Shanghai否则会话时间跟客服看到的时间对不上消息内容是否做了 XSS 过滤客服端和管理端都要做后端htmlspecialchars至少来一圈后台账号是否启用了强密码策略password_hash是否生效接口超时时间是否设置了合理的值轮询接口我设置了 30 秒超时Cron 日志脚本是否运行正常7. 常见问题与排查技巧实录7.1 中文乱码问题这是 PHP 里最经典的问题没有之一。现象是数据库里存的正常页面上显示乱码或者反过来。排查步骤按顺序来确认 PHP 文件本身是 UTF-8 编码保存——IDE 右下角看一下编码格式PDO 连接串必须声明 UTF-8new PDO($dsn, $user, $pass, [PDO::MYSQL_ATTR_INIT_COMMAND SET NAMES utf8mb4])确认数据库和表的字符集都是utf8mb4不是utf8页面meta charsetUTF-8最后检查中间件——Nginx 有时会因响应头缺charset造成浏览器默认按系统编码解析其中第 2 步是最容易被忽略的。很多人的 PDO 连接是这样写的new PDO(mysql:host127.0.0.1;dbnameai_chat, $user, $pass);没指定编码默认可能是latin1中文一发进去就变成问号。必须这样new PDO(mysql:host127.0.0.1;dbnameai_chat;charsetutf8mb4, $user, $pass);7.2 轮询接口的性能优化上线跑了几天后你可能会发现会话多了页面卡顿。排查后发现瓶颈一般不在数据库而是每次轮询都全量查消息。优化方向有两个第一轮询接口加一个last_message_id参数前端传上次收到的最大 ID后端只查id last_message_id的数据。这一个改动能把绝大多数无用查询干掉。$last_id intval($_POST[last_message_id] ?? 0); $sql SELECT sender_type, content, create_time FROM cs_messages WHERE session_id ? AND id ? ORDER BY id ASC LIMIT 50;第二会话列表接口不要一次性拉全部字段。列表页只需要id, visitor_id, is_human, status, last_message, update_time只需要查询这几列别把整行SELECT *出去。7.3 高并发的锁表问题InnoDB 默认行级锁理论上不容易锁表。但我发现实际业务里频繁的UPDATE cs_sessions SET update_time NOW()在上万条数据量时如果update_time没有索引MySQL 可能走全表扫描造成锁的粒度扩大。解决方案很简单ALTER TABLE cs_sessions ADD INDEX idx_update_time (update_time);另外一个容易踩的坑是INSERT和UPDATE混合使用时事务隔离级别的问题。默认的REPEATABLE READ下如果长事务没提交其他事务的读可能看到旧快照。解决方式每个请求的操作尽量短平快不要在一个请求里开事务跑多个大查询。7.4 转人工后机器人还在回复这个问题特别典型。访客转了人工机器人还在自动回复导致人工和机器人同时回复把客户搞懵。排查思路确认handle_send_message里is_human的判断是在保存访客消息之后、调用机器人引擎之前。而且一定要读取的是最新会话状态而不是之前缓存的$session数组。我加的编码实践是// 在调用 get_bot_reply 之前强制重新查一次会话状态 $stmt $pdo-prepare(SELECT is_human FROM cs_sessions WHERE id ?); $stmt-execute([$session_id]); $latest_status $stmt-fetchColumn(); if ($latest_status 1) { return [session_id $session_id, need_human true]; }7.5 知识库匹配不到内容的调试思路如果你的系统老回抱歉没有理解先不要急着加关键词按下面的顺序排查后台的命中调试功能里把原句贴进去看分词结果是什么确认知识库里的keywords是不是 JSON 格式有没有因为写错导致解析失败确认阈值min_score设得是不是太高一般 6-8 比较合适看停用词表里是不是误杀了一些关键词比如怎么被过滤了但你的关键词恰好是怎么退货那就全完蛋知识库里的扩展问法有没有覆盖真实聊天记录里的表达方式——这是最根本的原因经验之谈很多团队上线客服系统知识库第一条问题就只是复述 FAQ 原文而真实用户根本不会那么问。上线前最好拿 100 条真实聊天记录去跑一遍匹配命中率低于 60% 就别急着推广。这里的电子表格就是最好的打磨工具。这套系统后续还能怎么玩如果这套系统跑顺了想再升级我给几个实际可落地的方向第一接入 LLM 作为兜底。规则引擎没匹配到时把用户问题和知识库里的标准问题一起打包发给大模型接口让它从给定资料里找答案。注意输出要限定只依据给定资料回答不要自行编造。保留规则引擎的返回答类型不变平滑升级。第二加入简单的情绪识别。检测到投诉差评退款生气这类关键词时自动提升会话权重提醒客服优先响应。第三做会话满意度评价。会话结束后访客可点满意/不满意不满意标记会像连坐一样指向对应知识条目驱动运营去改进。我个人在实际操作中最大的体会是技术方案永远要为业务目标服务。这套系统最早的版本堆了很多花哨功能多轮对话、敏感词拦截、客服排班后来发现大部分功能根本没人用反而把核心链路拖得又慢又乱。删掉之后系统稳定性和可维护性都上来了。客服系统的核心永远是那句话——让访客的问题得到准确、快速的解答让客服的工作变得轻松、有序。做到这两点技术老套一点也无所谓。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑