简介:本资源为基于PHP与ThinkPHP框架的运营级在线客服系统源码,重点实现AI知识库接入能力,面向需要为企业级应用搭建智能客服模块的PHP开发者与运维人员。系统借助自然语言处理技术匹配客户问题并调用知识库作答,可提升客服响应效率与用户满意度。压缩包共2010个文件,涵盖613个js、368个html、258个css、163个xml、157个json、151个md、141个txt及131个php等,前端资源、配置数据、说明文档与后端逻辑分层清晰,整体约68.72MB。资源附带安装教程,涉及fileinfo与redis插件安装、php.ini中pcntl系列函数禁用等环境配置要点,并给出源码编译与Web服务器重启流程。目前已有137人学习下载,适合希望快速落地智能客服、参考完整目录结构与排错思路的中高级开发者。
1. PHP在线客服接入AI知识库:从关键词匹配到语义检索的落地路径
很多 PHP 在线客服系统上线半年后都会遇到同一个尴尬:知识库文章从 50 篇涨到 500 篇,客服机器人反而越来越不准。用户问“订单提交了但没收到确认短信”,老系统靠关键词命中“短信”就推一篇《短信模板配置指南》,答非所问。问题不在 PHP,而在检索方式——关键词匹配没有语义理解能力。把 AI 知识库接进 PHP 客服,本质是给客服系统换一套“先理解、再检索、后生成”的链路:用户问题先转向量,在知识库里做相似度召回,再把召回片段交给大模型组织成回答。这套方案适合已有 PHP 客服系统、知识库以文档或 FAQ 形式存在、希望在不重写整个系统的前提下提升首答准确率的团队。下面按“选型 → 建库 → 接入 → 排错 → 调优”的顺序拆开讲。
2. 选型与架构:PHP 客服系统怎么和 AI 知识库对接
2.1 三种接入形态的取舍
把 AI 知识库接进 PHP 客服,常见做法有三种,选错形态后面全是返工。
第一种是同步直连:用户发消息,PHP 接口里直接调大模型 API 和向量检索,拿到结果再返回。优点是链路短、实现快;缺点是用户要等 2 到 5 秒,高峰期接口容易超时。适合日咨询量几百、对响应速度不敏感的内部客服。
第二种是异步队列:PHP 把用户问题丢进 Redis 队列,后台 worker 消费后调 AI,结果通过轮询或 WebSocket 推回前端。优点是接口不阻塞、可重试;缺点是要多维护一套队列和推送逻辑。日咨询量上千时这是更稳的选择。
第三种是旁路增强:客服系统本身不动,只在“智能推荐”侧边栏里调 AI,人工客服参考后自己回复。改动最小,适合还没准备好让 AI 直接对客的场景。
我一般会建议:先用旁路增强跑两周,观察召回质量,再决定要不要升级到异步队列直接对客。直接上同步直连的,十个里有八个会在流量上来后翻车。
2.2 向量库和模型的分工
架构里有两个独立组件,别混在一起想:
- Embedding 模型:负责把文本转成向量,决定“语义理解”的上限。中文场景常见选择是 bge 系列或 m3e 系列,本地部署用 sentence-transformers 或 Ollama 拉模型。
- 向量数据库:负责存向量和做相似度检索。数据量小于 10 万条时,Milvus Lite、Qdrant、甚至 PostgreSQL 的 pgvector 都够用;超过百万级再考虑独立集群。
PHP 本身不擅长做向量计算,所以正确姿势是:PHP 只负责 HTTP 调用,向量化和检索都交给 Python 服务或向量库自带的 HTTP 接口。常见做法是用 FastAPI 包一层 embedding 服务,PHP 通过 cURL 调它。
2.3 最小可跑通的目录结构
project/ ├── php-service/ # 现有 PHP 客服系统 │ ├── api/ │ │ └── chat.php # 客服消息入口 │ └── config/ │ └── ai.php # AI 服务地址、密钥配置 ├── ai-service/ # Python 向量化与检索服务 │ ├── app.py # FastAPI 入口 │ ├── embedder.py # 文本转向量 │ └── retriever.py # 向量检索 └── data/ └── knowledge/ # 知识库原始文档这个结构的好处是 PHP 和 AI 服务解耦,AI 服务挂了不影响客服系统本身收发消息,只是智能推荐暂时不可用。
3. 知识库建库:把 FAQ 和文档变成可检索的向量
3.1 文档切分:别整篇塞进去
知识库文档动辄几千字,整篇转向量会导致语义被稀释——一篇讲“退款流程”的文章里顺带提了一句“发票”,用户问发票时反而召回这篇。常见做法是按语义切分成 200 到 500 字的片段,每个片段单独存一条向量。
# chunker.py import re def split_by_heading(text, max_len=400): """按标题和段落切分,控制单块长度""" # 先按 Markdown 标题切 sections = re.split(r'\n(?=#{1,3}\s)', text) chunks = [] for sec in sections: # 段落再按长度二次切分 if len(sec) <= max_len: chunks.append(sec.strip()) else: for i in range(0, len(sec), max_len): chunks.append(sec[i:i+max_len].strip()) return [c for c in chunks if len(c) > 20] # 过滤过短碎片逻辑说明:先按标题切保证语义边界,再按长度切防止单块过大。max_len=400是经验值,中文场景下 300 到 500 字召回效果比较稳。len(c) > 20过滤掉“注意事项:”这种没有信息量的碎片,否则它们会污染检索结果。
参数调整:知识库偏 FAQ 短问答,max_len可以降到 200;偏技术文档长段落,可以升到 600,但别超过 800,否则 embedding 模型会截断。
3.2 向量化与入库
# embedder.py from sentence_transformers import SentenceTransformer import qdrant_client from qdrant_client.models import PointStruct model = SentenceTransformer('BAAI/bge-small-zh-v1.5') client = qdrant_client.QdrantClient(path="./qdrant_data") def build_index(chunks, source_ids): vectors = model.encode(chunks, normalize_embeddings=True) points = [ PointStruct( id=i, vector=vec.tolist(), payload={"text": chunks[i], "source": source_ids[i]} ) for i, vec in enumerate(vectors) ] client.upsert(collection_name="kb", points=points)逻辑说明:normalize_embeddings=True让向量归一化,之后用余弦相似度检索时可以直接算点积,省一次除法。payload里存原文和来源 ID,召回后要拿原文给大模型,也要能追溯到是哪篇文档。
参数说明:bge-small-zh-v1.5输出 512 维向量,单条 400 字中文大约占 2KB 存储。1 万条知识片段约 20MB,Qdrant 本地模式完全扛得住。如果换bge-base或bge-large,维度变 768 或 1024,检索更准但内存和延迟上升,按机器配置权衡。
3.3 建库后的自检
建完库别急着接客服,先手动测几条。准备 10 个真实用户问法,跑检索看 Top3 里有没有正确答案。如果 Top3 命中率低于 70%,先别调客服代码,回头查切分和模型——大部分召回问题出在建库阶段,不是接入阶段。
4. PHP 侧接入:客服消息怎么走完 AI 链路
4.1 客服入口改造
现有 PHP 客服系统通常有一个接收用户消息的接口,改造点是在“返回机器人回复”之前插入 AI 调用。
// api/chat.php function getAiReply(string $question): array { $config = require __DIR__ . '/../config/ai.php'; $payload = json_encode([ 'question' => $question, 'top_k' => 3, 'threshold'=> 0.65 ], JSON_UNESCAPED_UNICODE); $ch = curl_init($config['ai_service'] . '/ask'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, // 超时兜底 CURLOPT_CONNECTTIMEOUT => 2, ]); $resp = curl_exec($ch); $err = curl_error($ch); curl_close($ch); if ($err || !$resp) { return ['answer' => '', 'fallback' => true]; // 降级到人工 } return json_decode($resp, true); }逻辑说明:PHP 只做转发,不碰向量计算。CURLOPT_TIMEOUT => 8是硬性兜底——AI 服务再慢也不能让用户等超过 8 秒,超时就走降级逻辑转人工。threshold传给 AI 服务,低于这个相似度就不硬答,避免“一本正经胡说”。
参数说明:top_k=3表示召回 3 个最相关片段拼进 prompt,太多会稀释重点,太少可能漏信息。threshold=0.65是余弦相似度阈值,中文场景下 0.6 到 0.7 比较合理,低于 0.6 基本是无关内容。
4.2 AI 服务端的检索与生成
# app.py from fastapi import FastAPI from pydantic import BaseModel from embedder import model, client app = FastAPI() class Query(BaseModel): question: str top_k: int = 3 threshold: float = 0.65 @app.post("/ask") def ask(q: Query): vec = model.encode([q.question], normalize_embeddings=True)[0] hits = client.search( collection_name="kb", query_vector=vec.tolist(), limit=q.top_k, score_threshold=q.threshold ) if not hits: return {"answer": "", "fallback": True} context = "\n\n".join(h.payload["text"] for h in hits) answer = call_llm(q.question, context) # 调大模型组织回答 return {"answer": answer, "sources": [h.payload["source"] for h in hits]}逻辑说明:先检索再生成,检索结果为空直接返回 fallback,不浪费一次大模型调用。score_threshold在向量库层面过滤,比拿回来再判断更省资源。
参数说明:call_llm里拼 prompt 时,把 context 放在用户问题前面,并加一句“仅根据以上资料回答,资料中没有的信息不要编造”。这句约束能显著降低幻觉率,血泪经验。
4.3 降级与兜底
AI 链路再稳也有挂的时候。PHP 侧要保证:AI 服务超时、返回空、返回格式错误,三种情况都走同一条降级路径——转人工或返回预设话术。别让用户看到“系统错误”,那比答错还伤体验。
5. 避坑与排查:接入 AI 知识库最常见的 5 个翻车点
5.1 召回全是无关内容,相似度却很高
现象:用户问“怎么修改绑定手机”,召回的是“手机端 App 下载指南”,相似度 0.72。
原因:知识库里有大量“手机”相关但语义不同的片段,embedding 模型对短文本的区分度不够,加上切分时把不同主题混在一块。
解决:先检查切分粒度,把“手机”相关的不同主题拆开;再考虑换更大的 embedding 模型(bge-base 起步);最后可以在检索前加一层意图分类,把问题先归到“账号”“订单”“支付”等类目,再在类目内检索。
5.2 PHP 接口偶发 504,日志里 curl 超时
现象:白天正常,晚上高峰期客服接口大量 504,PHP 错误日志里是 curl timeout。
原因:AI 服务串行处理请求,embedding 计算是 CPU 密集操作,并发上来后排队,PHP 侧 8 秒超时先触发。
解决:AI 服务侧加并发控制或批处理,embedding 模型换成 ONNX 加速版;PHP 侧把超时降到 5 秒,超时直接降级,别让用户干等。根治方案是上异步队列,把同步调用改成“先返回受理,后推送结果”。
5.3 大模型回答里出现知识库没有的内容
现象:知识库里只写了退款 7 个工作日到账,AI 回答成“3 到 5 个工作日”。
原因:prompt 约束不够强,或者 context 里混入了相似但不同的片段,模型做了“合理推测”。
解决:prompt 里明确“只使用提供的资料,资料未提及则回答‘暂无相关信息’”;检索阈值调高到 0.7;召回片段里如果包含数字、日期,在 prompt 里单独标注“以下为准确信息”。
5.4 中文问句向量化后检索效果差
现象:英文知识库检索正常,中文问句召回率明显偏低。
原因:用了以英文语料为主的 embedding 模型,中文语义空间没对齐。
解决:换中文或中英双语模型,bge 系列和 m3e 系列在中文场景下表现稳定。换模型后必须重建整个向量库,旧向量和新模型不兼容。
5.5 知识库更新后 AI 还在答旧内容
现象:运营改了 FAQ,AI 回答还是老版本。
原因:向量库没有增量更新机制,或者更新了但没删旧向量。
解决:建库时给每条向量打上doc_id和version,更新文档时先按doc_id删除旧向量再插入新向量。别直接覆盖,否则旧片段会残留。定期跑一次全量重建,清理孤儿向量。
6. 调优技巧:把首答准确率从 70% 推到 90%
接入跑通只是及格线,真正拉开差距的是调优。分享几个我反复验证过的具体手法。
第一,查询改写。用户问“付了钱没到账”,直接检索可能召回“支付方式说明”。在检索前加一步轻量改写,把口语问句转成“支付成功但账户余额未更新”,召回质量明显提升。改写可以用小模型做,也可以用规则模板,成本很低。
第二,混合检索。纯向量检索对专有名词(订单号、产品型号)不敏感。常见做法是向量检索和关键词检索各跑一遍,用 RRF(倒数排名融合)合并结果。专有名词靠关键词兜底,语义靠向量兜底,两者互补。
def rrf_merge(vec_hits, kw_hits, k=60): """倒数排名融合,k 是平滑常数""" scores = {} for rank, hit in enumerate(vec_hits): scores[hit.id] = scores.get(hit.id, 0) + 1 / (k + rank + 1) for rank, hit in enumerate(kw_hits): scores[hit.id] = scores.get(hit.id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: -x[1])k=60是 RRF 的经典取值,实际用 40 到 80 之间差别不大。这个融合逻辑不依赖分数绝对值,只依赖排名,所以向量分和关键词分不在一个量纲也能合并。
第三,用真实日志做回归测试。上线后每周导出一次用户问法和 AI 回答,人工标注 100 条,算准确率。别凭感觉说“好像变好了”,要有数字。我习惯把标注结果存成 CSV,每次调参后跑一遍对比,避免改 A 坏 B。
第四,prompt 里加 few-shot 示例。给大模型两三个“问题 + 资料 + 标准回答”的示例,比单纯写规则有效。示例要覆盖“资料里有答案”“资料里没答案”“资料里有矛盾”三种情况,模型会学着按同样逻辑处理。
第五,监控召回分布。统计每天召回相似度的分布,如果大量请求落在阈值边缘(0.6 到 0.68),说明知识库覆盖不足或切分有问题,该补文档补文档,该调切分调切分。这个指标比准确率更早暴露问题。
最后说个习惯:每次改完 embedding 模型或切分策略,我一定先在小批量(200 条)真实问法上跑对比,确认没有回退再全量重建。直接全量重建再发现问题,回滚成本太高,这个后悔药不好吃。希望帮到你。
本文还有配套的精品资源,点击获取