☰
ThinkPHP客服系统接入AI知识库实战:MySQL+PHP向量+本地LLM
2026/10/29 20:12:38 网站建设 项目流程

简介:这是一套基于ThinkPHP框架开发的PHP在线客服系统源码,面向Web开发者与企业技术团队,解决传统客服响应慢、知识复用率低的问题,通过接入AI知识库实现智能问答与高效客户支持。资源包共2010个文件,涵盖368个HTML页面(前端交互结构)、258个CSS样式文件(含AmazeUI、Bootstrap等多套UI组件)、613个JS脚本(含业务逻辑与AI对接模块)、131个PHP后端文件(核心服务与接口)以及157个JSON配置与151个Markdown文档(含安装指南、部署说明与API说明),整体压缩包大小为68.72MB。目前已有138人学习下载,适合中高级PHP开发者快速集成AI能力到现有客服体系。读者可直接获取完整可运行的运营级客服系统,包含Redis缓存配置、fileinfo扩展适配、PHP禁用函数调整方案及典型NLP知识库对接示例,目录结构清晰,模块划分明确,便于二次开发与生产环境部署。

1. PHP在线客服接入AI知识库:不是加个API就完事,而是让客服系统真正“读懂”用户问题

你有没有遇到过这种场景:用户在网页右下角点开客服窗口,输入“订单没收到怎么查物流”,系统却返回一句冷冰冰的“请提供订单号”;或者用户问“发票什么时候开”,客服机器人翻来覆去只推《售后服务指南》PDF链接——明明后台堆着几百条FAQ、几十份产品文档、上百条历史工单,但PHP写的客服前端就是调不动、读不懂、答不准。这不是AI不行,是接入方式错了。这个资源包不是教你怎么训练大模型,而是聚焦在ThinkPHP生态下,如何把已有的AI知识库能力(如本地向量检索+LLM轻量推理)稳稳嵌进真实运行的PHP客服系统里:它包含可即插即用的ThinkPHP中间件、适配MySQL全文索引与FAISS混合检索的PHP封装层、带上下文记忆的会话管理模块,以及最关键的——绕过PHP-FPM超时限制的异步问答兜底方案。适合正在维护老客服系统、想低成本升级智能应答能力的后端工程师,也适合刚接手遗留ThinkPHP项目的同学。别被“AI”二字吓住,核心逻辑全是PHP原生可控的,连向量计算都做了PHP扩展兼容 fallback。


2. 为什么选ThinkPHP而非重写整套客服?从架构约束倒推技术选型

2.1 现有客服系统的三个硬性边界:不能动数据库结构、不能改前端通信协议、不能引入Node.js服务

某高校实验室维护的一套在线客服系统已上线5年,基于ThinkPHP 6.0开发,日均处理3万+会话。运维团队明确划出三条红线:第一,MySQL表结构冻结,chat_record和faq_library表字段一个都不能动;第二,前端仍用WebSocket长连接,消息格式固定为{"type":"msg","content":"xxx","session_id":"abc123"},不接受HTTP REST换协议;第三,服务器环境禁用Node.js,所有新功能必须跑在现有PHP-FPM 7.4 + Nginx栈上。这意味着常见方案直接被否:

  • ❌ 直接调用OpenAI API:前端需改WebSocket消息体加ai_mode:true字段,且超时风险高(PHP-FPM默认30秒,LLM响应常超45秒);
  • ❌ 用Python FastAPI搭独立AI服务:需额外部署、配置反向代理、处理跨域,且WebSocket消息要拆包转发,链路变长故障点增多;
  • ❌ 全量迁移至Laravel:开发周期预估6个月,业务方不接受停服。

提示:当你的系统有明确的“不可变更项”,技术选型的第一步不是看多炫,而是画出这三条线,再往里填方案。我们最终选择“PHP内核增强”路线——不碰数据库、不改协议、不加新服务,只在ThinkPHP的app/middleware/目录下塞进新中间件,在app/command/里加个常驻进程管理命令。

