版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、先把候选池喂进去返回的 5 条全部落在同一个簇里先看结果。同一份候选池、同一个 query两条检索路线各取 5 条原样输出如下完整输出见第 2 章策略 结果集 内部平均相似度 异主题条数 纯相似度 [A1, A4, A3, A6, A5] 0.978 0 MMR lam0.5 [A1, B1, A6, A4, A3] 0.530 1 MMR lam0.9 [A1, A4, A3, A6, A5] 0.978 0纯相似度取回来的 5 条5 条都来自同一个主题簇把 MMR 的 λ 抬到 0.9输出的还是这 5 条一位都没换。这就是「召回一堆重复内容」在数字上的样子不是某一条不相关而是整个结果集里没有新信息。标题里的「5 条有 4 条是同一篇」是这个问题最常见的形态——语料里同一主题有两三份近重复文档检索把它们全捞上来。本文的候选池把近重复密度调到极端同一主题 8 篇于是推到了 5 条全中。这不是另一种病只是把同一种病放大到看得清。这里先把两个词切开「相似度高的结果」讲的是单条「不冗余的结果集」讲的是集合。做 RAG 的时候我们大多在优化前者换 embedding、调分块、调 rerank 的 k而冗余是后者的属性盯单条的人再久也看不见它。1.1 候选池的形状决定了这件事能不能复现14 篇候选一个主题 8 篇近重复记作 A 组A1~A8另外三个主题各 2 篇B1/B2、C1/C2、D1/D2取 top-5。如果池子里每个主题只放 1 篇纯相似度取 top-5 天然就是 5 个不同主题λ 怎么调都测不出差别——**要复现冗余候选池里必须先有近重复。**这也解释了为什么本地 demo 一切正常、接进真实语料后页面上却出现五段差不多的话。Weaviate 官方发布博客对这件事的描述可以直接引用原句标准向量检索有一个已知的副作用它会把近重复项聚到一起——一条「意大利菜」的查询返回五张披萨图一条 RAG 链路检索回五段内容差不多的话。后面那句结论更重要光有相关性不够你还需要多样性。1.2 该盯哪个数三个量管三件不同的事量量的是什么能不能看见冗余典型误用单条相似度Sim(d, q)这一条跟 query 有多像看不见看到第一条 0.968 就认为召回质量没问题结果集内部平均相似度这个集合自己跟自己有多像能看见不知道该算或者拿它当排序分数用异主题条数集合里有几条来自不同主题能看见最直观只报这个数不够它丢掉了「像到什么程度」三行里最关键的是中间那行**它是集合量把「像不像同一篇」从单条身上挪到了整个结果集上。**异主题条数好懂但太粗而单条得分全在 0.95 以上时看报表的人最容易以为召回质量没问题——其实它们相关的是同一件事。二、把这个判据跑成一段代码这一章把上一章那个判据跑成代码并贴上原样输出。它能跑是因为没有外部依赖向量合成、只用 numpy不需要 embedding 服务与向量库换台机器也能复现同一份数字。2.1 先声明口径否则这 14 篇的数字没法用候选池14 篇。A 组 8 篇由「簇中心 0.03 倍噪声」生成B/C/D 三个主题各 2 篇由各自簇中心0.05 倍噪声生成全部归一化。向量维度32 维合成向量随机种子固定。queryA 簇中心 0.05 倍噪声再归一化——也就是查询本身就问的是 A 那个主题。冗余度定义结果集内部两两余弦相似度的平均取上三角、不含对角。异主题条数定义结果集里名称不以 A 开头的条数上限 5。k固定取 5。把「冗余度」定义在集合上而不是单条上是这一章唯一需要你照着做的一步。如果你的报表里没有这一列那么无论 MMR 有没有生效你都看不出来。2.2 实测块 1核心逻辑与真实输出 实测环境Python 3.13.12 / macOS / numpy 2.5.4下面是脚本的核心两段节选完整脚本含合成向量构造部分defmmr(k,lam):sel,cand[],list(range(len(pool)))whilelen(sel)kandcand:ifnotsel:bestmax(cand,keylambdai:sim[i])else:bestmax(cand,keylambdai:lam*sim[i]-(1-lam)*max(float(pool[i] pool[j])forjinsel))sel.append(best);cand.remove(best)returnseldefredundancy(sel):vpool[sel];mv v.T iunp.triu_indices(len(sel),k1)returnfloat(m[iu].mean())forlamin(0.3,0.4,0.5,0.6,0.7,0.9):selmmr(k,lam)print(f{MMR lamstr(lam):16}{str([names[i]foriinsel]):42}{redundancy(sel):12.3f}f{sum(1foriinselifnotnames[i].startswith(A)):10})原样输出含 λ 扫描表候选池 14 篇主题 A 近重复 8 篇 其他 6 篇取 top-5 策略 结果集 内部平均相似度 异主题条数 ---------------------------------------------------------------------------------- 纯相似度 [A1, A4, A3, A6, A5] 0.978 0 MMR lam0.3 [A1, B1, C2, D1, A6] 0.092 3 MMR lam0.4 [A1, B1, C2, D1, A6] 0.092 3 MMR lam0.5 [A1, B1, A6, A4, A3] 0.530 1 MMR lam0.6 [A1, A6, A4, A3, A2] 0.977 0 MMR lam0.7 [A1, A4, A3, A6, A2] 0.977 0 MMR lam0.9 [A1, A4, A3, A6, A5] 0.978 0 纯相似度结果里来自同一主题 A 的条数5/5 → A 组内部两两相似度都 0.95对读者来说等于同一篇。 → lam 调大更看重相似度会退化回纯相似度去冗余就失效了。这份输出里有两处要单独指出否则很容易读漏。**第一处λ0.3 与 λ0.4 结果完全一样。**不是巧合也不是打印问题第二位往后谁被选中是离散跳变λ 在小范围内挪动不改变任何一次「谁赢」。别指望把 λ 从 0.35 微调到 0.40 能看到变化。**第二处λ 从 0.5 到 0.6内部平均相似度从 0.530 跳到 0.977一步跳到顶。**这不是渐变是塌方。第 4 章会把这个跳变点算出来。「结果集」那一列值得单独读一遍λ0.3/0.4 时 5 条里 3 条来自 B/C/Dλ0.5 只剩 1 条λ0.6 之后一条都不剩第一位永远是 A1。MMR 只动第 2 名往后。三、这段代码里其实只有 3 件事上面那段代码看着绕拆开只有三件事而且三件事各自对应一个会踩的坑。把这三件事分清楚比记住 MMR 的公式有用得多。3.1 第一件第一条直接照抄纯相似度的第一名if not sel: best max(cand, keylambda i: sim[i])这一行没有 λ 参与。已选集合为空时MMR 的公式退化成「只看相关性」所以第一名与纯相似度一模一样。Weaviate 官方概念文档把这条写成了显式结论结果按 MMR 分数排序而不是按 query 相似度排序第一位永远是最相关的那一条。**顺带的后果是单看 top-1 永远发现不了 MMR 有没有生效。**你必须在 k≥3 的位置看结果集才看得出差异。3.2 第二件惩罚项取的是「与已选集合的最大相似度」不是平均这是整段代码里唯一容易写错的地方。max(float(pool[i] pool[j]) for j in sel)取的是 max。取 max 的含义是候选只要跟已选里的任何一条撞了就要付出全额惩罚撞多条也不会重复扣。写成 mean 就完全不同候选跟 4 条已选项都撞 0.97 时惩罚被摊薄到 0.78 左右那条近重复还有机会挤进来。「撞到就罚」与「撞的人多才罚得多」的差别就是 MMR 和「给结果打个多样性折扣」这类土办法的差别。3.3 第三件冗余度必须是集合内部的量跟单条得分无关redundancy()只吃结果集的下标返回一个数。它不读sim也就是说它完全不关心每条跟 query 有多像。这三件事合起来构成一个闭环前两件决定「选谁进来」第三件决定「选完之后怎么验收」而验收用的量跟选择用的量不是同一个量。**所以「结果集内部平均相似度」不能拿来当排序分数用也不能拿来和单条相似度比大小。**它唯一的用途是作为一条回归检查改完参数跑一遍这个数该降还是该升本来就该由你来定。四、λ 一调大MMR 就退化回纯相似度上表里 λ≥0.6 的三行异主题条数全是 0、内部平均相似度 0.977~0.978与纯相似度的 0.978 基本重合。λ 一旦调过临界点MMR 就在做纯相似度排序去冗余完全失效——参数还在功能没了。4.1 实测块 2λ 扫描表的完整口径 实测环境Python 3.13.12 / macOS / numpy 2.5.4合成向量模拟、取数日期 2026-10-13策略异主题条数内部平均相似度与纯相似度0.978的差纯相似度不开 MMR00.978——MMR λ0.330.092−0.886MMR λ0.430.092−0.886MMR λ0.510.530−0.448MMR λ0.600.977−0.001MMR λ0.700.977−0.001MMR λ0.900.9780.000口径候选池 14 篇同一主题近重复 8 篇 其他 3 个主题各 2 篇k5冗余度取结果集内部两两余弦相似度的平均上三角、不含对角全部为合成向量模拟。取数日期 2026-10-13。这张表不需要读第二遍只需要看最后两列**λ≥0.6 时你花在 MMR 上的算力换回来的是与纯相似度差 0.001 的结果集。**0.001 这个量级说明它已经彻底不工作了。4.2 临界点不是 1.0而是 0.52把不等式写出来官方文档把 λ1.0 标成「等价于标准向量检索」那是数学上的端点但在真实候选池里塌方远在 1.0 之前就发生了。这一步可以手算不用跑代码。第二位要决出胜负时A 组候选记 s_A 为它与 query 的相似度r_A 为它与已选第一条的相似度对上其他主题候选s_B、r_B。A 组胜出的条件以及整理后的翻盘点λ · s_A − (1 − λ) · r_A λ · s_B − (1 − λ) · r_B λ* (r_A − r_B) / [ (r_A − r_B) (s_A − s_B) ]把同一候选池上的量代进去取数日期 2026-10-13A 组内部两两平均余弦 0.9769A4 与 A1 的余弦 0.9779B1 与 A1 的余弦 −0.1704A4 对 query 0.9641B1 对 query −0.0938。代入得λ 0.5205*。这个数正好落在扫描表里「0.5 还能翻、0.6 翻不动」之间两处结果对得上。**分母里 (s_A − s_B) 是「近重复主题对 query 的优势」分子里 (r_A − r_B) 是「近重复主题对已选项的劣势」。前者越接近后者临界点越靠近 0.5。**语料里近重复越多、越像s_A 越大近重复文档之间越像r_A 也越大——两者同步上升于是「λ 稍微调大一点就翻盘」成为常态λ 的可用窗口被压得很窄。很多人第一次用 MMR 会得出「这东西没用」他们把 λ 从 0.5 调到 0.7看着结果没变就放弃了。不是 MMR 没用是 0.7 已经在临界点另一侧。4.3 独立信源对得上λ 的两端语义是公开写明的LangChain 对lambda_mult的官方描述是逐字它是 0 到 1 之间的数决定结果的多样程度0 对应最大多样性、1 对应最小多样性默认值 0.5。同页还给了fetch_k默认 20作用是「取多少篇文档交给 MMR 算法」——候选池大小是单独一个参数这一点第 5 章还会用到。Weaviate 官方概念文档把同一件事写成三档逐字λ0.0 是纯多样性最大化与已选项的差异λ0.5 是平衡每条结果既要相关又要不同λ1.0 是纯相关性等价于标准向量检索。它的搜索页还补了一句关键的结果按 MMR 分数排序而不是按查询相似度排序第一条永远最相关后续结果可能查询相似度更低因为它们是靠「带来差异」被选上的。原论文Carbonell GoldsteinSIGIR’98摘要里的原句是MMR 准则力图在重排检索文档、挑选摘要段落时降低冗余同时保持与 query 的相关性正文对「边际相关性」的定义是一份文档具有高边际相关性当且仅当它既与 query 相关又与已选文档的相似度最小。这条定义就是第 3 章第二件事的出处——惩罚项针对的是「与已选项的相似度」而不是别的什么。一份「MMR 参数与验收清单」λ 取值对照、结果集冗余度算法、临界点算式整理成一页配本章代码用。放在资料包里扫码即可获取五、阈值就取 0.5并且只看结果集那一列**结论先给默认 λ 取 0.5验收只看结果集内部平均相似度如果你把一个池子的 λ 调到 0.6 以上而「异主题条数」还是 0那不是语料的问题是参数的问题。**下面给出分档、接入位置和停手条件。5.1 分档表λ 落在哪一段做什么决定λ 区间结果集现象本池实测该做什么不该做什么λ ≤ 0.4异主题 3 条内部平均 0.092只在「用户明确要看广度」时用别当 RAG 问答默认值相关性让得太多0.4 λ ≤ 0.55异主题 1 条内部平均 0.530RAG 问答的默认区间别在这个区间里继续微调结果会离散跳变λ ≥ 0.6异主题 0 条内部平均 0.977等于没开回去检查参数别写成「已启用 MMR 多样性重排」第三行是这张表存在的理由**λ≥0.6 时配置项还在、算法还在跑只有效果没了。**这种静默失效比报错难查——没有异常、没有日志结果集悄悄变回原样。5.2 把检查接进检索流水线⚠️代码待验证下面这段没有在真实向量库上跑过本机没有部署向量服务只给出接入位置与参数口径k/fetch_k/lambda_mult三个参数名与含义来自 LangChain 官方 API 文档见附表 A。hitsstore.max_marginal_relevance_search(queryquestion,k5,# 最终进上下文的条数等价于本文的 kfetch_k40,# 先放大候选池MMR 才有得挑官方默认 20建议取 2~4 倍的 klambda_mult0.5,# 越小越多样0纯多样性1纯相关性)fetch_k是最容易被忽略的一个**候选池小于就等于没有选择余地。**如果 fetch_k 只给 5那 MMR 面对的就是已经定死的 5 条只能原地重排近重复已经占满 5 个名额时怎么排都是它们。5.3 什么时候该停手**第一条停手线候选池没放大之前不要调 λ。**先把 fetch_k 提到 k 的 2~4 倍再看 λ 有没有效果。跳过这一步就调 λ你会把「池子太小」误判成「MMR 没用」。**第二条停手线结果集内部平均相似度本来就在 0.5 以下的语料不要开 MMR。**这类语料本身就分散λ0.5 时只有 0.1~0.2 的下降空间代价却是有可能把一条真正有用的同主题补充材料挤出去。先量一遍不开 MMR 时的内部平均相似度低于 0.5 就先修语料与分块。**第三条停手线λ 调到 0.6 以上仍无变化时停手去查配置不要一路加到 1.0。**0.9 与 0.6 在本池里结果一模一样再加上去只是在重复确认已经确认过的事。最后一条是给验收用的**报告里必须同时有「异主题条数」和「结果集内部平均相似度」两列只有一个都不够。**异主题条数会在 λ 刚好跨过临界点时从 3 直接掉到 0看不出中间那一步内部平均相似度会在同一刻从 0.530 跳到 0.977两者一起看才能定位到你到底在临界点的哪一侧。这一篇用到的脚本与验收清单合成向量脚本、λ 扫描输出、临界点算式都在里面换成你自己的语料就能跑。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表#事实 / 表述出处文档名 发布方 链接本文位置A1论文摘要原句MMR 准则在重排检索文档、挑选摘要段落时降低冗余reduce redundancy同时保持与 query 的相关性The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing SummariesCarbonell GoldsteinSIGIR’98pp.335-336ACM DOI https://doi.org/10.1145/290941.291025 作者单位托管 PDF https://www.cs.cmu.edu/~jgc/publication/The_Use_MMR_Diversity_Based_LTMIR_1998.pdf1.2、4.3A2正文对边际相关性的定义原句与 query 相关且与已选文档相似度最小的文档边际相关性高同 A1SIGIR’98 论文正文3.2、4.2A3论文说明该权重取 1 时退化为标准相关度排序、取 0 时得到结果集内的最大多样性排序中间取值是两个准则的线性组合作者单位托管 PDF 的文本抽取层把该希腊字母渲染成 X对应各实现里的 λ同 A1SIGIR’98 论文正文4.2A4lambda_mult官方语义0 到 1 之间的数0 对应最大多样性、1 对应最小多样性默认 0.5fetch_k默认 20含义是交给 MMR 算法的文档数LangChain API 文档《max_marginal_relevance_search》VectorStore / InMemoryVectorStoreLangChainhttps://reference.langchain.com/python/langchain-core/vectorstores/base/VectorStore/max_marginal_relevance_search4.3、5.2A5官方三档原句λ0.0 纯多样性λ0.5 平衡λ1.0 纯相关性等价于标准向量检索且结果按 MMR 分数排序、第一条永远最相关Weaviate 官方文档《Vector Search》概念页Weaviatehttps://docs.weaviate.io/weaviate/concepts/search/vector-search3.1、4.3、5.1A6官方博客原句标准向量检索有把近重复项聚到一起的已知副作用「意大利菜」返回五张披萨图、RAG 检索回五段内容差不多的话仅靠相关性不够、还需要多样性截至 2026-10-13该博客标注 MMR 多样性选择自 v1.37 起为预览能力、v1.39 起转为正式可用Weaviate 发布博客《Weaviate 1.37 Release》与《Weaviate 1.39 Release》Weaviatehttps://weaviate.io/blog/weaviate-1-37-release 、https://weaviate.io/blog/weaviate-1-39-release1.1、4.3A7λ0.3/0.4/0.5/0.6/0.7/0.9 六档与纯相似度基准的结果集、内部平均相似度、异主题条数纯相似度 0.978/0λ0.3 与 0.4 均为 0.092/3λ0.5 为 0.530/1λ≥0.6 为 0.977~0.978/0本文实测合成向量模拟14 篇候选、32 维、k5Python 3.13.12 / macOS / numpy 2.5.4取数日期 2026-10-132.2、4.1A8临界点算式 λ* (r_A − r_B) / [(r_A − r_B) (s_A − s_B)]代入同一候选池得 λ* 0.5205与扫描表「0.5 可翻、0.6 不翻」一致本文按同一候选池计算取数日期 2026-10-134.2说明A1–A3 取自 SIGIR’98 论文ACM DOI 页面已核该页标注「No abstract available」原句取自作者单位托管的 PDF 版本该 PDF 文本层存在个别识别噪声本文只引识别无误的片段。A4–A6 取自 LangChain 与 Weaviate 的官方文档与官方发布博客A7–A8 为本文实测与本文计算。候选池为合成向量绝对数值不可与你的真实语料直接比较可比的是趋势与判据。写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。