13 - 提示词与消息模板


本章课程目标:

  • 理解 Prompt 是什么,知道它为什么会从“一段字符串”逐步演化成“多角色消息 + 模板 + 占位符”。
  • 掌握 LangChain 中与输入组织最相关的三块内容:消息类型(Message)模型调用方式(invoke / stream / batch)提示词模板(PromptTemplate / ChatPromptTemplate)
  • 会运行并理解本章全部案例:输入类型、同步与异步调用、文本模板、对话模板、消息占位符、从 JSON / YAML 加载提示词,为后续 输出解析器LCEL 与链式调用记忆与对话历史 打基础。

学习建议: 读这一章时,拿一条普通用户问题练手,把它改造成消息列表:系统消息放规则,用户消息放任务,历史消息放上下文。先理解模型到底吃进去什么,再看 PromptTemplateChatPromptTemplateMessagesPlaceholder 怎么帮你复用这些结构。读完后最好能判断:什么时候写死提示词,什么时候抽成模板,什么时候需要把模板放到文件里。

官方文档与资源:详见 工具导航与参考资料索引 - 提示词与结构化输出


1、Prompt 简介

本章对应 第 11 章 Model I/O 里的 输入格式化(Format)模型调用(Predict) 两部分。模型并不是“凭空思考”,它始终是在读取我们提供的输入后再生成输出;而 Prompt、Message、Template,正是组织这份输入的核心工具。

如果你已经学过 1-2 提示词工程基础,那么本章可以直接理解为它的代码实现版:前一章讲“怎么把角色、任务、上下文、输入、输出和约束说清楚”,这一章讲“这些内容进入 LangChain 后,应该以什么输入形态存在、如何复用、如何插入多轮历史、如何外置到文件”。

1.1 定义

Prompt(提示词),就是你发给大模型的输入内容。

最简单的 Prompt,是一句自然语言,比如“什么是 LangChain?”;再进一步,你会给它增加角色,比如“你是一名法律顾问,请用 50 字介绍广告法”;再往后,为了让代码可复用、可维护、可协作,你会把这段输入写成模板,并把会变化的部分改成占位符。这就是从“随手提问”走向“工程化输入管理”的过程。

入门阶段可先把握一个基本判断:Prompt 不只是把一句话写漂亮,而是把模型输入组织清楚。

在真实项目中,Prompt 通常承担几件事:

  • 告诉模型它是谁,要扮演什么角色。
  • 告诉模型当前任务是什么,回答边界是什么。
  • 告诉模型输出格式应该长什么样。
  • 把用户问题、历史对话、检索结果、工具结果组合成一次完整输入。

这也是为什么随着项目复杂度提高,Prompt 会从“一个字符串”逐步演化成“多角色消息 + 模板 + 占位符 + 外部配置文件”。

DeepSeek 官方提示词示例库入口

1.2 Prompt 的作用

很多人在学 LangChain 时,会把注意力都放在“接哪个模型”“换哪个平台”“怎么配 API Key”上,但真正到了项目里,最容易让系统效果不稳定的,往往不是模型接入,而是输入组织得不够清楚

下面这些真实开发场景,都离不开本章内容:

  • 智能客服:需要系统提示词规定语气、身份和拒答策略。
  • 企业知识库问答:需要把“检索出来的上下文 + 用户问题”组合成一条清晰提示。
  • 多轮聊天:需要把历史对话插回当前输入,而不是每轮都从头问。
  • 结构化输出:需要提前在 Prompt 里写清楚输出格式要求,方便后面交给解析器处理。
  • 团队协作与 A/B 测试:需要把 Prompt 模板从代码里抽出来,放到 JSON / YAML 中做版本管理。

一句话:模型能力决定上限,Prompt 设计决定你能不能稳定接近这个上限。

1.3 本章在项目中的位置

本章对应仓库中的 案例与源码-2-LangChain框架/04-prompt 目录,按学习顺序可以分成 4 类:

目录 作用 你会学到什么
invoke/ 模型调用方式 invokestreambatch 及异步版本
prompt_templates/ 文本模板 PromptTemplate 的创建、格式化、复用
chat_prompt_template/ 对话模板 ChatPromptTemplate、消息参数、消息占位符
load_external/ 外部文件加载 如何从 JSON / YAML 加载 Prompt

这一章不是零散的 API 介绍,而是在回答一个问题:开发 LLM 应用时,怎样把 1-2 提示词工程基础 里学到的写法和原则,组织成一套可复用、可维护、可扩展的结构?


2、调用大模型的入参类型

