LangChain示例选择器详解:四种策略选型与调优实战

发布时间:2026/10/21 22:16:05
LangChain示例选择器详解:四种策略选型与调优实战 做RAG或者指令微调项目的人几乎没有没跟 few-shot prompt 打过交道的。多数人的默认动作是把示例一股脑塞进 prompt 模板跑一两次觉得还行等示例库涨到几十上百条问题就来了token 预算爆炸、模型注意力被无关示例稀释、响应延迟变高、账单也跟着变厚。LangChain 里的示例选择器ExampleSelector就是为解决这个问题设计的组件。它不是一个独立功能模块而是 few-shot prompt 体系里专门负责“在把 prompt 提交给模型之前从示例库中挑选最优子集”的决策器。目前最常用的四条技术路线分别是按长度截断的 Length 策略、按语义相似度排序的 Similarity 策略、平衡相关性与多样性的 MMR 策略以及纯文本重叠度打分的 NGram 策略。很多人只知道它们存在却不太清楚各自底层怎么算、参数怎么调、适合什么场景。这篇文章适合三类人刚接触 LangChain、对选择器只有模糊概念的初学者已经在用 Similarity 但效果不如预期、想换 MMR 试试的开发者以及想在离线环境下做示例筛选、不想引入重型向量组件的工程人员。我会把四种策略的原理、实操代码、参数细节和踩过的坑一次讲清楚。1. 示例选择器到底解决了什么问题1.1 少样本提示的规模困境先看没有选择器时的 few-shot 流程。每个示例都会被格式化成一个“输入-输出”对然后依次拼接进最终的 prompt 模板。示例数量少时模型能很好地跟着示例风格走示例一旦多起来整条 prompt 结构会变得臃肿模型在长上下文中对末尾指令的遵循度会显著下降这背后有明确的注意力机制原因——上下文越长中间部分的有效注意力越容易被稀释。更现实的是成本每次请求都把全部示例算一遍 token按 token 计费的接口里这部分开销几乎是纯浪费。Length 策略解决的是 token 预算问题。Similarity、MMR、NGram 解决的是“从这么多示例中选哪些最有用”的问题。想清楚这一点你就明白它们并不是互相排斥的竞品而是同一决策链路里不同环节的组件。例如可以先让 Length 控制最终 prompt 的总体规模再用 Similarity 或 MMR 从语义上挑选高质量子集也可以先做语义召回再按长度做二次裁切。不同策略可以组合叠加只是 LangChain 默认给的是单个选择器接口组合使用时通常需要自己在外部编排。1.2 接口设计选择器如何嵌入 few-shot 链路LangChain 里所有选择器都继承同一个抽象基类 BaseExampleSelector核心方法只有两个select_examples(input_variables) 负责接收用户输入的变量字典、返回选中的示例列表add_example(example) 负责向示例库中追加新样本。这个设计非常克制不管底层是向量检索、长度计算还是自定义算法对外暴露的都是同一套接口。接入 FewShotPromptTemplate 时只需要把原来的examples参数替换成example_selector其余模板结构不用动。这个设计最有价值的地方在于它把“示例库有多大”和“本次请求用哪些示例”彻底解耦。示例库可以持续增长prompt 模板保持稳定选择器内部自行取舍。如果你想快速验证一个自定义的筛选算法继承基类、实现 select_examples然后立刻替换内置方案即可完全不用重写 prompt 的解析逻辑。后面我会专门写一个自定义选择器的完整示例把这条路走一遍。2. 四种策略的底层逻辑与差异2.1 Length 策略不感知语义的容量控制阀LengthBasedExampleSelector 的算法非常直白按示例在数组中的顺序逐个将示例通过 example_prompt 格式化累加已选文本的总长度一旦超过 max_length 就停止。它不关心示例内容与用户问题之间有没有语义关联也不做任何排序调整本质是一个纯粹的容量控制阀。这里有一个容易踩坑的点max_length 的单位。LangChain 内部通过 get_token_ids 函数来估算长度默认实现会把文本按空白字符切分统计切分结果的片段数。对英文来说这约等于单词数量还算可用对中文来说整句话没有空格会被算成“一个片段”导致 max_length 几乎不可能触发截断示例再多也会被全部拼进去。解决思路是给 LengthBasedExampleSelector 传入自定义的 get_token_ids 函数例如把中文字符数或模型真实 tokenizer 的结果作为统计单位。使用 Length 策略的典型场景有两类。第一类是模型上下文窗口很小比如一些轻量级模型输入上限只有 2k token需要硬性保证 prompt 不超限。第二类是成本敏感型业务希望把每次请求的 token 数压在一个稳定预算内。它的优点是零模型依赖、速度快、结果可预测缺点是选出来的示例完全不管相关性可能把最没用的几条留在 prompt 里。2.2 Similarity 策略语义检索直出 top-kSemanticSimilarityExampleSelector 做的事情本质上就是一次 top-k 语义检索。它把所有示例文本用 embedding 模型转成向量用户输入到来时也做同样的向量化然后计算余弦相似度取相似度最高的 k 个示例返回。这个策略非常直观效果在大多数问答场景里都优于随机抽取和长度截断。但它的隐性成本在两个方面。第一是 embedding 模型的选型和向量库的持久化LangChain 默认使用内存向量库 InMemoryVectorStore每次进程重启都要重新计算全部示例的 embedding如果示例有几万条初始化耗时不可忽视。常见缓解方案是把向量库持久化到磁盘或者给 embedding 结果做本地缓存。第二是 Similarity 完全不处理多样性。10 个候选示例里可能有 8 个在表达同一个意思取了 top-k 等于只覆盖了一个角度模型学到的东西非常有限。这也是很多人从 Similarity 转向 MMR 的直接原因。还有一点值得注意Similarity 选择器在构造时会一次性 embedding 所有示例。若在调用 select_examples 之前调用了 add_example 追加新样本不同版本的行为不完全一致有些实现不会自动更新向量库导致新样本在检索中“隐身”。所以追加样本后最好重新构建选择器或者确认你所用版本的 add_example 覆盖了向量库更新逻辑。2.3 MMR 策略给相似度加上多样性约束MaxMarginalRelevanceExampleSelector 是对 Similarity 的重要扩展。它引入了 fetch_k 参数先从示例集中取出与用户输入相似度最高的 fetch_k 个候选再在候选集合内部进行重排让最终返回的 k 个示例既与问题相关彼此之间又尽量不同。这个“相关与多样”的权衡由一个 lambda_mult 参数控制取值范围在 0 到 1 之间。理解 MMR 的关键是那个 lambda_mult。它可以近似看成对目标函数的加权最终得分 ≈ (1 - lambda_mult) × 相关性得分 - lambda_mult × 与已选示例的最大相似度。lambda_mult 等于 1 时MMR 退化为纯相似度选择多样性权重为零lambda_mult 等于 0 时它倾向于选“跟已选示例都不像”的样本哪怕相关性弱一些。这个参数不是越大越好实际测试里我通常放在 0.5 到 0.7 之间效果最稳。如果设置到 0.8 以上多样性惩罚几乎可以忽略设置到 0.2 以下选出来的示例可能跟当前问题完全不搭边。还有一个极容易忽略的细节fetch_k 必须大于 k。如果 fetch_k 等于 k候选集合和最终返回集合大小一样MMR 的重排就没有任何操作空间多样性约束形同虚设。我见过不少人在代码里把 fetch_k 和 k 都设成 2然后跑来问为什么 MMR 效果和 Similarity 没区别原因就在这里。2.4 NGram 策略无模型依赖的字面重叠打分NGramOverlapExampleSelector 是四种策略里唯一不需要 embedding 模型就能实现“相似度排序”的选项。它把示例文本和用户输入都切分成 n-gram也就是连续 n 个基本单元组成的片段计算两者之间的重叠比例作为得分然后设置一个 threshold把低于阈值的示例过滤掉最终返回得分最高的 k 个。这个思路在离线部署、隐私敏感、或者不想引入外部模型依赖的场景下非常好用。它最大的局限是字面重叠不等于语义相似。同义词改写、语序调整、抽象表达都会让得分大幅下降所以它更适合术语固定、格式规范的垂直领域比如设备故障码查询、合同条款匹配、产品型号参数说明这类任务。在这些场景里字面重叠恰恰是比语义向量更可靠的信号。NGram 有几个关键参数需要理解。n 控制 n-gram 的粒度n 越小越能匹配短片段但误报也越多n 越大越严格但对长文本的要求也越高。ngram_delimiter 用来控制切分单元英文分词场景通常用空格或标点中文连续字场景可以设为空字符串直接按字符滑动窗口。threshold 则控制过滤强度设得太低等于没过滤设得太高可能直接返回空列表需要结合示例库的具体文本分布去调整。3. 实操用统一示例集跑通四种选择器3.1 准备示例集和公共模板为了对比差异我准备了一份非常简单的电商文案示例集每条示例包含商品名、风格和文案三个字段。这个场景足够直观方便你在本地快速验证结果差异。from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate examples [ {product: 纸纤手机壳, style: 极简, text: 轻若无物还原裸机手感}, {product: 商务保温杯, style: 商务, text: 24小时保温会议不断档}, {product: 运动耳机, style: 运动, text: IPX5防水跑步甩不掉}, {product: 电竞键盘, style: 电竞, text: 全键热插拔RGB随光而动}, {product: 便携榨汁杯, style: 居家, text: 30秒出汁一冲就干净}, {product: 桌面双屏支架, style: 办公, text: 升降折叠颈椎不被绑架}, ] example_prompt PromptTemplate( input_variables[product, style, text], template商品: {product}\n风格: {style}\n文案: {text}, )注意选择器内部需要借助 example_prompt 把示例格式化后才能计算长度或重叠度所以 example_prompt 是 Length 和 NGram 两个选择器的必传参数。Similarity 和 MMR 因为走向量化不强制要求 example_prompt但加上之后示例向量会包含模板结构信息检索效果通常更稳。3.2 Length 与 NGram零依赖的两个实现Length 选择器的构建很简单。把 examples 和 example_prompt 传进去设置 max_length 即可。from langchain_core.example_selectors import LengthBasedExampleSelector length_selector LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length120, ) prompt FewShotPromptTemplate( example_selectorlength_selector, example_promptexample_prompt, prefix你是一名电商文案策划。, suffix商品: {input}\n风格: {style}\n文案:, input_variables[input, style], ) result prompt.format(input降噪耳机, style通勤) print(result)输出里示例会按原始顺序被拼接。max_length 120 意味着当已选示例的累计 token 数超过 120 时后续示例被截断。这里有一个我在 2.1 提到的坑默认的 token 估算对中文不友好。实测中文场景下建议把自定义 tokenizer 传进去例如按字符数统计def char_tokenizer(text: str) - list[str]: return list(text) length_selector_cn LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length120, get_token_idschar_tokenizer, )NGram 选择器的构建也类似只是多了一个 threshold 和 n 参数。from langchain_core.example_selectors import NGramOverlapExampleSelector ngram_selector NGramOverlapExampleSelector( examplesexamples, example_promptexample_prompt, threshold0.1, n2, ) selected ngram_selector.select_examples({input: 运动时防水不掉}, {style: 运动}) print(selected)这里 select_examples 接收的是和最终 prompt 变量同名的字典。我传入了两个键一个是用户问题的 input另一个是风格字段 style。NGram 会从这些变量中提取文本与示例格式化后的文本计算重叠度。如果阈值设得过高可能出现空列表所以我习惯先设一个较小的值观察分布再逐步调大。3.3 Similarity 与 MMR向量化方案落地Similarity 和 MMR 在 LangChain 里的构建方式高度一致都依赖 embedding 和 vectorstore。为了确保示例代码能在本地直接跑通这里先用确定性假向量 DeterministicFakeEmbedding 演示。它在测试环境里非常有用不依赖网络也不下载模型文件。实际项目可以换成 HuggingFaceEmbeddings 或其他 embedding 服务。from langchain_core.embeddings import DeterministicFakeEmbedding from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_core.vectorstores import InMemoryVectorStore embeddings DeterministicFakeEmbedding(size32) similarity_selector SemanticSimilarityExampleSelector.from_examples( examplesexamples, embeddingsembeddings, vectorstoreInMemoryVectorStore, k2, ) selected similarity_selector.select_examples({input: 想找一款防水耐用的运动耳机}) print(selected)from_examples 是一个便捷类方法它会自动完成“把示例列表逐条 embedding、写入向量库”的全过程。如果你已经有持久化的向量库也可以直接传入 vectorstore 实例避免每次启动都重新计算。真实环境里我建议先把示例集存成 JSON 或数据库表启动服务时检测向量库是否存在不存在才做 embedding 初始化。MMR 的构建方式几乎一样只是多了 fetch_k 和 lambda_mult。from langchain_core.example_selectors import MaxMarginalRelevanceExampleSelector mmr_selector MaxMarginalRelevanceExampleSelector.from_examples( examplesexamples, embeddingsembeddings, vectorstoreInMemoryVectorStore, k2, fetch_k4, lambda_mult0.6, ) selected mmr_selector.select_examples({input: 想找一款防水耐用的运动耳机}) print(selected)同样的用户输入Similarity 返回的可能是“运动耳机”和另一个与运动耳机向量最近似的样本MMR 在 fetch_k 足够大时会在相关性允许的范围内尽量让两个示例分属不同风格例如一条运动耳机、一条便携榨汁杯。这种差异在真实业务中往往决定了模型输出角度的丰富程度。3.4 自定义选择器一个最小但完整的标签过滤示例内置选择器再灵活也不如自己写一个来得踏实。BaseExampleSelector 接口只有两个方法我写了一个按难度标签过滤的选择器用来演示自定义算法如何接入 few-shot 链路。from langchain_core.example_selectors.base import BaseExampleSelector class DifficultySelector(BaseExampleSelector): def __init__(self, examples): self.examples examples def add_example(self, example): self.examples.append(example) def select_examples(self, input_variables): max_difficulty input_variables.get(max_difficulty, 5) selected [ ex for ex in self.examples if ex.get(difficulty, 0) max_difficulty ] return selected使用时只需要把 examples 换成带 difficulty 字段的数据然后传给 FewShotPromptTemplate。注意 select_examples 的参数是一个字典里面可能有用户输入、可能有控制参数这些约定完全由你自己定。我在实际项目里曾经用类似思路写过“按数据类型过滤”“按输出格式过滤”“轮询打散”三种自定义选择器替换的时候几乎零成本。需要留意的一点是自定义选择器的 add_example 是抽象方法必须实现。如果示例库是静态的方法体里留一个简单的 append 就够了但绝不能删掉否则实例化会直接报错。3.5 参数调优时必须知道的三处关键细节第一max_length 估算要匹配你的实际 tokenizer。这点在上面已经反复强调真正上线时不要依赖 LangChain 默认的空格切分估算要么自定义 get_token_ids要么直接用 len(tokenizer.encode(text)) 确保单位一致。第二fetch_k 至少要明显大于 k。MMR 的整个价值都建立在候选集大于返回集这个前提之上。fetch_k 通常设成 k 的 2 到 3 倍比如 k2 时 fetch_k 设 4 或 6。设得太大也会导致 MMR 在“候选池比选”时的计算量上升但对于几百条示例的规模完全不是问题。第三threshold 的初始值宁低勿高。NGram 的 threshold 如果设太高容易得到空列表校验过的工程人员都知道空示例集传入 few-shot prompt 不会报错但模型输出会明显失去 few-shot 的规范约束问题排查起来反而更难。我建议第一次跑先设 0 或负值把所有示例得分打印出来观察分布后再确定阈值。4. 横向对比与选型建议4.1 四种策略差异对照表为了让你一眼看清差异我把四种策略的核心信号、依赖条件和适用边界整理成一张表。这张表是我在实际选型时反复用到的速查参考。策略核心信号外部依赖主要优势主要局限建议场景Length文本长度无控制 prompt 体积零成本不感知语义随机性高上下文窗口小、成本敏感Similarity语义向量余弦相似度embedding 模型语义匹配准部署简单忽略多样性需向量库问答知识库、意图匹配MMR语义相似度 多样惩罚embedding 模型兼顾相关和多样参数多需调优示例覆盖多个角度时NGram字面 n-gram 重叠度无离线可用结果可解释无法处理同义改写术语固定、格式规范从这张表能看出一个规律越偏向语义的方案效果上限越高但外部依赖和运行成本也越高越偏向字面规则的方案越轻量但能处理的表达变化也越少。选型本质上是在效果、成本、可维护性之间做权衡不存在某一种策略在所有场景下都最优。4.2 不同场景下的选择建议如果你是刚起步的小项目示例只有二三十条我的建议是直接用 Length 或者直接用全部示例没必要折腾向量库。这个阶段模型体积小、数据量少引入 embedding 带来的收益有限反而增加部署复杂度。当示例库规模超过一百条或者语言多样化明显时再用 Similarity 也不迟。它是四种策略里性价比最高的一个实现简单、理解直观绝大多数业务用它已经足够。只有当你不止一次发现“Similarity 选出的示例翻来覆去都是同一个风格”时才有必要升级到 MMR。对离线环境或不允许调用外部模型的场景不要犹豫直接用 NGram它是唯一能稳定落地又不依赖外部服务的基线方案。还有一个容易忽略的场景模板结构非常固定的任务例如实体抽取、信息格式化。这种情况下示例之间的差异往往集中在极少数字段里NGram 的稳定性和可解释性反而比 embedding 更可靠。我用 NGram 做过一次法律法规条款的辅助检索复述效果比同参数的语义检索更稳因为法律文本的词汇高度固定字面重叠本身就是强信号。4.3 选择器与记忆、缓存模块的协同选择器不是孤立工作的。在一个完整的 LangChain 链路里它通常与记忆模块、缓存模块同时存在。记忆模块保留的是历史对话状态选择器关注的是当前这一轮需要哪些示例两者互不干扰。一个常见的设计是历史对话经过记忆模块压缩后写入 context示例选择器根据最新用户输入从静态示例库中挑选 few-shot 示例最后一起拼入 prompt。缓存方面如果示例库规模很大建议给选择器的检索结果加一层 LRU 缓存。因为同一用户的输入往往会在短时间窗口内反复触发相同的向量检索白做 embedding 计算非常浪费。ManyChat 类的对话应用里我见过把“用户问题的 embedding 向量”作为缓存键的做法命中率可以做到相当可观。要提醒的是缓存键如果包含当时的时间戳缓存就没有意义了这一点经常被忽略。5. 常见问题与排查技巧实录5.1 选择器返回空列表最常见的原因是 NGram 的 threshold 设得过高所有示例的分都低于阈值。排查方法很简单把 threshold 临时调成负值打印所有示例的得分看看真实分布再调整阈值。另一个原因是字段匹配错误select_examples 的字典键名与 example_prompt 中存在差异导致格式化出来的文本为空计算任何相似度都得不到有效结果。这种情况下建议先把传入的 input_variables 和 example_prompt 直接拼出来打印肉眼检查一遍。5.2 本地 embedding 模型初始化卡顿使用 HuggingFaceEmbeddings 或类似本地模型时首次调用会触发模型权重下载。生产环境如果网络受限构建选择器时很容易超时。解决方式是提前把模型下载到本地缓存目录或者干脆打包进镜像。我经历过几次线上服务冷启动卡在加载词表和权重上后来完全离线部署才稳定下来。如果你用的是远端 embedding 服务记得给初始化过程加好超时和重试不能在启动时一次性全量 embedding 几万条示例那会让服务健康检查直接失败。5.3 MMR 结果越选越偏用户输入是“运动耳机”MMR 却选出了一条“便携榨汁杯”和一条“桌面支架”看起来非常离谱。出现这种情况多半是 lambda_mult 设置太低多样性惩罚压过了相关性。把这组参数代入公式你会发现只要候选与已选样本相似度很低哪怕它与问题相关性一般也能拿到较高排名。排查时先把 lambda_mult 调到 0.8 以上确认结果回到正常语义轨道再缓慢往下调找到相关性和多样性的平衡点。每次调整都要结合真实 prompt 效果去看不要只看向量距离。5.4 中文下 NGram 得分失灵英文按空格分词中文没有天然词边界这是 NGram 在中文场景最常见的坑。默认的 ngram_delimiter 设成空格时中文整句话被当成一个巨型 n-gram 块重叠率往往趋近于 0。我的做法是把 n 设为 2 或 3、ngram_delimiter 设为空字符串让它按连续字滑窗。还有一个小技巧做 n-gram 之前先去掉标点和助词避免把“的、地、得、了”这类高频字带进特征。这个预处理对中文场景的得分稳定性提升非常明显。5.5 向量库里的“幽灵示例”这个问题我遇到过两次。第一次是往 Similarity 选择器里 add_example 后新示例在检索结果中始终不出现。翻源码发现部分版本的 add_example 只更新了内部示例列表没有同步更新向量库。所以项目里如果存在动态追加样本的需求建议不要用选择器实例自带的 add_example而是自己维护示例库源表每次追加后重新构建选择器或直接操作向量库写入向量索引。第二种“幽灵示例”是反方向的选择了持久化向量库但向量库里残留着历史旧示例删除时没有同步清理导致检索结果里出现已经废弃的样本。定期的向量库重建成全量任务但我建议在关键业务节点上至少保留一条全量重建的脚本路径。最后再分享一个我自己用的套路项目初期不要急着上 MMR。先用 Length 把 prompt 压到可控范围效果差再升级到 Similarity只有当你明确观察到“选出来的示例重复度高、彼此覆盖”的时候再引入 fetch_k 和 lambda_mult 去调 MMR。这条升级路径不是越复杂越好而是按需添加复杂度。另外NGram 在离线环境里的价值被大多数人严重低估它给无法使用向量化手段的场景提供了一个可靠、可复现、可解释的 baseline。离线跑通一版再跟在线语义方案做结果对比往往能帮你发现真正影响效果的到底是不是示例选择策略本身。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询