☰
基于聚类的智能客服问题分类系统实现
2026/10/19 10:54:21 网站建设 项目流程

做客服问题分类这件事,最让人头疼的往往不是“已知问题怎么分得更细”,而是“面对一堆没有标签的用户问题,怎么快速知道里面到底都在聊什么”。我最近帮某电商团队搭了一套基于聚类的智能客服问题分类系统,完全用 Python 实现。这套系统的核心思路很直接:用无监督聚类把相似问题自动聚到一起,再由人工给每个簇贴标签,后续新问题进来后直接归到已有簇,离所有簇都很远的问题单独标记出来。相比一开始就训练一个有监督分类模型,这种方法在冷启动阶段特别占优势,不需要上万条标注数据,也能让客服工单的初步归类、热点问题监控快速跑起来。

1. 智能客服问题分类,为什么我选了聚类而不是分类

1.1 先认清需求:标注数据不够才是常态

如果只看“分类系统”这四个字,很容易默认选分类算法,但客服问题分类场景里最常见的现实问题是:系统要上线,标注数据几乎没有。公司的客服工单虽然积累了海量历史记录,但这些记录没有统一的类别字段;就算有,也是客服人员随手选的,粒度不一致、错别字一堆,直接拿来训练有监督模型,效果往往很糟糕。这时候,聚类的作用是把“未知”变成“已知”:先用无监督方法把相似问题聚成若干堆,再让业务人员给每一堆取名字,相当于把人工整理类别的工作量从“逐条打标”降到了“逐簇打标”。

如果一开始就上 BERT 分类模型,反而会因为标准答案都没定、样本数量不够,陷入反复返工的泥潭。我当时的判断是:第一版先用聚类跑出一个可解释的类别雏形,让业务方看到一个个具体的簇和示例,再来讨论类别体系怎么定。这个顺序比直接拍脑袋定义十个类别、再花两周标注要靠谱得多。

1.2 聚类和监督分类,怎么选才理性

两者不是替代关系,而是阶段不同。监督分类适合类别体系已经稳定、样本标注足够多的线上成熟阶段;聚类适合冷启动、动态发现新问题阶段。可以把几个维度放在一起对比:

维度无监督聚类有监督分类
标注要求基本不需要每类至少几百条
类别体系由聚类结果反推预先人工定义
新问题发现天然支持,靠距离判断无法识别,只能硬归到已知类
落地速度快,几个小时能出雏形慢,标注和训练迭代周期长
精度上限取决于文本表征和簇质量标注质量够高时通常更高

所以方案评审时定的基调是:第一版用聚类做冷启动,形成类别雏形,再逐步收集“人工确认后的样本”,为后续训练分类模型做准备。这样做的好处是业务方能看到一个个具体的簇和示例,修改起来也直观。等积累的标注数据足够多之后,再切换成有监督分类模型也不迟。

1.3 系统整体架构:从原始问题到业务可用

整个系统可以拆成五段流水线:

  1. 数据采集:从客服后台导出用户原始问题文本,附上时间、渠道等字段。
  2. 文本预处理:清洗 URL、表情、重复标点,分词,去停用词。
  3. 文本向量化:用 TF-IDF 把每一条问题转成向量。
  4. 聚类分析:用 K-Means 或 DBSCAN 对向量聚类,自动确定簇数。
  5. 结果消费:生成簇标签、高频词、代表问题,写入结果表,供工单路由、热点监控使用。

这五段看似简单,但每一段都有不少“坑”。文本预处理不到位,后面聚类结果就是一团浆糊;向量化方式选错,业务上明明不同的两类问题会被硬凑到一起;聚类算法参数没调好,簇的粒度就会出现“一个簇里什么都有”或者“同一个问题被切得稀碎”。我后面会按模块展开,把实际处理中反复踩过的细节都写出来。

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_df=1 表示只出现一次的词也会保留,数据量小的时候可以;如果数据有几万条,min_df=1 会产生大量噪声,需要调高到 2 或 3。max_df=0.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(r"http\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_df=1, max_df=0.8, sublinear_tf=True) X = vec.fit_transform(df["seg"])

跑完后 X 是稀疏矩阵,行对应每条问题,列对应词。sublinear_tf=True 会让词频出现饱和,防止重复词干扰,这条参数我几乎每次都会开。

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_components=100, random_state=42) X_reduced = svd.fit_transform(X) for k in range(2, 13): km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_reduced) score = silhouette_score(X_reduced, labels, sample_size=2000) print(f"k={k}, silhouette={score:.4f}")

如果样本量很大,silhouette_score 的 sample_size 可以设 2000,否则全量计算很慢。看到输出后,我会选轮廓系数明显拐点,或者业务上能解释的 k。比如 k=5 轮廓系数 0.23,k=6 也是 0.22,但 k=6 版本里“开发票”和“发票抬头”分成了两簇,业务方觉得有必要保留,那就选 6。数字只是参考,最终解释权在业务。

4.4 簇主题提取与落地

聚类完成后,把 cluster_label 字段写回 DataFrame。接着生成每个簇的主题名称。自动提取 Top 关键词的函数可以这样写:

def get_top_words(model, vectorizer, cluster_labels, k, n=8): 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_, metric="euclidean").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-Means,k 每加 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 条,用肉眼扫一遍,确认这个簇像一个“客服问题类”;再看簇间的代表问题是否明显可区分。如果簇间混入了同一种关键词,先不要急着换算法,去查预处理和向量化。

很多时候,一条问题的错别字、分词错误、标点残留就能把簇打乱。小数据量时,把向量维度降一些、把停用词调准,效果比换一个更复杂的聚类模型更明显。上线前我还喜欢把“别人问的和聚类结果完全对不上”的样本单独拉出来,那往往是新需求或者话术变体,价值很高。这些样本不需要急着归入某个簇,可以单独建立一个“待确认”列表,让业务方每周看一次,慢慢积累出新的服务专题。

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

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

立即咨询