欢迎光临
我们一直在努力

【LangChain实战】LangChain 学习笔记(三):提示词模板与示例选择器

👋 欢迎阅读

🏠个人主页:愿旖旎 📘专栏传送门:算法专栏 💻当前学习内容:LangChain

📑 目录

  • 一、前置讲解

  • 二、提示词模板

  • 三、示例选择器(Example Selectors)

  • 四、复盘(附答案)


一、前置讲解

在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。

🧠 前置知识一:PromptTemplate

字符串提示词模板,用占位符动态生成提示词。

🗨️ 前置知识二:ChatPromptTemplate

聊天消息模板,可组合 System/User/AI 消息。

📍 前置知识三:MessagesPlaceholder

消息占位符,在指定位置插入一整段对话历史。

🔤 前置知识四:少样本提示(Few-shot)

给模型看几个示例再提问,比写长篇指令更有效。

🎯 前置知识五:示例选择器

从大量示例中按策略挑最合适的几个:长度/语义相似/MMR/NGram。


二、提示词模板

2.1 认识提示词模板

提示词模板(Prompt Template) 是 LangChain 的核心抽象之一,被广泛应用于构建大语言模型(LLM) 应用的各个环节。简单来说,只要是需要动态、批量或有结构地向大语言模型发送请求的地方,几乎都会用到提示词模板。

一个简单的例子:假设我们想根据城市名询问 LLM 其历史,按之前做法,需定义 HumanMessage("请介绍上海的历史")、HumanMessage("请介绍西安的历史") 等,每次都会重复"请介绍 xxx 的历史"。在 LangChain 中,可定义模板:

  • 固定文本(模板):"请介绍{city}的历史。"

  • 输入变量:["city"]

查询变量 city 赋值生成的提示词
北京 "北京" 请介绍北京的历史。
上海 "上海" 请介绍上海的历史。

提示词模板就是一个可复用的提示词蓝图,允许我们动态地生成提示词,而不是每次都手动编写完整的提示词。它类似于编程中的字符串格式化功能:创建一个带有占位符的模板,然后在运行时用具体的值(变量)填充这些占位符,从而生成最终发送给 LLM 的完整提示词。

提示词模板解决了以下几个核心问题:

核心问题说明
可复用性 只需定义一个模板,即可用于无数个类似的查询
关注点分离 将提示词的结构和逻辑与具体内容分离:提示工程师优化模板,应用程序提供变量值
一致性 确保发送给 LLM 的提示词结构统一,获得更稳定、可预测的输出
可维护性 修改提示词风格或结构时,只需改一个模板文件,无需在代码多处修改
2.2 用法
2.2.1 字符串模板(PromptTemplate)

LangChain 提供了 PromptTemplate 类来轻松实现这一功能。PromptTemplate 实现了标准的 Runnable 接口。

代码示例:

from langchain_core.prompts import PromptTemplate

# 1. 定义模板({language} 是变量占位符)
prompt_template = PromptTemplate.from_template("把下面的内容翻译成{language}")

# 2. 实例化模板(传入变量值,{language} 被替换成实际内容)
print(prompt_template.invoke({"language": "中文"}))

运行结果:

text='把下面的内容翻译成中文'

PromptTemplate 类的参数与方法:

项说明
template 提示模板
input_variables 需要其值作为提示输入的变量名称列表
from_template() 从模板定义提示模板,返回一个 PromptTemplate 实例

因此,除了通过 PromptTemplate.from_template 定义提示模板的方式外,下面这种方式也可以直接初始化模板:

代码示例:

prompt_template = PromptTemplate(
   input_variables=["language"],
   template="Translate the following into {language}",
)

2.2.2 聊天消息模板(ChatPromptTemplate)

ChatPromptTemplate 模板:专为 LangChain 聊天模型设计。可以方便地构建包含 SystemMessage、HumanMessage、AIMessage 的消息模板。

代码示例:

from langchain_core.prompts import ChatPromptTemplate

# 1. 设置模板(两段消息:system 定义角色/规则,user 是用户内容)
#   {language}、{text} 是变量占位符
prompt_template = ChatPromptTemplate(
  [
      ("system", "把下面的内容翻译成{language}。"),
      ("user", "{text}"),
  ]
)
# 说明:
# 在 0.2.24 版本后可以直接使用 ChatPromptTemplate() 来初始化模板
# 在 0.2.24 版本前,需要使用 ChatPromptTemplate.from_messages() 来初始化模板