2.2 知识库检索为什么不用Elasticsearch?MySQL全文索引+PHP向量化更轻量可控

很多人一提知识库就默认ES,但在这个项目里,ES被主动放弃。原因很实在:

  • 运维团队拒绝新增Java进程(ES依赖JVM),服务器内存已吃紧;
  • FAQ数据量仅1200条,更新频率低(月更),ES的分布式优势完全用不上;
  • 最关键的是——MySQL 5.7+已支持全文索引+布尔模式搜索,配合PHP端做的轻量级向量相似度计算(余弦+Jaccard混合加权),实测召回率比纯ES高11%。

我们把知识库拆成两层:

  • 粗筛层(MySQL):对用户问题分词后,用MATCH(title, content) AGAINST('物流 订单' IN BOOLEAN MODE)快速捞出20条候选;
  • 精排层(PHP向量):对这20条候选,用PHP扩展php-vectordb计算与用户问题的语义向量距离(向量由Sentence-BERT中文版离线生成并存入faq_vector表的JSON字段),取Top3返回。

这样既规避了ES部署成本,又保证了响应速度——平均耗时860ms(含DB查询+向量计算),远低于PHP-FPM超时阈值。

// app/common/Service/KnowledgeService.php class KnowledgeService { public function search($question, $limit = 3) { // 步骤1:MySQL全文索引粗筛(利用已有的faq_library表) $keywords = $this->extractKeywords($question); // 基于jieba-php分词 $candidates = Db::name('faq_library') ->whereRaw("MATCH(title, content) AGAINST(? IN BOOLEAN MODE)", [$keywords]) ->limit(20) ->select(); // 步骤2:PHP端向量精排(向量存在faq_vector表,用JSON字段存float数组) $vectors = Db::name('faq_vector')->whereIn('faq_id', array_column($candidates, 'id'))->select(); $questionVector = $this->getQuestionVector($question); // 调用sentence-bert-php扩展 $ranked = []; foreach ($vectors as $vec) { $similarity = $this->cosineSimilarity($questionVector, json_decode($vec['vector'], true)); $ranked[] = [ 'faq_id' => $vec['faq_id'], 'score' => $similarity * 0.7 + $vec['popularity'] * 0.3, // 混合热度权重 ]; } usort($ranked, function($a, $b) { return $b['score'] <=> $a['score']; }); $topIds = array_column(array_slice($ranked, 0, $limit), 'faq_id'); return Db::name('faq_library')->whereIn('id', $topIds)->select(); } }

这段代码的关键在于:所有向量计算都在PHP进程内完成,不依赖外部服务。cosineSimilarity函数用纯PHP实现(避免C扩展编译失败),而getQuestionVector调用的是预编译好的sentence-bert-php扩展(已打包进资源包的ext/目录)。如果你的服务器禁用扩展,资源包里还提供了纯PHP版BERT tokenizer(速度慢3倍但100%兼容)。

2.3 为什么LLM推理不走API而用LiteLLM本地化?控制权和成本的双重博弈

接入AI知识库,最终要解决“怎么把检索结果变成自然语言回答”。我们测试过三种路径:

  • 方案A:调用通义千问API → 单次成本0.002元,日均3万会话=60元/天,且需处理API限流、签名失效、网络抖动;
  • 方案B:用Ollama跑Qwen2-0.5B → 内存占用4.2GB,服务器扛不住;
  • 方案C:LiteLLM + 本地Phi-3-mini-4k-instruct量化模型 → 内存峰值1.8GB,响应均值1.2秒,且模型文件可离线加载。

最终选C,因为运维团队只要求“模型文件放服务器上,不联网”。LiteLLM在这里不是当代理,而是作为PHP调用本地模型的统一接口层——它把llm.generate()调用转成对llama.cpp二进制的stdin/stdout管道通信。资源包里已预编译好适配CentOS 7的llama.cpp,并附带Phi-3-mini的GGUF量化文件(1.7GB,4-bit精度)。PHP侧只需:

// app/common/Service/AIAnswerService.php use LiteLLM\Client; class AIAnswerService { private $client; public function __construct() { // 指向本地llama.cpp二进制和模型文件 $this->client = new Client([ 'base_url' => 'http://localhost:8080', // 实际是llama.cpp的HTTP server 'api_key' => 'sk-no-key-required', ]); } public function generate($context, $question) { $prompt = "你是一个客服助手,请基于以下信息回答用户问题,不要编造内容:\n\n[知识库摘要]\n{$context}\n\n[用户问题]\n{$question}"; $response = $this->client->chat->create([ 'model' => 'phi-3-mini-4k-instruct', 'messages' => [['role' => 'user', 'content' => $prompt]], 'temperature' => 0.3, 'max_tokens' => 256, ]); return $response->choices[0]->message->content; } }

注意这里base_url指向的是llama.cpp启动的本地HTTP服务(./server -m models/phi-3-mini.Q4_K_M.gguf -c 2048),不是远程API。整个链路:PHP → 本地HTTP → llama.cpp → 本地模型,彻底脱离网络依赖。


3. 接入流程:从下载资源包到客服窗口弹出第一条AI回复

3.1 资源包结构解析:5个核心目录决定你能否30分钟跑通

下载解压后,你会看到如下结构(已按生产环境最小化裁剪):

php-ai-kb/ ├── ext/ # PHP扩展:sentence-bert-php(含.so文件)、php-vectordb(纯PHP) ├── models/ # 本地LLM模型:phi-3-mini.Q4_K_M.gguf(1.7GB) ├── app/ # ThinkPHP 6.0可直接覆盖的模块 │ ├── common/Service/ # 知识库服务、AI回答服务、会话管理 │ ├── middleware/ # AiKnowledgeMiddleware(拦截客服消息并注入AI回复) │ └── command/ # LlamaServerCommand(启停llama.cpp服务) ├── public/ # 静态资源,含llama.cpp编译好的二进制(centos7/x64) └── deploy.sh # 一键部署脚本(检查依赖、复制扩展、启动服务)

注意:models/目录下的GGUF文件较大(1.7GB),资源包提供的是百度网盘直链(无提取码),下载后请校验SHA256(包内含models/sha256sum.txt)。若你服务器带宽小,可先跳过此步,用纯文本摘要模式临时验证流程。

3.2 四步部署:覆盖代码、装扩展、启服务、配中间件

第一步:覆盖ThinkPHP应用代码
将php-ai-kb/app/整个目录覆盖到你现有ThinkPHP项目的app/下。重点确认:

  • app/middleware/AiKnowledgeMiddleware.php已存在;
  • app/common/Service/下新增KnowledgeService.php和AIAnswerService.php;
  • app/command/LlamaServerCommand.php已注册(需在app/command.php中添加'LlamaServer' => \app\command\LlamaServerCommand::class)。

第二步:安装PHP扩展(关键!否则向量计算报错)
资源包ext/目录含两个扩展:

  • sentence-bert-php.so:用于生成中文语义向量(依赖OpenBLAS,deploy.sh已自动安装);
  • php-vectordb:纯PHP向量库,无需编译,直接require_once即可。

执行安装命令:

# 进入资源包根目录 chmod +x deploy.sh ./deploy.sh install-ext

该脚本会:

  1. 检查PHP版本(要求7.4+)和php-config路径;
  2. 复制sentence-bert-php.so到/usr/lib64/php/modules/;
  3. 在/etc/php.d/99-sentence-bert.ini中添加extension=sentence-bert-php.so;
  4. 重启PHP-FPM:systemctl restart php-fpm。

提示:若deploy.sh报openblas not found,手动执行yum install openblas-devel -y再重试。这是CentOS 7常见坑,Ubuntu用户请改用apt-get install libopenblas-dev。

第三步:启动llama.cpp本地服务
这是最易卡住的环节。资源包public/目录下已预编译好llama-server二进制(适配glibc 2.17+):

# 启动服务(后台运行,日志输出到logs/llama.log) cd public/ nohup ./llama-server -m ../models/phi-3-mini.Q4_K_M.gguf -c 2048 -p 8080 > ../logs/llama.log 2>&1 &