初学者容易误以为:调用聊天模型时,输入永远只能是“一段字符串”。实际上,LangChain 聊天模型为了适配真实对话场景,通常支持多种输入形态。这些写法表面不同,核心都一样:1-2 章 里讲过的角色、任务、上下文、输入和约束,以合适的消息结构交给模型。

2.1 入参形态总览

同一次 invoke,左侧可以是多种类型的输入,中间由聊天模型处理,右侧典型返回值是 AIMessage。这也是为什么你在 第 11 章 会经常看到“正文一般通过 .content 读取”。

聊天模型 invoke:常见入参类型与 AIMessage 输出

常见对应关系如下:

入参形态 调用时大致长什么样 适合场景
str model.invoke("请解释什么是 LangChain") 单轮、轻量、快速试接口
PromptTemplate.format(...) 后的字符串 model.invoke(prompt_str) 固定句式 + 少量变量替换
消息对象列表 model.invoke([SystemMessage(...), HumanMessage(...)]) 推荐写法,适合系统提示、多轮对话
(role, content) 元组列表 [("system", "..."), ("user", "...")] 简洁、贴近 ChatPromptTemplate 风格
{"role": "...", "content": "..."} 字典列表 [{"role":"system","content":"..."}, ...] 贴近 OpenAI 风格 JSON 数据

【案例源码】案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke_InputTypes.py

LLM_Invoke_InputTypes.py

2.2 写法一:纯字符串

最简单的输入方式,把整段说明写在一个字符串里交给模型,适合快速试接口、或单轮一句话任务。

1
2
resp = model.invoke("用一句话解释什么是 LangChain")
print(resp.content)

不过在真实项目里,纯字符串也有明显局限:不方便表达系统角色与用户问题的边界;不方便插入历史对话;不利于后期维护和多人协作。

所以,纯字符串适合起步,不适合复杂对话场景长期使用。

2.3 写法二:模板 + 占位符

如果一句 Prompt 里只有少数变量会变化,就没必要每次手写整段长文本。更合理的做法,是把固定部分写成模板,把变化部分留成占位符。

这一节本质上仍然是“字符串输入”,只是我们把字符串的生成过程工程化了。

1
2
3
4
5
6
7
8
from langchain_core.prompts import PromptTemplate

template = PromptTemplate.from_template(
"用不超过 50 字介绍:{topic} 是什么?"
)
prompt_str = template.format(topic="LangChain")
resp = model.invoke(prompt_str)
print(resp.content)

也就是说:

  • PromptTemplate 负责生产输入
  • model.invoke(...) 负责把输入发给模型

它和 2.2 的区别不在于模型收到了不同类型,而在于我们是手写字符串,还是用模板生成字符串

2.4 写法三:多角色消息列表

当你开始做聊天机器人、问答助手、企业知识库、代码助手时,最推荐的入参方式通常不是字符串,而是消息列表

原因很简单:聊天模型更擅长理解“谁在说话”。把输入拆成 SystemMessageHumanMessageAIMessage 等不同角色,模型更容易正确理解上下文结构,而不是把所有东西都当成一大段平铺文本。

1
2
3
4
5
6
7
8
9
10
11
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

messages = [
SystemMessage(content="你是只回答技术问题的助手,回答要简短。"),
HumanMessage(content="什么是 LangChain?"),
# 多轮示例:
# AIMessage(content="LangChain 是用于编排 LLM 应用的框架……"),
# HumanMessage(content="它和直接调 API 有什么区别?"),
]
resp = model.invoke(messages)
print(resp.content)

在实际项目里,这种写法特别常见:

  • 系统提示词放在 SystemMessage
  • 用户问题放在 HumanMessage
  • 历史回复可放回 AIMessage
  • 工具执行结果后续可用 ToolMessage

如果你后面要做多轮对话、Agent、RAG,这种消息列表思维会反复用到。

还有一种等价包装是 ChatPromptValue:链式编排里更常见的是由 ChatPromptTemplate.invoke(...) 得到 ChatPromptValue;下面直接构造对象,便于理解「PromptValueto_messages()model.invoke」的衔接。

1
2
3
4
5
6
7
8
9
10
11
12
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.prompt_values import ChatPromptValue

prompt_value = ChatPromptValue(
messages=[
SystemMessage(content="You are a helpful AI bot. Your name is Bob."),
HumanMessage(content="Hello, how are you doing?"),
AIMessage(content="I'm doing well, thanks!"),
HumanMessage(content="What is your name?"),
]
)
resp = model.invoke(prompt_value.to_messages())

2.5 写法四:元组列表与字典列表

