什么是少样本提示

少样本提⽰是⼀种通过向LLM提供少量具体⽰例或样本,来教会它如何执⾏某项特定任务的技术。提⾼模型性能的最有效⽅法之⼀是给出⼀个【模型⽰例】指导⼤模型你想做什么、怎么做。

就实际应用来说,可以帮小学生解决定义新运算类型的问题,不使用少样本提示的代码如下:

1
2
3
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
print(model.invoke("表达式 4 🤓 3是多少?").content)
1
表达式 "4 🤓 3" 中的符号 "🤓" 并不是一个标准的数学运算符。如果你能提供更多的上下文,例如这个符号的具体意义或者想要进行的具体计算,我将更好地帮助你解决这个问题。

少样本提⽰通俗来讲就是在给出考题前,先给它看⼏道类似的、附有正确答案的例题。添加⽰例输⼊和预期输出的技术给到模型提⽰,让模型通过例题来理解任务应该怎么做。

比如我们补充几个用例

1
2
3
4
5
6
7
8
9
10
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
model = ChatOpenAI(model="gpt-4o-mini")

messages = [
HumanMessage("已知 2 🤓 3 = 222"),
HumanMessage("已知 3 🤓 2 = 33"),
HumanMessage("表达式 4 🤓 3是多少?")
]
print(model.invoke(messages).content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
根据你提供的例子,似乎“🤓”这个符号代表了一种特定的运算规则,而不是通常的数学运算。

从你给的例子来看:
1. 2 🤓 3 = 222
2. 3 🤓 2 = 33

我们可以推测出这个运算符号可能是将两个数按照某种方式结合在一起。通过观察,我们可以发现:

- 在第一个例子中,2 和 3 拼接成为了 "2" + "2" + "2" = "222"。
- 在第二个例子中,3 和 2 拼接成为了 "3" + "3" = "33"。

因此,按照相同的规则,我们可以进行 4 🤓 3 的运算:
- 4 和 3 可以拼接为 "4" + "4" + "4" = "444"。

所以,4 🤓 3 = 444。

可以看到它经过一定的推理得出了答案。

为什么使用少样本提示

实际上有些抽象的概念或者输出格式的要求,比起费尽心思地写长篇幅的提示词或者描述,还不如直接给出一些例子让LLM去学习和参考,还可以有效防止LLM胡说八道,输出一些意料之外的内容或者格式

而且从上面的例子也能看出,LLM还会输出思考过程,让我们可以进一步地检查LLM是否在思考的哪一环节出错过

进一步改进

但是,在我们实现少样本提示的过程中,这样子手动写还是太低效了,有没有办法更高效地采用从数据结构转换成提示词的方法呢?我们接下来介绍这种方法,首先我们先放一个存储样例的数据结构

1
2
3
4
examples = [
{"input":"2 🤓 3","output":"222"},
{"input": "3 🤓 2", "output": "33"},
]

实现少样本提示

实际上我们前面准备好的数据结构就是实现的第一步,即准备好少样本数据集,然后我们就可以用数据集构建出示例集了,langchain提供了FewShotChatMessagePromptTemplate来构建示例集,同时我们还要用到先前学到的提示词模板,提供把数据集变成提示词的蓝本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain_openai import ChatOpenAI
from langchain_core.prompts import FewShotChatMessagePromptTemplate,ChatPromptTemplate
model = ChatOpenAI(model="gpt-4o-mini")

# 1.准备数据集
examples = [
{"input":"2 🤓 3","output":"222"},
{"input": "3 🤓 2", "output": "33"},
]

# 准备提示词模板
prompt_template = ChatPromptTemplate(
[
("human", "{input}"),
("ai", "{output}")
]
)

few_shot_promt = FewShotChatMessagePromptTemplate(
example_prompt=prompt_template,
examples=examples
)
print(few_shot_promt.invoke({}).to_messages())

然后我们输出一下看看内容

1
[HumanMessage(content='2 🤓 3', additional_kwargs={}, response_metadata={}), AIMessage(content='222', additional_kwargs={}, response_metadata={}), HumanMessage(content='3 🤓 2', additional_kwargs={}, response_metadata={}), AIMessage(content='33', additional_kwargs={}, response_metadata={})]

可以看到已经成功实例化为提示词消息列表了。如果想要获取的是字符串而不是消息列表的话,则只需把to_messages()换成to_string()

获取到少样本提示的消息列表后,我们来利用提示词模板组装一下完整的消息列表,组装方式则是把少样本提示的示例集直接嵌套进模板,发起请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from langchain_openai import ChatOpenAI
from langchain_core.prompts import FewShotChatMessagePromptTemplate,ChatPromptTemplate
model = ChatOpenAI(model="gpt-4o-mini")

# 1.准备数据集
examples = [
{"input":"2 🤓 3","output":"222"},
{"input": "3 🤓 2", "output": "33"},
]

# 准备提示词模板
prompt_template = ChatPromptTemplate(
[
("human", "{input}"),
("ai", "{output}")
]
)

few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=prompt_template,
examples=examples
)

