纲要
- 静态示例与动态示例
- 静态示例:示例固定在模板中,无法随任务或上下文长度调整
- 动态示例:根据任务需求和上下文窗口动态选择最合适的示例子集
- 示例选择器核心概念
LengthBasedExampleSelector:基于长度动态选择示例,控制最终提示词的总长度- 工作原理:根据
max_length和已占用的提示词长度,自动截取示例列表
- 关键组件
examples:示例列表(输入-输出对)example_prompt:示例格式化模板FewShotPromptTemplate:组合前缀、示例和后缀的完整提示词模板
- 示例:Python 脚本,展示长短输入下示例数量的自适应变化
引言
在上一篇关于 Few Shot 提示词工程的文章中,我们介绍了如何通过静态示例让模型学会自定义规则。然而在实际应用中,大模型的上下文窗口是有限且宝贵的资源。如果示例过多或输入过长,就可能超出窗口限制,甚至引入与当前任务无关的噪声。
LangChain 提供了示例选择器(Example Selector)来解决这一问题,其中 LengthBasedExampleSelector 是最简单直接的一种:它根据最终提示词的长度,动态决定使用哪些示例以及使用多少示例,确保提示词始终控制在合理长度内。
静态示例 vs 动态示例
在传统 Few Shot 中,示例是"写死"在模板里的:无论输入什么任务,所有示例都会被完整地拼接到提示词中。这种方式存在两个问题:
- 浪费上下文窗口:大量示例可能占用过多 token,挤压实际任务的空间。
- 引入无关信息:示例组中包含与当前任务不相关的例子,反而干扰模型输出。
动态示例选择器则允许我们准备一个大而全的示例池,在运行时根据输入长度和上下文窗口,只挑选合适的示例子集注入提示词。
| 特性 | 静态示例 | 动态示例 |
|---|---|---|
| 示例调整 | 不可变 | 根据输入长度动态截取 |
| 上下文利用 | 可能浪费或溢出 | 精确控制在窗口内 |
| 实现复杂度 | 低 | 中(使用示例选择器) |
| 适用场景 | 示例少且固定 | 示例多、输入长度变化大 |
根据长度动态选择示例的工作原理
LengthBasedExampleSelector 的核心逻辑是:在格式化提示词时,先计算前缀(系统指令)和后缀(用户问题)的长度,然后用剩余的 max_length 空间,从示例列表中从头开始依次添加示例,直到下一个示例会导致总长度超出限制为止。
这样,较长的输入会自动压缩示例数量,避免超出上下文窗口。
其处理流程可用下图表示:
#mermaid-svg-mqLU5ogBegc36RaW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mqLU5ogBegc36RaW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mqLU5ogBegc36RaW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mqLU5ogBegc36RaW .error-icon{fill:#552222;}#mermaid-svg-mqLU5ogBegc36RaW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mqLU5ogBegc36RaW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mqLU5ogBegc36RaW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mqLU5ogBegc36RaW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mqLU5ogBegc36RaW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mqLU5ogBegc36RaW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mqLU5ogBegc36RaW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mqLU5ogBegc36RaW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mqLU5ogBegc36RaW .marker.cross{stroke:#333333;}#mermaid-svg-mqLU5ogBegc36RaW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mqLU5ogBegc36RaW p{margin:0;}#mermaid-svg-mqLU5ogBegc36RaW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mqLU5ogBegc36RaW .cluster-label text{fill:#333;}#mermaid-svg-mqLU5ogBegc36RaW .cluster-label span{color:#333;}#mermaid-svg-mqLU5ogBegc36RaW .cluster-label span p{background-color:transparent;}#mermaid-svg-mqLU5ogBegc36RaW .label text,#mermaid-svg-mqLU5ogBegc36RaW span{fill:#333;color:#333;}#mermaid-svg-mqLU5ogBegc36RaW .node rect,#mermaid-svg-mqLU5ogBegc36RaW .node circle,#mermaid-svg-mqLU5ogBegc36RaW .node ellipse,#mermaid-svg-mqLU5ogBegc36RaW .node polygon,#mermaid-svg-mqLU5ogBegc36RaW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mqLU5ogBegc36RaW .rough-node .label text,#mermaid-svg-mqLU5ogBegc36RaW .node .label text,#mermaid-svg-mqLU5ogBegc36RaW .image-shape .label,#mermaid-svg-mqLU5ogBegc36RaW .icon-shape .label{text-anchor:middle;}#mermaid-svg-mqLU5ogBegc36RaW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mqLU5ogBegc36RaW .rough-node .label,#mermaid-svg-mqLU5ogBegc36RaW .node .label,#mermaid-svg-mqLU5ogBegc36RaW .image-shape .label,#mermaid-svg-mqLU5ogBegc36RaW .icon-shape .label{text-align:center;}#mermaid-svg-mqLU5ogBegc36RaW .node.clickable{cursor:pointer;}#mermaid-svg-mqLU5ogBegc36RaW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mqLU5ogBegc36RaW .arrowheadPath{fill:#333333;}#mermaid-svg-mqLU5ogBegc36RaW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mqLU5ogBegc36RaW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mqLU5ogBegc36RaW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mqLU5ogBegc36RaW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mqLU5ogBegc36RaW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mqLU5ogBegc36RaW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mqLU5ogBegc36RaW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mqLU5ogBegc36RaW .cluster text{fill:#333;}#mermaid-svg-mqLU5ogBegc36RaW .cluster span{color:#333;}#mermaid-svg-mqLU5ogBegc36RaW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mqLU5ogBegc36RaW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mqLU5ogBegc36RaW rect.text{fill:none;stroke-width:0;}#mermaid-svg-mqLU5ogBegc36RaW .icon-shape,#mermaid-svg-mqLU5ogBegc36RaW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mqLU5ogBegc36RaW .icon-shape p,#mermaid-svg-mqLU5ogBegc36RaW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mqLU5ogBegc36RaW .icon-shape .label rect,#mermaid-svg-mqLU5ogBegc36RaW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mqLU5ogBegc36RaW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mqLU5ogBegc36RaW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mqLU5ogBegc36RaW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
准备完整示例列表
计算前缀与后缀长度
剩余长度 = max_length - 前缀长度 - 后缀长度
从示例列表头部开始逐个添加
添加后总长度 <= max_length?
保留该示例
跳过该示例及后续所有示例
继续下一示例
输出最终选择的示例子集
由于示例是按顺序添加的,因此将最重要、最通用的示例放在列表前面是一个最佳实践,可以保证它们在大部分情况下都会被选中。
示例代码
以下代码基于 LangChain 实现了一个反义词示例的 LengthBasedExampleSelector,并演示了输入较短和较长时,最终提示词中示例数量的动态变化。
首先安装依赖:
bash
pip install langchain langchain-core
完整代码:
python
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from langchain_core.example_selectors import LengthBasedExampleSelector
# 1. 准备反义词示例池
examples = [
{"input": "happy", "output": "sad"},
{"input": "高兴", "output": "悲伤"},
{"input": "sunny", "output": "gloomy"},
{"input": "up", "output": "down"},
{"input": "大", "output": "小"},
]
# 2. 定义示例格式化模板
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="原词: {input}\n反义词: {output}"
)
# 3. 创建长度选择器
# max_length 控制整个提示词的最大字符数(用于演示,设得较小)
example_selector = LengthBasedExampleSelector(
examples=examples,
example_prompt=example_prompt,
max_length=100, # 限制最终提示词不超过100个字符
)
# 4. 构建动态 Few Shot 提示词模板
dynamic_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="给出每个输入词的反义词:",
suffix="原词: {input}\n反义词:",
input_variables=["input"],
)
# 5. 测试:输入较短时,可以容纳更多示例
short_input = "hot"
print("=== 短输入(hot)===")
print(dynamic_prompt.format(input=short_input))
print()
# 6. 测试:输入非常长时,示例会被自动压缩
long_input = "非常" * 30 # 构造一个很长的输入
print("=== 长输入(" + "非常"*30 + ")===")
print(dynamic_prompt.format(input=long_input))
运行后会观察到,短输入时,最终提示词中包含了多个示例(如 happy、高兴、sunny 等);而长输入时,由于用户问题本身就占用了大量长度,示例会被压缩到只剩一两个甚至完全没有,确保总长度不超过 max_length。
使用建议与注意事项
- 示例排序很重要:选择器从前往后挑选示例,因此将典型、通用的示例排在前面,能保证在截断时优先保留高质量示例。
- 合理设置
max_length:该值应略小于模型的实际上下文窗口长度(以字符或 token 计),为回答预留一定空间。若使用 token 分词器,可以自定义get_text_length函数以 token 数计算长度。 - 配合其他选择器 :当仅按长度选择不够智能时,LangChain 还提供了
SemanticSimilarityExampleSelector等基于语义相似度的选择器,可以实现"选择与输入最相关的示例",后续文章将详细介绍。
总结
动态示例选择器是 Few Shot 提示工程走向实用化的关键一环。LengthBasedExampleSelector 以其简单高效的机制,保证了提示词长度始终处于可控范围内,非常适合输入长度波动较大或示例池丰富的场景。
结合良好的示例排序策略,可以在不增加额外计算开销的前提下,显著提升大模型应用的稳定性和响应质量。