资讯动态

基于聚类的智能客服问题分类系统实现

发布时间:2026/10/19 10:54:27 来源:尧图企业网站定制
做客服问题分类这件事最让人头疼的往往不是“已知问题怎么分得更细”而是“面对一堆没有标签的用户问题怎么快速知道里面到底都在聊什么”。我最近帮某电商团队搭了一套基于聚类的智能客服问题分类系统完全用 Python 实现。这套系统的核心思路很直接用无监督聚类把相似问题自动聚到一起再由人工给每个簇贴标签后续新问题进来后直接归到已有簇离所有簇都很远的问题单独标记出来。相比一开始就训练一个有监督分类模型这种方法在冷启动阶段特别占优势不需要上万条标注数据也能让客服工单的初步归类、热点问题监控快速跑起来。1. 智能客服问题分类为什么我选了聚类而不是分类1.1 先认清需求标注数据不够才是常态如果只看“分类系统”这四个字很容易默认选分类算法但客服问题分类场景里最常见的现实问题是系统要上线标注数据几乎没有。公司的客服工单虽然积累了海量历史记录但这些记录没有统一的类别字段就算有也是客服人员随手选的粒度不一致、错别字一堆直接拿来训练有监督模型效果往往很糟糕。这时候聚类的作用是把“未知”变成“已知”先用无监督方法把相似问题聚成若干堆再让业务人员给每一堆取名字相当于把人工整理类别的工作量从“逐条打标”降到了“逐簇打标”。如果一开始就上 BERT 分类模型反而会因为标准答案都没定、样本数量不够陷入反复返工的泥潭。我当时的判断是第一版先用聚类跑出一个可解释的类别雏形让业务方看到一个个具体的簇和示例再来讨论类别体系怎么定。这个顺序比直接拍脑袋定义十个类别、再花两周标注要靠谱得多。1.2 聚类和监督分类怎么选才理性两者不是替代关系而是阶段不同。监督分类适合类别体系已经稳定、样本标注足够多的线上成熟阶段聚类适合冷启动、动态发现新问题阶段。可以把几个维度放在一起对比维度无监督聚类有监督分类标注要求基本不需要每类至少几百条类别体系由聚类结果反推预先人工定义新问题发现天然支持靠距离判断无法识别只能硬归到已知类落地速度快几个小时能出雏形慢标注和训练迭代周期长精度上限取决于文本表征和簇质量标注质量够高时通常更高所以方案评审时定的基调是第一版用聚类做冷启动形成类别雏形再逐步收集“人工确认后的样本”为后续训练分类模型做准备。这样做的好处是业务方能看到一个个具体的簇和示例修改起来也直观。等积累的标注数据足够多之后再切换成有监督分类模型也不迟。1.3 系统整体架构从原始问题到业务可用整个系统可以拆成五段流水线数据采集从客服后台导出用户原始问题文本附上时间、渠道等字段。文本预处理清洗 URL、表情、重复标点分词去停用词。文本向量化用 TF-IDF 把每一条问题转成向量。聚类分析用 K-Means 或 DBSCAN 对向量聚类自动确定簇数。结果消费生成簇标签、高频词、代表问题写入结果表供工单路由、热点监控使用。这五段看似简单但每一段都有不少“坑”。文本预处理不到位后面聚类结果就是一团浆糊向量化方式选错业务上明明不同的两类问题会被硬凑到一起聚类算法参数没调好簇的粒度就会出现“一个簇里什么都有”或者“同一个问题被切得稀碎”。我后面会按模块展开把实际处理中反复踩过的细节都写出来。2. 数据处理与文本向量化聚类效果的底层逻辑2.1 清洗与分词先把“口语化”压下去客服问题文本和新闻文本差别很大。用户经常写“我的订单怎么还不发货啊啊啊啊”或者“发票发票给我发票”如果不去重、不清理聚类时这些重复字符会成为主导特征把真正有区分度的词盖住。我常用的清洗步骤是去掉 URL、手机号、邮箱把连续重复标点替换成单个把中英文标点统一去掉 emoji对纯数字串做归一化。清洗之后再分词我用 jieba 的精确模式同时准备一份客服领域的停用词表除了常见“的、了、吗”还要把“请问、麻烦”这类礼貌用语按场景决定是否删除。这里有一个容易忽略的细节停用词不是越多越好。比如把“怎么”删掉会让“怎么申请退款”和“多久退款”这两个问题在词袋层面失去部分区分信息但如果不删每个问题里都带“怎么”又会把许多不同类型的咨询往同一个方向上拉。我的做法是先保留一批高频疑问词观察聚类后发现某一簇混杂严重再回过来逐个尝试删除而不是一开始就把所有疑问词全部删光。2.2 TF-IDF、词向量与句向量怎么选文本向量化是最影响聚类效果的一步。第一版我强烈建议用 TF-IDF因为它简单、可解释性强、对短文本效果稳定。TF-IDF 的本质是用词频 TF 乘逆文档频率 IDF一个词在越少文档里出现IDF 越高它在区分文档时越重要。比如“发货”在很多订单问题里都会出现IDF 会被压低而“拒收”只在特定问题里出现IDF 会抬高聚类时更容易把这些簇分开。如果换成 Word2Vec 或 Sentence-BERT表达能力强但需要大量同领域语料微调在只有几千条工单的冷启动阶段效果不一定比 TF-IDF 好。而且词向量的可解释性弱出了问题不好排查。我的建议是先跑通 TF-IDF把聚类流程建好如果后续发现簇内语义混杂严重再升级成句向量。不要一开始就追求“最先进”否则会把时间浪费在模型选型上而不是业务效果上。2.3 向量化之后的降维到底降不降高维稀疏向量的距离计算在 K-Means 里并不致命但维度太高会让聚类中心更新时噪声变大。我习惯用 sklearn 的 TruncatedSVD 把 TF-IDF 向量降到 100 维左右再做聚类。这个操作有几个实际好处一是运行速度明显提升二是聚类中心更稳定三是对轮廓系数评估有一定稳定作用。降维不是必须的但建议做。如果使用 UMAP 降维到二维可以方便可视化但会把原始距离关系改变不少不建议直接把降维后的向量输入 K-Means 做正式结果更适合做展示和探索。实际项目中我会同时保留两组向量一组降维后用于速度和稳定性一组原始稀疏向量用于找簇内关键词。这样既能跑得快又不丢失可解释性。3. 聚类算法选型与参数调节3.1 K-Means第一版的主力但要选对初始化K-Means 是客服问题聚类最常用的算法核心逻辑是随机初始化 k 个中心迭代更新直到中心不再变化。它快适合几千到几十万条文本但对初始中心敏感且假设簇是凸的。在 sklearn 里n_init 参数默认是 10意味着会跑 10 次取最优结果所以不要为了省时间把它设成 1否则每次结果可能不一样业务方会拿“上次和这次结果不一样”来找你。另一个容易踩的坑是 KMeans 之前要不要做标准化。TF-IDF 向量本身是加权词频我倾向于让 TfidfVectorizer 使用 L2 归一化这样内积和余弦距离等价聚类时不会出现长文本主导的情况。如果数据里混着超短句和长句不归一化会导致长句向量模长更大对簇中心的影响更大结果往往偏向长文本。3.2 DBSCAN噪声检测和簇数不确定时可以考虑K-Means 要求预先指定 k但如果业务上根本不知道问题应该分成多少类可以试 DBSCAN。它按密度聚类把低密度区域的点标记为噪声不需要指定簇数只需调 eps 和 min_samples。文本向量场景下eps 很难直接猜我通常先降维再按 eps 从 0.1 到 1.0 逐步扫描观察簇数变化和噪声比例。DBSCAN 的问题是如果用户问题分布密度差异很大一个 eps 很难同时照顾到密集簇和稀疏簇。客服问题通常是围绕几个核心话题展开密度差异明显所以我会把 DBSCAN 结果作为 K-Means 的补充用它找“那些离所有簇都比较远”的异常问题而不是作为唯一模型。简单说K-Means 负责“常规分类”DBSCAN 负责“边界和新话题发现”。3.3 用轮廓系数评估聚类看完数字还要看样本很多人拿到轮廓系数就当作唯一指标这在文本聚类里容易误判。轮廓系数衡量的是样本和自身簇内相似度与最近其他簇相似度的差值取值 -1 到 1越大越好。但在短文本 TF-IDF 场景下轮廓系数普遍不高0.1 到 0.3 都很常见不代表聚类没用。我的评估套路是先用轮廓系数初筛 k 的取值范围再对每个候选 k 跑一次聚类人工去看每个簇里“排名靠前的代表问题”和“高频关键词”。如果某个簇的问题句句都能用同一个标签解释即使轮廓系数不到 0.2也可以上线如果数字好看但簇内完全混杂那说明向量化或预处理有问题。聚类本质上是一个“帮人归纳”的工具只有人工看得懂业务才愿意用。3.4 从簇到标签让业务人员只看摘要不用翻全量数据聚类输出的只是“第 0 簇、第 1 簇”业务方不可能逐条翻。我给每个簇自动生成三段信息高频名词短语、中心距离最近的几个代表性样本、簇内样本的时间分布。高频词可以直接用 TF-IDF 特征名加权重排序更复杂一点可以提取名词短语代表性问题就取离簇中心最近的 3 到 5 条原始问题。这样业务人员在确认标签时只需要读 5 到 10 句话就能判断这个簇是不是同一个需求。如果某一簇里混入多个主题代表性问题很快会暴露出来这时候我会把簇数加大或者修改停用词重新聚类。这个过程要来回几轮但每次迭代的成本都不高几分钟就能跑完一轮比人工逐条打标快太多。4. 完整实现流程与关键代码4.1 准备一个最小的数据样例为了演示我构造了一个完全虚构的数据集共 60 条问题大致覆盖发货、物流、发票、退款、账号登录几类。真实环境肯定比这个量大但流程是相同的。数据字段包括 question_id、content、channel、created_at下面所有代码都假设数据已经读成 Pandas DataFrame列名为 content。准备数据这一步很多人会忽略但类别不平衡、内容重复率高的问题会直接影响聚类效果。比如“订单怎么还没发货”出现了 30 次“发票抬头怎么改”只出现了 3 次聚类时前者会形成一个超大簇后者可能被并到别的簇里。如果业务上对低频话题也敏感可以先按文本去重或者对小簇做保护处理而不是让 K-Means 只看数量。4.2 预处理与 TF-IDF 向量化的可运行代码先写一个 clean_text 函数处理重复符号和特殊字符然后用 jieba 精确模式分词并用空格拼接交给 TfidfVectorizer。注意 min_df 和 max_df 的设定min_df1 表示只出现一次的词也会保留数据量小的时候可以如果数据有几万条min_df1 会产生大量噪声需要调高到 2 或 3。max_df0.8 用于去掉几乎没有区分度的常见词。import re import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer stopwords set([的, 了, 吗, 呢, 啊, 请问, 怎么, 什么, 如何]) def clean_text(s): s re.sub(rhttp\S|www\.\S, , s) s re.sub(r[!。,~], , s) s re.sub(r\d{11}, 手机号, s) return s def seg_text(s): s clean_text(str(s)) words jieba.lcut(s) return .join([w for w in words if w.strip() and w not in stopwords]) df[seg] df[content].apply(seg_text) vec TfidfVectorizer(min_df1, max_df0.8, sublinear_tfTrue) X vec.fit_transform(df[seg])跑完后 X 是稀疏矩阵行对应每条问题列对应词。sublinear_tfTrue 会让词频出现饱和防止重复词干扰这条参数我几乎每次都会开。4.3 K-Means 聚类与最优 K 选择代码K 值选择我用轮廓系数和业务可解释性结合。代码里先循环 k 从 2 到 12计算轮廓系数打印结果。这里有一个小细节轮廓系数在降维后的空间计算比在高维稀疏空间稳定所以我先做 TruncatedSVD 再算指标。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score from sklearn.decomposition import TruncatedSVD svd TruncatedSVD(n_components100, random_state42) X_reduced svd.fit_transform(X) for k in range(2, 13): km KMeans(n_clustersk, n_init10, random_state42) labels km.fit_predict(X_reduced) score silhouette_score(X_reduced, labels, sample_size2000) print(fk{k}, silhouette{score:.4f})如果样本量很大silhouette_score 的 sample_size 可以设 2000否则全量计算很慢。看到输出后我会选轮廓系数明显拐点或者业务上能解释的 k。比如 k5 轮廓系数 0.23k6 也是 0.22但 k6 版本里“开发票”和“发票抬头”分成了两簇业务方觉得有必要保留那就选 6。数字只是参考最终解释权在业务。4.4 簇主题提取与落地聚类完成后把 cluster_label 字段写回 DataFrame。接着生成每个簇的主题名称。自动提取 Top 关键词的函数可以这样写def get_top_words(model, vectorizer, cluster_labels, k, n8): result {} for cluster in range(k): docs df.loc[cluster_labels cluster, content] if len(docs) 0: continue centroid model.cluster_centers_[cluster] top_idx centroid.argsort()[::-1][:n] words [vectorizer.get_feature_names_out()[i] for i in top_idx] result[cluster] { size: len(docs), top_words: words, examples: docs.head(3).tolist() } return result要注意这里的 model 应该是在原始稀疏特征 X 上训练的而不是降维后的否则 cluster_centers_ 的维度对不上特征名也取不出来。实际操作中我会在原始 X 上训练 KMeans再用降维结果画图。最后把 result 写成 JSON 或 Excel人工确认后保存簇标签表。簇标签表包括 cluster_id、cluster_name、关键词、代表问题、确认状态这个表之后要给工单系统做映射。4.5 增量更新与新问题检测系统上线后不能每次都全量重聚类。第一版我采用“预测最近中心”的方式新问题向量化后计算它到每个簇中心的距离归入最小距离簇如果最小距离超过某个阈值就标记为“疑似新问题”。阈值怎么定我拿历史数据的簇内距离分位数来定比如取第 95 分位数作为参考。from scipy.spatial.distance import cdist def assign_new_text(text, vectorizer, km, threshold): seg seg_text(text) x vectorizer.transform([seg]) distances cdist(x.toarray(), km.cluster_centers_, metriceuclidean).flatten() min_idx distances.argmin() if distances[min_idx] threshold: return -1, distances[min_idx] return min_idx, distances[min_idx]这里阈值最好每个簇分开算因为不同簇的紧凑程度不同比如“退款”簇很集中“账号问题”簇很分散同一个阈值会导致前者过窄、后者过宽。更严谨的做法是每个簇保存一个距离阈值字典。增量更新的好处是响应快成本低缺点是如果业务出现大规模变化旧簇中心会失真所以我会每周跑一次全量重聚类用聚类结果修正中心点。5. 上线部署中的常见问题与排查技巧5.1 簇太碎或太粗怎么调整粒度客服问题聚类最常见的问题是“一个簇里面什么都有”或者“同一个问题被拆成好几个簇”。前者一般是簇数太少或停用词没删干净后者是簇数太多或关键词权重太分散。我调整顺序是先看簇内代表问题再决定是调高或调低 k还是改停用词。如果是 K-Meansk 每加 1 通常会把最大簇拆开k 减 1 会合并一些相近簇。小簇要不要保留我一般会把样本量很少的簇单独打上“低频/其他”标记而不是强行做成一个正式类别。因为客服管理上更关心的是高频问题低频簇数量太多反而增加标签维护成本。如果某些低频簇确实很重要比如“发票”类问题虽然量少但影响财务流程那就要通过业务规则把它单独摘出来而不是让聚类算法替你决定。5.2 聚类结果不稳定每次跑都不一样K-Means 的结果受随机初始化影响这是新手最容易踩的坑。解决办法有三个固定 random_state把 n_init 调大比如 20对最终结果做一次稳定度检查连续跑 10 次看相同样本的分簇是否一致。如果还不行考虑把数据顺序打乱后重跑或者改用层次聚类作为交叉验证。另一个细节是sklearn 版本变化时随机数生成逻辑可能变导致结果改变所以代码里除了 random_state还要把 sklearn 版本记录到实验日志里方便复现。我在项目里会把向量化参数、聚类参数、聚类结果版本号一起写进配置表每次重跑都有版本记录。这个习惯在“业务质疑结果”时特别有用能快速定位是数据变了还是参数变了。5.3 冷启动和业务闭环聚类结果怎么用起来聚类本身不是终点结果要能进到业务系统里。我的做法是把聚类结果写入一张“问题类别映射表”包含 cluster_id、cluster_name、关键词、代表问题、确认状态。客服工单进来后先经过这个映射表做初判人工客服再修正修正结果作为标注回流。运行一段时间后如果某一类累计样本量超过几百条就可以训练一个小型有监督分类器作为线上分流模型聚类则退居二线专门做新问题监测。这个闭环比单纯追求聚类算法精度更重要。我见过不少团队把精力全花在调聚类参数上结果聚类效果明明还行业务却不知道怎么用最后项目不了了之。与其这样不如一开始就和业务方说清楚聚类只是帮你归纳原始数据真正的价值在后续的工单路由、热点监控和知识库更新。5.4 实测中的一点经验别只看算法指标最后说一个我在实际调试中的感受文本聚类的“好坏”很难只靠一两个数字判断。我会把每个簇的样本随机打印 20 条用肉眼扫一遍确认这个簇像一个“客服问题类”再看簇间的代表问题是否明显可区分。如果簇间混入了同一种关键词先不要急着换算法去查预处理和向量化。很多时候一条问题的错别字、分词错误、标点残留就能把簇打乱。小数据量时把向量维度降一些、把停用词调准效果比换一个更复杂的聚类模型更明显。上线前我还喜欢把“别人问的和聚类结果完全对不上”的样本单独拉出来那往往是新需求或者话术变体价值很高。这些样本不需要急着归入某个簇可以单独建立一个“待确认”列表让业务方每周看一次慢慢积累出新的服务专题。

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

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

免费获取报价 →
↑