前阵子我带着团队给一个在线学习平台做了一版智能客服,内部项目代号叫“路飞学城”。说实话,从接到需求到V1上线,我们并没有机会“先把所有理论知识学完再动手”,整个过程基本是边做边学、边上线边补洞,也就是题目里写的“干中学”。这篇复盘没有什么高深的理论,就是把一个能跑的智能客服V1是怎么从0搭出来的,中间踩了哪些坑、为什么这么做、哪些参数是试出来的,全部摊开讲讲。如果你也要从零做一个客服机器人,或者正在被“机器人答非所问”折磨,这篇文章应该能给你不少可以直接抄作业的参考。
1. 为什么V1选择了“干中学”而不是“学好了再干”
1.1 先定边界:V1到底要解决什么问题
智能客服这个需求,一开始接过来的时候其实特别模糊。业务方的原话是“最近咨询量太大,人工接不过来,你们搞个机器人先顶上”。这句话听着简单,真要落地全是问题:机器人顶哪些问题?顶到什么程度算合格?回答错了谁来负责?用户不满意了怎么转人工?
所以我们做的第一件事,不是选模型、不是写代码,而是把目标框住。这里我非常建议大家先把以下三个数字定义清楚。
- FAQ解答成功率不低于80%(在内部标注好的测试集上衡量);
- 平均响应时间小于1秒;
- 机器人兜不住的时候,能顺畅地把用户转给人工,转人工率允许在20%左右。
也就是说,V1的目标不是“让用户觉得像真人”,而是“能稳定答对高频问题,答不了就体面地转出去”。目标一旦清晰,后面所有技术选型都变得很简单:凡是能让成功率更高、响应更快、转人工更顺畅的方案就优先,凡是又贵又难维护的炫技方案就往后放。
1.2 我刻意不做的三件事
干中学不代表什么都往里面塞。V1阶段,我们明确砍掉了三块。
第一,不做大模型直接生成式回答。虽然当时GPT类API已经很好用,但客服场景有几个很现实的问题:成本不确定、回答不可控、标准答案有合规风险。比如“退费政策”“报名条件”“协议条款”这类问题,必须一字不差地按官方口径答复,让模型自由发挥就等于埋雷。
第二,不做复杂的多轮对话引擎。V1先只处理“单轮问答+最浅的会话记忆”,也就是用户问一句答一句,最多在上下文里记住他刚才提到了哪门课。多轮对话涉及的意图继承、指代消解、槽位填充,水太深,放到V1后面再说。
第三,不做全渠道统一大脑。一开始只接入网站和App内的咨询入口,微信、抖音这些渠道全部后置。渠道越多,消息格式、权限、接口差异就越乱,V1根本扛不住。
把这三块砍掉之后,项目范围一下就清晰了。这也算是干中学教会我的第一课:先学会拒绝需求,才有机会把核心需求做好。
2. “规则掩护、向量开火、人工补枪”的整体框架
2.1 为什么不把所有问题都丢给大模型
我见过不少团队做客服机器人,上来就接入大模型,然后把知识库文档一股脑喂进去,指望它“理解一切”。结果往往是:演示的时候很惊艳,一上线就被用户骂。为什么?
因为大模型生成回答有两个硬伤。一是幻觉,它心情好的时候会给你编一个从来没存在过的优惠活动;二是不可追踪,用户问“为什么我的课程打不开”,模型给了一个模棱两可的答案,出了问题你连是哪条知识库内容触发的都查不到。
但在V1阶段,我们实际要回答的问题大多是高频且固定的:怎么报名、怎么退款、课程有效期、证书怎么发、视频卡顿怎么办。这类问题本质上就是FAQ检索,几千条知识库条目,用户问法再花哨,也万变不离其宗。所以我们定下了“规则掩护、向量检索打主力、人工兜底”的整体框架:
- 预处理和规则层:先做错别字纠正、同义改写、问题归一化;
- 向量召回层:把用户问句和知识库所有问题做相似度匹配,取Top K;
- 置信度判断:相似度够高就给出标准答案,不够高就引导转人工;
- 人工衔接层:转人工的时候把用户原问题、相似度分数、推荐的知识条目都带过去,让人工客服不用重复问一遍。
这个框架最大的优点就是可控。每一个用户问进来,最后命中哪条知识、相似度多少、为什么转人工,全部有日志可查。运营同学可以像维护Excel一样维护知识库,不需要理解任何模型原理。
2.2 知识库的实体结构比算法更重要
说句得罪人的话:很多智能客服效果差,根子不在算法,而在知识库根本没法用。我们一开始从业务方手里拿到的知识库是什么样呢?一个巨大的Word文档,几百页,各种答复混在一起,没有结构,连“用户问什么问题会触发这条答案”都没写清楚。
后来我们花了整整一周的时间做知识库清洗,最终确定了一条知识记录包含以下字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| qid | 唯一编号 | K001234 |
| question | 标准问法 | 课程购买后可以退款吗 |
| keywords | 关键词表 | 退款、退费、取消订单 |
| synonyms | 同义问法 | 买了课后悔了怎么办;钱能退吗 |
| category | 分类 | 售后政策 |
| answer | 标准答案 | 标准话术原文 |
| status | 启用状态 | enabled / disabled |
这个结构看起来简单,但每条知识都要求业务方填“synonyms”是相当难的,因为他们也不知道用户会怎么问。我们的做法是把平台近三个月的客服会话记录全部导出来,人工聚类出高频问题,再把高频问法塞到synonyms字段里。这一步非常耗时,但后来验证下来,知识库质量对最终效果的影响,可能比模型选型的影响还要大。
2.3 为什么要给匹配结果留一条“人工后路”
智能客服最怕的不是答错,而是答错了还自信满满。用户问“你们的服务器是不是又崩了”,如果检索系统稀里糊涂匹配到“如何重置密码”,然后一本正经给了个答案,那用户在屏幕那一头血压直接就上来了。
所以我们设计了一个“置信度分级”的策略。相似度高于某个高阈值(比如0.65),直接给答案;介于低阈值和高阈值之间,把候选答案转给人工客服审核后再发送;低于低阈值(比如0.35),直接转人工。这样做的好处是,机器人宁可承认自己不会,也不能瞎答。客服场景里,“对不起,这个问题我需要转接人工给您确认一下”永远比一个错误答案更让人安心。
3. 相似度检索背后那些不能省的工程细节
3.1 问句预处理:先把用户的话“擦干净”
用户不是输入干净的书面语,这点做客服的人感受最深。现实中的问句长这样:“你好我想问下怎莫退款啊?”“课程买错了能退不?”“你们这个VIP能退吗?”
如果不做预处理,直接拿去和知识库匹配,效果会很差。我们V1里做了三件很简单但极其有效的事情。
第一,错别字纠正。自己维护了一个针对业务场景的错词表,比如“怎莫→怎么”“ID→账号”“收不到码→收不到验证码”。不用整句纠错,只需要覆盖高频业务词就够了,成本极低。
第二,同义扩展。把“退款”“退费”“退钱”“取消订单”“不想要了”这些词在预处理阶段统一映射到一个标准词族,然后把标准词族里的所有词都拿去参与匹配。
第三,问题类型轻归一化。去掉“你好”“请问”“麻烦问一下”这类语气词,再统一简繁体、全半角。这一步做完了,接下来的向量检索会轻松很多。
3.2 Embedding模型和相似度计算怎么配合
预处理之后,核心匹配走的是Embedding路线。我们当时选了一款开源的中文文本向量模型,具体版本就不说了,反正思路是一样的:把用户问句编码成一个向量,再把知识库里的每一条“标准问法+同义问法”也编码成向量,然后用余弦相似度衡量距离。
这里有个容易踩的坑。第一次测试的时候,我们只对“question”字段做了向量化,结果用户换一种问法就匹配不上,因为标准问法和用户的口语问法向量距离很远。后来我们把“question + keywords + synonyms”拼成一句长文本再编码,效果立刻好了不少。原因也很好理解:对Embedding模型来说,输入文本里包含的语义越丰富,编码出的向量就越能覆盖多种表达方式。
相似度计算本身不复杂,我放一段伪代码,大家感受一下整个检索主链路:
import numpy as np def retrieve(query, faiss_index, encoder, top_k=5): processed = preprocess(query) # 错别字纠正、同义映射、语气词去除 query_vec = encoder.encode([processed]) # 得到1x768维向量 scores, idxs = faiss_index.search(query_vec, top_k) results = [] for score, idx in zip(scores[0], idxs[0]): if idx == -1: continue results.append({ "qid": knowledge_base[idx]["qid"], "score": float(score), "answer": knowledge_base[idx]["answer"] }) return results def decision(question): results = retrieve(question) if not results: return "transfer_to_human" best = results[0] if best["score"] >= HIGH_THRESHOLD: return best["answer"] if best["score"] >= LOW_THRESHOLD: return "human_review_suggest", results return "transfer_to_human"实际工程里我们用Faiss做索引,单条查询耗时在毫秒级,完全能满足客服场景的响应要求。知识库几千条数据的情况下,根本不需要什么高并发架构,一台普通服务器就足够了。
3.3 阈值是试出来的,不是拍脑袋定出来的
这是我觉得最有必要单独拿出来讲的经验。很多初学者喜欢设一个固定阈值比如“0.5”,然后问为什么效果不好。阈值这个东西,必须用测试集去标定。
我们内部先让运营同学标注了200多条用户问法,每条问法都标好对应应该命中的知识条目。然后跑了一遍检索,把每个样本的最高相似度分数记录下来,做了个简单统计。演示性的数据大概是这样的:
| 阈值 | 正确率 | 召回率 | 转人工率(演示数据) |
|---|---|---|---|
| 0.30 | 70% | 92% | 8% |
| 0.40 | 78% | 88% | 12% |
| 0.45 | 82% | 85% | 15% |
| 0.55 | 88% | 72% | 28% |
阈值调低,机器人敢说话,但答错的增多;阈值调高,答对的多了但大量用户被转人工。我们的目标是“宁可少答也不能乱答”,所以最后取了0.45作为高阈值、0.35作为低阈值。这里大家一定不要照抄,你的知识库质量、Embedding模型、问题分布都不一样,必须拿自己的测试集去跑一遍,看分数分布再定。
4. 上线一周被客诉淹没后,我从日志里捞出的三类典型问题
V1正式上线第一周,数据其实还可以,FAQ解答成功率大概在76%左右,距离目标80%还有差距。但用户的吐槽很密集,主要集中在“答非所问”“话术太死板”“同一个问题换个人问就变智障”。我们拉了一周日志,逐条看相似度分数和实际命中的知识条目,最后揪出了三个最典型的翻车场景。
4.1 孪生问题:知识库里的“多胞胎”把检索带偏了
第一个问题特别隐蔽。我们的知识库里有一条“课程有效期是多长”和一条“课程购买后多久能开通”,这两条问题在语义空间里的距离非常近。用户问“我这门课多长时间能看完”,检索系统直接命中“课程有效期”那条,给出的答案是在讲课程从购买日起365天内有效,而用户问的明明是“学习周期多久”。
排查链路是这样的:先找出反馈量最大的几个问题,看用户原话和命中答案,发现答非所问;再看日志里的命中分数,发现分数其实不低,说明不是阈值问题;最后打开知识库,才发现存在大量语义相近但答案完全不同的“孪生条目”。
解决办法有两步。第一步,给这类相近问题手动标记互斥关系,检索时如果候选结果里出现互斥条目,直接丢弃;第二步,把这类问题的“标准问法”写得更有区分度,比如明确带上“有效期”“有效期是从哪天开始算”“课程视频可以反复看吗”,拉大向量距离。这个经验后来我们沉淀成了一个检查规范:知识库里凡是答案完全不同的两个问题,标准问法之间不应该高度相似。
4.2 用户说的“黑话”和业务方写的“书面语”完全对不上
第二个问题也很有代表性。业务方在知识库里写的是“课程激活流程”,但用户根本不这么说,他们说的是“兑换码怎么用”“买了课从哪里开始看”。如果这些口语问法没有被写进synonyms字段,Embedding模型再强也很难把它们和“课程激活流程”关联起来。
所以后面我们调整了工作方式:每次从客服对话记录里挖出高频用户问法,先丢给运营同学,让他们判断对应哪条标准问法,然后补充进synonyms字段。上线第一个月我们就是这么一点一点“喂”知识库的,每周大概新增50到80条同义问法,FAQ解答成功率也慢慢从76%涨到了84%以上。
这里提醒一句:synonyms字段千万别靠技术团队脑补,一定要用真实会话记录。我们一开始自己编了一些问法,比如“如何开通课程”,结果用户根本不会这么问,纯粹浪费知识库容量。
4.3 阈值变成了“薛定谔的边界”
第三个问题最有意思。有用户问同一个问题,其中一句“怎么申请延期考试”能直接拿到答案,另一句“我想延考咋弄啊”却转人工了。从相似度分数看,前一句0.68,后一句0.42,刚好卡在阈值两侧。用户感觉就是“这个机器人时灵时不灵”。
这说明什么?说明单一全局阈值太粗糙。后续我们给每个知识条目单独维护了一个“灵敏度”配置:某些高价值、高投诉风险的问题(比如退费、改期),把高阈值调低到0.40,宁可多答也不要漏答;某些涉及政策条款、答错代价高的问题,高阈值调高到0.55,宁可转人工。这其实就是给每个问题在“答错”和“漏答”之间做一个权衡,而不是所有问题一刀切。
5. 别忘了把运营放进去:机器人不是上线就完事
5.1 人工交接的那一刻,才是用户真正评价机器人的时刻
我发现很多人做智能客服有个误区,觉得机器人是主角,人工是配角。但真实体验里,用户最恼火的往往不是机器人答错,而是答错之后想找人工,结果绕了半天找不到入口。
我们在V1里做了个非常朴素的交互约定:任何转人工的节点,都必须把用户已经输入的原话、系统能给的候选答案、相似度分数一并传给人工侧。客服接到对话时,屏幕上直接弹出“用户可能是想问XX,但置信度不足,建议确认”,这样人工客服不用让用户再复述一遍,用户心里的烦躁感能降低很多。这中间没有高深算法,但体验的提升特别明显。
另外,转人工的话术也要讲究。不要说“对不起我还在学习中”,用户会觉得你在敷衍。我们用的是“这个问题建议由人工为您准确核实,正在为您转接,请稍候”。核心原则是:把责任从机器人身上卸下来,落到“更可靠的人工服务”上。
5.2 让运营同学能自己维护知识库,别什么都找开发
V1上线之前,我们就确定了一个原则:知识库的维护权要交给运营。开发的角色是提供工具、监控稳定性和优化匹配效果,而不是每天帮运营改答案。
所以我们给运营做了一个极简的维护后台,功能就三个:
- 新增/编辑/下线一条知识,不需要写任何格式;
- 查看每一个问答日志里,用户实际说的句子和检索命中的相似度分数;
- 把“未命中”的问题收集起来,一键聚类成候选新知识。
这个后台帮我们省了巨多的沟通成本。运营同学现在每天自己看未命中日志,看到连续出现的高频问法,就直接补一条同义问法或者新知识。技术团队彻底从“改答案”这种琐事里解脱出来,专心做模型调优和系统稳定性。
5.3 如果要做V1.1,我会把大模型放在哪个位置
很多朋友问我,既然都已经2025年了,为什么不让大模型参与进来。我的看法是:大模型不是不用,而是要用在它擅长的位置。
V1.1我打算在两个场景引入大模型。第一个是“椭圆化兜底”,当检索分数在低阈值附近、没有标准答案可以给的时候,让大模型基于已有的知识条目重新组织一段话术回答用户,但仍然要在回答末尾提示“最终政策以人工客服解释为准”。第二个是“问法聚类”,每周把未命中的用户问题丢给大模型做聚类,让运营快速发现新问题。
但无论如何,我不会让大模型直接生成标准答案,因为客服场景里的错误答案是有真实代价的——轻则用户投诉,重则涉及纠纷。大模型可以做“回答的加工厂”,不能做“答案的决策者”。
说到底,智能客服V1这个项目给我最大的感受就是:先把目标砍到最小、把知识库整理干净、把转人工做顺,效果往往比上一个重型模型还要好。干中学不是蛮干,而是让每一次失败都成为下一次调整的依据。如果你也在做类似的系统,我建议你也准备一个日志查询工具,把每个问答的原始输入、命中条目、相似度分数都记录清楚,那是你优化系统时最有价值的素材。