ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ThinkPHP在线客服系统接入AI知识库:检索增强生成与人工兜底实战

ThinkPHP在线客服系统接入AI知识库:检索增强生成与人工兜底实战

简介:这份资源面向使用 PHP 开发在线客服系统的开发者,重点解决客服系统接入 AI 知识库时的环境配置与部署问题,基于 ThinkPHP 框架构建,适合具备一定 PHP 基础、希望搭建运营级客服平台的技术人员参考。压缩包共 2010 个文件,约 68.72MB,以 js、html、css 等前端资源为主,配合 xml、json、md、txt 等配置与说明文档,另有 131 个 php 业务文件、3 个 sql 数据库脚本及少量 sh 部署脚本,整体结构完整,便于按模块查阅。资源附带安装教程,涵盖 fileinfo、redis 插件安装,以及 pcntl_signal、pcntl_fork 等禁用函数的配置说明,可帮助读者快速完成运行环境准备。目前已有 138 人学习,适合需要参考 AI 知识库接入思路、了解客服系统目录组织与前后端资源构成的开发者下载研究。

1. 需求先立住:为什么客服系统要接知识库,而不是堆话术

做过在线客服的人都有一个共同体会:用户问的问题,八成在历史会话里出现过,但客服还是得重新打字。传统 PHP 客服系统靠快捷回复和话术模板撑场面,访客一多、业务一复杂,话术库就变成一锅粥,新人上手要背几百条,老员工也经常答得前后不一致。这套方案要解决的就是这件事——在 ThinkPHP 搭建的在线客服系统里接入 AI 知识库,让访客提问先经过知识检索,命中就自动给出带出处的答案,没命中再转人工,人工答复同时沉淀回知识库。适合正在做客服系统毕设、课程设计,或者要给现有 PHP 项目补一套智能问答能力的开发者,结构可以直接照着搭,模块可以按业务裁剪。

2. 总体架构设计:四层结构与知识库接入点

这套系统的核心不是「接一个大模型就完事」,而是把知识库当成一等公民,围绕它设计检索、生成、兜底、沉淀四条链路。整体分四层:接入层负责多渠道会话入口,应用层跑 ThinkPHP 的控制器与业务服务,知识层管文档切分、向量化与检索,模型层封装大模型调用与提示词。四层之间用接口隔离,换模型、换向量库都不动业务代码。

2.1 分层职责与请求流转

接入层收访客消息,统一转成内部消息结构体,带上渠道标识、会话 ID、访客画像。应用层拿到消息后先走意图判断:寒暄、转人工、投诉这类直接走规则;业务咨询类进知识检索。知识层返回 Top-K 片段和相似度分数,应用层按阈值决定是「直接答」「生成答」还是「转人工」。模型层只负责把检索片段和问题拼成提示词,产出答案并附引用来源。

请求流转的关键在于「检索先行、生成在后」。很多项目翻车就翻在让模型自由发挥,答案看着像那么回事,实际和业务文档对不上。检索先行等于给模型戴了缰绳,答案必须落在召回片段里,召回不到就老实说不知道,转人工。这条原则定下来,后面所有模块都是为它服务的。

2.2 技术选型:ThinkPHP 做骨架,检索与生成解耦

骨架用 ThinkPHP,是因为它的路由、中间件、ORM 对中小型客服系统足够,团队上手成本低。知识检索部分不硬塞进 PHP 进程里做向量计算,通常的做法是单独起一个检索服务,PHP 侧通过 HTTP 调用,返回 JSON。这样做的原因很实际:PHP 做稠密向量相似度计算性能一般,而检索服务可以用更合适的运行时,还能独立扩容。

模型调用同样走适配器模式,定义一个LlmAdapter接口,底下可以有不同厂商的实现,配置里切换。提示词模板放配置文件而不是硬编码在控制器里,方便运营调整语气和兜底话术。数据库方面,会话表、消息表、知识文档表、知识片段表分开建,片段表存向量 ID 和原文,向量本身放检索服务里,两边用片段 ID 关联。