除了显式使用 SystemMessageHumanMessage 等类,LangChain 聊天模型通常还支持两种常见简写。

  • 元组列表:每项写成 (role, content)
  • 字典列表:每项写成 {"role": "...", "content": "..."}

这两种写法与消息对象列表在语义上基本等价,只是更接近手写列表或 OpenAI 风格的数据结构。

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
import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.0,
base_url=os.getenv("OPENAI_BASE_URL"),
api_key=os.getenv("OPENAI_API_KEY"),
)

messages_as_tuples = [
("system", "你是一个专业的数学助手"),
("user", "你好,你是谁"),
]
messages_as_dicts = [
{"role": "system", "content": "你是一个专业的数学助手"},
{"role": "user", "content": "你好,你是谁"},
]

resp = llm.invoke(messages_as_tuples)
print(type(resp))
print(resp.content)

resp2 = llm.invoke(messages_as_dicts)
print(resp2.content)

关于选择策略,可按下面的思路理解:

  • 想学得最清楚:优先用 Message
  • 想写得最简洁:可以用元组列表
  • 想和 OpenAI 风格请求体对齐:可以用字典列表

无论哪种方式,同步 invoke 的返回值通常仍是 AIMessage

2.6 扩展:Java 生态中的多角色

“System / User / Assistant / Tool” 这套思路并不是 LangChain Python 独有的,Java 生态中的 LangChain4JSpring AI 也有类似设计。理解这一点很有价值,因为它说明:

多角色消息并不是某个框架的语法技巧,而是现代聊天模型交互的一种通用抽象。

LangChain4J:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package dev.langchain4j.data.message;

public enum ChatMessageType {
SYSTEM(SystemMessage.class),
USER(UserMessage.class),
AI(AiMessage.class),
TOOL_EXECUTION_RESULT(ToolExecutionResultMessage.class),
CUSTOM(CustomMessage.class);

private final Class<? extends ChatMessage> messageClass;

ChatMessageType(Class<? extends ChatMessage> messageClass) {
this.messageClass = messageClass;
}

public Class<? extends ChatMessage> messageClass() {
return messageClass;
}
}

Spring AI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.springframework.ai.chat.messages;

public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
TOOL("tool");

private final String value;

MessageType(String value) {
this.value = value;
}

public static MessageType fromValue(String value) { ... }

public String getValue() {
return this.value;
}
}

3、入参的消息类型

当你把输入组织成“消息列表”之后,就需要知道每种消息类型代表什么。在 LangChain 官方语境里,最核心的几类消息是:SystemMessageHumanMessageAIMessageToolMessage

文档https://docs.langchain.com/oss/python/langchain/messages (英文);https://docs.langchain.org.cn/oss/python/langchain/messages (中文)

3.1 四类核心消息

类型 说明 项目里最常见的用途
SystemMessage 系统消息,通常用于规定角色、风格、边界、输出格式 设定人设、回答规则、拒答策略
HumanMessage 用户消息,对应用户当前输入 放用户问题、补充条件、后续追问
AIMessage 模型回复消息 保存上一轮回复,支持多轮上下文
ToolMessage 工具执行结果消息 把外部工具/函数的返回结果回传给模型

需要注意两个细节:

  1. **LangChain 的类名叫 HumanMessage,但很多平台的角色字段写的是 user。**这不是冲突,而是不同层的命名习惯。你可以把它们理解成同一个角色。

  2. **旧版本资料里可能会看到 FunctionMessage。**在 LangChain 1.x 语境下,更常见的是 ToolMessage。如果你阅读旧教程或旧代码,看到这类差异,先把它理解为工具调用结果的旧命名即可。

3.2 SystemMessage 的作用

很多新手会把系统提示词和用户问题混在一段字符串里写,这样虽然也能跑,但可维护性很差。更稳妥的做法是把“规则”和“问题”拆开:

  • SystemMessage 负责定义系统层规则
  • HumanMessage 负责承载当前用户问题

例如:

  • “你是一个法律助手,只回答法律问题”
  • “输出请控制在 80 字内”
  • “如果超出范围,请明确拒答”

这些内容更适合放在 SystemMessage 里,因为它们属于“长期规则”,而不是某一轮具体问题。

这正对应了 1-2 提示词工程基础 里强调的那条原则:**稳定约束和动态输入要拆开写。**在 LangChain 里,这条原则最直接的落地方式就是把稳定规则放进 SystemMessage,把当前任务放进 HumanMessage

