夜雨聆风学习资料网

ARTICLE · 1216237

大模型实战开发——提示词模板

大模型实战开发——提示词模板

提示词工程(Prompt Engine,PE)中,经常提到为模型设定角色和主题,也就是为其设定对应的模板,这样方便使用者切换不同的角色、技术领域、场景,甚至是回答问题的语言。langchain提供了对应的技术实现——ChatPromptTemplate。

1. 聊天提示词模板

ChatPromptTemplate 是用于构建提示词模板。它在保证基本结构的基础上,支持替换文本中的变量。通常包含三个部分:

位置
内容
组织方式
示例
系统消息
专业领域、回答语言
固定文本与变量组合
“你是某个{domain}方面的专家”
历史消息
当前问题之前的对话
通过消息占位符插入
用户消息
本次需要回答的问题
将问题填入消息模板
"请回答{question}"

值得注意的是:模板负责组织输入,其输出为消息。它不会调用模型、生成答案,也不会保存历史消息 。

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
保存和恢复 Agent 的会话状态

直接调用模板与模型时,历史通常由应用维护,再通过占位符插入;使用已配置 checkpointer 的 Agent 时,框架根据会话标识恢复状态,同一会话的后续请求通常只需提交新消息。

两种方式可以按应用需要配合使用,但如果 Agent 已经恢复了历史,就不应再把同一份历史作为新输入重复提交,否则可能造成对话重复。消息占位符本身也不会自动完成历史摘要或按 token 预算裁剪,输入长度仍需按照实际场景管理。

5. 总结

ChatPromptTemplate 将消息角色、排列顺序和可变内容统一组织起来。通过 from_messages() 或构造函数创建模板后,可以使用字典提供多个变量;只有一个必填变量时,也可以直接传入字符串。MessagesPlaceholder 用于插入保留角色的消息列表,optional=True 允许省略该列表;partial() 则可以预填稳定配置,减少后续调用需要提供的变量。模板生成的 ChatPromptValue 是模型输入,交给模型后才会产生回答。历史保存与会话恢复仍由应用或 Agent 的检查点机制负责。

相关学习资料