// config/knowledge.php —— 知识库与模型的可调参数集中管理 return [ 'retrieval' => [ 'top_k' => 5, // 召回片段数,太大噪声多,太小易漏 'score_threshold' => 0.72, // 低于此分不直接作答,转人工 'timeout' => 2, // 检索超时秒数,超时走兜底 ], 'llm' => [ 'adapter' => 'default', // 适配器名,对应 LlmAdapter 实现 'max_tokens' => 512, 'temperature' => 0.3, // 客服场景要稳,别让它自由发挥 ], 'fallback' => [ 'no_hit_reply' => '这个问题我暂时没查到准确答案,已为您转接人工客服。', ], ];

这段配置把三个关键参数暴露出来:top_k控制召回广度,score_threshold是「敢不敢直接答」的闸门,temperature压低是为了答案稳定可复现。参数不是拍脑袋定的,上线前要拿一批真实历史问题跑一遍,看命中率和误答率,再回调阈值。我一般会先把阈值设高一点,宁可多转人工,也不让错误答案流出去,等知识库补厚了再逐步下调。

2.3 数据表设计:会话、消息、文档、片段四张核心表

表结构决定了后面检索和沉淀顺不顺。会话表记访客和状态,消息表记每轮问答和来源标记,文档表记知识原文和版本,片段表记切分后的块和向量引用。四张表用外键逻辑关联,不强制物理外键,方便分库和清理。

表名关键字段作用
cs_sessionid、visitor_id、channel、status、created_at会话主表,status 区分机器人/人工/已结束
cs_messageid、session_id、role、content、source_ids、score消息表,source_ids 记录引用的片段,便于追溯
kb_documentid、title、category、version、status知识文档,version 支持灰度更新
kb_chunkid、doc_id、content、vector_ref、enabled片段表,vector_ref 指向检索服务里的向量

cs_message.source_ids这个字段值得单独说。它存的是本条答案引用了哪些片段,出了问题能顺着它回查是哪块知识写错了。没有这个字段,答案错了只能靠猜,排查成本极高。kb_chunk.enabled用于单片段下架,不用整篇文档下线,运营改知识时粒度更细。

3. 知识入库与检索实现:从文档切分到召回排序

知识库好不好用,七分在入库,三分在检索。入库阶段把散落的 FAQ、产品手册、工单记录整理成结构化片段,检索阶段才能召得准。这一章把入库流水线和检索调用拆开讲,每步都给可落地的做法。

3.1 文档切分策略:按语义边界切,不按固定字数切

最常见的错误是按 500 字一刀切,结果一句话被劈成两半,召回片段读起来莫名其妙。通常的做法是按语义边界切:先按标题层级切大块,再按段落切小块,单块控制在 200 到 400 字,块与块之间留 10% 到 20% 重叠,避免边界信息丢失。表格类内容单独成块并保留表头,代码类内容整段保留不切。

# chunker.py —— 按标题与段落切分,保留重叠上下文 import re def split_document(text, max_len=400, overlap=60): # 先按二级标题切大节,再按空行切段落 sections = re.split(r'\n(?=## )', text) chunks = [] for sec in sections: paras = [p.strip() for p in sec.split('\n\n') if p.strip()] buf = '' for p in paras: if len(buf) + len(p) <= max_len: buf = (buf + '\n' + p).strip() else: if buf: chunks.append(buf) # 重叠部分取上一块尾部,保证跨块语义连续 buf = (buf[-overlap:] + '\n' + p).strip() if buf else p if buf: chunks.append(buf) return chunks

切分函数里max_len和overlap是两个要调的参数。块太大,检索精度下降,模型提示词也塞不下;块太小,单块信息不完整,答案容易断章取义。重叠是为了让跨段落的问答不被切断,代价是存储和检索量略增,一般可以接受。切完的每块要带上来源文档 ID 和标题路径,检索命中后能拼出「出自哪篇文档哪一节」,这对客服场景的可信度很重要。

3.2 向量化与索引构建:批量入库、增量更新

片段切好后要转成向量写入检索服务。批量入库时按批提交,每批 64 到 128 条,避免单次请求过大超时。每条记录带chunk_id、doc_id、content、vector四个字段,检索服务建索引时把chunk_id作为主键,方便后续按 ID 删除或更新。

增量更新是运营最关心的:改了一条 FAQ,不能整库重建。做法是文档表记version,片段表记doc_id,更新时先按doc_id删旧片段向量,再写新片段,两步放在一个任务里串行执行,失败可重试。重建全库放在低峰期跑,用新索引别名切换,切完观察一阵再删旧索引,留出后悔药。