在真实项目里,SystemMessage 往往决定:

  • 回答口吻是否专业
  • 是否允许推测
  • 是否必须引用上下文
  • 是否必须按 JSON 输出
  • 是否需要保守拒答

这也是为什么 Prompt 工程里经常会说:系统提示词是行为边界,用户提示词是当前任务。

3.3 ToolMessage 什么时候会出现

ToolMessage 不会在普通问答里频繁出现,它更常见于函数调用、工具调用、Agent 编排场景。

简单理解就是:

  • 模型先在 AIMessage 里表达“我想调用某个工具”
  • 代码真的去执行工具
  • 工具结果再以 ToolMessage 的形式回传给模型

这样模型才能基于工具结果继续回答。

下面是一个简化示例:

1
2
3
4
5
6
7
8
9
10
11
12
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage

messages = [
SystemMessage(content="你是一位乐于助人的智能小助手"),
HumanMessage(content="你好,请你介绍一下你自己"),
AIMessage(content="我是一名人工智能助手,请问您有什么想问的吗?"),
ToolMessage(
content='{"population": 21540000, "area": "16410平方公里"}',
tool_call_id="call_abc123",
),
]
print(messages)

对当前章节来说,你只需要先建立基本印象:最常用的是 System / Human / AI 三类;ToolMessage 是后续 Agent、工具调用章节的重要铺垫。


4、调用大模型的调用方式

当输入组织好之后,下一步就是把它交给模型。LangChain 聊天模型常见的调用方式有四类:普通调用、流式调用、批量调用,以及它们各自的异步版本。

这部分看起来像 API 记忆题,其实可以用一个更直观的方式理解:

  • invoke / ainvoke:一次发一条
  • stream / astream:一边生成一边返回
  • batch / abatch:一次发很多条

4.1 普通调用(invoke / ainvoke)

【案例源码】案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke.pyLLM_aInvoke.py

  • invoke:同步调用,最常用,适合单轮问答与脚本演示。
  • ainvoke:异步调用,适合异步 Web 服务、并发任务和高吞吐场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, SystemMessage

