版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
一、「多取几条」是第一反应,也是最容易踩空的一步
1.1 现场长什么样
一个常见的现场:知识库里同一套报销流程有三个版本,2024 年的旧流程、2025 年补了「票据留存」的版本、还有某位同事按自己理解重写的一版。用户问「报销单据怎么提交和审批」,模型答得四平八稳,唯独没提「差旅报销要七个工作日内提单」——而用户恰恰是出差回来要报销的那位。
第一反应几乎都是同一个动作:把 Top-K 调大。k=4 改成 k=8,不够再改 12。这个动作看起来成本为零,不动模型、不动提示词、不动知识库,只改一个数字。
实测下来,这个动作的收益比想象中窄得多,副作用却有两种很难察觉的形态:一是重复内容持续挤占上下文,二是你以为取到了 4 条,代码其实只给了你 2 条,而且不报错。这篇就用本机实测的数字说明「多取几条」买到了什么、又付出了什么。
1.2 为什么自己造一个能复现的小实验
真跑一套语义嵌入模型需要模型文件和算力,不是每台机器都备着。这里换了个取巧但完全可复现的办法:字符 2-gram 配 crc32 哈希到 256 维,再做 L2 归一化。它不懂语义,但结果是确定的——同样的输入永远得到同样的向量,而排序、打分、多样性重排走的仍然是向量库里那段真实的代码路径。
importzlib,mathfromlangchain_core.documentsimportDocumentfromlangchain_core.embeddingsimportEmbeddingsfromlangchain_core.vectorstoresimportInMemoryVectorStore DIM=256classHashEmbeddings(Embeddings):def_vec(self,text):v=[0.0]*DIMforiinrange(len(text)-1):v[zlib.crc32(text[i:i+2].encode("utf-8"))%DIM]+=1.0n=math.sqrt(sum(x*xforxinv))or1.0return[x/nforxinv]defembed_documents(self,texts):return[self._vec(t)fortintexts]defembed_query(self,text):returnself._vec(text)rows=[("报销流程:先在系统提交单据,再由直属主管审批,最后财务打款。","finance",2024,1),("报销流程:先在系统提交单据,主管审批后财务打款,票据要留存。","finance",2025,1),("报销单据提交后由主管审批,财务复核票据再打款。","finance",2025,2),("差旅报销:出差回来七个工作日内提单,超期需要说明。","finance",2025,2),("年假规则:入职满一年五天,满三年十天,需提前三天申请。","hr",2025,1),("调休规则:加班可换调休,三个月内使用,过期作废。","hr",2024,1),("采购申请:单笔超过两万需要部门负责人和财务双重审批。","admin",2025,1),]docs=[Document(page_content=t,metadata={"dept":d,"year":y,"level":l})fort,d,y,linrows]vs=InMemoryVectorStore(embedding=HashEmbeddings())vs.add_documents(docs)q="报销单据怎么提交和审批"fordoc,scoreinvs.similarity_search_with_score(q,k=7):print(f"{score:.4f}",doc.metadata["dept"],doc.page_content[:12])这个方法的边界要说清楚:它不适合用来讨论「真实嵌入模型的分数是多少」,只适合观察「排序怎么变、重复怎么挤、参数怎么起作用」。绝对分数别照抄,行为可以照抄。样本是 7 条虚构的人事与财务制度片段,其中 3 条是同一套报销流程的不同说法。环境是langchain-core 1.6.6、numpy 2.5.2、Python3.13.14,运行在 Windows 上。
1.3 把分数摊开看,问题一目了然
先看纯相似度检索把 7 条文档排成什么样。
| 排名 | 分数 | 主题归属 | 内容摘要 |
|---|---|---|---|
| 1 | 0.3371 | finance / 2025 / L2 | 报销单据提交后由主管审批,财务复核票据再打款 |
| 2 | 0.2840 | finance / 2025 / L1 | 报销流程:先在系统提交单据,主管审批后财务打款 |
| 3 | 0.2752 | finance / 2024 / L1 | 报销流程:先在系统提交单据,再由直属主管审批 |
| 4 | 0.1265 | hr / 2024 / L1 | 调休规则:加班可换调休,三个月内使用 |
| 5 | 0.1155 | hr / 2025 / L1 | 年假规则:入职满一年五天,满三年十天 |
| 6 | 0.0632 | admin / 2025 / L1 | 采购申请:单笔超过两万需双重审批 |
| 7 | 0.0620 | finance / 2025 / L2 | 差旅报销:出差回来七个工作日内提单 |
表里最刺眼的不是前三条有多高,而是 0.2752 到 0.1265 这道断崖——掉了 0.1487,比前三条之间的全部差距加起来还大。断崖在说一件事:能高分匹配这个问题的内容,只有「报销流程」这一个主题,从第 4 名开始都是勉强沾边。
更值得盯住的是最后一行。差旅报销在语义上是最该被补进来的那条,同属报销、同属财务、用户真实需求就在这儿,可它排在采购申请后面,分数只有 0.0620。原因很朴素:它的措辞里没有「提交」「审批」,说的是「提单」「超期说明」。字符层面重合度低,排序就把它压到最后。
这就是「答不全」的根:不是 K 不够大,是排序信号本身不认「互补」。相似度衡量的是「像不像问题」,而用户缺的那条,往往恰恰是「不像问题、但有用」。
二、重复挤占:K 越大,你替重复付的账越多
2.1 三条近似重复是怎么长出来的
多数人第一反应是「谁把重复文档放进库里的」,其实几乎没人主动放。三种来源都很正常:
一是版本迭代。2024 版和 2025 版都留在库里,谁也不敢删旧的,怕有人还在引用。二是分块重叠。切片时留了重叠区,同一段话被切进相邻两个块,两块都进了库。三是多人各自总结。同一份制度被三个人写成三份问答,措辞不同、意思一样。
三种做法单看都没错,合起来的效果是:同一段信息在 Top-K 里出现多次,占掉多个名额。
2.2 K 从 1 拉到 7,进来的到底是什么
把 K 依次调大,数一数重复条数和命中的主题数。
| K | 返回条数 | 其中报销重复条数 | 命中主题数 | 最低分 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 0.3371 |
| 2 | 2 | 2 | 1 | 0.2840 |
| 3 | 3 | 3 | 1 | 0.2752 |
| 4 | 4 | 3 | 2 | 0.1265 |
| 5 | 5 | 3 | 2 | 0.1155 |
| 6 | 6 | 3 | 3 | 0.0632 |
| 7 | 7 | 3 | 3 | 0.0620 |
两件事同时成立,得分开说。
第一件:K 小的时候你并没躲开重复。K=3 时三条全是重复,占比百分之百。重复不是 K 变大才出现的,它一开始就站在最前排,因为近似重复的向量本来就离查询最近。
第二件:K 变大之后,新增的几条分数一路走低。0.3371、0.2840、0.2752 之后直接掉到 0.1265,再往后是 0.1155、0.0632、0.0620。到 K=6 以后,新进来的条目分数只有 0.06 上下,基本等于噪声。
这里要老实交代这个样本的局限:7 条里重复项只有 3 条,所以 K 从 3 涨到 7,重复条数锁在 3 不动。换到几十上百条的真实语料里不是这样,同一制度的重复可能有十几条,K 越大进来的重复越多。这个 7 条的小实验能证明的是另一半:K 变大换来的新增条目,边际价值几乎为零。
2.3 代价不只是 token
第一层是上下文预算,好理解:三条说同一件事的文档占掉三个名额,本来能放三个不同主题的位置只剩一个。
第二层代价是注意力稀释。同一说法出现三次,模型很容易把它当成「更重要」或「更权威」的信号——重复在人类写作里确实常常意味着强调,但在检索结果里,重复只说明向量近。
第三层最要命:错误会被复制。三份重复文档里如果两份是旧版流程、一份是新版,模型按「多数」答旧流程的概率不低,而且答得理直气壮,因为它看到的是二比一。这不是模型的问题,是喂进去的东西本来就在投票。
所以判据不是「K 该多大」,而是先看一件事:同一主题在 Top-K 里占了几条。占比超过三分之一,就该去处理重复,而不是继续调 K。
三、MMR 在做什么:把「像问题」和「像前面挑出的」分开算
3.1 一句话版本
MMR 全称 Maximal Marginal Relevance,中文常译作最大边际相关性。它不是换一种相似度算法,而是换一种「挑下一条」的规则。
普通的 Top-K 是一条一条独立打分,谁分高谁上,彼此之间不发生关系。MMR 是串行挑:每挑一条之前,既看它和问题有多像,也看它和前面挑出来的那几条有多像,第二个「像」要扣分。
下一条的得分 = λ ×(与问题的相似度)− (1 − λ) ×(与前面挑出集合中最像的那条的相似度)
这个式子的结构来自 MMR 原论文(Carbonell 与 Goldstein,SIGIR 1998),本文按公式的组成方式复述,没有逐字照抄原文。λ 就是代码里的lambda_mult,它把两个目标拧在同一根轴上:λ 越大越偏「相关」,越小越偏「不同」。
它要解决的问题正是上一章那道断崖——如果三条重复占住前三个位置,靠调 K 是救不回来的,得有个机制在挑第二条的时候就问一句:这条和第一条是不是在说同一件事。
3.2 四档实测:λ 变一点,结果变一截
固定k=4、fetch_k=7(7 条即全库),只改lambda_mult。
forlamin(1.0,0.7,0.5,0.0):picked=vs.max_marginal_relevance_search(q,k=4,fetch_k=7,lambda_mult=lam)print(lam,len(picked),[doc.page_content[:4]fordocinpicked])| lambda_mult | 返回构成 | 报销重复条数 | 补进差旅 | 补进年假 |
|---|---|---|---|---|
| 1.0 | 3 条报销 + 调休 | 3 | 否 | 否 |
| 0.7 | 2 条报销 + 调休 + 年假 | 2 | 否 | 是 |
| 0.5 | 1 条报销 + 调休 + 差旅 + 年假 | 1 | 是 | 是 |
| 0.0 | 1 条报销 + 年假 + 差旅 + 调休 | 1 | 是 | 是 |
这张表把「λ 越大越好」这个直觉直接推翻。1.0 的时候前三条还是那三条重复,和第一章纯相似度的结果几乎一样;0.7 保留两条重复,换进来一条年假;0.5 只剩一条报销,差旅和年假都进来了;0.0 多样性最大,四条覆盖四个主题,但相关性明显掉了。
最有价值的对比是 0.7 和 0.5 之间。0.7 虽然挤掉了一条重复,但补进来的是年假,而用户真正缺的是差旅报销。年假和报销的关系比差旅远得多,它被选中只是因为「和前面挑出的不像」。这说明 MMR 只保证「不一样」,不保证「有用」。λ 调得太低,换进来的就是这种「不一样但没关系」的内容。
3.3 为什么 λ=1.0 等于没开
λ=1 的时候,后面那个惩罚项的系数变成 0,整个式子退化成纯相似度重排。代码跑了一遍 MMR 的完整流程,算了半天,结果和直接取 Top-K 一样。
这是「开了 MMR 但没效果」最常见的原因:参数留在默认值附近,或者有人觉得「1.0 最相关、肯定最好」。反方向的 0.0 是另一个极端,相关性项归零,排序完全由差异决定,返回的四条里「最相关」和「最不相关」混在一起,交给模型反而更难判断该信谁。
两端都不能用,能用的是中间那一段。下一章就来找这一段在哪儿。
四、lambda_mult 从几开始试:0.5 起步,0.4 到 0.6 是可操作区间
4.1 细扫一遍才发现它不是连续可调
只看四个点,容易以为 λ 是根平滑的旋钮。多取几个点再看。
forlamin(1.0,0.9,0.8,0.7,0.6,0.5,0.4,0.3):picked=vs.max_marginal_relevance_search(q,k=4,fetch_k=7,lambda_mult=lam)dup=sum(1fordocinpickedif"报销"indoc.page_contentand"差旅"notindoc.page_content)print(lam,dup)| lambda_mult | 报销重复条数 | 结果是否与上一档不同 |
|---|---|---|
| 1.0 | 3 | 基准 |
| 0.9 | 3 | 否,与 1.0 一致 |
| 0.8 | 3 | 顺序变了,条数没变 |
| 0.7 | 2 | 是,掉一条重复 |
| 0.6 | 2 | 否,与 0.7 同为 2 条 |
| 0.5 | 1 | 是,再掉一条 |
| 0.4 | 1 | 否,与 0.5 一致 |
| 0.3 | 1 | 否,与 0.5 一致 |
它是一级一级的台阶,不是斜坡。0.8 到 0.7 掉一条,0.6 到 0.5 再掉一条,0.4 以下不再变化。
台阶现象解释了一件容易被误解的事:很多人调了参数、跑出来结果没变,就以为参数没用或者代码没生效。实际上是这个语料在这个区间里本来就没有可换的候选——重复项只有三条,掉到一条之后,剩下的全是不相干文档,再怎么降 λ 也只能打乱它们的顺序。
4.2 为什么默认值是 0.5
这版langchain-core里lambda_mult的默认值是 0.5(下一章会把签名打出来看)。默认值代表的是「通用场景的折中」,不是「你的场景的最优」。
0.5 的含义是相关性和差异性各占一半。语料重复度高的时候,这个折中刚好够用,既能保住最相关的那条,又能把重复挤出去。语料本身很干净、每条一个独立主题的时候,0.5 反而会把本该保留的好内容换掉,因为「不一样」这件事在你库里根本不稀缺。
4.3 判据:三步停手
第一步,先用 0.5 跑一遍,数重复条数。这是起点,不是答案。
第二步,重复条数大于等于 2、并且你有明确想补进来的主题时,降到 0.4 再看。降完如果结果没变,说明台阶走到头了,回到 0.5 就行,继续往下只会打乱顺序。
第三步,看补进来的东西对不对。如果新进来的条目和问题明显无关(像 0.7 换进年假那样),说明 λ 降过头了,往回调一档。MMR 保证的是「不一样」,能不能用还得你自己看内容。
还有一个更省事的选择:语料本来就干净、每条一个独立主题时,别开 MMR。纯相似度配一个合理的 K,比开一个调不明白的 MMR 更可控。
五、fetch_k 小于 k:不报错,只是少给你几条
5.1 实测就一行输出
把k设成 4、fetch_k设成 2,沿用第一章那个vs与同一个查询:
print(len(vs.max_marginal_relevance_search(q,k=4,fetch_k=7)))# 4 条print(len(vs.max_marginal_relevance_search(q,k=4,fetch_k=2)))# 2 条返回 2 条。没有异常,没有警告,日志里就一行「返回 2 条」。你以为取到 4 条,实际只有 2 条。
5.2 为什么会静默
MMR 是两段式:先从库里按相似度取fetch_k条候选,再在这批候选里做多样性重排,最后取前k条。候选池只有 2 条,重排再怎么算也只能给你 2 条。
代码逻辑上这是对的,不能凭空造出文档。错的是调用方的预期:k=4看起来像「给我 4 条」,实际含义是「最多给我 4 条」。
危险的地方在于,它的症状和「检索质量差」完全一样。模型没答全,你以为是召回不行,于是去调 K、调 λ、换嵌入模型,折腾一圈回来,真正的原因是自己把候选池设小了。
5.3 怎么定 fetch_k
规则很短:fetch_k至少是k的 3 倍,语料小的时候直接等于库的总条数。这个实验里 7 条文档、k=4,fetch_k=7就是全库,不会漏。
fetch_k的默认值是 20。语料少于 20 条时它等于全库,所以你感觉不到问题;语料上千之后,k=10配默认的fetch_k=20就危险了,候选只有 20 条,重排要在 20 条里挑 10 条,多样性的空间被压得很窄,重复照样挤得进来。
还有一层连带关系:fetch_k是在元数据过滤之后取的候选。过滤条件越严,能进候选的文档越少,fetch_k就得相应放大,否则又回到「候选不够」的老问题上。下一章会把过滤和 MMR 放在一起跑一遍。
检索参数速查表:把 Top-K、fetch_k、lambda_mult 三者的先后顺序和取值范围画成一页图,配合本章的调用链看更容易记住。放在资料包里,扫码即可获取:
六、元数据过滤:这版签名换了,照旧教程写会直接炸
6.1 先用 inspect 把签名打出来
参数怎么传别猜,直接从装好的包里读。
importinspectfromlangchain_core.vectorstoresimportInMemoryVectorStoreprint(inspect.signature(InMemoryVectorStore.max_marginal_relevance_search_by_vector))print(inspect.signature(InMemoryVectorStore.similarity_search_with_score))打出来的关键字参数是k=4、fetch_k=20、lambda_mult=0.5,后面跟着一个filter,类型标注是Callable[[Document], bool] | None。源码里这一行出现在langchain_core/vectorstores/in_memory.py的第 295、338、426 行,三个方法各一处。
这句话翻译成人话:filter要的是一个函数,吃进一个Document,吐出一个布尔值。不是字典,不是字符串,不是查询表达式。
6.2 传字典会怎样
网上大量教程写的是filter={"dept": "hr"}这种字典形式,那种写法在别的向量库实现里成立,在这一版的InMemoryVectorStore里不成立。传进去之后,库会把这个字典当成函数去调用,于是抛出一个类型错误,提示字典对象不可调用。
要把这件事说准:不是过滤器坏了,也不是功能被删了,是接口从「声明式条件」换成了「谓词函数」。谓词函数更灵活,任意逻辑都能写;代价是不能序列化、不能下推给数据库执行,只能把文档拉到本地一条条判断。
6.3 六个条件实测
换成函数之后,逐个验证。
| 过滤条件写法 | 返回条数 | 返回的部门 |
|---|---|---|
| 部门等于人事 | 2 | 人事、人事 |
| 年份大于等于 2025 | 5 | 财务、财务、人事、行政、财务 |
| 层级等于 2 | 2 | 财务、财务 |
| 部门属于人事或行政 | 3 | 人事、人事、行政 |
| 部门不等于财务 | 3 | 人事、人事、行政 |
| 恒为假 | 0 | 空 |
前五行都正常。要盯住的是最后一行:恒为假的条件返回 0 条,不报错,不警告。
这意味着「过滤条件写错了」和「库里确实没有这类数据」在返回值上完全一样,都是空列表。线上排查时这两件事的处理方式截然不同,但代码层面没有任何区分。
6.4 字段缺失:下标会当场炸,get 会安静地返回空
同一个过滤函数,取字段的方式不同,出错时机完全不同。
用下标d.metadata["level"]取一个确实存在的字段,正常返回 2 条。把字段名换成不存在的,运行期抛键错误——过滤函数是对每条文档逐个调用的,只要有一条缺这个键,整次检索就中断。
改成d.metadata.get("nosuch")之后不再抛错,返回 0 条,安静得像什么都没发生。
两种都不可爱,但危险等级差得远。下标写法当场炸,你立刻知道参数写错了;get写法静默返回空,你会以为是数据没入库、是索引没建、是过滤条件太严,排查方向一开始就偏了。
判据可以很简单:这条文档一定要有这个字段才允许参与检索,就用下标,宁可炸;这个字段只是可选条件,就用get并写明确的默认值,让缺失的文档按「不满足」处理。
6.5 过滤和 MMR 一起用
两件事可以同时做:先在函数里筛部门,再在筛完的候选里做多样性重排。
defonly_finance(doc):"""只允许财务部文档进入候选池。"""returndoc.metadata.get("dept")=="finance"print("过滤后条数:",len(vs.similarity_search(q,k=10,filter=only_finance)))picked=vs.max_marginal_relevance_search(q,k=3,fetch_k=5,filter=only_finance)print("过滤 + MMR:",len(picked),[doc.metadata["dept"]fordocinpicked])结果是 3 条,全部来自财务部,其中包含那条差旅报销,这是纯相似度在前四名里给不了的东西。注意这里fetch_k=5是在过滤之后生效的,财务部一共 4 条文档,所以 5 就够把候选池开满。
顺序要记牢:先过滤、再取候选、最后重排。过滤越严,候选越少,fetch_k越要给足。
七、判据表:四个参数各自的停手条件
7.1 四个问题,四组答案
把前六章的结论收成一张能照着做的表。
| 要定的事 | 默认值 | 起步值 | 什么时候动 | 停手信号 |
|---|---|---|---|---|
| Top-K | 4 | 4 到 6 | 分数出现断崖、且断崖后仍缺主题 | 新增条目分数低于断崖前的一半 |
| 是否上 MMR | 不开 | 语料有重复就开 | Top-K 里同主题占比超过三分之一 | 重复条数降到 1、且不缺主题 |
| lambda_mult | 0.5 | 0.5 | 重复仍有 2 条以上 | 再降一档结果不变 |
| fetch_k | 20 | 不小于 3 倍 k | 语料条数少于 3 倍 k 时 | fetch_k 等于全库 |
| 过滤写法 | 无 | 谓词函数 | 需要按部门、年份、层级收窄 | 返回条数等于候选总数 |
最后一行那个「停手信号」值得多说一句:如果过滤之后返回的条数正好等于符合条件的所有文档数,说明过滤没起作用,条件写宽了或者写错了。这时候该回头查条件,而不是继续调 K。
7.2 什么时候不该用 MMR
MMR 不是万能钥匙,三种场景用它反而更差。
第一种,你要的是唯一权威版本。制度类问答里「以现行版本为准」是硬要求,MMR 的多样性偏好会把旧版本换进来,它不判断哪条更新,只知道哪条不一样。这种场景该用元数据过滤加时间排序,把旧版本挡在候选池外面。
第二种,语料本身每条独立。一份手册切成 50 个互不重叠的段落,段与段之间本来就没有重复,MMR 能做的只有打乱相关性顺序。这时候它就是纯粹的负担。
第三种,k 很小。k=2、k=3 的时候多样性几乎没有腾挪空间,MMR 挑完第一条,后面可选的本来就不多,调 λ 的收益趋近于零。
7.3 上线前的三条自检
第一条,给每次检索打一条日志:返回条数、请求的 k。两个数不相等就去查fetch_k。这一条能挡住第五章那个静默少返回的坑。
第二条,给每次检索打一条日志:Top-K 里同来源或同标题的条数占比。这个数字长期偏高,说明该处理重复文档或者开 MMR 了,而不是继续调 K。
第三条,把过滤函数里用到的每个字段列出来,逐条问一句「这条文档一定会有这个字段吗」。答案是否定的,就改成get加默认值;答案是肯定的,就保留下标写法,让错误在第一时间暴露。
这三条都不涉及模型,全是代码侧的观测点。做完之后再去调参数,才有依据可循。
向量库检索调参清单:把本章这四条判据和三条自检整理成一页可打印的核对表,选型或排障时对着勾。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 序号 | 事实 | 出处 | 本文位置 |
|---|---|---|---|
| 1 | MMR 论文标题为 The use of MMR, diversity-based reranking for reordering documents and producing summaries,作者 Jaime Carbonell、Jade Goldstein,收录于第 21 届 ACM SIGIR 会议论文集,1998 年,页码 335–336,DOI 10.1145/290941.291025 | ACM Digital Library 该 DOI 落地页(标题与会议名)+ Semantic Scholar 书目记录(作者、页码、年份) | 第 3 章 3.1 |
| 2 | MMR 的选取规则为「λ 加权的问题相似度 减去 (1−λ) 加权的候选集合最大相似度」 | 同上论文;本文按公式组成方式复述,未逐字引用原文 | 第 3 章 3.1 |
| 3 | lambda_mult默认 0.5、fetch_k默认 20、k默认 4 | 本机实测(inspect打印max_marginal_relevance_search_by_vector签名,环境见第 15 条) | 第 4 章 4.2、第 5 章 5.3 |
| 4 | filter参数类型标注为「接收 Document、返回布尔值」的可调用对象,可为空;源码位置langchain_core/vectorstores/in_memory.py第 295、338、426 行 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py;签名另用inspect.signature单独打印核对) | 第 6 章 6.1 |
| 5 | 纯相似度 Top4 分数为 0.3371、0.2840、0.2752、0.1265,前三条为同一报销流程的近似重复 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志work/2026-10-20_向量库过滤与MMR实测.log) | 第 1 章 1.3 |
| 6 | 第 5 至 7 名分数为 0.1155(年假)、0.0632(采购)、0.0620(差旅),差旅报销排名最后 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,K=7 全量排序补充扫描,同一脚本同一向量构造) | 第 1 章 1.3 |
| 7 | K 从 1 到 7 时,报销重复条数为 1、2、3、3、3、3、3,命中主题数为 1、1、1、2、2、3、3 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,K 递增补充扫描) | 第 2 章 2.2 |
| 8 | lambda_mult四档结果:1.0 为 3 条重复加调休;0.7 为 2 条重复加调休与年假;0.5 为 1 条重复加调休、差旅、年假;0.0 为 1 条重复加年假、差旅、调休 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 5) | 第 3 章 3.2 |
| 9 | lambda_mult细扫:0.9 为 3 条重复、0.8 为 3 条仅顺序变化、0.7 与 0.6 为 2 条、0.5 与 0.4 与 0.3 为 1 条 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,lambda_mult细粒度补充扫描) | 第 4 章 4.1 |
| 10 | fetch_k=2, k=4时只返回 2 条,不抛异常 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 6) | 第 5 章 5.1 |
| 11 | 传字典作为filter抛出类型错误,提示字典对象不可调用 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 2) | 第 6 章 6.2 |
| 12 | 函数式过滤六种条件返回条数:人事 2 条、年份大于等于 2025 为 5 条、层级等于 2 为 2 条、人事或行政 3 条、非财务 3 条、恒假 0 条 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 3) | 第 6 章 6.3 |
| 13 | 过滤函数用下标取不存在的字段抛键错误;改用get后静默返回 0 条 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 4) | 第 6 章 6.4 |
| 14 | 过滤与 MMR 同时使用时k=3, fetch_k=5返回 3 条,全部来自财务部 | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py,日志同名.log实验 7) | 第 6 章 6.5 |
| 15 | 实测环境为langchain-core 1.6.6、numpy 2.5.2、Python3.13.14(Windows) | 本机实测(脚本路径work/2026-10-20_向量库过滤与MMR实测.py首段版本打印;日志work/2026-10-20_向量库过滤与MMR实测.log首行) | 全文 |
| 16 | 字符 2-gram 配 crc32 哈希构造的向量不懂语义,只可用于观察排序与多样性行为,不代表真实嵌入模型的分数分布 | 本文自述的方法学局限(作者说明,非外部来源) | 第 1 章 1.2 |
附表 B:术语速查表
| 术语 | 一句话解释 | 在本文哪里用到 |
|---|---|---|
| Top-K | 按相似度取前 K 条,K 是条数不是阈值 | 第 1、2 章 |
| 相似度分数 | 查询向量与文档向量的接近程度,同一批数据里可比,跨模型不可比 | 第 1 章 |
| 重复挤占 | 同一主题的多条近似文档占掉多个返回名额,本文自述的观察角度,非标准术语 | 第 2 章 |
| 语义断崖 | 相邻两名分数突然大幅下降,说明可用内容到此处为止 | 第 1 章 |
| MMR | 最大边际相关性,挑下一条时同时惩罚「与前面挑出的太像」 | 第 3 章 |
| lambda_mult | MMR 里相关性与多样性的权重,1.0 纯相关,0.0 纯差异 | 第 3、4 章 |
| fetch_k | MMR 的候选池大小,先取这么多条再重排 | 第 5 章 |
| 静默失败 | 不抛错、不警告,但结果不是你要的 | 第 5、6 章 |
| 元数据过滤 | 按部门、年份等字段先筛文档,再做向量检索 | 第 6 章 |
| 谓词函数 | 吃一个对象、返回真或假的函数,用来表达过滤条件 | 第 6 章 |
| 分块重叠 | 切片时相邻块共享一段文字,容易在库里制造近似重复 | 第 2 章 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。