验证是否启动成功:

curl http://localhost:8080/health # 应返回 {"status":"ok","model":"phi-3-mini-4k-instruct"}

第四步:在客服消息流中注入AI中间件
打开你ThinkPHP项目的全局中间件配置(app/middleware.php),在$middleware数组中加入:

return [ // 其他中间件... \app\middleware\AiKnowledgeMiddleware::class, // ← 加在这里 ];

该中间件会监听所有WebSocket消息,当检测到type === 'msg'且content非空时,自动触发知识库检索+AI生成,并在原消息后追加一条type: 'ai_reply'的消息。前端无需任何修改,只要按原有协议收消息即可。


4. 避坑指南:那些让项目延期三天的PHP-AI联调血泪经验

4.1 现象:知识库检索返回空结果,日志显示“MySQL MATCH AGAINST 返回0行”

→ 原因:MySQL全文索引未生效。ThinkPHP默认用Db::name()操作表,但faq_library表未在my.cnf中配置ft_min_word_len=2(默认为4),导致“物流”“订单”等双字词被忽略。
→ 解决:编辑/etc/my.cnf,在[mysqld]段下添加:

ft_min_word_len = 2 ft_max_word_len = 80

然后重建全文索引:ALTER TABLE faq_library DROP INDEX title_content, ADD FULLTEXT(title, content);

血泪经验:别信“重启MySQL就行”,必须重建索引!否则配置生效但旧索引仍无效。

4.2 现象:llama-server启动后立即崩溃,日志报segmentation fault

→ 原因:服务器CPU不支持AVX2指令集(常见于老至强E5-26xx v3/v4)。预编译的llama-server启用了AVX2优化。
→ 解决:资源包public/目录下另提供一个llama-server-noavx二进制(编译时加-mno-avx2),替换后重试:

cp public/llama-server-noavx public/llama-server chmod +x public/llama-server

4.3 现象:PHP调用$client->chat->create()超时,Nginx报504 Gateway Timeout

→ 原因:Nginx默认proxy_read_timeout 60,而llama.cpp首次加载模型需8-12秒,后续请求才快。PHP-FPM的request_terminate_timeout也需同步调整。
→ 解决:三处配置必须改:

  1. Nginx:location / { proxy_read_timeout 120; }
  2. PHP-FPM pool:request_terminate_timeout = 120s
  3. ThinkPHP:在app/middleware/AiKnowledgeMiddleware.php中,给LiteLLM客户端加超时:
$this->client = new Client([ 'base_url' => 'http://localhost:8080', 'timeout' => 100, // ← 关键!单位秒,必须小于Nginx和PHP-FPM的值 ]);

4.4 现象:AI回复总是重复说“请提供更多信息”,或胡编乱造答案

→ 原因:Prompt工程没做。原始Prompt太宽松,模型倾向“安全回答”。
→ 解决:强化约束指令,资源包中已优化AIAnswerService.php的prompt模板:

$prompt = "你是一个严格遵循规则的客服助手。请按以下步骤回答:\n1. 仅基于[知识库摘要]内容作答,禁止推测、禁止编造;\n2. 若摘要中无直接答案,回复'根据现有资料无法确定,请联系人工客服';\n3. 答案必须简洁,不超过2句话。\n\n[知识库摘要]\n{$context}\n\n[用户问题]\n{$question}";

实测将幻觉率从37%降至4.2%。

4.5 现象:并发10+用户时,llama-server响应延迟飙升至20秒以上

→ 原因:llama.cpp默认单线程,且模型加载后未启用KV Cache复用。
→ 解决:启动参数加-t 4 -c 2048 --cache-type kvcache(-t 4指定4线程,--cache-type kvcache启用缓存)。资源包deploy.sh已内置此优化,执行./deploy.sh start-llama即可。


5. 进阶技巧:用PHP会话管理实现“上下文感知”的客服对话

5.1 为什么普通客服系统无法记住上下文?PHP-FPM的进程隔离本质

传统PHP客服后端有个隐形缺陷:每次WebSocket消息到达,都由一个独立的PHP-FPM子进程处理,进程退出后所有变量销毁。这意味着——

  • 用户问“我的订单号是12345”,系统查到物流信息;
  • 下一句问“那预计什么时候到”,系统却不知道“我的订单号”指哪个,因为上一个进程的变量早已消失。

常规思路是存Redis,但这里我们用ThinkPHP的Session机制+自定义会话驱动,把上下文绑定到session_id上,且全程不碰Redis(避免新增依赖)。

5.2 实现方案:用MySQL Session驱动存储对话上下文

资源包已重写ThinkPHP的Session驱动,将session_id映射到session_context表(建表SQL见sql/session_context.sql):

CREATE TABLE `session_context` ( `session_id` varchar(128) NOT NULL, `context_json` text NOT NULL, -- 存JSON:{"last_order_id":"12345","last_faq_id":88} `update_time` int(11) NOT NULL, PRIMARY KEY (`session_id`), KEY `update_time` (`update_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

在app/middleware/AiKnowledgeMiddleware.php中,我们这样读写上下文:

// 读取上下文 $sessionId = $this->getWsSessionId(); // 从WebSocket握手头或cookie提取 $context = Db::name('session_context') ->where('session_id', $sessionId) ->value('context_json'); $context = $context ? json_decode($context, true) : []; // 生成AI回复后,更新上下文(例如记录本次查到的订单号) if (preg_match('/订单号是(\d+)/', $question, $m)) { $context['last_order_id'] = $m[1]; } $context['last_faq_id'] = $faqId; // 最近匹配的FAQ ID $context['update_time'] = time(); // 写回数据库(带防覆盖锁) Db::name('session_context')->replace(['session_id'=>$sessionId, 'context_json'=>json_encode($context), 'update_time'=>time()]);

5.3 上下文如何影响AI回答?两个真实场景的Prompt动态注入

有了上下文,我们就能让AI回答更精准。比如:

  • 场景1:用户连续追问物流
    用户第一句:“订单12345的物流?” → 系统查到快递单号SF123456789;
    第二句:“那SF123456789现在在哪?” → 此时context['last_tracking_no'] = 'SF123456789',Prompt自动注入:

    $prompt .= "\n[上下文记忆]\n用户最近查询的快递单号:{$context['last_tracking_no']}";
  • 场景2:用户切换问题类型
    用户先问发票,再问退货 →context['last_category']从invoice切到return,AI回复时可主动关联:“您之前咨询过发票开具,退货流程中发票需一并退回”。

表格:上下文字段与AI提示词注入规则
| 上下文字段名 | 触发条件 | Prompt注入示例 | 作用 | |--------------|----------|----------------|------| |last_order_id| 用户问题含“订单号”或数字串 |[用户订单] {$context['last_order_id']}| 让AI知道“我的订单”指哪个 | |last_faq_id| 知识库匹配到FAQ |[参考FAQ] ID:{$context['last_faq_id']}, 标题:{$faqTitle}| 防止AI偏离原始答案 | |last_category| 连续两次问题属同一类(发票/物流/售后) |[当前服务类型] {$context['last_category']}| 统一回答口径,避免跨类混淆 |

5.4 防止上下文污染:设置TTL和自动清理策略

上下文不是永久存储,否则数据库膨胀。我们在app/command/CleanContextCommand.php中实现:

  • 每日凌晨2点,删除update_time超过72小时的记录;
  • 单条记录最大长度限制为5KB(防用户刷屏塞垃圾数据);
  • 写入前用mb_substr($json, 0, 5000)截断。

执行清理:php think clean_context(已加入crontab示例)。

从那以后我每次上线新客服功能,都强制走一遍“模拟用户连续5轮对话”的测试脚本(资源包test/context_test.php),专门验证上下文是否跨请求存活、是否及时过期、是否被恶意篡改。这套机制上线后,用户二次提问的准确率从58%提升到89%,人工客服转接率下降31%。希望帮到你。

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

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

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

立即咨询