model = init_chat_model(
model="qwen-plus",
model_provider="openai",
api_key=os.getenv("aliQwen-api"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
messages = [
SystemMessage(content="你是一个法律助手,只回答法律问题,超出范围回答:非法律问题无可奉告"),
HumanMessage(content="简单介绍下广告法,一句话 50 字以内")
]
response = model.invoke(messages)
print(type(response))
print(response.content)

实际项目里怎么选:

  • 命令行脚本、教学示例、简单后台任务:优先 invoke
  • FastAPI、异步服务、并发请求:优先 ainvoke
  • 本地开源模型(Ollama 等):实例化与端点配置见 第 12 章 Ollama 本地部署与调用,本章的 invoke / stream / batch 用法同样适用。

LLM_Invoke.py

LLM_aInvoke.py

4.2 流式调用(stream / astream)

【案例源码】案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Stream.pyLLM_aStream.py

  • stream:同步流式输出
  • astream:异步流式输出

流式的最大价值不是“更快算完”,而是更快把正在生成的内容展示给用户。在聊天机器人、报告生成、代码生成等场景里,用户体验会明显更好。

1
2
3
4
5
6
7
messages = [
SystemMessage(content="你叫小问,是一个乐于助人的AI助手"),
HumanMessage(content="你是谁")
]
for chunk in model.stream(messages):
print(chunk.content, end="", flush=True)
print()

真实项目里,stream / astream 很常用于:

  • 聊天界面的“打字机效果”
  • 长回答提前回显
  • 减少用户等待焦虑

LLM_Stream.py

LLM_aStream.py

4.3 批处理(batch / abatch)

【案例源码】案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Batch.pyLLM_aBatch.py

  • batch:一次提交多条输入,统一获得多条结果
  • abatch:异步批处理

它特别适合离线任务,而不是交互式聊天。例如:

  • 批量摘要一批文档
  • 批量清洗问答数据
  • 批量评估 Prompt 效果
  • 批量为商品、评论、工单做标签分类
1
2
3
4
5
6
7
questions = [
"什么是 Redis?简洁 100 字以内",
"Python 的生成器是做什么的?简洁 100 字以内",
]
response = model.batch(questions)
for q, r in zip(questions, response):
print(f"问题:{q}\n回答:{r.content}\n")

LLM_Batch.py

LLM_aBatch.py

4.4 小结

场景 同步 异步 适合什么情况
单条调用 invoke ainvoke 单轮问答、接口服务
流式输出 stream astream 聊天 UI、长文本生成
批量处理 batch abatch 离线任务、批量评估

如果你一时记不住,也没关系,先记住这条基本规则:

先会 invoke,再学 stream,最后再补 batch 和异步版本。


5、提示词模板概览

5.1 提示词简介

在真正的项目里,Prompt 几乎不可能永远写死在代码中。原因很现实:

  • 用户问题会变
  • 角色设定会变
  • 输出要求会变
  • 业务策略会调整
  • 团队成员需要一起维护

如果每次都手写一整段 Prompt,不仅容易重复,还非常难维护。提示词模板的作用,就是把固定部分沉淀下来,把变化部分改成变量,从而让同一套提示逻辑可以反复复用。

这和 Python 的 f-string 很像:

1
2
3
4
5
def hello(name: str) -> None:
print(f"你好:{name}")

if __name__ == "__main__":
hello("李四")

这里的 {name} 就像 Prompt 模板里的占位符,直接把它看成“先留坑,后填值”就行。

5.2 提示词模板类型

LangChain 中常见的提示词模板主要有下面几类,本课程重点掌握前两种即可:

类型 说明 本课程定位
PromptTemplate 面向纯文本模板,填值后通常得到一条字符串 重点掌握
ChatPromptTemplate 面向聊天模型的多角色模板,填值后得到多条消息 重点掌握
FewShotPromptTemplate 把若干“示例输入-输出”嵌入提示词 了解即可
PipelinePrompt 把多个子提示按顺序组合 了解即可

入门阶段可以先作一个简化理解:

  • 单条文本任务,先看 PromptTemplate
  • 聊天模型、多角色、多轮对话,重点看 ChatPromptTemplate

5.3 Few-shot 模板

Few-shot 的意思是:不要只告诉模型“按什么规则做”,还给它几组“输入应该怎么变成输出”的示例。

它适合下面这类场景:

  • 分类标签容易混,需要给几个标准样例
  • 输出风格有要求,单靠文字说明不够稳
  • 任务规则不复杂,但希望模型模仿固定格式

入门阶段不用急着背 FewShotPromptTemplate 的所有参数,先记住一句话:**Few-shot 是把示例变成提示词的一部分,让模型照着样子做。**等你后面做分类、抽取、客服话术生成时,再把它作为提高稳定性的手段即可。


6、文本提示词模板(PromptTemplate)

6.1 简介

PromptTemplate 是 LangChain 中最基础的模板类,适合把一段文本 Prompt 做成“固定骨架 + 动态变量”的形式。

它最适合的场景是:

  • 摘要、改写、翻译、分类等单轮文本任务
  • 还不需要明确区分 system / user 角色
  • 需要频繁替换少量变量

如果你把它和第 2 节联系起来看,会更容易理解:PromptTemplate 的结果通常还是字符串,只不过这条字符串不再靠手写,而是由模板生成。

6.2 常用参数

参数 说明
template 模板字符串,内部可包含 {变量名} 占位符
input_variables 调用时需要传入的变量名列表
partial_variables 在模板创建阶段就预先固定的一部分变量

其中,partial_variables 尤其值得理解。它的作用很直接:

把那些“经常不变”的变量先固定住,后续每次只传真正会变化的部分。

典型例子:

  • 系统角色长期固定为“Python 工程师”
  • 但用户问题每次都不同

这样一来,你就不用每次都重复传 role="Python 工程师"

6.3 常用方法

方法 返回值 适合场景 案例源码
format(...) str 最常用,拿到字符串后直接给模型或自己继续拼接 PromptTemplate_FormatMethod.py
invoke({...}) PromptValue 需要接入 LangChain 链时更自然 PromptTemplate_InvokeMethod.py
partial(...) 新的 PromptTemplate 先固定部分变量,再多次复用 PromptTemplate_PartialMethod.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_core.prompts import PromptTemplate

template = PromptTemplate.from_template(
"你是一个专业的{role}工程师,请回答我的问题,我的问题是:{question}"
)

# 1)format:得到 str
prompt_str = template.format(role="python开发", question="二分查找怎么写?")

# 2)invoke:得到 PromptValue
prompt_value = template.invoke({"role": "python开发", "question": "冒泡排序怎么写?"})
prompt_value.to_string()
prompt_value.to_messages()

# 3)partial:固定 role,得到新模板
new_template = template.partial(role="python开发")
prompt_str = new_template.format(question="快速排序怎么写?")

对初学者的实用建议是:

  • 刚入门时优先用 format
  • 做 LCEL 或链式调用时再逐渐理解 invoke
  • 同一模板长期复用时再考虑 partial

【案例源码】
format:案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_FormatMethod.py
invoke:案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_InvokeMethod.py
partial:案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_PartialMethod.py

PromptTemplate_FormatMethod.py

PromptTemplate_InvokeMethod.py

PromptTemplate_PartialMethod.py

6.4 创建方式

PromptTemplate 常见有两种创建方式:

  • 构造函数:手动指定 templateinput_variables
  • from_template(...):由 LangChain 自动推断变量名
1
2
3
4
5
6
7
8
9
10
11
12
from langchain_core.prompts import PromptTemplate

# 方式一:构造函数
template = PromptTemplate(
template="你是一个专业的{role}工程师,请回答:{question}",
input_variables=["role", "question"]
)
prompt = template.format(role="python开发", question="快速排序怎么写?")

# 方式二:from_template
template = PromptTemplate.from_template("请给我一个关于{topic}的{type}解释。")
prompt = template.format(topic="量子力学", type="详细")

从经验上看,可按下面的方式选择:

  • 模板简单:优先 from_template(...)
  • 你想显式表达变量名:用构造函数

【案例源码】构造函数:案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Constructor.py
【案例源码】from_template案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_FromTemplate.py

PromptTemplate_Constructor.py

PromptTemplate_FromTemplate.py

除了创建方式,本章还保留了两个补充案例。

第一,组合多个模板。
当一个 Prompt 由多个子部分拼起来时,可以通过 + 组合模板,而不是手写超长字符串。真实项目里,这在“角色说明 + 业务规则 + 当前任务”这种分段组织里很有用。

PromptTemplate_Combined.py

第二,比较 partial_variablespartial()
二者都能做到“先固定一部分变量,后续只传剩余变量”,但一个发生在模板创建阶段,一个发生在已有模板基础上。

PromptTemplate_PartialVariables.py


7、对话提示词模板(ChatPromptTemplate)

7.1 简介

如果说 PromptTemplate 更适合“单条文本输入”,那么 ChatPromptTemplate 就是为“聊天模型场景”准备的模板类。

它比 PromptTemplate 更贴合真实项目,因为真实聊天应用往往不只是“一句话”,而是:

  • 一条系统设定
  • 一条或多条用户消息
  • 可能还有历史 AI 回复
  • 可能再插入工具结果或历史上下文

这时,把所有内容写成一大段纯文本虽然也能跑,但不如把它们拆成带角色的消息来得清晰。

1
2
3
4
5
6
7
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

messages = [
SystemMessage(content="你是一个AI开发工程师"),
HumanMessage(content="你能开发哪些AI应用?"),
AIMessage(content="我能开发很多AI应用,比如聊天机器人、图像识别等")
]

所以你可以把 ChatPromptTemplate 简单理解成:“面向多角色消息的模板系统”。

7.2 常用参数

ChatPromptTemplate 的核心不是单个 template 字符串,而是一组“消息模板”。每一项都可以是下面这些形式:

类型 说明 案例源码
元组 ("system", "你是{name}") ChatPromptTemplate_TupleParam.py
字典 {"role": "system", "content": "你是{name}"} ChatPromptTemplate_DictParam.py
Message 类 SystemMessage(content="你是{name}") ChatPromptTemplate_MessageParam.py
MessagesPlaceholder 在模板中预留一段“消息列表占位” 见 7.5 节

三种常规写法里,没有绝对的谁对谁错,可以按下面的经验来选:

  • 元组:最简洁,教学和业务代码里都很常见
  • 字典:更贴近 OpenAI 风格 JSON,方便和网关数据结构对齐
  • Message 类:最显式,角色最清楚,适合教学和复杂场景

【案例源码】
元组:案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_TupleParam.py
字典:案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_DictParam.py
Message 类:案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_MessageParam.py

ChatPromptTemplate_TupleParam.py

ChatPromptTemplate_DictParam.py

ChatPromptTemplate_MessageParam.py

下面这个基础示例能帮助你直观看懂三种写法的共同点:它们都是在定义“系统说什么、用户说什么、哪些部分由运行时填值”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate

prompt1 = ChatPromptTemplate.from_messages([
("system", "你是助手,名字叫{name}。"),
("human", "{question}")
])

prompt2 = ChatPromptTemplate.from_messages([
{"role": "system", "content": "你是助手,名字叫{name}。"},
{"role": "user", "content": "{question}"}
])

prompt3 = ChatPromptTemplate.from_messages([
SystemMessage(content="你是助手,名字叫{name}。"),
HumanMessage(content="{question}")
])

7.3 常用方法

方法 返回值 使用建议
format_messages(...) List[BaseMessage] 最直观,得到消息列表后交给模型
invoke({...}) ChatPromptValue 适合与 LangChain 链条衔接,也可直接交给模型
format(...) str 适合查看最终拼接效果,不推荐作为聊天主写法

这三个方法最容易混淆,建议这样理解:

  • format_messages:我要的是“消息列表”
  • invoke:我要的是“PromptValue 对象”
  • format:我要的是“纯字符串”
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
from langchain_core.prompts import ChatPromptTemplate

chat_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个{role},请回答我提出的问题"),
("human", "请回答:{question}")
])

