《LangChain RAG 全链路学习笔记》系列 · 第 4 篇共 13 篇作者AVA环境Python 3.11 LangChain 1.x⚠️这一章的导入路径比前几章温和——langchain_core.example_selectors在 0.3.x 和 1.x 上都能用不会当场ModuleNotFoundError。真正变了的是embedding 那个包langchain_community.embeddings已经进入 sunset1.x 里单独成了一个包langchain_huggingface。用旧路径还能跑但会有弃用警告。这一章解决什么问题第 3 章的 FewShot 是静态的写死几条例子每次都用这几条。真上生产马上撞墙例子多到塞不下—— 攒了 500 条全塞进 prompt 直接爆上下文不相关的例子会干扰模型—— 问「签合同」结果给的一堆都是「运动鞋」的例子这一章给例子加了一层动态挑选每次来问题现从例库里挑几条最合适的。⭐一句话看懂本质示例选择器就是给「例子」做了一次检索。第 12 章的 Retriever 检索的是「文档」这里检索的是「示例」——换了个对象思路一模一样。核心结论一四种选择器Length长度MMR最大边际相关Similarity相似度自定义类名LengthBasedExampleSelectorMaxMarginalRelevanceExampleSelectorSemanticSimilarityExampleSelector继承BaseExampleSelector挑选依据字符长度预算相似度 多样性惩罚纯相似度 Top-K你自己写的规则要 embeddings 吗不要要要看你要向量库吗不要要FAISS要FAISS不要和书写顺序有关吗有关从前往后取无关无关由你的规则决定适合场景prompt 预算紧张、示例长短差异大示例彼此像、容易扎堆示例区分度高已有业务规则按类目、按权限四个选择器里只有 Length 是「不算语义」的—— 它纯粹按长度切。这一点决定了它的行为最反直觉见下面「核心结论三」。核心结论二k/fetch_k/lambda_mult各归谁管 ⭐⭐这三个参数是本章最容易记混、也最容易被坑的一组。参数是什么归谁管k最终要几条示例selectorfetch_k先从库里粗筛几条候选默认 20selectorlambda_mult相关性 vs 多样性的旋钮0~1❌ 不在 selector 上在向量库那层MMR 的工作方式是两段式先按相似度捞fetch_k条 → 再在这fetch_k条里用 MMR 挑出k条。所以必须k ≤ fetch_k。⚠️lambda_mult真的不在 selector 上 —— 这是实测出来的。MaxMarginalRelevanceExampleSelector.from_examples()的签名里根本没有这个参数deffrom_examples(cls,examples,embeddings,vectorstore_cls,k:int4,input_keysNone,fetch_k:int20,example_keysNone,vectorstore_kwargsNone,**vectorstore_cls_kwargs:Any,# ← 就是这里把多余的参数吞了)最后那个**vectorstore_cls_kwargs是关键你多传的参数不会报「不认识的参数」而是被原样转发给FAISS.from_texts(...)然后在向量库构造时报一个看着毫不相干的错TypeError: FAISS.__init__() got an unexpected keyword argument lambda_mult这个报错信息会把你往「FAISS 装错了」的方向带但真正的问题是参数放错了层。lambda_mult的真实位置在向量库VectorStore.max_marginal_relevance_search(query,k4,fetch_k20,lambda_mult0.5,...)lambda_mult 1→ 完全等于普通相似度检索只要相关允许重复lambda_mult 0→ 完全追求多样性可能选到不太相关的lambda_mult 0.5→ 默认值折中selector 层你只能调k和fetch_k。想动lambda_mult得绕到向量库那层。核心结论三Length 选择器数的不是 token是「你给它的那个函数」LengthBasedExampleSelector有个参数get_text_length不传的话用默认值。而默认值和课程里写的完全不是一回事# 不传 get_text_length —— 库自带的默认实现def_get_length_based(text:str)-int:returnlen(re.split(r\n| ,text))# ← 按「换行或空格」切数量词也就是说默认的max_length2048单位是「词块」不是字符更不是 token。而课程代码里显式传了一个函数把它换掉了get_text_lengthlambdax:len(x)# ← 改成按字符数算这两套单位的差距是数量级的—— 同一条示例默认算出来可能是4换成len(x)就变成20。所以max_length到底该写 50 还是 2048取决于你有没有传这个 lambda。真实的裁剪算法源码级defselect_examples(self,input_variables):inputs .join(input_variables.values())remaining_lengthself.max_length-self.get_text_length(inputs)# ① 先扣掉问题的长度i0examples[]whileremaining_length0andilen(self.examples):new_lengthremaining_length-self.example_text_lengths[i]ifnew_length0:break# ② 放不下就整段停examples.append(self.examples[i])remaining_lengthnew_length i1returnexamples两个细节都非常反直觉① 预算先被「问题」吃掉。所以问题越长能塞的示例越少。② 放不下就break不是跳过 continue。顺序在前的示例把预算用光后面的再短也轮不上。结论把最重要的示例放在examples列表最前面。手算一遍课程里的输出max_length50、get_text_lengthlambda x: len(x)、5 条示例的example_text_lengths实测为[20, 18, 24, 20, 20]输入问题长度剩余预算逐条扣减结果大14949−2029 ✅ → 29−1811 ✅ → 11−24−13 ❌ 停2 条又大又大,又大又高,高大无比,高耸入云193131−2011 ✅ → 11−18−7 ❌ 停1 条问题从 1 个字变成 19 个字示例就从 2 条掉到 1 条 ——不是因为问题本身贵是它先把预算吃了。⚠️ 还有一点这个max_length只减了input_variables没减prefix/suffix。所以它不是「最终 prompt 的真实长度」只是「示例部分的大致预算」。别拿它当上下文窗口算。实测02 和 03 只差一行类名选出来的就不一样课程里02-MMRSelector.py和03-SSimiSelector.py用的是同一份 6 条 examples只有类名差一行。跑出来的结果# 我本机实测k3query 签合同03SemanticSimilarity-[3,6,2]02MaxMarginalRelevance-[3,6,4]id3 商务,皮鞋,正装 id6 咖啡厅,商务餐,包厢 id2 休闲,运动鞋,篮球 id4 户外,徒步鞋,探险前两条一样第三条换了。为什么把 MMR 的公式手算一遍就清楚了MMR λ × sim(问题, 候选) − (1−λ) × max(sim(候选, 已选中))已选中3、6之后第三条的争夺λ0.5候选和问题的相似度和已选中最大的相似度MMR 得分id2休闲,运动鞋,篮球0.34030.6116跟 id3 撞−0.1356id4户外,徒步鞋,探险0.31900.5594−0.1202✅id2明明更像问题却输了—— 因为它和已经选中的id3太像0.61多样性惩罚把它拽了下来。再把 λ 调成 1 验证一下λ1时惩罚项整项消失结果立刻变回[3,6,2]和纯相似度一模一样。这就证明了λ1 纯相似度这个说法不是比喻。所以两个选择器的差别只有一句话MMR 会嫌「跟已选中的太像」Similarity 不会。示例库里如果有大量彼此相似的条目SemanticSimilarityExampleSelector会一口气给你捞一堆同质的这种场景就换 MMR。最小可运行代码Length 选择器不需要模型、不需要向量库是本章最容易跑起来的fromlangchain_core.example_selectorsimportLengthBasedExampleSelectorfromlangchain_core.promptsimportFewShotPromptTemplate,PromptTemplate examples[{input:快乐,output:悲伤},{input:高,output:矮},{input:精力充沛,output:昏昏欲睡},{input:阳光,output:阴暗},{input:喧哗,output:安静},]example_promptPromptTemplate(input_variables[input,output],templateinput: {input}\noutput: {output},)example_selectorLengthBasedExampleSelector(examplesexamples,example_promptexample_prompt,max_length50,get_text_lengthlambdax:len(x),)dynamic_promptFewShotPromptTemplate(example_selectorexample_selector,# ⭐ 用 selector不是 examplesexample_promptexample_prompt,suffixInput: {input}\nOutput:,input_variables[input],)print(dynamic_prompt.format(input大))# 选中 2 条print(dynamic_prompt.format(input又大又大,又大又高,高大无比,高耸入云))# 只选中 1 条print(example_selector.example_text_lengths)# [20, 18, 24, 20, 20]跑起来之前记住两条约定example_selector和examples二选一—— 一个动态一个静态同时给会报错。example_text_lengths量的是「example_prompt渲染之后的整段」—— 包含input:/output:这些模板字面量不是光量示例内容。⚠️ 我踩到的坑1. ⭐ selector 拿去向量化的不是example_prompt渲染的结果这条最隐蔽。看源码staticmethoddef_example_to_text(example,input_keys):ifinput_keys:return .join(sorted_values({key:example[key]forkeyininput_keys}))return .join(sorted_values(example))# ← 按 key 排序后的「值」空格连接example_prompt只负责把选出来的示例渲染进最终 prompt跟检索没关系。真正被向量化的是「按 key 名排序后的所有值拼起来」。拿课程的示例实测{id:1,features:时尚, 运动鞋, 跑步}# example_prompt 渲染 - id: 1\n描述: 时尚, 运动鞋, 跑步# 实际被向量化的 - 时尚, 运动鞋, 跑步 1 ← id 被甩到最后了⚠️这意味着id这种「只是想当标签」的字段也会被算进语义里。想让检索只认某一个字段得显式传input_keys[features]。2. ⭐ 给 MMR selector 写lambda_mult会报一个误导性的错见「核心结论二」。参数掉进**vectorstore_cls_kwargs转发给了 FAISSTypeError: FAISS.__init__() got an unexpected keyword argument lambda_mult⚠️同一个规律还会以另一种形式出现fetch_k是 MMR 专有的写到SemanticSimilarityExampleSelector上同样被**vectorstore_cls_kwargs吞掉报同样的FAISS.__init__()错。签名里没有的参数一律别写。3.get_text_length不传的话单位和你以为的不一样见「核心结论三」。默认是按「空格/换行」切词块不是字符。4.example_selector和examples不能同时给语义互斥一个动态挑一个固定用。两个都给会直接报校验错误。5. 自定义select_examples返回[]是静默失败不报错只是最终 prompt 里一条示例都没有 —— 模型照样能跑只是质量悄悄掉了。建议加个兜底示例或者至少打条日志。6. Length 选择器遇到放不下的示例是整段停不是跳过if new_length 0: break—— 顺序在前的示例把预算吃光后面的再短也轮不上。重要的示例往前放。⚠️ 1.x 和 0.3.x 的导入路径差异这一章比前面几章温和langchain_core.example_selectors这个路径在 0.3.x 和 1.x 上都能用不会当场ModuleNotFoundError。真正不一样的是embedding 那个包# 网上 0.3.x 教程fromlangchain_community.embeddingsimportHuggingFaceEmbeddings# 本机 1.x 实际用的fromlangchain_huggingfaceimportHuggingFaceEmbeddings# ⭐ 单独成包了两条路径在本机都还能 import 成功我实测过但langchain-community已经进入 sunset用它会看到弃用警告。新写的代码直接上langchain_huggingface。补充 ①我一开始以为「示例选择器」是……就是挑选和当前问题最像的问题的回答然后分析这些示例来进行回答✅ 自测三问from_examples里k和fetch_k分别是什么lambda_mult在哪能调k你最终要几条数据fetch_k从多少条数据当中来挑选向量库当中来选⚠️ 这题的三问我只答了两问lambda_mult漏了。补上它在向量库那一层selector 上写不了——from_examples()的签名里根本没这个参数硬写会被**vectorstore_cls_kwargs吞掉再转发给 FAISS报一个指向 FAISS 的TypeError就是前面「坑 2」。同一份 examples为什么02和03两个文件结果会不一样这个就是选择器使用的区别。MMR 是有多样性惩罚的他适合在样本大多数是类型一样的样本当中挑选结果会给你把当前的范围更扩一点另一个就是纯粹的相似性选择器。LengthBasedExampleSelector的example_text_lengths量的是什么想让某条示例必被选中该怎么办一个是你要分的每一个小的示例有多长第二个是你的示例文本总长是多少放最前面。⚠️第二个答串了——example_text_lengths只量每条示例各自的长度不存在「总长」这个概念「总长度」是另一个参数max_length预算总额两者不是一回事。另外它量的是example_prompt渲染之后的整段含input:/output:这些模板字面量不是光量示例内容。「放最前面」是对的 ✅ —— 因为遇到放不下的就break后面的再短也轮不上。本系列共 13 篇下一篇05 输出解析器 —— 让模型吐出能直接喂给代码的结构化数据笔记同步更新在 GitHubgithub.com/MaDai66/langchain-rag-notes代码基于 LangChain 1.x 实测有问题欢迎评论区交流。