# 约94万条热线问题怎么去重?动态相似度阈值+Milvus,不用LLM一毛钱

约94万条热线问题怎么去重?动态相似度阈值+Milvus,不用LLM一毛钱

背景

政务热线每天的来电都会转成文字记录。积累了几年下来,问题库动辄几十万条。拿来做RAG知识库之前,必须先去重------不然"养老保险怎么交"和"养老金怎么缴纳"是同一条知识,存两份就浪费,检索时还可能把两个相似但略有不同的答案都捞出来,LLM拿到矛盾的上下文就开始胡编。

问题来了:94万条问题,怎么去重?

方案对比

方案 思路 问题
关键词去重 分词后比关键词重叠度 "社保卡丢了怎么办"和"社会保障卡遗失如何补办"零重叠,但语义完全一样
编辑距离 字符串相似度 94万×94万的比对矩阵,算到明年
LLM分类 让大模型判断两条是否重复 94万条,每条调一次API,费用上千,速度还慢
向量相似度聚类 Embedding + Milvus余弦相似度 本地部署零成本,毫秒级检索

选第四条路。

核心思路:给个值,跑跑看,效果不够再降

94万条问题不可能一开始就定好阈值。实际过程是这样的:

第一次跑,给了0.9。 结果出来一看,"社保卡丢了怎么办"和"社保卡丢了咋办"确实合并了,但"社保卡丢失如何补办"没进来------表述差异大了一点点,相似度掉到0.88,被0.9的阈值挡住了。

降到0.85再跑。 这回好多了,表述差异大的也进来了。但又发现新的问题------有些命中的条目看一眼就知道不是同一个意思,但相似度偏偏过了0.85。这说明0.85在这个数据集上是临界点:往上安全但漏得多,往下能捞到更多但开始混入误合并。

再降到0.8试了试。 误合并明显增多,"社保卡丢了"和"医保卡丢了"开始混在一起。这俩在热线里是不同的业务,不能合并。

所以最终结论不是"设计了三级阈值",是试出来的

  • 0.9:安全但保守,漏掉不少
  • 0.85:最佳平衡点,大部分重复能抓到,误合并少
  • 0.8:再往下就危险了,必须人工复核

代码里的三级递减不是一开始就写好的,是先跑0.9,看命中的条目数------如果太少(<10条),说明这个锚点的问题比较特殊,表述差异大,自动放宽到0.85再搜;如果还是太少(<5条),放宽到0.8,但这批结果要人工看。

实现

数据结构

Milvus集合存储每条热线问题的向量:

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient(
    uri="http://your_milvus_host:19530",
    token="your_token",
    db_name="default"
)

# 集合结构:uid(主键) + Question(原文) + embeddings_Q(向量, 1024维)

向量用本地Ollama的bge-m3模型生成,零API费用:

python 复制代码
def vectorize_text(text):
    url = "http://localhost:11434/api/embeddings"
    data = {"model": "bge-m3:latest", "prompt": text}
    response = requests.post(url, json=data)
    if response.status_code == 200:
        return response.json().get('embedding', [])
    return None

去重主流程

python 复制代码
search_params = {"metric_type": "COSINE", "params": {"radius": 0.9}}
getedlist = []

file = open("d:\\q\\问题分类.txt", "w", encoding="utf-8")

for i in range(1, 93936):
    # 取第i条问题的向量和原文
    qs = client.get(
        collection_name="hotline_questions",
        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="hotline_questions",
        data=[v],
        limit=10000,
        output_fields=["uid", "Question"],
        search_params=search_params
    )

    # 所有命中的都标记为已归类
    for j in range(0, res[0].__len__()):
        uid2 = res[0][j]["entity"]["uid"]
        getedlist.append(uid2)
        # 输出:锚点uid, 命中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.9,跑一批看结果,发现漏了不少;降到0.85,好多了;再降到0.8试试,开始出误合并。每个数据集的最佳阈值不一样,没法抄别人的,只能自己试。

代码里把这个过程固化了:以0.9起步,看命中的数量------如果命中太少(<10条),说明这个锚点表述比较特殊,自动放宽到0.85再搜一轮;如果还是太少(<5条),放宽到0.8:

python 复制代码
r = 0.9
if res[0].__len__() < 10:
    r = 0.85
    search_params["params"]["radius"] = r
    res = client.search(...)  # 放宽再搜
    if res[0].__len__() < 5:
        r = 0.8
        search_params["params"]["radius"] = r
        res = client.search(...)  # 再放宽

<10和<5这两个触发条件,也是试出来的。先跑了一批数据看分布,大部分问题在0.9能命中几十条,少数冷门问题才不到10条。所以拿10作为分界线。

实际跑下来,94万条问题中:

  • 0.9档吃掉了大部分重复------换个说法的同一问题
  • 0.85档吃掉了表述差异更大的重复------口语化vs书面化的区别
  • 0.8档需要人工复核------这个区间里开始混入语义相近但实际不同的问题

输出格式

每行四列:锚点uid\t命中uid\t相似度\t原文

复制代码
1    1      1.000    养老保险怎么交
1    5234   0.943    养老保险如何缴纳
1    18762  0.912    养老保险缴费方式
1    45021  0.901    怎么交养老保险

同一组的都指向同一个锚点uid。后续处理时,每组取一条作为代表,其余标记为重复。

关键参数说明

参数 为什么
radius 0.9 / 0.85 / 0.8 三级递减,先紧后松
limit 10000 热门问题相似条目多,limit要大
metric_type COSINE 余弦相似度,语义比对的标准选择
Embedding模型 bge-m3 中英文混合,1024维,本地Ollama部署零成本

踩坑

1. Milvus的radius不是"大于",是"大于等于"

radius=0.9意味着相似度≥0.9的都会返回。别理解反了。

2. 已归类的必须跳过

getedlist就是干这个的。94万条逐条做锚点,如果不跳过已归类的,同一个问题会被多次当锚点,重复检索。虽然结果一样,但耗时翻倍。

3. limit要给够

热门政策(比如社保缴费)相关的问题可能有几千条。limit=10000确保不遗漏。Milvus的IVF_FLAT索引在这个量级下性能没问题。

4. 0.8档必须人工复核

相似度0.8是个分水岭。低于0.8,"社保卡丢了"和"医保卡丢了"可能被合并------这俩在热线里是不同的业务。高于0.85基本安全。0.8-0.85之间是灰色地带,必须人工看。

费用

  • Embedding:本地Ollama + bge-m3,零费用
  • Milvus检索:自部署,零费用
  • LLM调用:------全程不用大模型,纯向量相似度搞定

94万条问题去重,唯一成本就是一台跑Ollama的机器(8G显存够了)和Milvus服务器。

总结

  • 94万条热线问题去重,核心策略是"先紧后松"的动态相似度阈值
  • 0.9→0.85→0.8三级递减,大部分重复在前两级就解决了
  • 全程不用LLM,纯向量相似度,零API费用
  • 0.8以下需要人工复核,这是语义边界,算法搞不定
相关推荐
咚咚王者2 小时前
人工智能之大模型应用 基础入门第二章 主流大模型发展历程解析
人工智能
AI木马人2 小时前
2.【多模型接入架构】如何同时接入GPT、Gemini、Claude并统一管理?(完整实现方案)
人工智能·gpt·深度学习·神经网络·自然语言处理
zhangyueping83852 小时前
大模型学习笔记-AI通识
人工智能·笔记·学习
南宫惠泽2 小时前
深度学习章节:模型的选择与训练.交叉验证.测试集, 诊断偏差与方差,正则化与偏差方差,建立基准性能水平
人工智能·深度学习
Swift社区2 小时前
并行容错:OpenClaw的多智能体协作革命
人工智能·agent·openclaw
kongba0072 小时前
AI 项目初始化规范指南 V3.1 提示词模板
人工智能
薛定猫AI2 小时前
【深度解析】GPT 5.5 类 Agent 模型的工程能力:从多步骤规划、Token 效率到 AI 编码工作流落地
人工智能·gpt
珠海西格电力2 小时前
零碳园区管理系统如何守护能源与数据安全?
大数据·人工智能·分布式·架构·能源
墨染天姬2 小时前
【AI】2026 年具身智能模型和世界模型总结
人工智能