94万条热线问题的分析之路——KMeans聚类、动态相似度与大模型分类
文章目录
某政务热线有94万条历史问题,怎么搞清楚这些问题到底在问什么?哪些是高频的?哪些知识库还没覆盖?这篇记录我用三种方法分析这批数据的过程:KMeans聚类做粗分、向量相似度做精分、大模型做语义分类。
一、问题:94万条问题,人工看不完
94万条问题,格式是这样的:
退休了医保怎么办
社保卡丢了怎么补办
灵活就业人员怎么参保
...
没有分类标签,没有结构化信息,就是一堆文本。要回答三个问题:
- 这些问题可以分成几大类? 参保?缴费?社保卡?
- 哪些问题是重复的? 94万里有多少是同一件事换了个问法
- 高频问题我们的知识库覆盖了多少?
二、方法一:KMeans + TF-IDF粗分
原理
TF-IDF把每条问题转成一个稀疏向量(关键词权重),KMeans把向量聚成N个簇,同一个簇里的问题被认为是一类。
import jieba
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
import csv
texts = []
with open('problem.csv', 'r', encoding="utf-8") as file:
csv_reader = csv.reader(file)
for row in csv_reader:
texts.append(row[0])
def chinese_tokenizer(text):
return jieba.cut(text)
vectorizer = TfidfVectorizer(tokenizer=chinese_tokenizer)
X = vectorizer.fit_transform(texts)
kmeans = KMeans(n_clusters=5000, random_state=42)
kmeans.fit(X)
labels = kmeans.labels_
with open("result.txt", "w", encoding="utf-8") as file:
for i, text in enumerate(texts):
file.write(f"{text}\t{labels[i]}\n")
jieba分词 → TF-IDF提取特征 → KMeans聚5000个簇。
效果
5000个簇意味着平均每个簇约188条问题(94万/5000)。同一个簇里的问题确实有关联性,比如簇#1234里可能全是"退休"相关的问题。
问题:TF-IDF是基于关键词的,不理解语义。"退休怎么办"和"到了法定年龄怎么办理退休手续"分词结果不同,可能被分到不同的簇。粗分可用,精细不够。
三、方法二:向量相似度动态聚类
上一篇文章里把问题都向量化存到了Milvus。现在用向量相似度做更精准的聚类。
思路
从第一条问题开始,在Milvus里搜所有和它余弦相似度>0.95的问题,归为一类。然后找下一条还没被归类的问题,重复这个过程。
search_params = {"metric_type": "COSINE", "params": {"radius": 0.95}}
getedlist = []
for i in range(1, 93936):
qs = client.get(collection_name="p_hotline", ids=[i],
output_fields=["uid", "Question", "embeddings_Q"])
v = qs[0]["embeddings_Q"]
uid = qs[0]["uid"]
if uid in getedlist:
continue
res = client.search(
collection_name="p_hotline",
data=[v],
limit=10000,
output_fields=["uid", "Question"],
search_params=search_params
)
for j in range(0, res[0].__len__()):
getedlist.append(res[0][j]["entity"]["uid"])
file.write(str(uid) + "\t" + str(res[0][j]["entity"]["uid"]) + "\t"
+ str(res[0][j]["distance"]) + "\t"
+ res[0][j]["entity"]["Question"] + "\n")
关键设计:
- 相似度阈值0.95起步——非常严格,只有几乎一样的问题才会归到一起
- 已归类的问题跳过——
getedlist记录所有已归类的问题ID,避免重复处理 - 逐条遍历93936条——对每条未归类的问题,搜索所有和它相似的问题
效果
比KMeans精准得多。"退休怎么办"和"到了退休年龄怎么办理"相似度>0.95,会被归到一类。因为用的是语义向量,不是关键词。
问题:慢。93936条,每条都要搜一次Milvus,跑了几个小时。
四、方法三:大模型语义分类
KMeans粗分、向量聚类精分,都还需要人去看每个簇/类的代表问题来确定分类名称。能不能让大模型直接分类?
DeepSeek分类
from openai import OpenAI
ctext = ("分类为参保、缴费、社保卡、养老保险、失业保险、医疗保险、"
"工伤保险、生育保险、人事、就业培训、就业、社保档案、其他档案、其他"
"之间那一类问题,只需返回的那分类的那几个字")
def f_classifiy(txt):
client = OpenAI(api_key="sk-xxx", base_url="https://api.deepseek.com")
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": txt}],
stream=False
)
return response.choices[0].message.content
for str1 in texts:
str2 = f_classifiy(str1 + " " + ctext)
file.write(str1 + "\t" + str2 + "\n")
把问题文本 + 分类要求一起发给DeepSeek,让它返回类别名称。14个预设类别覆盖了社保的主要领域。
效果
分类准确率很高。DeepSeek对中文社保领域的语义理解比TF-IDF强太多了。“退休后医保还能报销吗"被正确分类为"医疗保险”,不是"养老保险"。
问题:成本。每条问题调用一次API,94万条费用不低。实际上先用向量聚类把重复问题去掉,剩下几万条不重复的再用大模型分类,成本可控。
五、知识库覆盖率分析
分类完了,还要回答一个问题:高频问题我们的知识库覆盖了没有?
# 从p2集合取出所有问题向量,在SI_knowledge知识库中检索
for j in range(0, qs[0].__len__()):
res = client.search(
collection_name="SI_knowledge",
data=[qs[0][j]["entity"]["v"]],
limit=10,
output_fields=["uid", "Question"],
search_params={"metric_type": "COSINE", "params": {"radius": 0.0}}
)
if res[0].__len__() > 0:
file.write(qs[0][j]["entity"]["Question"] + "\t"
+ str(res[0][0]["distance"]) + "\t"
+ res[0][0]["entity"]["Question"] + "\n")
把热线问题拿到知识库里搜,看匹配的最高相似度是多少。相似度>0.8说明知识库有覆盖,<0.5说明是知识盲区。
这个分析直接告诉我们:该往知识库补什么内容。
六、三种方法的对比
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| KMeans+TF-IDF | 关键词特征聚类 | 快、不需要额外服务 | 不理解语义、簇需要人工解读 |
| 向量相似度聚类 | 语义向量+余弦相似度 | 精准、理解语义 | 慢、需要先建向量库 |
| 大模型分类 | LLM语义理解 | 最精准、直接出类别名 | 有API成本、有速率限制 |
实际工作流:
- 先用向量相似度去重(94万→几万条不重复)
- 再用大模型分类(几万条→14个类别)
- 最后用覆盖率分析找出知识盲区
三种方法不是替代关系,是流水线关系。每一步为下一步准备更干净的数据。
相关阅读:
- 《向量数据库实战——用Milvus+Ollama搭建社保知识检索系统》
- 《约94万条热线问题怎么去重?动态相似度阈值+Milvus》
- 《知识库建好了但够不够用?向量检索量化覆盖率》
423

被折叠的 条评论
为什么被折叠?