# 增量更新单篇文档的片段索引(示意命令,按实际检索服务接口调整) curl -X POST http://retrieval-svc/internal/reindex \ -H "Content-Type: application/json" \ -d '{"doc_id": 1024, "mode": "replace", "batch_size": 128}'

mode: replace表示先删后写,batch_size控制单批条数。这个接口只对内部服务开放,不暴露给前端。执行完要校验片段数和文档字数是否匹配,数量对不上说明切分或写入有遗漏,得回查日志。索引更新不是「提交就完事」,校验这一步省不得。

3.3 检索调用与阈值判定:PHP 侧怎么接

ThinkPHP 侧封装一个KnowledgeService,负责拼检索请求、解析返回、按阈值分流。检索服务返回片段列表和分数,服务层做三件事:过滤低于阈值的片段、按分数排序取 Top-K、把片段拼成带编号的上下文。分流逻辑写在服务层而不是控制器,控制器只负责收消息和回消息,职责清晰。

// app/service/KnowledgeService.php class KnowledgeService { public function retrieve(string $question, int $topK = 5): array { $cfg = config('knowledge.retrieval'); $resp = $this->httpPost('/search', [ 'query' => $question, 'top_k' => $topK, ], $cfg['timeout']); $hits = array_filter($resp['hits'] ?? [], function ($h) use ($cfg) { return $h['score'] >= $cfg['score_threshold']; }); usort($hits, fn($a, $b) => $b['score'] <=> $a['score']); return array_slice($hits, 0, $topK); } public function buildContext(array $hits): string { $lines = []; foreach ($hits as $i => $h) { $lines[] = '[' . ($i + 1) . '] ' . $h['content']; } return implode("\n\n", $lines); } }

retrieve里阈值过滤放在排序之前,先砍掉不靠谱的,再排序取前几,避免低分片段挤占名额。buildContext给每段加编号,是为了让模型在答案里能引用[1]、[2],前端再把这些编号映射回原文链接。超时参数从配置读,检索服务抖动时不至于把整个会话卡死,超时后走兜底话术转人工。

4. 对话生成与人工兜底:让答案可控、可追溯

检索到片段只是原料,怎么组织成答案、什么时候不该答,才是客服场景和通用问答的分水岭。这一章讲提示词怎么约束、兜底怎么设计、人工答复怎么回流知识库,三件事串起来形成闭环。

4.1 提示词约束:只答片段里有的,答不了就说不知道

提示词的核心不是让模型「聪明」,而是让它「守规矩」。系统提示里明确三条:只能依据给定片段作答;片段不足以回答时输出固定兜底标记;答案末尾列出引用的片段编号。用户问题原样传入,片段按编号拼在上下文里。温度压低到 0.3 左右,减少措辞漂移。

// app/service/PromptBuilder.php public function build(string $question, string $context): array { $system = <<<PROMPT 你是客服助手。只能依据下方【知识片段】回答,不得编造。 若片段不足以回答,只输出:NO_ANSWER。 回答末尾用 [编号] 标注引用了哪些片段。 【知识片段】 {$context} PROMPT; return [ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $question], ]; }

NO_ANSWER这个标记是整条链路的开关。应用层拿到模型输出先判断是否等于该标记,是就转人工,不是才落库并返回访客。用固定标记而不是靠语义判断,是因为语义判断本身就不稳,标记判断是确定性的。片段编号让答案可追溯,访客点编号能看到原文,客服复盘也能定位到具体知识块。

4.2 兜底与转人工:三种触发条件要分清

转人工不能只有一个「答不上来」的触发点。实际项目里至少三种情况要转:检索无命中或全低于阈值、模型输出NO_ANSWER、访客连续两轮表达不满或明确要求人工。前两种是知识层面的兜底,第三种是情绪层面的兜底,缺一个体验就断。

转人工时要把上下文一起带过去,包括访客问题、召回片段、模型尝试过的答案。人工客服接手时不用重新问一遍,直接看到机器人卡在哪。会话状态从「机器人」切到「人工」,消息表里记一条系统消息说明转接原因,方便后续统计哪类问题最容易触发转人工,反过来指导知识库补哪块。

4.3 人工答复回流:把工单变成新知识

人工答完不是终点。每条人工答复在会话结束后进入待审队列,运营确认后可一键转成知识片段:问题做标题,答复做正文,自动走切分和向量化入库。这一步是知识库能持续变厚的根本,否则知识库永远是上线那天的样子。

