客服记录里的问句怎么变成知识库条目:意图聚类的 Python 实现

上千条客户问句摊在表格里,看着每条都不一样,去掉说法差异之后往往只剩一百多个意思。「质保期多久」「这个保修几年」「出问题能免费换吗」说的是同一件事。知识库需要的不是原始问句,而是意图:一个意图配一条标准问、一条答案、一组别名说法。原始问句直接入库,检索时同一条答复会被三种表述各自命中,答案还不一定一致。

下面这套流程不依赖大模型:字符 n-gram 加 TF-IDF 表示问句,层次聚类按距离阈值切簇,再用规则从簇里挑标准问,落成带字段的条目。五步里每一步都说清楚它容易在哪里失效。

一、清洗与归一:先把「问的是什么」剥出来

问句前面挂着寒暄,后面挂着标点,中间还可能夹着指代。清洗只针对聚类用的那份文本,原文一份都不能改------条目要能回溯到客户真实说过什么。

python 复制代码
import re

NOISE = re.compile(r"^(你好[,,]?|在吗[,,]?|请问|麻烦问一下?|师傅[,,]?)+")
TAIL = re.compile(r"[??。!!,,、~\s]+$")

def normalize(raw):
    """只用于聚类;原文另存一份,别在这一步改写。"""
    t = NOISE.sub("", raw.strip())
    t = re.sub(r"\s+", "", t)
    return TAIL.sub("", t).lower()

def split_short(lines, min_len=5):
    """过短的问句不带意图信息,单独归桶,不要混进聚类。"""
    keep = [l for l in lines if len(normalize(l)) >= min_len]
    brief = [l for l in lines if 0 < len(normalize(l)) < min_len]
    return keep, brief

「多少钱」「在吗」这类短句归进「待补上下文」桶。它们不带主题信息,混进聚类会把不同话题的句子拉到一起,事后拆簇比重新聚一遍还麻烦。

二、向量化:中文短问句用字符 n-gram 就够

分词不是必需项。问句通常十几个字,错别字与口语省略多,切词一旦出错就把整句推到别的方向上;2 到 4 字符的 n-gram 稳定得多。

python 复制代码
from sklearn.feature_extraction.text import TfidfVectorizer

def vectorize(questions):
    """sublinear_tf 压高频词,min_df 压一次性写法。"""
    vec = TfidfVectorizer(analyzer="char", ngram_range=(2, 4),
                          sublinear_tf=True, min_df=2, max_features=20000)
    return vec.fit_transform(questions)

min_df 是这一步里需要反复调的参数。设成 1,只出现一次的写法会自成方向,簇数直接翻几倍;设太大,低频但真实存在的说法被压成零向量,全部落进噪声簇。做法是从 2 起步,观察簇数随 min_df 的变化曲线,找到拐点再定。

三、聚类与阈值:簇数是结果,不是目标

层次聚类的好处在于阈值语义直观------两点之间的距离表示「两个问句说得有多不像」,不必事先猜要分多少个簇。

python 复制代码
from scipy.spatial.distance import pdist, squareform
from sklearn.cluster import AgglomerativeClustering

def cluster(matrix, threshold=0.62):
    """n_clusters=None:让阈值决定簇数,簇数不是事先定的指标。"""
    dist = squareform(pdist(matrix.toarray(), metric="cosine"))
    model = AgglomerativeClustering(n_clusters=None, metric="precomputed",
                                    linkage="average", distance_threshold=threshold)
    return model.fit_predict(dist)

阈值往上调,簇变少,不同意思被合并的风险上升;往下调,簇变多,同一个意思被切成好几份。判断标准不是簇数好不好看,而是随机抽十个簇,看其中有几个能共用同一条答案------这个比例可以量化复核,比盯着聚类图可靠。

四、簇的体检:异常形态各有成因

| 现象 | 大概率原因 | 处置 |

| --- | --- | --- |

| 簇数上千,多数簇只有一条 | 阈值过低,或清洗没做够 | 先查归一,再逐步上调阈值 |

| 一个大簇吃掉三成问句 | 高频虚词权重压不住 | 提高 n-gram 下限,或换 complete 联动 |

| 同主题被切成三簇 | 问句里掺了型号、地名 | 聚类前把可变实体换成占位符 |

| 簇内句子通、答案对不上 | 同形不同义 | 拆成两条条目,不强行合并 |

| 新说法全部落进噪声簇 | 沿用了半年前的词表 | 定期重跑词表,而不是只重跑聚类 |

实体占位这一步很容易被低估:「某型号能不能延保」和「另一型号能不能延保」在字符层面差别不小,在意图层面是同一句。聚类前把型号、地区、日期换成占位标记,簇的质量会有明显变化。

五、从簇到条目:标准问挑的不是字数多的那句

一个簇给一条条目。标准问既不是出现次数多的那句,也不是写得完整的那句,而是脱离上下文仍然读得懂的那句。

python 复制代码
VAGUE = re.compile(r"(这个|那个|它|上面|如前|你们)")

def pick_std(members):
    """members: [(原文, 归一句, 出现次数)]。含指代的降权,但不删除。"""
    scored = []
    for raw, norm, count in members:
        weight = 0.4 if VAGUE.search(norm) else 1.0
        scored.append((count * weight + min(len(norm), 24) * 0.05, raw, norm))
    scored.sort(reverse=True)
    return scored[0][1], [r for _, r, _ in scored[:6]]

降权而不是丢弃,是因为客户原话很值钱:检索命中靠的就是这些说法。挑完标准问,剩下的工作是把条目落成字段,一个簇一行。

| 字段 | 内容 | 约束 |

| --- | --- | --- |

| standard_q | 挑出的标准问 | 脱离上下文可读 |

| aliases | 簇内原文若干条 | 保留口语与错别字 |

| answer | 一条成文答复 | 不许写「视情况而定」 |

| applies_to | 型号、地区、时间窗口 | 无条件时明确写「通用」 |

| source_window | 会话日期区间 | 超过半年未复核的标记待核 |

applies_to 单独成列看着是小事,实际决定答案能不能被直接用:答复写「质保十二个月」而不带条件,检索命中之后还得再问一句是哪个型号,问答就退回人工。

条目字段确定之后,写出来的结构是一段可以直接进库的记录,答案留空位由人工补。

python 复制代码
def make_entry(cid, members, answers, window):
    std, aliases = pick_std(members)
    return {
        "entry_id": "Q%04d" % cid,
        "standard_q": std,
        "aliases": aliases,            # 保留口语与错别字,命中靠它们
        "answer": answers.get(std, "待补:本簇没有可用答案"),
        "applies_to": answers.get(std + "#scope", "通用"),
        "source_window": window,       # 会话日期区间,便于日后复核
        "status": "draft",             # draft / reviewed / retired
    }

六、评估与回流:簇会自己漂

· 纯度抽检:随机抽二十个簇,人工判「能不能共用一条答案」,共用率低于八成说明阈值需要往下调。

· 覆盖度:没被聚进任何簇的单条问句占比,超过三分之一要回头查清洗,而不是先动聚类参数。

· 新增复检:每周把新问句投进已有簇,统计归错率。归错率突然上升,通常是新业务带来了一批新语义。

· 淘汰:一条答复三个月没被命中,不直接删,先降到兜底位置再观察一轮。

这套流程里没有任何一步需要训练模型:清洗、向量化、聚类、挑标准问、落字段,每步都能单独复核。它能解决的问题也有限------只保证同义说法被归到一起,不保证归进去的那条答案是对的。答案对不对要人写完再核对,聚类的意义是把「该核对哪几条」从一千句压到一百句,剩下那九十句留给下一轮。

(本文由一支长期做企业信息结构化与检索侧观察的技术团队整理,欢迎同行交流指正。)

相关推荐
严谨的麻辣烫1 小时前
为什么 API 接口偶尔报错,重试一次又恢复了?从 HTTP 连接池到 Keep-Alive 排查
python·网络协议·tcp/ip·http·性能优化
学Linux的语莫2 小时前
uv的使用
python·uv
asdzx672 小时前
使用 Python 按页或节拆分 Word 文档
python·word
技术人生0012 小时前
Selenium 工程化模板(日志+异常截图,可直接复制使用)
python·selenium·测试工具
我的xiaodoujiao2 小时前
Django 基础知识详细图文教程 15-Django 内置Admin系统
后端·python·django
夜晚回家2 小时前
Python 变量入门详解 | 用 BMI 案例串起赋值、乘方与取整
android·python
技术人生0012 小时前
Selenium4 Python 完整教程(适合网页自动化、页面数据采集、UI自动化测试)
python·ui·自动化
用户3494467912032 小时前
我把午饭外包给了 AI:用麦当劳官方 MCP 造了个每天自己写日记的 GitHub 仓库
自动化运维
小小张说故事2 小时前
Python 里 a is b 有时 True 有时 False?小整数缓存和常量折叠的底层真相
python