# 2. 实例化模板,获取消息实例(把变量值填进去)
messages_value = prompt_template.invoke(
  {
       "language": "中文",
       "text": "你叫什么名字?",
  }
)
# to_messages():把 PromptValue 转成消息列表
messages = messages_value.to_messages()
print(messages)

运行结果:

[SystemMessage(content='把下面的内容翻译成中文。', …), HumanMessage(content='你叫什么名字?', …)]

ChatPromptTemplate 同样实现了标准的 Runnable 接口,因此我们还可以通过链来完成调用:

代码示例:

import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 定义大模型
model = ChatOpenAI(
   model="deepseek-chat",
   api_key=os.getenv("DEEPSEEK_API_KEY"),
   base_url="https://api.deepseek.com",
)

# 1. 设置模板(system 定义规则,user 是用户内容)
prompt_template = ChatPromptTemplate(
  [
      ("system", "把下面的内容翻译成{language}。"),
      ("user", "{text}"),
  ]
)

# 2. 定义输出解析器(把 AIMessage 转成纯字符串)
parser = StrOutputParser()

# 3. 定义链:模板填变量 → 调模型 → 解析成字符串
chain = prompt_template | model | parser

# 流式输出:模型生成一点就吐一点
for token in chain.stream(
  {
       "language": "English",
       "text": "你好,我叫斯蒂芬,很高兴认识你",
  }
):
   print(token, end="|")

运行结果:

|Hello|,| my| name| is| Stephen|.| Nice| to| meet| you|.|||

2.2.3 消息占位符(MessagesPlaceholder)

在上面的 ChatPromptTemplate 中,我们看到了如何格式化两条消息,每条消息都是字符串。但如果希望将消息插入特定位置怎么办?使用 MessagesPlaceholder——它负责在特定位置添加消息列表。

代码示例:

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

# 设置模板:system 消息 + 一个"消息占位符"
# MessagesPlaceholder("msgs") = 留一个位置,运行时把一整段对话消息塞进来
prompt_template = ChatPromptTemplate(
  [
      ("system", "你是一个聊天助手"),
       MessagesPlaceholder("msgs"),  # 消息占位符
  ]
)

# 要传入占位符的消息列表(多轮对话历史)
messages_to_pass = [
   HumanMessage(content="中国首都是哪里?"),
   AIMessage(content="中国首都是北京。"),
   HumanMessage(content="那法国呢?"),
]

# 实例化模板:把消息列表填进 msgs 占位符
formatted_prompt = prompt_template.invoke({"msgs": messages_to_pass})
print(formatted_prompt)

在不显式使用 MessagesPlaceholder 类也可以完成该能力:

2.3 使用 LangChain Hub 的提示词模板

LangChain Hub 是一个用于上传、浏览、拉取和管理提示词(Prompt) 的地方。随着 LLM 的发展,提示词变得越来越重要。LangChain 正在打造一个类似于 GitHub 的平台——GitHub 长期以来一直是共享和协作代码的首选平台,于是推出了 LangChain Hub。

LangChain Hub 创建一个分享和发现 Prompt 的平台,使开发者更容易发现新用例和精炼提示词。这一举措使提示工程师更容易合作、复用现有提示词,并对其进行微调以实现特定结果,从而加速对话代理和其他基于语言的应用的开发与部署。

LangChain Hub 官网地址:LangSmith

目前收藏最高的提示词模板是:hardkothari/prompt-maker。我们就以它为例,演示如何使用 LangChain Hub 上的提示词。Prompt Maker 模板是一个提示生成器,可以自动化优化提示过程,从而提高语言模型在各种应用中的质量和效果。

要使用该功能,需先申请并配置 LangSmith 环境变量:LANGSMITH_API_KEY="你的 LangSmith API Key"。然后从 hub 拉取相应提示并使用。

代码示例:

import os
from langchain_openai import ChatOpenAI
from langsmith import Client

# 从 LangSmith Hub 拉取 "hardkothari/prompt-maker" 提示词模板
# dangerously_pull_public_prompt=True:确认信任该公共模板(新版 SDK 的安全要求)
client = Client()
prompt = client.pull_prompt(
"hardkothari/prompt-maker",
include_model=True,
dangerously_pull_public_prompt=True,
)

# 定义模型(适配 DeepSeek)
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)

# 定义链:Hub 模板 → 模型
chain = prompt | model

# 交互式问答循环
while True:
task = input("\\n你的任务是什么?(输入 quit 退出聊天)\\n")
if task == "quit":
break
lazy_prompt = input("\\n你当前的提示是什么?(输入 quit 退出聊天)\\n")
if lazy_prompt == "quit":
break
print("\\n Response:")
chain.invoke({"lazy_prompt": lazy_prompt, "task": task}).pretty_print()

通过使用这个模板,可以大幅减少手动调整提示词所需的工作量,从而节省时间和资源。Prompt Maker 通过分析初始提示词的结构和内容,然后应用一组预定规则或算法来优化提示词,以提高响应质量、清晰度和相关性。这在提示词质量对模型输出影响较大的场景中特别有用,例如客服机器人、对话代理或数据分析任务。

2.4 少样本提示(Few-shot)

少样本提示是一种通过向 LLM 提供少量具体示例或样本,来教会它如何执行某项特定任务的技术。提高模型性能的最有效方法之一是给出一个模型示例,指导大模型你想做什么、怎么做。

零样本 vs 少样本:我们直接给模型一道考题,不给任何例子——例如输入 "What is 2 🦜 9?",此时模型的解法有千百种,因此没什么效果;但如果给上样本,它就有了固定意义。例如以下样例,模型通过示例学习后可以发现 🦜 与 ➕ 含义相似,并按照此规则得出 What is 2 🦜 9? 的结果是 11:

代码示例(示例集):

examples = [
{"input": "2 🦜 2", "output": "4"},
{"input": "2 🦜 3", "output": "5"},
]

少样本提示主要解决以下核心问题:

核心问题说明
强制输出格式 要求模型以特定格式(JSON/XML/列表)输出,样例作为格式样板,避免自由格式导致解析困难
指令难以描述的任务 如"用莎士比亚风格写作",提供几个示例比写长篇指令更有效
多步推理场景 示例展示思考链(Chain of Thought),引导模型遵循类似推理路径,减少逻辑错误
实现少样本提示(FewShotChatMessagePromptTemplate)

要让大模型"看懂"示例,需要将示例集转换为聊天模型可识别的消息格式。在 LangChain 中,通过 FewShotChatMessagePromptTemplate 对象来实现这一目的——它是一个提示词模板,专门用于将示例集实例化为聊天消息。

代码示例:

import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import (
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
)

# 单个示例的格式化模板(一个示例 = 一问一答)
example_prompt = ChatPromptTemplate(
[
("human", "{input}"),
("ai", "{output}"),
]
)

# 样本示例:教模型"🦜"表示加法
examples = [
{"input": "2 🦜 2", "output": "4"},
{"input": "2 🦜 3", "output": "5"},
]

# 少样本提示词模板:把多个示例拼进提示词,让模型照着范例学习
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt, # 用于格式化单个示例
examples=examples, # 样本示例
)

# 最终完整提示词:system 角色 + 示例 + 用户真实提问
final_prompt = ChatPromptTemplate(
[
("system", "你是一个神奇的数学奇才。"),
few_shot_prompt,
("human", "{input}"),
]
)

# 定义模型
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)

# 链:最终模板 → 模型
chain = final_prompt | model
chain.invoke({"input": "2 🦜 9 等于多少?"}).pretty_print()

运行结果:

根据我们之前的规律(🦜 代表加法),2 🦜 9 = 2 + 9 = 11。

先来看看 FewShotChatMessagePromptTemplate,它也实现了标准的 Runnable 接口:

参数说明
examples 静态示例列表,与 example_selector 二选一
example_selector 动态示例选择器
example_prompt 用于格式化每个示例的模板
prefix 示例前的提示消息(可选)
suffix 示例后的提示消息(可选)
input_variables 模板输入变量列表(可自动推断)
example_separator 连接各部分的分隔符(默认 "\\n")

// 返回值: // FewShotChatMessagePromptTemplate:配置好的少样本聊天消息提示模板对象。

生成的提示结构为:前缀消息 + 示例消息 + 后缀消息;支持静态示例列表或基于输入的动态示例选择。

使用案例 1:推理引导(FewShotPromptTemplate)

代码示例:

import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate

# 创建字符串模板:一个示例 = "Question: 问题\\n答案"
example_prompt = PromptTemplate.from_template("Question: {question}\\n{answer}")

# 创建示例集(教模型:复杂问题要拆成一步一步推理,最后给出最终答案)
examples = [
{
"question": "李白和杜甫,谁更长寿?",
"answer": """
是否需要后续问题:是的。
后续问题:李白享年多少岁?
中间答案:李白享年61岁。
后续问题:杜甫享年多少岁?
中间答案:杜甫享年58岁。
所以最终答案是:李白
""",
},
{
"question": "腾讯的创始人什么时候出生?",
"answer": """
是否需要后续问题:是的。
后续问题:腾讯的创始人是谁?
中间答案:腾讯由马化腾创立。
后续问题:马化腾什么时候出生?
中间答案:马化腾出生于1971年10月29日。
所以最终答案是:1971年10月29日
""",
},
]

# 少样本模板:示例 + 后缀(用户真正的问题)
prompt = FewShotPromptTemplate(
examples=examples, # 示例集
example_prompt=example_prompt, # 每个示例怎么格式化
suffix="Question: {input}", # 示例后面的用户问题模板
input_variables=["input"], # 后缀里的变量
)

# 生成 [HumanMessage] 消息列表
prompt_messages = prompt.invoke(
{"input": "阿狸和亚索是否来自同一个地区?"}
).to_messages()

model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)

model.invoke(prompt_messages).pretty_print()

运行结果:

阿狸和亚索并非来自同一个地区。

阿狸是艾欧尼亚的瓦斯塔亚狐狸,而亚索是艾欧尼亚的战士,虽然他们都与艾欧尼亚有关,但阿狸的故乡是艾欧尼亚的某片幽静森林,而亚索则来自艾欧尼亚的某个部族(通常被提及为诺克萨斯边境附近的村庄)。严格来说,他们都属于艾欧尼亚,但背景设定中并未明确说他们来自同一具体地区。如果按“地区”广义理解为同一大陆,那么确实都是艾欧尼亚的;如果按具体出生地,则不是同一个地方。所以最终答案取决于“地区”的划定——若指艾欧尼亚,则是;若指具体地点,则不是。

FewShotPromptTemplate 相关方法:

方法说明
.invoke() 输入一个字典给提示模板,返回完整的提示内容 PromptValue
PromptValue.to_string() 将提示值作为字符串返回
PromptValue.to_messages() 将提示作为消息列表返回
使用案例 2:增强信息提取能力

代码示例:

import os
from langchain_openai import ChatOpenAI
from typing import List, Optional
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import SystemMessage
from langchain_core.utils.function_calling import tool_example_to_messages

# 1. 定义结构化输出
class Person(BaseModel):
"""一个人的信息。"""
name: Optional[str] = Field(default=None, description="这个人的名字")
hair_color: Optional[str] = Field(default=None, description="如果知道这个人头发的颜色")
skin_color: Optional[str] = Field(default=None, description="如果知道这个人的肤色")
height_in_meters: Optional[float] = Field(default=None, description="以米为单位的高度")

class Data(BaseModel):
"""提取关于人的数据。"""
people: List[Person]

# 2. 定义 2 个示例(教模型提取行为:什么该提取、信息缺失时怎么办)
examples = [
("海洋是广阔而蓝色的。它有两万多英尺深。", Data(people=[])), # 没有人物信息
("小强从中国远行到美国。", Data(people=[
Person(name="小强", height_in_meters=None, skin_color=None, hair_color=None),
])), # 部分信息缺失
]

# 3. 定义提示词模板(system 定规则 + 示例占位 + 用户真问题)
prompt_template = ChatPromptTemplate(
[
SystemMessage(content="你是一个提取信息的专家,只从文本中提取相关信息。"
"如果您不知道要提取的属性的值,属性值返回null"),
MessagesPlaceholder("example_messages"),
("user", "{new_message}"),
]
)

# 4. 获取样例消息列表(把每个示例转成模型能理解的聊天消息格式)
example_messages = []
for txt, tool_call in examples:
ai_response = "检测到人" if tool_call.people else "未检测到人"
example_messages.extend(
tool_example_to_messages(txt, [tool_call], ai_response=ai_response)
)

# 5. 调用
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
structured_model = model.with_structured_output(schema=Data, method="function_calling")

# 定义链:示例+问题 → 结构化提取
chain = prompt_template | structured_model
result = chain.invoke({
"example_messages": example_messages,
"new_message": "篮球场上,身高两米的中锋王伟默契地将球传给一米七的后卫挚友李明,完成一记绝杀。",
})
print(result)

# 额外案例:没有人名的文本
result_no_people = chain.invoke({
"example_messages": example_messages,
"new_message": "海边的日出很美,金色的阳光洒在平静的海面上。",
})
print(result_no_people)

运行结果:


三、示例选择器(Example Selectors)

3.1 概念

一旦我们有了示例数据集,就需要考虑提示中应该有多少个示例。关键的权衡是:更多的示例通常会提高性能,但更大的提示会增加成本和延迟;超过某个阈值,太多示例可能会开始混淆模型。

找到正确数量的示例在很大程度上取决于模型、任务、示例质量以及成本和延迟限制。有趣的是,模型越好,它需要的精准示例就越少。但其实,最佳方法是使用不同数量的示例进行一些实验。

若此时我们有大量的示例数据集,对于大模型来说,没必要全部使用作为参考。我们需要一种方法,根据给定输入从数据集中选择示例。在 LangChain 中,示例选择器可以帮助我们从一组示例集合中,根据具体策略选择正确的示例子集,构建少样本提示。

选择策略:

策略原理
Length 根据特定长度内可容纳的数量选择示例
Similarity 使用输入和示例之间的语义相似性决定选择哪些示例
MMR 使用输入和示例之间的最大边际相关性决定选择哪些示例
Ngram 使用输入和示例之间的 n-gram 重叠决定选择哪些示例
3.2 按长度选择示例(Length)
概念

当我们担心构造提示时会超过上下文窗口长度时,可根据特定长度内可容纳的数量选择示例——对于较长的输入,选择更少的示例;对于较短的输入,选择更多的示例。实现按长度选择示例的选择器是:LengthBasedExampleSelector。

常见参数:

参数说明
examples 可用的示例列表,每个示例为字典
example_prompt 用于格式化每个示例的 PromptTemplate 对象
max_length 提示词允许的最大长度(默认 2048),超出则截断
get_text_length 自定义计算文本长度的函数(默认按单词计数)

// 返回值: // LengthBasedExampleSelector:配置好的示例选择器对象。

内置方法:

  • add_example:将新示例添加到列表中。

    SemanticSimilarityExampleSelector::add_example(example: dict[str, str]);

    # 功能:
    # 向当前的示例选择器中动态添加一个新示例。该示例会被向量化并存储到底层的向量数据库中,
    # 以便后续的 `select_examples` 查询能够基于语义相似度检索到它。
    # 适用于需要在线更新示例库的场景(如用户反馈或增量学习)。

    # 参数说明:
    # example: 一个字典,键为示例中的变量名(如 "input"、"output"),值为对应的文本内容。
    # 该字典的结构应与创建时传入的示例结构完全一致。

    # 返回值:
    # 无(void)

  • select_examples:根据输入选择要使用的示例。

    SemanticSimilarityExampleSelector.select_examples(
    input_variables: dict
    ) -> List[dict]:

    # 功能:
    # 根据输入的变量字典,从示例存储中选择与输入语义最相似的 k 个示例(k 在构造函数中指定)。
    # 该方法会自动将输入变量组合成文本,计算其嵌入向量,然后在向量库中检索最相似的示例。

    # 参数说明:
    # input_variables: 包含当前输入变量的字典,例如 {"input": "large"}。
    # 字典中的键值对会被用于构建检索文本,以匹配示例的语义。

    # 返回值:
    # List[dict]:选中的示例列表,每个示例为字典,包含所有示例字段(如 input、output 等)。

代码示例:

import os
from langchain_openai import ChatOpenAI
from langchain_core.example_selectors import LengthBasedExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate

# 反义词示例集合
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
{"input": "energetic", "output": "lethargic"},
{"input": "sunny", "output": "gloomy"},
{"input": "windy", "output": "calm"},
]

# 字符串模板(单个示例的格式)
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\\nOutput: {output}",
)

# 长度示例选择器:按长度动态选示例(从前往后塞,超长就停)
example_selector = LengthBasedExampleSelector(
examples=examples,
example_prompt=example_prompt,
max_length=15,
)

# 用选择器创建动态模板
dynamic_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="给出每个输入的反义词",
suffix="Input: {adjective}\\nOutput:",
input_variables=["adjective"],
)

#只看组装好的提示词(不调模型)
print(dynamic_prompt.invoke({"adjective": "happy"}).to_messages()[0].content)

运行结果:

=== 提示词内容 ===
给出每个输入的反义词

Input: happy
Output: sad

Input: tall
Output: short

Input: energetic
Output: lethargic

Input: happy
Output:

可见:5 个反义词示例只塞进了前 3 个(长度达到 max_length 就停止)。

3.3 按语义相似性选择示例(Similarity)
概念

语义相似是衡量文本在含义上的接近程度。例如:

文本对表面语义
"我喜欢猫" / "我讨厌狗" 表面相似度低 相似:都表达对动物的态度
"苹果很甜" / "苹果市值创新高" "苹果"一词相同 不相似:一个指水果、一个指公司

LangChain 能根据输入和示例之间的语义相似性决定选择哪些示例,它通过查找与输入具有最大余弦相似性的嵌入示例来实现。实现该策略的选择器是:SemanticSimilarityExampleSelector。

常见参数:

参数说明
vectorstore 已初始化的向量存储实例(如 Chroma、FAISS)
k 要选择的示例数量(默认 4)
example_keys 过滤示例中需要返回的键
input_keys 限制用于搜索的输入变量

内置方法 from_examples:

参数说明
examples 示例列表
embeddings 嵌入模型实例
vectorstore_cls 向量存储类(默认 Chroma)
k 要选择的示例数量

其他方法:add_example(添加新示例)、select_examples(根据输入选择示例)。

代码示例:

import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HUB_DISABLE_XET"] = "1"

from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
from langchain_core.embeddings import Embeddings
from fastembed import TextEmbedding

# 嵌入封装:DeepSeek 没有 embedding 接口,这里用 fastembed(本地轻量,免 torch)
class FastEmbed(Embeddings):
def __init__(self, model="BAAI/bge-small-en-v1.5"):
self.m = TextEmbedding(model)
def embed_documents(self, texts):
return [v.tolist() for v in self.m.embed(list(texts))]
def embed_query(self, text):
return next(self.m.embed([text])).tolist()

# 反义词示例集合
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
{"input": "energetic", "output": "lethargic"},
{"input": "sunny", "output": "gloomy"},
{"input": "windy", "output": "calm"},
]

# 字符串模板
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\\nOutput: {output}",
)

# 语义相似示例选择器:挑和用户问题【语义最接近】的示例(不是按顺序/长度)
example_selector = SemanticSimilarityExampleSelector.from_examples(
examples,
FastEmbed(), # 嵌入类用于生成语义相似度向量
Chroma, # 向量库,存储嵌入并做相似度搜索
k=1, # 选几个示例
)

# 用选择器实例化模板
similar_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="给出每个输入的反义词",
suffix="Input: {adjective}\\nOutput:",
input_variables=["adjective"],
)

# 只看组装好的提示词(看选了哪些示例)
print(similar_prompt.invoke({"adjective": "worried"}).to_messages()[0].content)

运行结果:

=== 提示词内容(worried【担心】语义最接近哪个示例)===
给出每个输入的反义词

Input: happy
Output: sad

Input: worried
Output:

可见:worried(担心)在语义上最接近 happy(开心),所以只选中了它。

3.4 按最大边际相关性选择示例(MMR)
概念

最大边际相关性(MMR,Maximum Marginal Relevance) 是一种重排序算法,它利用语义相似性作为基础工具,从候选集中选出一组既能代表查询主题又彼此多样化的结果。

它与语义相似性的区别可类比为:

方式比喻特点
语义相似性 面试官衡量每个应聘者与职位要求的匹配度,逐个打分 只看单点相关性
最大边际相关性(MMR) 团队经理组建团队:既要技术达标,又要技能互补(前端/后端/算法/测试) 相关性 + 多样性/新颖性

典型使用场景:

场景说明
语义相似性 搜索引擎基础排序、重复检测、聚类、语义搜索
MMR 推荐系统(相关但不同类型,避免"信息茧房")、文档摘要(覆盖主旨且不重复)、RAG(检索后去重+多样化筛选,提升答案质量、减少幻觉)

实现该策略的选择器是:MaxMarginalRelevanceExampleSelector。

内置方法:from_examples、add_example、select_examples。

代码示例:

import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HUB_DISABLE_XET"] = "1"

from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.example_selectors import MaxMarginalRelevanceExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
from langchain_core.embeddings import Embeddings
from fastembed import TextEmbedding

# 嵌入封装:DeepSeek 没有 embedding 接口,这里用 fastembed(本地轻量,免 torch)
class FastEmbed(Embeddings):
def __init__(self, model="BAAI/bge-small-en-v1.5"):
self.m = TextEmbedding(model)
def embed_documents(self, texts):
return [v.tolist() for v in self.m.embed(list(texts))]
def embed_query(self, text):
return next(self.m.embed([text])).tolist()

# 反义词示例集合
examples = [
{"input": "happy", "output": "sad"},
{"input": "tall", "output": "short"},
{"input": "energetic", "output": "lethargic"},
{"input": "sunny", "output": "gloomy"},
{"input": "windy", "output": "calm"},
]

# 字符串模板
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\\nOutput: {output}",
)

# MMR 示例选择器:既看重"和问题的相似度",又兼顾"示例之间不重复"
example_selector = MaxMarginalRelevanceExampleSelector.from_examples(
examples,
FastEmbed(), # 嵌入类
Chroma, # 向量库
k=2, # 选 2 个(MMR 特点:相似的 + 补充多样性的)
)

# 用选择器实例化模板
similar_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="给出每个输入的反义词",
suffix="Input: {adjective}\\nOutput:",
input_variables=["adjective"],
)

#只看组装好的提示词(MMR 选了哪 2 个示例)
print(similar_prompt.invoke({"adjective": "worried"}).to_messages()[0].content)

运行结果:

=== 提示词内容(MMR 选中的 2 个示例)===
给出每个输入的反义词

Input: happy
Output: sad

Input: windy
Output: calm

Input: worried
Output:

可见:worried → 选了最像的 happy + 补充多样性的 windy。

3.5 按 n-gram 重叠选择示例(NGram)
概念

n-gram 指一个文本序列中连续的 n 个词(word)或字符(character)。n-gram 重叠 是一种通过计算两段文本之间共同拥有的 n-gram 数量来衡量相似度的方法。

文本对n-gram 重叠语义
"苹果手机很好用" / "这款手机很好用" 高(重复词多) 相似
"苹果手机很好用" / "iPhone 非常不错" 0(无相同词) 相似但表面不匹配

因此,传统 n-gram 重叠 是一种表面形式匹配,只关心词是否完全一样,无法处理同义词。

语义 n-gram 重叠:它不再比较词本身,而是比较词背后的语义向量(Embedding)——计算它们在语义空间中的向量是否相似,若相似度超过阈值就认为它们"重叠"。例如 "苹果" vs "iPhone" 向量相似度 0.95 → 视为重叠;"好用" vs "不错" 相似度 0.82 → 视为重叠。常用于剽窃检测,能发现"改换了词汇但保留了核心思想"的"智能"剽窃。

LangChain 实现该策略的选择器是:NGramOverlapExampleSelector,其参数如下:

参数说明
example_prompt PromptTemplate,用于格式化示例的提示模板
examples 模板所需的示例列表
threshold 算法停止的阈值,默认 -1.0(见下表)

threshold 取值行为:

阈值行为
负值 按重叠分数排序,不排除任何示例
0.0 按重叠分数排序,排除与输入没有 n-gram 重叠的示例
> 1.0 排除所有示例,返回空列表

内置方法:add_example(example)、select_examples(input_variables) → list[dict]。

代码示例:

import os
from langchain_openai import ChatOpenAI
from langchain_community.example_selectors import NGramOverlapExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate

# 翻译示例
examples = [
{"input": "See Spot run.", "output": "看见Spot跑。"},
{"input": "My dog barks.", "output": "我的狗叫。"},
{"input": "Spot can run.", "output": "Spot可以跑。"},
]

# 字符串模板
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\\nOutput: {output}",
)

# NGram 示例选择器:按 n-gram 重叠度来挑和用户问题最像的示例
example_selector = NGramOverlapExampleSelector(
examples=examples,
example_prompt=example_prompt,
threshold=0, # 按重叠分数排序,并排除与输入没有 n-gram 重叠的示例
)

# 用选择器实例化模板
dynamic_prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="给出每个输入的中文翻译",
suffix="Input: {sentence}\\nOutput:",
input_variables=["sentence"],
)

# 定义大模型(适配 DeepSeek)
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)

# 方式一:只看组装好的提示词(选了哪些示例)
print(dynamic_prompt.invoke({"sentence": "Spot can run fast."}).to_messages()[0].content)

运行结果:

=== 提示词内容(NGram 选中的示例)===
给出每个输入的中文翻译

Input: Spot can run.
Output: Spot可以跑。

Input: See Spot run.
Output: 看见Spot跑。

Input: Spot can run fast.
Output:

可见:Spot can run fast. 与 Spot can run.、See Spot run. 的 n-gram 重叠最多,被优先选中。

3.6 总结对比
选择器挑选逻辑例子里的表现
LengthBased(按长度) 从前往后塞,长度超 max_length 就停,跟问题无关 5 个反义词 → 只塞前 3 个(长度到 13 停)
SemanticSimilarity(语义相似) 用向量算"示例和问题多像",选最像的 k 个 worried 最像 happy → 只选它
MMR(相似+多样) 既要和问题相关,又要示例之间不重复 worried → 选 happy(最像)+ windy(补充多样)
NGramOverlap(n-gram 重叠) 数示例和问题相同的单词/词组,按重叠度挑 Spot can run fast. 和 Spot can run. 重叠多

四、复盘(附答案)

💡 思考题

  • 提示词模板解决了哪些核心问题?PromptTemplate 和 ChatPromptTemplate 有什么区别?

  • MessagesPlaceholder 是干什么的?适合什么场景?

  • 什么是少样本提示?它主要解决哪三类问题?

  • 示例选择器的四种策略(Length / Similarity / MMR / NGram)分别是怎么挑示例的?

  • 语义相似性选择和 MMR 有什么区别?为什么 RAG 场景常用 MMR?

  • 📝 答案

  • 解决四大问题:可复用性(一个模板无限复用)、关注点分离(结构与内容分离)、一致性(提示结构统一、输出稳定)、可维护性(改一处模板即可)。区别:PromptTemplate 生成字符串提示,适合纯文本 LLM;ChatPromptTemplate 生成消息列表(可组合 system/user/ai 角色),专为聊天模型设计。

  • MessagesPlaceholder 在模板的特定位置插入一整段消息列表(多轮对话历史),适合需要在 system 消息后动态拼接历史的场景,如带上下文的聊天机器人。

  • 少样本提示:给模型提供少量"输入-输出"示例,让它照葫芦画瓢。解决三类问题:强制输出格式(JSON/XML 等格式样板)、指令难以描述的任务(如风格仿写,几个示例比长篇指令更有效)、多步推理场景(示例展示思考链 CoT,引导模型按同样路径推理)。

  • Length:按长度从前往后塞示例,超 max_length 就停,与问题无关;Similarity:用向量算语义相似度,选最像的 k 个;MMR:既要相关又要彼此多样(相关 + 新颖性);NGram:数示例与问题相同的 n-gram 重叠度来挑选。

  • Similarity 只看"和问题最像",可能选出一堆同质示例;MMR 在相关性基础上增加多样性约束——先选最相关的,再选"相关且与已选不重复"的。RAG 检索常返回大量高度相似的文档,用 MMR 去重 + 多样化筛选后再交给 LLM,能提升答案质量、减少幻觉。


  •  🎯 闭幕

    从"提示词模板"到"少样本提示",从"LangChain Hub"到"四大示例选择器"——这一篇帮你把 LangChain 的提示工程走了一遍。

    如果本文对你有帮助,欢迎:

    👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

    你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

    赞(0)
    未经允许不得转载:171主机测评 » 【LangChain实战】LangChain 学习笔记(三):提示词模板与示例选择器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址