# 方式一:得到消息列表
messages = chat_prompt.format_messages(
role="python开发工程师",
question="堆排序怎么写"
)
result = model.invoke(messages)

# 方式二:得到 ChatPromptValue
prompt_value = chat_prompt.invoke({
"role": "python开发工程师",
"question": "快速排序怎么写"
})
result = model.invoke(prompt_value)

# 方式三:得到纯字符串
prompt_str = chat_prompt.format(
role="python开发工程师",
question="快速排序怎么写"
)
print(prompt_str)

对实际项目来说,建议优先采用这两种:

  • format_messages(...) -> model.invoke(messages)
  • invoke({...}) -> model.invoke(prompt_value)

因为这两种方式都能保留清晰的消息角色结构。

【案例源码】案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_FormatMessages.py

ChatPromptTemplate_FormatMessages.py

7.4 创建方式

ChatPromptTemplate 常见也有两种创建方式:

  • ChatPromptTemplate.from_messages([...])
  • ChatPromptTemplate([...])

它们的核心差别不大,本质上都是把一组消息模板交给 ChatPromptTemplate

1
2
3
4
5
6
7
8
9
10
11
12
from langchain_core.prompts import ChatPromptTemplate

messages = [
("system", "你是一个{role},请回答我提出的问题"),
("human", "请回答:{question}")
]