回流要做去重,同一问题反复出现只保留一条高质量答复,避免片段表膨胀。审核环节不能省,人工答复里可能有临时口径、内部信息,直接入库会污染知识库。通常的做法是待审队列按周清理,审核通过的标enabled=1,驳回的记原因,积累一段时间能看出知识盲区分布。

5. 避坑与排查:上线后最容易翻车的五个点

这套系统在演示环境跑通不难,难的是上线后稳定。下面五条是这类项目里出现频率最高的问题,每条按现象、原因、解决写,照着排查能省不少时间。

5.1 答案答非所问,召回片段看着相关实则无关

现象是访客问「退款多久到账」,召回的是「退款申请流程」,答案绕了一圈没说到账时间。原因通常是切分把「到账时效」那段切到了别的块,或者向量模型对时效类表述不敏感。解决办法是先看召回片段的原文边界,把时效、价格、期限这类关键信息单独成块并在块首保留问题式标题;再拿一批同类问题测召回,命中率低于预期就调切分粒度,而不是急着换模型。

5.2 检索超时拖垮整个会话

现象是高峰期访客发消息后长时间无响应,日志里检索调用耗时飙到数秒。原因是检索服务单点、批量请求挤占,或者 PHP 侧没设超时。解决是检索调用强制设超时(配置里那个timeout就是干这个的),超时直接走兜底转人工;检索服务做限流和独立部署,别和业务库抢资源;批量入库任务放低峰期,和在线检索错开。

5.3 模型偶尔「自由发挥」,答案里出现知识库没有的内容

现象是答案语气很顺,但提到的政策或数字在文档里查不到。原因是提示词约束不够硬,或者温度偏高。解决是把「只能依据片段」写进系统提示并配合NO_ANSWER标记做确定性判断,温度压到 0.3 以下;上线前用一批「知识库故意不覆盖」的问题做对抗测试,看模型是否老实说不知道,不老实就继续收紧提示词。

5.4 知识更新后旧答案还在出

现象是运营改了 FAQ,访客拿到的还是旧口径。原因是增量更新只写了新片段没删旧片段,或者检索服务有缓存没失效。解决是更新走「先删后写」并校验片段数,缓存按doc_id设失效键,更新完成主动清一次;文档表用version标记,检索结果里带上版本号,排查时一眼能看出命中的是新是旧。

5.5 转人工后上下文丢失,访客要重复描述

现象是转人工后客服第一句还是「请问有什么可以帮您」。原因是转接只切了会话状态,没把消息和召回片段带过去。解决是转接时把最近若干轮消息、召回片段、模型尝试答案打包成上下文写入人工会话,客服端首屏直接展示;消息表用session_id串起来,人工和机器人共用一条会话线,别另起新会话。

6. 验收与答辩:怎么证明这套方案真的能用

验收这套系统,别只看「能对话」,要看四个可量化的面:召回准不准、答案稳不稳、兜底及不及时、知识能不能长。准备一份 50 到 100 条的真实历史问题做测试集,人工标注每条的标准答案来源片段,跑一遍算召回命中率和答案一致率。命中率看检索层,一致率看生成层,两个数分开报,答辩时能说清问题出在哪一层。

验收项测法合格线(按项目规模自定)
召回命中率测试集问题是否召回标注片段前 5 条内命中
答案一致率生成答案与标准答案语义一致抽样人工评,记录分歧
兜底及时性无知识覆盖的问题是否转人工全部走兜底,不硬答
知识回流人工答复转片段后能否被召回新片段可检索到

答辩时最容易被问的是「模型答错了怎么办」。别答「调提示词」,要答「有NO_ANSWER标记做确定性拦截、有阈值做前置过滤、有source_ids做追溯、有人工回流做修正」,四层防线说清楚,比任何演示都有说服力。演示环节建议现场改一条知识再提问,让评委看到增量更新生效,这比放十段录屏都管用。

血泪经验是:演示前一定用冷启动环境跑一遍完整链路,别在本地缓存热乎的状态下上台。我一般会在答辩前一天清一次检索缓存、重启一次服务、用测试集完整过一遍,把超时和兜底都触发一次确认话术正常。从那以后我每次交付这类系统都强制走一遍「清缓存、重启、跑测试集」的流程,没再在台上翻过车。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进