final_template = ChatPromptTemplate(
[
("system","你是一名中国的小学数学老师,擅于定义新运算题目,并且每句话前面都会加个小朋友这样的亲切称呼"),
few_shot_prompt,
("human","{input}")
]
)

chain = final_template | model
print(chain.invoke({"input":"9 🤓 2 是多少?"}).content)

输出如下:

1
小朋友,9 🤓 2 的结果是 99!如果你有其他问题,随时可以问我哦!

示例选择器

随着我们各种各样的示例增加,我们必须有选择地为LLM选择示例,否则将会有极大的性能浪费!甚至超过某个阈值,太多⽰例可能会开始混淆模型

在LangChain中,⽰例选择器就可以帮我们从⼀组【⽰例的集合】中根据具体策略选择正确的【⽰例⼦集】构建少样本提⽰

而选择的策略有:

  • Length :根据特定【⻓度】内可以容纳的数量选择⽰例。
  • Similarity :使⽤输⼊和⽰例之间的【语义相似性】来决定选择哪些⽰例。
  • MMR :使⽤输⼊和⽰例之间的【最⼤边际相关性】来决定要选择哪些⽰例。
  • Ngram:使⽤输⼊和⽰例之间的【ngram重叠】来决定要选择哪些⽰例。

这些其实都是⾃然语⾔处理(NLP)⾥的相似性衡量问题。

接下来我们一一介绍这些选择策略

Length按长度选择

该策略保证示例+输入不会超过窗口大小,也就是说较大的输入时,选取较少的示例,反之则是选取较多的示例

实现该策略的组件是langchain_core.example_selectors.length_based.LengthBasedExampleSelector

其参数如下:

  • **example_prompt**:PromptTemplate,用于格式化示例的提示模板
  • **examples**:模板所需的示例列表
  • **max_length**:提示的最大长度,超过该长度将剪切示例
  • **get_text_length**:测量提示长度的方法,默认为字数统计
  • **add_example(example: dict[str, str])**:将新示例添加到列表中
    • 输入:一个字典,其中键作为输入变量,值作为其值
  • **select_examples(input_variables: dict[str, str]) → list[dict]**:根据输入长度选择要使用的示例
    • 输入:一个字典,其中键作为输入变量,值作为其值
    • 输出:要包含在提示中的示例列表

接下来我们用一组反义词示例来演示长度选择器,总共分为以下几步

  1. 给⼀个⽰例集,输⼊和输出互为反义词
  2. 定义PromptTemplate 字符串模板,包含输⼊和输出两个”占位符”
  3. 定义LengthBasedExampleSelector ⻓度⽰例选择器,设置初始⽰例集与最⼤⻓度
  4. 定义⼀个FewShotPromptTemplate 模板对象,⽤于实例化⽰例,将⽰例转化为聊天消息
  5. 先不调用模型,先打印实例化出来的消息列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from langchain_core.example_selectors.length_based import LengthBasedExampleSelector