chat_prompt1 = ChatPromptTemplate.from_messages(messages)
chat_prompt2 = ChatPromptTemplate(messages)

print(chat_prompt1.format_messages(role="python开发工程师", question="堆排序怎么写"))
print(chat_prompt2.format_messages(role="python开发工程师", question="堆排序怎么写"))

经验上更推荐优先使用 from_messages(...),因为可读性更好,也更符合官方文档和社区示例的主流写法。

【案例源码】案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_Constructor.py

ChatPromptTemplate_Constructor.py

7.5 MessagesPlaceholder

MessagesPlaceholder(消息占位符)这是本章最重要的知识点之一,很多人一开始会觉得它抽象,但一旦理解,就会发现它几乎是多轮对话、记忆、历史上下文拼接的关键。

先说结论:MessagesPlaceholder 的作用,就是在模板里先留出一段“消息列表的位置”,等真正调用时再把历史对话整块塞进去。

MessagesPlaceholder 将历史消息动态插入 ChatPromptTemplate:模板先留占位,运行时再填入多轮消息

为什么它重要?因为真实项目里的“历史对话”往往不是固定写死的:

  • 有时只有 2 轮
  • 有时有 10 轮
  • 有时还要先裁剪、总结、过滤

如果没有占位符,你就只能在代码里手动拼接 HumanMessageAIMessage,又乱又难维护。

它常见有两种写法:

  • 显式写法MessagesPlaceholder("memory")
  • 隐式写法("placeholder", "{memory}")

二者的核心思想完全一样,只是语法风格不同。

典型结构通常是:

  • 系统设定
  • 历史消息占位
  • 当前用户问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
("system", "你是一个资深的Python应用开发工程师,请认真回答我提出的Python相关的问题"),
MessagesPlaceholder("memory"),
("human", "{question}")
])

prompt_value = prompt.invoke({
"memory": [
HumanMessage(content="我的名字叫亮仔,是一名程序员"),
AIMessage(content="好的,亮仔你好")
],
"question": "请问我的名字叫什么?"
})

print(prompt_value.to_string())

这段代码的价值在于,它让模板本身变得非常稳定,而把“历史有几轮、具体内容是什么”延迟到运行时再决定。后面学 第 16 章 记忆与对话历史 时,你会频繁看到这种模式。

【案例源码】
显式:案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ExplicitPlaceholder.py
隐式:案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ImplicitPlaceholder.py

ChatPromptTemplate_ExplicitPlaceholder.py

ChatPromptTemplate_ImplicitPlaceholder.py


8、从文件加载提示词

