主题
Prompt Template
要点
- Prompt Template 的作用是把「固定结构」和「变化变量」分开写,让每一轮输入都能稳定生成。
- 最简单的用法是用
ChatPromptTemplate.fromMessages()定义消息模板,再用formatMessages()注入变量。 - 多轮对话场景里,用
MessagesPlaceholder给历史消息留插槽,避免把历史消息硬编码进模板字符串。 systemPrompt和 Prompt Template 不要重复:长期设定放systemPrompt,当前轮次上下文放模板。
1. 背景:为什么需要 Prompt Template
消息协议解决的是「输入怎么组织成结构化消息」。接下来要解决的问题是:这些消息里的内容,哪些部分每次请求都一样,哪些部分每次都变?
直接看一个最小 Agent 调用:
typescript
const stream = await agent.stream(
{
messages: [
{
role: "user",
content: "解释一下消息协议。",
},
],
},
{
streamMode: "messages",
},
);这段代码在最小示例里没问题。但只要场景稍微复杂一点,输入很快就不止一条 user 消息。同一轮请求里可能还会有:
- 用户身份或角色
- 当前任务场景
- 额外补充说明
- 历史消息
- 固定的输出格式要求
这些东西继续手写在消息数组里,代码会越来越散。Prompt Template 的作用,就是先把这一轮输入整理好,再交给 Agent。
2. 用 ChatPromptTemplate 组织当前输入
假设有一个任务处理型 Agent,已经定义好了默认设定:
typescript
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
apiKey: process.env.MODEL_API_KEY,
model: process.env.MODEL_NAME ?? "deepseek-chat",
configuration: {
baseURL: process.env.MODEL_BASE_URL ?? "https://api.deepseek.com/v1",
},
});
const agent = createAgent({
model,
tools: [],
systemPrompt: "你是一名专业、简洁的技术任务助手。",
});现在这一轮请求里,想补充几项动态信息:
- 任务类型
- 当前上下文
- 本轮输入
可以用模板把它们组织成一条 user 消息:
typescript
import { ChatPromptTemplate } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
[
"user",
["任务类型:{taskType}", "当前上下文:{context}", "本轮输入:{input}"].join(
"\n",
),
],
]);调用时先把变量格式化成消息:
typescript
const messages = await prompt.formatMessages({
taskType: "代码审查",
context: "正在 review 一个 Node.js 服务端接口的 PR",
input: "帮我看看这段错误处理是否有遗漏。",
});再把整理好的消息交给 Agent:
typescript
const stream = await agent.stream(
{
messages,
},
{
streamMode: "messages",
},
);这就是 Prompt Template 最核心的用法:先组织输入,再让 Agent 根据这份输入继续运行。
3. 模板解决的是固定部分与变化部分分离
如果不用模板,这一轮输入通常会直接写成这样:
typescript
const messages = [
{
role: "user",
content: [
"任务类型:代码审查",
"当前上下文:正在 review 一个 Node.js 服务端接口的 PR",
"本轮输入:帮我看看这段错误处理是否有遗漏。",
].join("\n"),
},
];只写一次当然没问题。但只要有变量变化,这段结构就要反复手写。Prompt Template 的写法在维护上更友好:
typescript
const prompt = ChatPromptTemplate.fromMessages([
[
"user",
["任务类型:{taskType}", "当前上下文:{context}", "本轮输入:{input}"].join(
"\n",
),
],
]);真正固定下来的是消息结构。真正变化的是 taskType、context、input 这些变量。后面要改字段、加字段、删字段,都会轻松很多。
4. formatMessages() 与 agent.stream() 是两步
这一步最好分清楚,不然后面很容易写乱。
模板调用:
typescript
const messages = await prompt.formatMessages({
taskType: "代码审查",
context: "正在 review 一个 Node.js 服务端接口的 PR",
input: "帮我看看这段错误处理是否有遗漏。",
});这里拿到的还不是模型回复,也不是 Agent 回复,而是格式化后的消息数组。可以先打印出来排查:
typescript
console.log(messages);这一步特别适合排查输入问题:变量有没有替换对,消息顺序对不对,一眼就能看出来。
真正让 Agent 开始工作的是下一步:
typescript
const stream = await agent.stream(
{
messages,
},
{
streamMode: "messages",
},
);分工很明确:
prompt.formatMessages(...):生成这一轮的消息输入。agent.stream(...)/agent.invoke(...):让 Agent 开始处理这一轮输入。
5. 多轮对话里用 MessagesPlaceholder
只有当前输入时,模板还比较简单。一旦进入多轮对话,历史消息就不能继续手塞在模板字符串里了。这时候要用 MessagesPlaceholder 给历史消息留一个插槽。
typescript
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
new MessagesPlaceholder({
variableName: "history",
optional: true,
}),
[
"user",
["任务类型:{taskType}", "当前上下文:{context}", "本轮输入:{input}"].join(
"\n",
),
],
]);这个模板的意思很直接:前面插入历史消息,后面放当前这一轮输入。调用时再把历史消息传进去:
typescript
const messages = await prompt.formatMessages({
history: [
{
role: "user",
content: "今天开会又改需求了。",
},
{
role: "assistant",
content: "最卡的是哪一段?",
},
],
taskType: "需求分析",
context: "正在梳理一个后端接口的变更范围",
input: "最烦的是昨天刚定下来,今天又推翻了。",
});整理好之后,再交给 Agent:
typescript
const result = await agent.invoke({
messages,
});这样历史消息和当前输入的顺序就稳定下来了,不需要每次手动拼。
6. systemPrompt 与模板不要重复
这里有一个常见坑。Agent 本身已经有 systemPrompt:
typescript
const agent = createAgent({
model,
tools: [],
systemPrompt: "你是一名专业、简洁的技术任务助手。",
});这时候模板里就不要再重复写一大段完全一样的 system 消息了。更稳的分工是:
- 长期稳定的人设、规则,放进
systemPrompt。 - 当前这一轮才会变化的上下文,放进 Prompt Template。
这样做的好处是:
- Agent 的长期设定固定在一个地方。
- 每一轮请求的动态输入由模板管理。
- 后面调整时不容易重复或冲突。
7. 总结
Prompt Template 的使用顺序可以压缩成下面四步:
- 先创建 Agent。
- 再用
ChatPromptTemplate整理这一轮输入。 - 如果有历史消息,就加
MessagesPlaceholder。 - 把模板生成的消息交给
agent.invoke()或agent.stream()。
写成代码,大致是这个形状:
typescript
const messages = await prompt.formatMessages({
history: [
{
role: "user",
content: "今天开会又改需求了。",
},
{
role: "assistant",
content: "最卡的是哪一段?",
},
],
taskType: "需求分析",
context: "正在梳理一个后端接口的变更范围",
input: "最烦的是昨天刚定下来,今天又推翻了。",
});
const stream = await agent.stream(
{
messages,
},
{
streamMode: "messages",
},
);