Skip to content

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",
    ),
  ],
]);

真正固定下来的是消息结构。真正变化的是 taskTypecontextinput 这些变量。后面要改字段、加字段、删字段,都会轻松很多。

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 的使用顺序可以压缩成下面四步:

  1. 先创建 Agent。
  2. 再用 ChatPromptTemplate 整理这一轮输入。
  3. 如果有历史消息,就加 MessagesPlaceholder
  4. 把模板生成的消息交给 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",
  },
);

基于 MIT 协议开源