当 Prompt 还很短时,把它直接写在 Python 代码里问题不大;但只要你开始做真实项目,就会很快遇到几个问题:Prompt 越来越长,代码越来越乱;产品、运营、算法同学希望一起改 Prompt;需要保留多个版本做 A/B 测试;想把 Prompt 与代码逻辑分离。

这时候,把 Prompt 放到 JSON / YAML 等外部文件中,就会更工程化。LangChain 提供了 load_prompt(...),可以根据文件内容加载为模板对象。对于本章案例来说,最常见的是 _type: "prompt",即加载为 PromptTemplate

8.1 从 JSON 加载

1
2
3
4
5
{
"_type": "prompt",
"input_variables": ["name", "what"],
"template": "请{name}讲一个{what}的故事"
}
1
2
3
4
from langchain_core.prompts import load_prompt

template = load_prompt("prompt.json", encoding="utf-8")
print(template.format(name="张三", what="搞笑"))

8.2 从 YAML 加载

YAML 与 JSON 的核心思路完全相同,只是文件格式更适合人工阅读,也更方便写注释。对团队协作来说,很多场景会更喜欢 YAML。

运行这类案例时,先确认当前工作目录,否则相对路径可能找不到文件。这一点和 第 10 章 HelloWorld 中的 .env 读取问题本质很像,都是“脚本运行位置”和“相对路径”的关系。

【案例源码】
JSON:案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo01.py
YAML:案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo02.py
配套文件:prompt.jsonprompt.yaml

PromptLoadDemo01.py

PromptLoadDemo02.py

到这里你可以看到,本章知识已经形成了一个比较完整的工程化闭环:

  • 消息类型 解决“输入按什么角色组织”
  • 调用方式 解决“输入如何发给模型”
  • PromptTemplate / ChatPromptTemplate 解决“输入如何复用”
  • MessagesPlaceholder 解决“历史对话如何动态插入”
  • JSON / YAML 外置文件 解决“模板如何协作与版本管理”

章节思考题:

  1. 普通字符串 Prompt 和消息模板最大的工程差别是什么?

    参考思路: 普通字符串适合临时调用,消息模板更适合长期维护。它把系统规则、用户输入、历史消息和变量占位分清楚,后续改规则、换输入、接多轮历史都会更稳。

  2. 什么时候应该用 MessagesPlaceholder,而不是把历史对话拼成一大段字符串?

    参考思路: 需要保留消息角色、顺序和多轮结构时,应使用 MessagesPlaceholder。拼字符串会丢掉角色信息,也不利于后续和记忆、工具调用、消息对象体系衔接。

  3. 如果一个模板变量越来越多,你会如何判断它是不是该拆分?

    参考思路: 看变量是否服务同一个任务、是否来自同一层上下文、是否经常一起变化。如果一个模板同时管规则、业务输入、检索结果、历史消息和输出格式,可能就该拆成更小的模板或链路节点。

  4. 把提示词放到外部文件有什么好处和风险?

    参考思路: 好处是便于版本管理、运营调整和复用;风险是变量名、格式和代码调用容易不一致。外置后要配套校验、示例输入和变更记录,不能只把文本搬出去。

本章小结:

  • Prompt 的本质:Prompt 不是“随便写一句话”,而是对模型输入进行结构化组织。随着项目复杂度提升,输入会从纯字符串演化成多角色消息,再进一步演化成模板、占位符与外部配置文件。
  • 消息与调用:聊天模型常见输入包括 str、消息对象列表、元组列表、字典列表;常见调用方式包括 invoke / ainvokestream / astreambatch / abatch。返回值通常是 AIMessage,正文一般通过 .content 读取。
  • 模板与工程化PromptTemplate 适合文本模板,ChatPromptTemplate 适合聊天模型与多角色场景,MessagesPlaceholder 是多轮历史拼接的关键;将 Prompt 放入 JSON / YAML 更适合真实项目中的版本管理、多人协作与 A/B 测试。
  • 学完本章后,你至少应该能区分四类输入组织方式:纯字符串、消息列表、模板 + 占位符、外部文件加载;也要知道 PromptTemplateChatPromptTemplateMessagesPlaceholder 分别适合什么场景。

建议下一步: 继续学习 第 14 章 输出解析器,把本章的“输入组织”与下一章的“输出结构化”连起来;再配合 第 15 章 LCEL 与链式调用,就能形成 LangChain 中最核心的“输入 -> 模型 -> 输出 -> 链式编排”主线。如果你读到这里,仍然对“为什么要这样拆 Prompt”不够踏实,也可以回看 1-2 提示词工程基础 中的六要素、Few-shot 和结构化组织方式部分。