from langchain_core.prompts import FewShotChatMessagePromptTemplate,PromptTemplate

examples = [
{"input": "happy", "output": "unhappy"},
{"input": "big", "output": "small"},
{"input": "fast", "output": "slow"},
{"input": "light", "output": "dark"},
{"input": "strong", "output": "weak"},
{"input": "safe", "output": "dangerous"},
]

example_prompt = PromptTemplate(
input_variables=["input","output"],
template="Input:{input} ---> Output:{output}",
)

# 长度示例选择器
length_selector = LengthBasedExampleSelector(
examples=examples,
example_prompt=example_prompt,
max_length=25,
)

# 用于实例化少样本提示的模板
few_shot_template = FewShotChatMessagePromptTemplate(
example_selector=length_selector,
example_prompt=example_prompt,
prefix="给出每个输入反义词",
suffix="Input:{adjective} ---> Output:",
input_variables=["adjective"]
)

print(few_shot_template.invoke({"adjective":"up"}).to_messages()[0].content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
给出每个输入反义词

Input:happy ---> Output:unhappy

Input:big ---> Output:small

Input:fast ---> Output:slow

Input:light ---> Output:dark

Input:strong ---> Output:weak

Input:safe ---> Output:dangerous

Input:up ---> Output:

可以看到它还是输出了所有示例,这是因为我们输入的长度25在默认情况下指的是分割出的单词的数量,显然目前还没到25个单词,但是如果我们输入了超级长多的单词会怎么样呢,会不会像示意图里的那样切割示例呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
long_usr_input = {"adjective":"非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 长的输入"}
print(few_shot_template.invoke(long_usr_input).to_messages()[0].content)

# 输出如下
给出每个输入反义词

Input:happy ---> Output:unhappy

Input:big ---> Output:small

Input:fast ---> Output:slow

Input:非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 非常 长的输入 ---> Output:

可以看到示例明显少了很多

Similarity按语义相似性选择⽰例

什么是语义相似?它是衡量⽂本在【含义上】的接近程度。例如下述两段⽂本:

1
2
text1 = "我喜欢西瓜"
text2 = "我讨厌草莓"

这两段⽂本表⾯文本相似度低,但语义上都是表达对水果的好恶态度。

我们再看一段有文本相似性但语义相似性很少的描述

1
2
text1 = 小米煮粥很好喝
text2 = 小米汽车创下营销大饼

很显然语义上此小米非彼小米

LangChain 能根据输⼊和⽰例之间的语义相似性来决定选择哪些⽰例,而计算机是怎么查找语义相似性的呢?没错,正是通过嵌入模型将文本转为可以表示语义的向量,它通过查找与输⼊具有最⼤余弦相似性的嵌⼊模型⽰例来实现这⼀点

同样的,它有一个实现该策略的组件,即langchain_core.example_selectors.semantic_similarity.SemanticSimilarityExampleSelector

其中我们要用到的嵌入式模型,则是来自langchain_community.embeddings.HuggingFaceEmbeddings,这是一个免费的本地模型

至于向量存储数据库,我们就不去使用那些大型数据库了,我们使用langchain_chroma.Chroma

我们来看看选择器的接口

  • **from_examples()**:根据示例集生成语义相似示例选择器
    • 输入:
      • **examples**:示例列表
      • **embeddings**:初始化的嵌入 API 接口,如 OpenAIEmbeddings()或者免费的HuggingFaceEmbeddings
      • **vectorstore_cls**:向量存储数据库接口类
      • **k**:最终要选择的示例数量,默认值为 4
    • 输出:语义相似性示例选择器
  • **add_example(example: dict[str, str])**:将新示例添加到列表中
    • 输入:一个字典,其中键作为输入变量,值作为其值
  • **select_examples(input_variables: dict[str, str]) → list[dict]**:根据输入选择要使用的示例
    • 输入:一个字典,其中键作为输入变量,值作为其值
    • 输出:要包含在提示中的示例列表

我们来写个示例代码,看看我们输入一个情绪相关的词,并限定最多输出两个示例会怎么样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from langchain_core.example_selectors.semantic_similarity import SemanticSimilarityExampleSelector
from langchain_core.prompts import FewShotPromptTemplate,PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings

examples = [
{"input": "happy", "output": "sad"},
{"input": "big", "output": "small"},
{"input": "fast", "output": "slow"},
{"input": "light", "output": "dark"},
{"input": "strong", "output": "weak"},
{"input": "safe", "output": "dangerous"},
]

example_prompt = PromptTemplate(
input_variables=["input","output"],
template="Input:{input} ---> Output:{output}",
)

# 语义相似度示例选择器
similarity_selector = SemanticSimilarityExampleSelector.from_examples(
examples=examples,
embeddings=HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2"),
vectorstore_cls=Chroma,
k=2,
)

# 用于实例化少样本提示的模板
few_shot_template = FewShotPromptTemplate(
example_selector=similarity_selector,
example_prompt=example_prompt,
prefix="给出每个输入反义词",
suffix="Input:{adjective} ---> Output:",
input_variables=["adjective"]
)

usr_input = {"adjective":"worried"}
print(few_shot_template.invoke(usr_input).to_messages()[0].content)

输出如下

1
2
3
4
5
6
7
8
9
给出每个输入反义词

Input:safe ---> Output:dangerous

Input:happy ---> Output:sad

Input:worried ---> Output:

Process finished with exit code 0

可以看到有一个示例是明显与情绪相关,另一个也是剩下的里面比较有关系的了

MMR最大边际相关性

如果我们把两个点之间的距离表示成语义相关性,把不同的方向表示成语意相关的角度,那么多个语意相关的点围起来的边长就可以比作NNG中所谓的边际,那么理解最大边际为什么指的是,使⽤语义相似性作为基础⼯具,从⼀个候选集中挑选出⼀组既能代表查询主题⼜彼此多样化的结果就很容易了

我们来从应用领域进一步理解MMR和语意相关性的关系

  • 语义相似性使⽤场景:搜索引擎的基础排序、重复检测、聚类、语义搜索。
  • MMR使⽤场景:
    • 推荐系统:推荐与⽤⼾兴趣相关但⼜不同类型的物品,避免“信息茧房”。
    • ⽂档摘要:从⻓⽂档中选择能代表主旨⼜包含不同信息的句⼦,避免摘要内容重复。
    • RAG(检索增强⽣成):在从知识库检索完⼀堆相关⽂档后,使⽤MMR进⾏去重多样化筛选,再交给LLM⽣成答案,能有效提升答案质量和减少幻觉

MMR与语意相关性有很大关系,因此它的实现组建也来自同样的地方,名字则是MaxMarginalRelevanceExampleSelector

我们来看看接口参数

  • **from_examples()**:根据示例集生成语义相似示例选择器
    • 输入:
      • **examples**:示例列表
      • **embeddings**:初始化的嵌入 API 接口,如 OpenAIEmbeddings()或者免费的HuggingFaceEmbeddings
      • **vectorstore_cls**:向量存储数据库接口类
      • **k**:最终要选择的示例数量,默认值为 4
    • 输出:MMR示例选择器
  • **add_example(example: dict[str, str])**:将新示例添加到列表中
    • 输入:一个字典,其中键作为输入变量,值作为其值
  • **select_examples(input_variables: dict[str, str]) → list[dict]**:根据输入选择要使用的示例
    • 输入:一个字典,其中键作为输入变量,值作为其值
    • 输出:要包含在提示中的示例列表

示例代码就不放了,就是把前面的语意相关性选择器的类名称换了一下

ngram 重叠示例选择器

ngram:指⼀个⽂本序列中连续的n个词(word)或字符(character)

ngram重叠:通过计算它们之间共同拥有的ngram数量来⼀种衡量两段⽂本相似度的⽅法

如下述两段⽂本:

1
2
text1 = "苹果⼿机很好⽤" (分词后:苹果 ⼿机 很 好⽤)
text2 = "这款⼿机很好⽤" (分词后:这款 ⼿机 很 好⽤)

这两段⽂本单词重复度很⾼,连续三个词的相同的情况也存在,因此ngram重叠⾼。

再看个例⼦:

1
2
text1 = 苹果⼿机很好⽤" (分词后:苹果 ⼿机 很 好⽤)
text2 = "iPhone ⾮常不错" (分词后: iPhone ⾮常 不错 )

这两段⽂本在含义上⾮常相似,但它们的ngram重叠度为0。

因此,传统ngram重叠是⼀种表⾯形式的匹配。它只关⼼词是否完全⼀样,但对于同义词却⽆法处理

为了解决此问题,我们引入语义ngram重叠,我们比较的是词语背后的语义向量重叠性。语义ngram重叠常⽤于需要更精准语义评估的场景,例如剽窃检测,能够发现那些改换了词汇但保留了核⼼思想的“智能”剽窃。

具体的实现组件来自from langchain_community.example_selectors.ngram_overlap import NGramOverlapExampleSelector

  • **example_prompt**:PromptTemplate,用于格式化示例的提示模板

  • **examples**:模板所需的示例列表

  • **threshold**:算法停止的阈值,默认值为 -1.0

    • 对于负阈值:按「重叠分数」对示例排序,但不排除任何示例
    • 对于等于 0.0 的阈值:按「重叠分数」排序,并排除与输入没有 ngram 重叠的示例
    • 对于大于 1.0 的阈值:排除所有示例,并返回空列表
  • **add_example(example: dict[str, str])**:将新示例添加到列表中

    • 输入:一个字典,其中键作为输入变量,值作为其值
  • **select_examples(input_variables: dict[str, str]) → list[dict]**:返回根据输入得到的重叠分数排序后的示例列表

    • 输入:一个字典,其中键作为输入变量,值作为其值
    • 输出:要包含在提示中的示例列表

接下来我们写个示例代码让它选取翻译文本的示例,特别的,这次测试的是字面重叠而不是语义重叠,这次操作会有所不同

  1. 给定翻译相关的示例集
  2. 定义提示词模板
  3. 定义NGramOverlapExampleSelector ⽰例选择器,设置初始⽰例集与阈值(-1),表⽰对⽰例进⾏排序,但不排除任何⽰例。
  4. 定义⼀个FewShotPromptTemplate 模板对象,⽤于实例化⽰例,将⽰例转化为聊天消息
  5. 打印消息排序结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ---------------NGRAM选择器
from langchain_community.example_selectors.ngram_overlap import NGramOverlapExampleSelector
from langchain_core.prompts import FewShotPromptTemplate,PromptTemplate

examples = [
{"input": "I see Supdriver flying", "output": "我看到了Supdriver在飞"},
{"input": "My dog barks", "output": "我的狗叫"},
{"input": "Supdriver can fly", "output": "Supdriver会飞"},
]

example_prompt = PromptTemplate(
input_variables=["input","output"],
template="Input:{input} ---> Output:{output}",
)

# 语义相似度示例选择器
mmr_selector = NGramOverlapExampleSelector(
examples=examples,
example_prompt=example_prompt,
threhold=-1
)

# 用于实例化少样本提示的模板
few_shot_template = FewShotPromptTemplate(
example_selector=mmr_selector,
example_prompt=example_prompt,
prefix="给出每个输入的中文翻译",
suffix="Input:{raw_text} ---> Output:",
input_variables=["raw_text"]
)

usr_input = {"raw_text":"Supdriver can fly high"}
print(few_shot_template.invoke(usr_input).to_messages()[0].content)
1
2
3
4
5
6
7
8
9
给出每个输入的中文翻译

Input:Supdriver can fly ---> Output:Supdriver会飞

Input:I see Supdriver flying ---> Output:我看到了Supdriver在飞

Input:My dog barks ---> Output:我的狗叫

Input:Supdriver can fly high ---> Output: