ARTICLE · 1216237
大模型实战开发——提示词模板
提示词工程(Prompt Engine,PE)中,经常提到为模型设定角色和主题,也就是为其设定对应的模板,这样方便使用者切换不同的角色、技术领域、场景,甚至是回答问题的语言。langchain提供了对应的技术实现——ChatPromptTemplate。
1. 聊天提示词模板
ChatPromptTemplate 是用于构建提示词模板。它在保证基本结构的基础上,支持替换文本中的变量。通常包含三个部分:
值得注意的是:模板负责组织输入,其输出为消息。它不会调用模型、生成答案,也不会保存历史消息 。
2. 创建模板
ChatPromptTemplate 可以通过 from_messages() 创建,也可以直接调用构造函数。
2.1 from_messages()
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
def build_prompt_1():
return ChatPromptTemplate.from_messages(
[
("system", "你是{topic}方面的专家,请用{language}回答所有的问题"),
MessagesPlaceholder(variable_name="messages", optional=True),
("human", "我的问题是{question}"),
]
)列表中的顺序就是生成消息时的顺序:先放系统要求,再插入历史,最后放当前问题。
其中,("system", "...") 和 ("human", "...") 是消息元组,在之前的文章中多次提及。至于其中的 {topic}、{language}、{question} 是变量占位符。MessagesPlaceholder 则用于插入消息列表,它的具体行为见本文3.2小节。
2.2 构造函数
相同的消息定义也可以直接交给构造函数:
def build_prompt_2():
return ChatPromptTemplate(
[
("system", "你是{topic}方面的专家,请用{language}回答所有的问题"),
MessagesPlaceholder(variable_name="messages", optional=True),
("human", "我的问题是{question}"),
]
)3. 模板输入
应用模板的过程需要将其中的模板中的占位符替换为具体的参数,如何怎样传参,取决于模板有哪些必填变量,以及是否需要插入历史消息。
3.1 多输入变量
前面的模板需要填入专业领域、回答语言和当前问题,因此可以使用字典分别提供:
prompt = build_prompt_1()
formatted_value = prompt.invoke(
{
"topic": "C++",
"language": "中文",
"question": "C++面向对象编程的三个特性是什么?",
}
)
print(formatted_value.to_string())这里没有提供历史消息,格式化后的文本应为:
System: 你是C++方面的专家,请用中文回答所有的问题
Human: 我的问题是C++面向对象编程的三个特性是什么?字典的键需要与模板变量对应。topic 和 language 填入系统消息,question 填入用户消息。
注意:调用invoke要补全所有缺失的占位符,否则会在模板处理阶段抛出 KeyError,此时还没有请求模型。
3.2 消息占位符
模板中的这一项用于插入历史消息:
MessagesPlaceholder(variable_name="messages", optional=True)variable_name="messages" 表示调用时从名为 messages 的字段读取消息列表。这个名称由开发者指定,也可以改为 history,但定义与传参需要保持一致。
下面传入一轮已有对话,历史消息仍使用元组表示:
formatted_value = prompt.invoke(
{
"topic": "C++",
"language": "中文",
"messages": [
("human", "接下来讨论C++面向对象编程。"),
("ai", "好的,我们围绕C++面向对象编程展开。"),
],
"question": "C++面向对象编程的三个特性是什么?",
}
)
for message in formatted_value.to_messages():
print(message.type, message.content)生成的消息顺序应为:
system 你是C++方面的专家,请用中文回答所有的问题
human 接下来讨论C++面向对象编程。
ai 好的,我们围绕C++面向对象编程展开。
human 我的问题是C++面向对象编程的三个特性是什么?由以上结果可知,占位符并没有生成一条名为“历史”的消息,而是在它所在的位置插入了两条消息,且保留了各自的角色。
因此,下面两种定义含义不同:
("human", "{messages}")
MessagesPlaceholder(variable_name="messages", optional=True)前者将变量值填入一条用户消息的文本位置,即使传入消息列表,也不会按列表中的角色展开;后者接收消息列表,并将其转换成多条消息插入。
optional=True 则表示这个消息列表可以省略。没有提供 messages 时,该位置按空列表处理。因此,3.1 的示例只有系统消息和当前问题,共两条;上面的示例插入两条历史后,共四条。
3.3 单输入变量
如果应用固定使用 C++ 领域和中文回答,可以直接把这两项写入系统消息:
def build_prompt_3():
return ChatPromptTemplate(
[
("system", "你是C++方面的专家,请用中文回答所有的问题"),
MessagesPlaceholder(variable_name="messages", optional=True),
("human", "我的问题是{question}"),
]
)当只剩 question 一个必填变量时,无需指定变量名称,可以直接向 invoke() 传入字符串即可:
formatted_value = prompt.invoke("C++面向对象编程的三个特性是什么?")这种写法等价于:
formatted_value = prompt.invoke(
{"question": "C++面向对象编程的三个特性是什么?"}
)单输入简写的判断依据是必填输入变量只有一个
3.4 预填变量
直接把主题和语言写死,需要单独定义一个模板。对于已经包含变量的通用模板,也可以通过 partial() 提前提供一部分值:
def build_prompt_1():
return ChatPromptTemplate.from_messages(
[
("system", "你是{topic}方面的专家,请用{language}回答所有的问题"),
MessagesPlaceholder(variable_name="messages", optional=True),
("human", "我的问题是{question}"),
]
)
prompt = build_prompt_1()
configured_prompt = prompt.partial(topic="C++", language="中文")
print(prompt.input_variables)
print(configured_prompt.input_variables)
formatted_value = configured_prompt.invoke(
"C++面向对象编程的三个特性是什么?"
)partial() 返回一个带预填变量的新模板,原模板仍然可以用于其他主题和语言。新模板已经具备 topic、language 的值,调用时只需提供 question,以及按需提供的历史消息。
这种方式适合应用在初始化时确定领域和语言,之后多次处理不同问题。它与上一节直接固定文本的方式不同,但都可以使后续调用只剩一个必填变量。
3.5 模板中的花括号
前面的模板使用默认格式,花括号用于声明变量。如果模板文本本身需要包含 JSON,直接写入单层花括号可能使其中的内容被识别为变量。此时需要用 {{、}} 表示字面量花括号:
prompt = ChatPromptTemplate.from_messages(
[
("system", '输出格式示例:{{"topic": "C++"}}'),
("human", "{question}"),
]
)
formatted_value = prompt.invoke(
{"question": '请解释这段JSON:{"topic": "C++"}'}
)
print(formatted_value.to_string())格式化后的文本应为:
System: 输出格式示例:{"topic": "C++"}
Human: 请解释这段JSON:{"topic": "C++"}系统消息中的 JSON 是模板源码的一部分,所以需要转义花括号;question 中的 JSON 是调用时传入的变量值,在这次格式化过程中不会再次作为模板解析,因此不需要重复转义。
这里处理的是模板语法。系统消息中书写了输出格式要求,并不意味着模型输出一定经过 JSON 格式校验。
3.6 查看格式化结果
调用后得到的 ChatPromptValue 保存了组织好的消息,可以通过以下两个接口查看:
to_string() | ||
to_messages() |
4. 容易混淆的概念
4.1 模板与模型的 invoke()
在前面的章节中,我们使用模型的 invoke() 发送请求;本文又使用了 ChatPromptTemplate 的 invoke()。虽然接口名称相同,但它们属于不同的类,接收的数据、执行的工作和返回的结果也不同,使用时需要区分。
ChatPromptTemplate | ChatPromptValue | ||
ChatPromptValue 或消息列表 | AIMessage |
因此,prompt.invoke() 完成后,我们只是得到了准备发送给模型的内容,此时还没有模型回答;继续调用 model.invoke(),才会将这些内容发送给模型。
下面将两次调用放在一起。build_prompt_3() 沿用前面的单输入模板,buildmodel() 沿用测试程序中的模型初始化函数:
def main() -> None:
prompt = build_prompt_3()
formatted_value = prompt.invoke(
"C++面向对象编程的三个特性是什么?"
)
print(formatted_value.to_string())
model = buildmodel()
response = model.invoke(formatted_value)
print(response.content)
if __name__ == "__main__":
main()这里的 formatted_value 是模板生成的 ChatPromptValue,作为下一次模型调用的输入;response 才是模型返回的 AIMessage,通过 content 获取回复内容。
4.2 历史消息与记忆的关系
MessagesPlaceholder 可以把历史消息插入输入,但历史来自调用方。应用需要在本次调用时提供消息列表;模板不会因为上次处理过某个问题,就自动在下次调用中带上它。
这与 记忆 中的 checkpointer 职责不同:
MessagesPlaceholder | |
checkpointer |
直接调用模板与模型时,历史通常由应用维护,再通过占位符插入;使用已配置 checkpointer 的 Agent 时,框架根据会话标识恢复状态,同一会话的后续请求通常只需提交新消息。
两种方式可以按应用需要配合使用,但如果 Agent 已经恢复了历史,就不应再把同一份历史作为新输入重复提交,否则可能造成对话重复。消息占位符本身也不会自动完成历史摘要或按 token 预算裁剪,输入长度仍需按照实际场景管理。
5. 总结
ChatPromptTemplate 将消息角色、排列顺序和可变内容统一组织起来。通过 from_messages() 或构造函数创建模板后,可以使用字典提供多个变量;只有一个必填变量时,也可以直接传入字符串。MessagesPlaceholder 用于插入保留角色的消息列表,optional=True 允许省略该列表;partial() 则可以预填稳定配置,减少后续调用需要提供的变量。模板生成的 ChatPromptValue 是模型输入,交给模型后才会产生回答。历史保存与会话恢复仍由应用或 Agent 的检查点机制负责。















































































