版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
第 1 章:先把脏语料喂进去,看它是怎么塌的
1.1 一行输出暴露的问题
本机(Python 3.13.12,macOS)上,我构造了一小段语料:一共 9 条,其中 5 条来自同一个来源 A(1 条原文,外加 4 条只把一个词改短的近重复),另外 4 条分别来自来源 B、C、D、E。用一个最朴素的词频加权打分做 Top-5 检索,程序打印出来的第一行是:
去重前 Top-5 来源: ['A', 'A', 'A', 'A', 'A'] 不同来源数: 1直观感觉是「检索完全成功」——它规规矩矩返回了 5 条。可把这 5 条拼进提示词以后,模型读到的其实是同一句话说了五遍,剩下四个来源一个字都没进来。
问题不在于检索算法写错了,而在于我们验收的口径选错了。返回 5 条、K 值拉满,这两个事实同时成立,却说明不了任何有用的信息。
1.2 判据换一个:数「不同来源的条数」
本文只用一个判据来衡量语料质量:Top-K 里不同来源的条数(去重前的语料里,我给每条都挂了来源标签)。
注意它和「召回条数」是两个量。召回条数是你设定的 K,永远等于 K,除非候选不够;不同来源条数才会随着语料变脏而往下掉。
| 看什么 | 去重前 | 去重后 | 能不能看出问题 |
|---|---|---|---|
| 召回条数(等于 K) | 5 | 5 | 看不出,两边都是 5 |
| Top-K 里不同来源的条数 | 1 | 5 | 差距全在这一格 |
| 拼进提示词的有效信息条数 | 1 | 5 | 同上一格,换个说法 |
这也是本文与「换个更大的模型试试」这类思路的分界:模型再强,也变不出语料里本来就不存在的第二份信息。
第 2 章:把这个判据跑成一段代码
2.1 最小复现(已实跑)
下面这段只用 Python 标准库,不联网、不装包、不调用任何 embedding 服务,可以直接复制运行。
importre,mathfromcollectionsimportCounterdefg(s,n):s=re.sub(r"\s+","",s)return{s[i:i+n]foriinrange(len(s)-n+1)}defjac(a,b):returnlen(a&b)/len(a|b)B={"A":"RAG检索增强生成需要先把文档切块再建立向量索引然后召回相关段落","B":"Agent智能体靠工具调用完成多步任务并且需要设置权限边界","C":"模型微调要准备高质量指令数据并划分训练集与验证集","D":"向量数据库用近似最近邻搜索来显著降低检索阶段的延迟","E":"提示词工程通过少样本示例来约束模型输出的字段格式"}docs=list(B.items())+[("A",B["A"].replace("建立","建"))for_inrange(4)]deftopk(q,ds,k):Q=g(q,2)df=Counter(xfor_,tindsforxinset(g(t,2)))sc=sorted(((sum(math.log(len(ds)/(1+df[x]))forxinQ&g(t,2)),s)fors,tinds),reverse=True)return[sfor_,sinsc[:k]]keep=[]fors,tindocs:ifall(jac(g(t,3),g(k,3))<0.5for_,kinkeep):keep.append((s,t))q="文档切块之后怎么建立向量索引并召回"fortag,dsin(("去重前",docs),("去重后",keep)):r=topk(q,ds,5);print(tag,"Top-5 来源:",r,"不同来源数:",len(set(r)))print("语料条数:",len(docs),"-> 去重后:",len(keep))运行环境:Python 3.13.12,macOS(Darwin 25.6.0,arm64)。它的原始输出如下,一个字都没有改过:
去重前 Top-5 来源: ['A', 'A', 'A', 'A', 'A'] 不同来源数: 1 去重后 Top-5 来源: ['A', 'D', 'E', 'C', 'B'] 不同来源数: 5 语料条数: 9 -> 去重后: 5去重前,Top-5 被来源 A 的 5 个版本占满,不同来源数是 1;去重后,语料从 9 条降到 5 条,Top-5 里出现了 5 个不同来源。召回条数两次都是 5,唯一的差别在最后一列。
2.2 这段代码里其实只有三件事
第一件,字符 n-gram(也叫分片)。n-gram 就是把一段文本切成固定长度的连续片段。g(s, 3)表示把字符串切成所有长度为 3 的连续子串,装进一个集合——集合会自动丢掉重复片段。它不需要中文分词,也就绕开了「切词切错了怎么办」这个坑。
第二件,Jaccard 相似度。它衡量两个集合的重合程度,等于「交集大小 ÷ 并集大小」,取值 0 到 1。两条文本的 3-gram 集合越重合,值越接近 1。本文实测:同一来源、只改了一个词的近重复,3-gram Jaccard 是 0.8438;两条不同来源的文本是 0.0。中间的差距足够大,所以一个很粗的阈值就能把近重复挑出来。
第三件,一个够用的打分函数。检索部分我用了「字符 2-gram + 逆文档频率」的加权求和:某个片段在语料里越常见,权重越低。这只是为了让 Top-K 有一个排序,它本身不承担判断语料质量的职责,换成别的打分也不影响上面的结论。
2.3 为什么这么小的代码也能复现真实故障
因为它复现的是词面层面的近重复,而这类脏数据根本不需要语义向量才能被发现。反向说也成立:如果你连字面高度重合的重复都没清掉,却指望靠调 embedding 模型或换检索器解决问题,方向就错了。
第 3 章:脏语料是怎么一步步把 Top-K 挤满的
3.1 近重复是从哪儿溜进来的
语料库里的近重复,基本都来自四个很普通的工程动作:
- 同一份文档被多次导入:先传了 PDF,又传了转出来的 Markdown,两份内容一样、格式不同;
- 正文和模板被一起抓下来:页眉页脚、导航栏、免责声明、操作步骤被重复抽取,一段模板文本进库几百次;
- 问答对拆条扩充:为了凑数据量,把一段话改写成多条高度相似的条目;
- 版本迭代不清旧:同一份制度文件的新旧版本同时留在库里。
这四种情况的共同点是:它们都不是「错的数据」,只是同一份信息的多个副本。而检索算法并不知道它们是副本。
3.2 机制:为什么「多」会变成「少」
RAG 原论文把它的非参数记忆描述成「a dense vector index of Wikipedia」(维基百科的稠密向量索引),并用「models which combine pre-trained parametric and non-parametric memory for language generation」来概括这类模型的构造。检索阶段做的事,就是在索引里按相似度取回最靠前的 K 条,交给生成模型当外部证据。
于是机制就清楚了:近重复文本在向量空间里几乎挨在一起,一旦查询命中这一簇,Top-K 的位置就会被同一簇的不同副本占满。系统会老实报告「已召回 K 条」,但那 K 条里的信息量只有 1 条。这不是检索器坏了,是它在忠实执行「按相似度取前 K 条」这个指令。
换句话说,脏语料的代价不是让检索失败,而是让检索「看起来很成功」。这比直接报错更难发现。
3.3 顺带损失的两件事
第一是上下文预算。模型的输入长度是有限的,被重复内容占掉的位置,原本可以放另外四条不同来源的信息。第二大且更隐蔽的是:某个来源如果因为重复而在索引里占比过高,它的语义邻域会被自己的副本填满,检索结果会系统性地向它倾斜——同一份文档反复出现,本身就成了一种隐形的加权。
官方文档其实早就把「分块」列为 RAG 的关键阶段,并提示分块与检索策略会直接影响最终效果;「召回片段数」在文档里的定义就是多路召回策略中的 K 值,并且明确写到 K 值不合适会导致正确的切片被漏掉。这两句话合起来读,正好对应本文实测里那个「K 永远是 5,但第 5 条根本换不进来」的现象。
第 4 章:去重前后 Top-K 命中分布(数据实测)
4.1 口径先写清楚
这份表的所有数字都来自本机实跑,取数日期 2026-10-07,环境为 Python 3.13.12 / macOS:
- 语料构造:5 个来源 A–E,各 1 条基准文本(约 28 字);对来源 A 追加 N 条近重复(仅把「建立」替换为「建」,其余一字不改);
- 分片与相似度:字符 3-gram 集合,Jaccard 相似度;
- 去重方式:单遍顺序去重,新条目与所有已保留条目的相似度都小于阈值才保留;
- 去重阈值:0.5;
- 检索打分:字符 2-gram + 逆文档频率加权求和;
- 查询:
文档切块之后怎么建立向量索引并召回;K = 5。
| 来源 A 的近重复条数 | 语料总条数 | 去重后条数 | 去重前 Top-5 不同来源数 | 去重后 Top-5 不同来源数 |
|---|---|---|---|---|
| 0 | 5 | 5 | 5 | 5 |
| 2 | 7 | 5 | 3 | 5 |
| 4 | 9 | 5 | 1 | 5 |
| 6 | 11 | 5 | 1 | 5 |
| 8 | 13 | 5 | 1 | 5 |
读这张表只看最右两列。第一行是干净语料的对照组:没有近重复时,Top-5 本来就给出 5 个不同来源。从第三行开始,语料涨到 9 条,召回条数依然是 5,但不同来源数掉到 1——多出来的全是同一个来源的副本。语料继续涨到 13 条,指标一动不动。
这就是本文开头那个判据的价值:如果验收指标是「返回了几条」,这五行的表现完全一致,你会以为语料一直很健康。
4.2 阈值改一个字,去重直接失效
在上一段代码的基础上,把写死的阈值换成变量循环,其余不动:
print("去重阈值 | 去重后条数")forthrin(0.2,0.5,0.8,0.95,1.0):keep=[]fors,tindocs:ifall(jac(g(t,3),g(k,3))<thrfor_,kinkeep):keep.append((s,t))print(f"{thr:^8}|{len(keep):^8}")原始输出(同一台机器,同一批语料):
去重阈值 | 去重后条数 0.2 | 5 0.5 | 5 0.8 | 5 0.95 | 6 1.0 | 6阈值取 0.5 到 0.8 时,去重后都是 5 条;一旦提到 0.95,去重后变成 6 条——有一条近重复被放过了。对照第 2 章量到的 0.8438,原因很清楚:那条近重复的真实相似度就在 0.84 附近,阈值定在 0.95,等于要求「几乎一模一样才算重复」,改一个词它就混过去了。
所以阈值不是越高越安全,定得过高会让去重形同虚设。反过来,定得过低会把只是同主题的正常文档也误删。公开教材里也提到这一点:不存在一个通用的分界值能自动区分「重复」和「相关但不同」,阈值必须结合分片定义、语料本身和下游效果一起校准。
4.3 什么时候该回去清语料,什么时候不该
给自己三条能立刻执行的判据:
- 连续几批查询,Top-K 不同来源数与 K 的比值都接近 1/K→ 语料有系统性重复,先回去清洗,别急着调检索器;
- 某个来源的条目数占语料总量的比例明显高于其他来源→ 它会在 Top-K 里形成压倒性优势,即使相似度不是最高;
- 去掉重复后,回答质量没有变化→ 说明这批重复本来就没被检索到,问题不在这里,别做无用功。
至于什么时候停手:清洗不是为了把语料压到最小。把一份制度文件的不同章节合并成一条,反而会让检索粒度变粗。只删除同一份信息的高度重合副本,保留信息不同的部分。
这份资料是什么:一张把 RAG 全链路(切块、向量化、检索、生成)串起来的学习路线图,本章讲的语料清洗正好是其中「数据准备」那一段。放在资料包里,扫码即可获取:
第 5 章:落地取舍:四种近重复检测方式怎么选
5.1 四种方式对照
| 方式 | 抓的是什么重复 | 成本 | 主要短板 | 适合的场景 |
|---|---|---|---|---|
| 精确哈希(如 SHA-256) | 一字不差的完全相同 | 极低 | 改一个词就抓不住 | 同一文件被重复导入 |
| 字符 n-gram + Jaccard | 字面高度重合 | 低(本文实测方式) | 同义改写、换词转述抓不住 | 中文近重复、模板文本 |
| MinHash / LSH | 大规模集合相似度 | 中 | 概率近似,会有误判 | 十万条以上的大语料 |
| 语义向量相似度 | 意思相近 | 高(要调 embedding) | 语义相近不等于重复 | 同义改写、多来源转述 |
MinHash 这套做法最早由 Andrei Broder 在 1997 年提出,最初被用在 AltaVista 搜索引擎里检测重复网页并把它们从结果中剔除。它解决的正是「语料太大,两两比对算不完」的问题:先用一组哈希函数给每篇文档算出一个固定长度的签名,再比较签名,就不必做全量两两比较了。
本文用的是第二行的办法,原因很实际:它不需要任何外部服务,也不需要 GPU,十分钟就能在本地跑完一次全量体检。截至 2026-10-07,对多数中小规模的中文知识库来说,先用它扫一遍,性价比最高。
5.2 一条可以直接照做的处理顺序
- 先用精确哈希去掉完全相同的文件,这一步最便宜,顺手就做了;
- 再用字符 n-gram + Jaccard 扫一遍近重复,阈值从 0.8 起步,然后按自己语料的分片长度和实际命中情况上下校准;
- 语料规模上到十万条以上、或者每次导入都要跑一遍的时候,再换 MinHash / LSH 之类的近似方案;
- 语义向量只用来「捞候选」交给人工看,不要拿它当重复的判定依据——意思相近不等于重复,可能是两份内容互补的文档。
5.3 上线前的最小验收动作
固定一批有代表性的查询(覆盖你的主要业务问题),每次语料更新后重跑一次 Top-K,记录不同来源条数的分布。这个分布就是你语料库的质量水位线:平时稳定在一个区间,某天突然掉下去,基本可以断定是这次新导入的数据带进了重复内容,而不是检索器出了问题。
这件事的价值在于它把「语料干不干净」从主观感觉变成了一个能打印出来的数字。一句经验不值得记住,一个能重跑的数字才值得。
这份资料是什么:一套从私有化部署讲到 Embedding+RAG 的视频课,第 4 个模块专门讲语料入库与检索调优,和本章的方法可以对照着看。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| # | 事实 | 出处 | 本文位置 |
|---|---|---|---|
| 1 | RAG 原论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》,作者含 Patrick Lewis 等,2020-05-22 提交、2021-04-12 修订至 v4;摘要中「combine pre-trained parametric and non-parametric memory for language generation」「a dense vector index of Wikipedia」等表述为原文 | arXiv:2005.11401 摘要页 https://arxiv.org/abs/2005.11401 | 第 3 章 |
| 2 | Jaccard 相似度系数定义为两个集合交集大小与并集大小之比,取值 0(不相交)到 1(相等) | 维基百科 MinHash 条目 https://en.wikipedia.org/wiki/MinHash | 第 2 章 |
| 3 | MinHash 由 Andrei Broder 于 1997 年提出,最初用于 AltaVista 搜索引擎检测重复网页并从搜索结果中剔除 | 维基百科 MinHash 条目;原始论文 Broder, “On the resemblance and containment of documents”, SEQUENCES 1997, doi:10.1109/SEQUEN.1997.666900 | 第 5 章 |
| 4 | 分块(chunking)是 RAG 的关键阶段,分块与搜索策略会直接影响最终效果 | 微软官方文档《RAG 分块阶段》https://learn.microsoft.com/zh-cn/azure/architecture/ai-ml/guide/rag/rag-chunking-phase | 第 3 章 |
| 5 | 「召回片段数」即多路召回策略中的 K 值;K 值不合适会导致正确的文本切片被漏掉 | 阿里云百炼官方文档《RAG 效果优化》https://www.alibabacloud.com/help/zh/model-studio/rag-optimization | 第 1、3、5 章 |
| 6 | 去重阈值没有通用取值,须结合分片定义、语料与下游质量校准(公开教材,非标准文件,供参考) | Machine Learning Systems(公开在线教材)https://mlsysbook.ai/vol1/data_selection/data_selection.html | 第 4 章 |
| 7 | 语料构造方式、去重前后 Top-K 命中分布表、阈值敏感性结果、各样本的 3-gram Jaccard 实测值(0.8438 / 0.0) | 本文 2026-10-07 本机实测,Python 3.13.12 / macOS | 第 2、4 章 |
附表 B:术语速查表
| 术语 | 一句话解释 | 在本文哪里用到 |
|---|---|---|
| RAG(检索增强生成) | 先从外部知识库检索证据,再让模型基于证据生成回答的做法 | 第 3 章 |
| 分块(chunk) | 把长文档切成一段段可被单独检索的小单元 | 第 3、5 章 |
| n-gram(分片) | 一段文本里所有长度为 n 的连续片段,比如 3-gram 就是所有 3 字连续串 | 第 2 章 |
| Jaccard 相似度 | 两个集合交集除以并集,衡量重合程度,0 到 1 | 第 2、4 章 |
| MinHash | 用一小段签名快速估算两个集合的 Jaccard 相似度,适合大语料 | 第 5 章 |
| 逆文档频率(IDF) | 片段在语料里越常见、权重越低,用来给检索打分 | 第 2 章 |
| Top-K | 检索返回的相似度最高的前 K 条结果 | 全篇 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。