首页 / 资讯中心 / 文章详情

LangChain FewShotPromptTemplate:少样本提示的工程化实践与动态示例选择

LangChain FewShotPromptTemplate:少样本提示的工程化实践与动态示例选择 ★ FEATURED ARTICLE
开场做提示工程做久了你会发现一个特别有意思的现象同一段指令你写“请严格按照以下格式输出”模型八成给你加一堆客套话你把同样的话改成“比如下面这样输入A输出B再来一个输入C”它立刻老老实实照着排。这个“给几个例子”的动作就是少样本提示few-shot prompting。而LangChain里的FewShotPromptTemplate就是把这个动作从“手搓字符串”变成“可管理、可复用、可动态挑选”的工程化组件属于智能提示工程里性价比最高的一环。我最初也是直接把示例硬拼进 prompt 里三个示例还行到第八个示例、还要按用户输入动态换示例的时候字符串拼接就开始失控了变量名对不上、分隔符多了少了、改一个示例要动三处。这篇文章就把我踩过的这些东西整理出来从FewShotPromptTemplate的设计意图、每个参数的取舍逻辑到两版可以直接复现的实现静态版和带语义选择器的动态版再到实际跑起来会撞上的坑和排查路子都摊开讲。适合已经能跑通基础的PromptTemplate、想把手上的提示词从“一次性”升级成“可维护资产”的读者纯新手也能照着抄因为我会把每一步的意图说清楚。1. 为什么提示工程需要“少样本”这个中间态1.1 零样本、少样本、微调三段式里少样本卡在哪先把这个谱系捋清楚不然容易误用工具。零样本zero-shot就是你只给任务描述不给任何参考模型全凭预训练里学到的模式去猜。它的好处是 prompt 短、成本低、响应快坏处是不稳定尤其是输出格式要求严格、或者任务定义带点“行话”的时候模型经常给你自由发挥。微调fine-tuning是另一头拿几百上千条标注数据把权重改一遍效果最稳但成本高、迭代慢、换个小需求就得重训而且对多数业务场景来说是拿大炮打蚊子。少样本正好卡在中间不动权重只在上下文里塞几条“输入-输出”配对让模型在推理时“看着办”。它的本质是用上下文做条件约束把任务从“你猜我要什么”变成“你照这个样子做”。实测下来在分类、抽取、格式转换、风格模仿这四类任务上三到五个高质量示例带来的稳定性提升往往比你把指令翻来覆去改十遍都明显。但手工维护示例有三个绕不过去的问题一是示例多了以后 prompt 会变得极长拼接逻辑混乱二是不同输入其实需要不同的示例用户问的是退货你给的全是物流示例就没用三是示例和指令、和用户实际输入混在一坨字符串里改一处错一处。FewShotPromptTemplate 干的就是把这三件事拆开管。1.2 FewShotPromptTemplate 到底封装了什么从结构上看它就干一件事把“示例列表”按统一模板渲染成文本块再和“前缀指令”“后缀待填输入”拼成一个完整的最终提示。听起来平平无奇但拆开看它把四样东西解耦了examples示例数据本身一堆字典纯数据和展示格式无关example_prompt每个示例长什么样一个独立的 PromptTemplate负责把单个字典渲染成一段文本prefix/suffix示例块前面和后面的固定内容suffix 里通常留着真正需要模型回答的那个变量example_selector可选一个选择器对象负责从大示例库里挑出最合适的几条。这个拆法的价值在于你改展示格式不用动数据改数据不用动指令换挑选策略不用动前两者。我在一个信息抽取的小项目里一开始是 8 个示例硬编码在字符串里后来要按输入语言切换示例集改了两小时换成模板化写法之后只是把 examples 换成一个查表的结果五分钟搞定。这就是工程化和手搓的差别。1.3 它真正解决的三类现实问题第一类是格式顽固型任务。比如要把一段自然语言简历压成固定的 JSON 结构字段名不能变、不能多不能少。你只写指令模型总会自作主张加个summary字段或者把数组写成字符串。给两三个“输入→标准 JSON”的示例格式跑偏率会肉眼可见地掉下来。第二类是判定标准模糊型任务。比如情感分类里“中性”和“轻微负面”的边界光靠文字描述永远说不清——“一般般吧”算中性还是负面给两个边界样本模型立刻就对齐了你的标准。这类任务里示例的作用不是教知识是校准尺度的刻度。第三类是风格与语气对齐。客服回复、文案改写、代码注释风格这类东西你描述一百个字不如给一句范例。FewShotPromptTemplate 允许你把这批范例做成可切换的“风格包”A 客户一套、B 客户一套共用一个模板骨架。注意少样本不是越多越好。示例每加一条输入 token 就涨一截延迟和成本都跟着走而且超过一定数量后模型会开始“过度模仿”最后几条示例反而忽略了指令。我在分类任务上做过粗略对比三到六条是甜点区超过十条基本是浪费钱。2. 核心组件与参数背后的设计逻辑2.1 example_prompt 决定单个示例长什么样example_prompt是一个普通的 PromptTemplateinput_variables必须和 examples 里字典的 key 完全对应。这一步是最容易出错的地方也是整个模板的“细胞”。为什么单独抽出来因为示例的呈现形式其实有讲究用“输入: xxx / 输出: yyy”这种对齐格式模型对结构的识别度通常比一大段自然语言描述高而如果你要做的是对话式任务示例就该渲染成多轮对话的样子那就不能用一个简单模板得换成带角色标记的写法。有个细节值得说示例里的“输出”部分最好是你真正希望模型产出的完整形态包括标点、缩进、大小写。我见过有人示例里写{name: 张三}指令里却要求输出不带空格的紧凑 JSON结果模型学会的是带空格的样子。示例是“示范”不是“示意”模型会连格式细节一起抄。另外示例的数量和多样性要平衡。五条一模一样的示例等于一条。我的经验是覆盖每个输出类别至少一条再补一到两条边界样本这个配置在多数分类和抽取任务上就够用了。2.2 prefix、suffix、example_separator 的拼装顺序与冲突最终文本的拼装顺序是prefix→ 示例块每个示例之间用example_separator连接默认是两个换行→suffix。理解这个顺序才能理解为什么很多“格式跑偏”问题其实出在分隔符上。prefix放任务指令和角色设定suffix放真正要模型回答的那个输入并且要以一个“待续”的姿态结尾——比如输入: {text}\n输出:把冒号后面留空模型自然就接着往下写。这个“留白引导”的技巧比写“请回答”有效得多因为它把任务变成了续写而不是问答。example_separator默认\n\n多数情况下够用。但如果你用的示例模板本身结尾就带换行两层换行会变成三个四个空行模型有时候会理解成“段落结束、重新开始”输出里就多出一堆寒暄。我在一个项目里就是因为示例模板末尾手滑加了个\n导致模型每条回答前面都带一句“好的以下是结果”排查了半天才发现是分隔符叠加。还有一个隐藏冲突prefix和suffix里不能出现和示例字段同名的占位符否则格式化时会报KeyError或者更糟——不报错但把示例的值填进了指令里。这个在 4.1 节会细讲。2.3 input_variables 的传递链路这个参数是新手最容易懵的。FewShotPromptTemplate的input_variables指的是最终渲染时需要你提供的变量也就是 suffix 里那些花括号占位符不包括示例里的字段。示例字段由example_prompt自己消化掉了它们不对外暴露。举个例子suffix 是输入: {text}\n输出:那input_variables[text]你调用时只传format(text...)就行示例里的input、output这些 key 不需要你传。很多人第一次写的时候把示例的 key 也塞进 input_variables结果格式化直接崩。如果你嫌手写这个列表麻烦LangChain 提供了从模板自动推断的写法但我不太推荐在初学阶段用——手动写一遍能帮你把“哪些变量对外、哪些对内”这件事想清楚等熟练了再用自动推断省事。2.4 example_selector动态少样本的关键开关静态示例的局限很明显示例库一大不可能全塞进去而不同输入需要的示例又不一样。选择器就是解决这个的。常用的有三种选择器挑选依据适合场景主要参数LengthBasedExampleSelector按字符长度累加装不下就停输入长短差异大、要严格控制长度max_length、可自定义长度函数SemanticSimilarityExampleSelector向量相似度取最像的 k 条示例库大、输入主题分散k、embeddings、向量库类MaxMarginalRelevanceExampleSelector相似度 多样性折中示例容易扎堆、怕同质化k、fetch_k长度型选择器不需要向量模型零额外成本是性价比最高的起点。语义型选择器效果好但要额外跑一次 embedding首次还要建索引延迟和费用都得算进去。我一般的做法是示例库小于 20 条、任务比较单一时用长度型甚至静态示例库上百条、主题跨度大的时候才上语义型。提示语义选择器在建索引时会把所有示例向量化一遍如果示例库是运行时才加载的记得做缓存不然每次请求都重建索引延迟会难看到怀疑人生。3. 从零跑通静态到动态的两版实现3.1 环境准备与版本差异先把依赖装好。这里我不绑定具体的模型供应商任何能提供对话补全和 embedding 的接口都能接进来代码里用占位符代替你换成手头的那套即可。pip install langchain langchain-core版本这块有个必须提前说的坑LangChain 从 0.1 到 0.2 之后做了一次比较大的拆分很多类的导入路径变了。老教程里写的from langchain.prompts import FewShotPromptTemplate在新版本上会给你一堆弃用警告甚至直接报错。新路径大致是# 新版本推荐的导入方式 from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate from langchain_core.example_selectors import ( LengthBasedExampleSelector, SemanticSimilarityExampleSelector, )如果你在跑别人的代码时看到ModuleNotFoundError或者ImportError第一反应应该是查导入路径而不是怀疑自己写错了逻辑。我建议在项目里把 LangChain 版本在依赖文件里锁死这东西的小版本更新经常带破坏性改动。模型和 embedding 的初始化我就不写具体厂商了统一用两个占位变量# 换成你手头的任意对话模型与 embedding 接口 llm your_chat_model embeddings your_embedding_model3.2 第一版手写静态示例跑通整条链路先做一个最朴素的情感三分类输出只允许positive/negative/neutral三个词。这个任务小到不能再小但正好能看清模板的每个部件。from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate # 1. 单个示例怎么渲染 example_prompt PromptTemplate( input_variables[text, label], template文本: {text}\n标签: {label}, ) # 2. 示例数据纯字典 examples [ {text: 这包装拆开就坏了, label: negative}, {text: 用了一周一切都好, label: positive}, {text: 还行吧没什么特别的, label: neutral}, {text: 客服态度不错但物流太慢, label: neutral}, ] # 3. 组装 few_shot_prompt FewShotPromptTemplate( examplesexamples, example_promptexample_prompt, prefix判断下面这段文本的情感倾向只能输出 positive、negative、neutral 三者之一不要输出任何解释。, suffix文本: {text}\n标签:, input_variables[text], example_separator\n\n, ) print(few_shot_prompt.format(text东西是好东西就是贵了点))跑出来的文本长这样你可以对照着看拼装顺序判断下面这段文本的情感倾向只能输出 positive、negative、neutral 三者之一不要输出任何解释。 文本: 这包装拆开就坏了 标签: negative 文本: 用了一周一切都好 标签: positive 文本: 还行吧没什么特别的 标签: neutral 文本: 客服态度不错但物流太慢 标签: neutral 文本: 东西是好东西就是贵了点 标签:这里有几个刻意的设计值得逐条说。第一prefix里明确写了“只能输出三者之一不要解释”——这是给模型设边界不给它留发挥空间。第二suffix结尾的“标签:”后面故意留空形成续写引导。第三示例里特意放了“客服态度不错但物流太慢 → neutral”这种带转折的边界样本用来校准“混合情绪”的判定标准这比写三行说明文字管用。真正调模型的时候就是把这个字符串丢进去resp llm.invoke(few_shot_prompt.format(text东西是好东西就是贵了点)) print(resp.content)实操心得第一版跑通之后先别急着加复杂度拿二三十条真实输入测一遍把输出不符合预期的样本记下来。这些“坏样本”就是下一版示例库的第一批候选比你自己拍脑袋想示例靠谱得多。3.3 第二版接上语义相似度选择器示例库涨到几十条之后全塞进去就不现实了。这时候把examples换成选择器模板其余部分基本不用动这是这套设计最舒服的地方。from langchain_core.example_selectors import SemanticSimilarityExampleSelector example_selector SemanticSimilarityExampleSelector.from_examples( examples, # 大示例库 embeddings, # embedding 接口 vector_store_cls, # 向量库实现类 k3, # 每次挑 3 条最相似的 ) dynamic_prompt FewShotPromptTemplate( example_selectorexample_selector, # 注意有了选择器就不要传 examples example_promptexample_prompt, prefix判断下面这段文本的情感倾向只能输出 positive、negative、neutral 三者之一不要输出任何解释。, suffix文本: {text}\n标签:, input_variables[text], )有个硬性规则必须记牢examples和example_selector二选一不能同时传。同时传会直接报错这算是框架给的善意提醒。很多人是从静态版改过来的只加了一行没删原来那行跑起来就懵了。k的取值我一般从 3 起调。k1 时最省但风险是匹配到的那条示例恰好是个边界样本会把模型带偏k 太大又回到成本问题。3 到 5 在多数任务上是平衡点。判断方法很简单固定一批测试输入k 分别取 1/3/5看准确率和平均 token 消耗的曲线拐点就是你的答案。语义选择器还有个细节默认情况下它会用示例字典里的所有字段去算相似度包括那个label字段。这意味着“negative”这个词本身也参与了向量匹配可能引入噪声。规范做法是只拿输入字段去算example_selector SemanticSimilarityExampleSelector.from_examples( examples, embeddings, vector_store_cls, k3, input_keys[text], # 只用 text 字段做相似度匹配 )这一行加上之后我在一个主题跨度很大的示例库上看到召回质量明显变好因为不再有“标签词”干扰。3.4 长度控制型选择器不花钱的那条路如果你的场景对延迟敏感、或者不想为了选示例再跑一次 embedding长度型选择器是个被低估的方案。它按顺序往 prompt 里塞示例累加长度超过阈值就停逻辑简单到没有黑箱。from langchain_core.example_selectors import LengthBasedExampleSelector length_selector LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length200, # 示例块的总长度上限 ) short_prompt FewShotPromptTemplate( example_selectorlength_selector, example_promptexample_prompt, prefix判断情感倾向只输出 positive、negative、neutral。, suffix文本: {text}\n标签:, input_variables[text], )max_length的算法是把每个渲染后的示例长度累加超过就截断。这里有个容易被忽略的点它默认按字符数算不是按 token 算。中文场景下字符和 token 大概是一比一到一比一点五的关系所以实际 token 会比你按字符估算的多一些。如果你的示例有长有短、混着中英文最好传一个自定义的长度计算函数改成调 tokenizer 算真实 token不然预估会偏。它的短板也很明显挑选逻辑是“按顺序装”和输入内容无关。示例库如果本身是按类别聚在一起的就会出现“前面全被负面示例占满”的情况。补救办法是在构造示例库时打散顺序或者干脆做一个简单的轮转。3.5 参数调优与 token 估算写到这里得聊聊成本因为很多人做 few-shot 的时候只盯效果不看账。粗略估算方法一段中文示例渲染后大概 20 到 40 个字加上分隔符五条示例的示例块在 150 到 250 字之间prefix 加 suffix 大概 50 到 100 字用户输入按 100 字算。整体输入在 300 到 450 字换算成 token 大概 300 到 600。这个量级对多数接口来说很轻但如果你的示例是长文本比如带完整 JSON 的抽取示例一条就可能几百 token五条下去直接破两千成本和延迟都会翻倍。我自己的调参顺序是这样的先把示例数量从 8 往下砍测 2/3/5/8 四档的准确率如果 3 条和 8 条差不多就用 3 条。然后在这个数量上调整示例的内容分布补边界样本、删重复样本。最后才去动k或者选择器策略。顺序很重要——很多效果问题其实不是选择器不够聪明而是示例库里根本没有覆盖那类输入。还有一个实用技巧把prefix里的指令压缩到极限。我见过 prefix 写了三百字的“角色设定 注意事项 输出要求”其实核心就两句任务是什么、输出长什么样。剩下的说明完全可以靠示例传达示例本身就是最好的说明书文字指令写多了反而稀释了示例的示范作用。4. 常见问题与排查实录4.1 变量名撞车导致的 KeyError这是出现频率最高的一类报错表现形式是format()的时候抛KeyError: xxx。原因通常是 prefix 或 suffix 里出现了示例字典里同名的字段。比如示例的 key 叫input而你的 suffix 写成输入: {input}——格式化时框架会拿示例的字段去填 suffix 的占位符一旦某条示例没有这个 key就崩了。排查手法很土但有效把format()的输出整个打印出来看哪些位置是空的、哪些位置填错了值。我一般会养成一个习惯示例字段和待填变量用不同的命名前缀比如示例里用src/tgt对外变量用user_text从命名上就避开撞车的可能。这个习惯省下的调试时间比多写几个字符的成本高得多。另一个变体是input_variables写漏了。表现是format()报缺少参数但你明明传了。检查方法把 suffix 字符串单独拿出来数一遍有几个花括号占位符和input_variables的列表一一对上。4.2 选择器没生效、或者选得莫名其妙“我明明加了选择器怎么感觉挑的示例还是不对”这种情况我遇到过三次原因各不相同。第一次是k设太大等于没筛。k8 而示例库只有 10 条那基本就是全塞进去了选择器形同虚设。第二次是忘了配input_keys相似度被标签字段污染导致“文本完全不像但标签词相同”的示例被排到前面。表现是模型的输出标签分布严重偏斜某一种标签出现得异常频繁。第三次最隐蔽示例库本身质量差。选择器只是把最相似的挑出来如果库里全是长句示例而用户输入是短句怎么挑都不对。这时候得回头补示例而不是换选择器。排查顺序建议固定成先打印选择器实际选中的那几条多数选择器有select_examples这类方法可以直接喂一个输入看结果确认挑选逻辑符合直觉再看召回内容是否真的相似最后才怀疑向量模型。4.3 输出格式依然跑偏示例给了、指令也写了“不要解释”模型还是给你来一句“好的这段文本的情感是 positive”。这种“礼貌性前缀”特别顽固。我试过几种办法按有效性排序最有效的是在示例的“输出”位置强化留白也就是 suffix 结尾用标签:而不是标签请回答让任务变成续写。其次是在示例里放一个“错误示范-正确示范”的对比但这会显著增加长度慎用。最后才是在 prefix 里加重语气比如“只输出一个词”“不要任何前缀和后缀”效果有但不如前两种稳。如果模型还是加戏可以在后处理层做一次清洗正则截取第一个符合格式的片段。但这是兜底不是解法长期靠后处理说明提示词没写到位。我一般会把后处理当成监控手段统计触发清洗的比例比例高就说明提示词该改了。4.4 成本、延迟与缓存的取舍上了选择器之后一次请求实际发生的事是embedding 一次用户输入 → 向量检索 → 拼 prompt → 调模型。链路从一跳变三跳延迟自然涨。如果示例库每次请求都重建索引那延迟会难看到没法上线。我的处理办法是示例库在服务启动时构建一次索引之后复用用户输入的 embedding 结果按输入哈希做一层短时缓存重复输入直接命中。这两步做完增量延迟基本可以忽略。另外如果k设为 3 已经够用就别设 5省下的不只是 token还有检索时间。注意示例库不是静态资产。业务在变示例也得跟着更新。我见过一个项目上线半年没动过示例库结果新出现的输入类型全被映射到旧标签上准确率悄悄掉了十几个点还没人发现。建议至少每月抽样复查一次。4.5 常见问题速查表现象大概率原因处理动作KeyError: xxxprefix/suffix 占位符与示例字段重名打印 format 结果重命名示例字段缺少参数报错input_variables漏写数一遍 suffix 里的花括号数量同时传 examples 和 selector 报错二选一规则删掉旧的那行选中的示例不相关未配input_keys标签词污染相似度指定只用输入字段匹配选择器形同虚设k接近示例库总数把 k 降到 3 到 5输出带礼貌前缀suffix 结尾没有形成续写引导改成标签:留空别写“请回答”输出格式细节不对示例本身的格式就不规范示例输出按最终形态逐字符写延迟明显变高每次请求重建向量索引启动时建索引 输入缓存效果随业务下滑示例库长期未更新每月抽样复查补新类型样本5. 几个长期维护上的经验5.1 把示例库当代码资产来管一开始我也觉得示例就是几行文本随手写在脚本里就完事。项目稍微大一点之后就会后悔示例散落在各个文件、版本对不上、改了一条不知道影响哪些任务。后来我改成用一个独立的配置文件或者数据文件管示例按任务分组每条示例带一个自增的标识和一段备注说明这条示例是为了校准什么边界模板代码只负责读和渲染。这个做法带来的最大好处是可回溯。当模型某天突然开始把某类输入判错你可以翻记录看是不是前一天有人动过示例。没有记录的时候这基本是黑箱排查。另外示例最好也进版本管理和代码一起走 review 流程——听起来夸张但一条表述含糊的示例造成的线上偏差修起来比一个函数 bug 更费劲因为它不会报错。5.2 用评估集代替感觉“感觉效果还行”是提示工程里最危险的一句话。我现在的习惯是任何一次示例改动之前先固定一个三十到五十条的小评估集覆盖每个标签、包含若干边界样本改动前后各跑一遍看准确率和格式合规率两个指标。样本量不用大但这个对比能挡掉绝大部分“越改越差还不自知”的情况。格式合规率这个指标特别值得单独统计因为它比准确率更敏感。示例格式稍微松一点合规率立刻掉而准确率可能看不出变化——但下游解析会直接崩。我一般把“输出能否被下游无损解析”作为硬门槛过不了这个门槛准确率再高也不上线。至于后续还能怎么扩我觉得有两个方向值得试一是把示例库和评估集联动起来把评估集里反复出错的样本自动回流成候选示例形成一个闭环二是把选择器从“相似度”升级成“相似度 覆盖度”既保证和当前输入相关又保证挑出来的几条示例在标签上分布均衡避免三条示例全是同一个类别。这两个方向我都在小范围试过效果有但还没到能直接抄的程度等跑稳了再整理。
阅读完成 · 觉得有帮助?
咨询建站