第 12 课: Prompts — 提示词模板系统

0 阅读6分钟

课程目标

掌握 LangChain.js 的 Prompt 模板体系:ChatPromptTemplate 的创建与变量填充、MessagesPlaceholder 动态消息注入、FewShotPromptTemplate 动态示例、PipelinePromptTemplate 多模板组合、ImagePromptTemplate 图片提示,以及 PromptValue 中间表示层。


12.1 Prompt 的 Runnable 本质

Prompt 模板在 LangChain.js 中不只是字符串拼接工具,它是一个完整的 Runnable

输入: Record<string, any>(变量字典)
输出: ChatPromptValue(包装了 BaseMessage[] 的中间对象)

这意味着 Prompt 可以直接参与 pipe() 组合:

const chain = prompt.pipe(model).pipe(parser);

源码位置: libs/langchain-core/src/prompts/chat.ts

继承关系:

BasePromptTemplate                     // 所有 Prompt 的抽象基类
  ├── BaseChatPromptTemplate           // Chat 类型 Prompt 基类
  │     └── ChatPromptTemplate         // 最常用的聊天模板
  │     └── FewShotChatMessagePromptTemplate
  ├── BaseStringPromptTemplate         // 字符串输出的 Prompt 基类
  │     ├── PromptTemplate             // 基础字符串模板
  │     └── FewShotPromptTemplate      // Few-shot 字符串模板
  └── PipelinePromptTemplate           // 管道组合模板

12.2 ChatPromptTemplate — 核心模板

12.2.1 fromMessages 创建

最常用的方式是 ChatPromptTemplate.fromMessages(),接受消息模板数组:

import { ChatPromptTemplate } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant that translates {input_language} to {output_language}."],
  ["human", "{input}"],
]);

// prompt 是一个 Runnable,调用 invoke 返回 ChatPromptValue
const result = await prompt.invoke({
  input_language: "English",
  output_language: "Chinese",
  input: "Hello, how are you?",
});
// result.messages → [SystemMessage, HumanMessage]

源码关键: fromMessages 内部会将元组 ["human", "..."] 转换为 HumanMessagePromptTemplate 实例,通过 _coerceMessagePromptTemplateLike 函数完成。

12.2.2 变量占位符 {variable}

使用 f-string 风格的 {variable} 语法。框架在构造时会自动提取 inputVariables 并做校验:

// 构造函数中的校验逻辑(chat.ts 第 960-1001 行)
// 会检查 inputVariables 和 promptMessages 中的变量是否匹配
// 不匹配时抛出错误

12.2.3 formatMessages 核心方法

ChatPromptTemplate.formatMessages() 是核心方法,遍历所有 promptMessages,逐个格式化:

// 源码简化(chat.ts 第 1060-1105 行)
async formatMessages(values): Promise<BaseMessage[]> {
  const allValues = await this.mergePartialAndUserVariables(values);
  let resultMessages: BaseMessage[] = [];
  for (const promptMessage of this.promptMessages) {
    if (promptMessage instanceof BaseMessage) {
      // 静态消息直接加入
      resultMessages.push(promptMessage);
    } else {
      // 模板消息需要格式化
      const message = await promptMessage.formatMessages(inputValues);
      resultMessages = resultMessages.concat(message);
    }
  }
  return resultMessages;
}

12.2.4 partial — 预填充部分变量

const fullPrompt = ChatPromptTemplate.fromMessages([
  ["system", "You speak {language}. Your name is {name}."],
  ["human", "{question}"],
]);

// 预填充 language,返回只需 name 和 question 的新模板
const partialPrompt = await fullPrompt.partial({ language: "Chinese" });
const result = await partialPrompt.invoke({ name: "Assistant", question: "Hi" });

12.3 MessagesPlaceholder — 动态消息注入

源码位置: libs/langchain-core/src/prompts/chat.ts 第 101-180 行

MessagesPlaceholder 用于在模板中预留一个"插槽",运行时注入任意数量的消息。最典型的用途是注入对话历史(配合第 10 课的 RunnableWithMessageHistory)。

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant."],
  new MessagesPlaceholder("history"),
  ["human", "{input}"],
]);

const result = await prompt.invoke({
  history: [
    new HumanMessage("What is 1+1?"),
    new AIMessage("1+1 = 2"),
  ],
  input: "And what is 2+2?",
});
// result.messages → [SystemMessage, HumanMessage("What is 1+1?"), AIMessage("1+1 = 2"), HumanMessage("And what is 2+2?")]

12.3.1 optional 参数

// 默认情况下,如果变量缺失会抛出错误
// 设置 optional: true 后,缺失时返回空数组
new MessagesPlaceholder({ variableName: "history", optional: true });

12.3.2 元组简写

可以用 ["placeholder", "{history}"] 元组形式替代手动创建:

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant."],
  ["placeholder", "{history}"],  // 等效于 new MessagesPlaceholder({ variableName: "history", optional: true })
  ["human", "{input}"],
]);

注意:用元组形式时默认 optional: true


12.4 PromptValue — 中间表示层

源码位置: libs/langchain-core/src/prompt_values.ts

PromptValue 是 Prompt 格式化后、传入 Model 前的中间数据结构。它提供两种输出方式:

abstract class BasePromptValue extends Serializable {
  abstract toString(): string;           // 转为纯文本(给 LLM 用)
  abstract toChatMessages(): BaseMessage[]; // 转为消息列表(给 ChatModel 用)
}

三种具体实现:

用途toString()toChatMessages()
StringPromptValue纯文本模板结果原始字符串包装为 [HumanMessage]
ChatPromptValue聊天模板结果getBufferString(messages)原始消息列表
ImagePromptValue图片模板结果图片 URL包装为多模态 HumanMessage

这个设计让同一个 Prompt 既能对接 BaseLLM(需要字符串),又能对接 BaseChatModel(需要消息列表)。


12.5 FewShotPromptTemplate — 动态 Few-Shot 示例

源码位置: libs/langchain-core/src/prompts/few_shot.ts

12.5.1 静态示例

import { FewShotPromptTemplate } from "@langchain/core/prompts";
import { PromptTemplate } from "@langchain/core/prompts";

const examplePrompt = PromptTemplate.fromTemplate(
  "Input: {input}\nOutput: {output}"
);

const fewShotPrompt = new FewShotPromptTemplate({
  examples: [
    { input: "happy", output: "sad" },
    { input: "tall", output: "short" },
  ],
  examplePrompt,
  prefix: "Give the antonym of every input.",
  suffix: "Input: {adjective}\nOutput:",
  inputVariables: ["adjective"],
});

const result = await fewShotPrompt.format({ adjective: "big" });
// "Give the antonym of every input.\n\nInput: happy\nOutput: sad\n\nInput: tall\nOutput: short\n\nInput: big\nOutput:"

12.5.2 动态示例(配合 ExampleSelector)

const fewShotPrompt = new FewShotPromptTemplate({
  exampleSelector: mySelector,  // 第 13 课详解
  examplePrompt,
  prefix: "Give the antonym of every input.",
  suffix: "Input: {adjective}\nOutput:",
  inputVariables: ["adjective"],
});

源码逻辑(few_shot.ts 第 167-180 行):

private async getExamples(inputVariables: InputValues): Promise<InputValues[]> {
  if (this.examples !== undefined) {
    return this.examples;        // 静态示例直接返回
  }
  if (this.exampleSelector !== undefined) {
    return this.exampleSelector.selectExamples(inputVariables); // 动态选择
  }
  throw new Error("One of 'examples' and 'example_selector' should be provided");
}

12.5.3 FewShotChatMessagePromptTemplate

用于聊天场景的 Few-Shot 模板,输出 BaseMessage[] 而非字符串:

import { FewShotChatMessagePromptTemplate } from "@langchain/core/prompts";

const fewShotChatPrompt = new FewShotChatMessagePromptTemplate({
  examples: [
    { input: "happy", output: "sad" },
    { input: "tall", output: "short" },
  ],
  examplePrompt: ChatPromptTemplate.fromMessages([
    ["human", "{input}"],
    ["ai", "{output}"],
  ]),
  inputVariables: [],
});

const messages = await fewShotChatPrompt.formatMessages({});
// [HumanMessage("happy"), AIMessage("sad"), HumanMessage("tall"), AIMessage("short")]

12.6 PipelinePromptTemplate — 模板组合

源码位置: libs/langchain-core/src/prompts/pipeline.ts

PipelinePromptTemplate 将多个子模板的输出作为变量传递给最终模板,实现模板的模块化组合:

import { PipelinePromptTemplate } from "@langchain/core/prompts";
import { PromptTemplate } from "@langchain/core/prompts";

const introPrompt = PromptTemplate.fromTemplate("You are impersonating {person}.");
const examplePrompt = PromptTemplate.fromTemplate(
  "Here's an example:\nQ: {example_q}\nA: {example_a}"
);
const startPrompt = PromptTemplate.fromTemplate("Now answer:\nQ: {input}\nA:");

const finalPrompt = PromptTemplate.fromTemplate(
  "{introduction}\n{example}\n{start}"
);

const pipeline = new PipelinePromptTemplate({
  pipelinePrompts: [
    { name: "introduction", prompt: introPrompt },
    { name: "example", prompt: examplePrompt },
    { name: "start", prompt: startPrompt },
  ],
  finalPrompt,
});

const result = await pipeline.format({
  person: "Elon Musk",
  example_q: "What's your favorite car?",
  example_a: "Tesla",
  input: "What's your favorite social media site?",
});

核心流程pipeline.ts 第 124-150 行):

  1. 遍历每个 pipelinePrompt,用当前所有值格式化其输出
  2. 将输出结果作为新变量名加入 allValues
  3. 最终用 allValues 格式化 finalPrompt

computeInputValues() 方法自动剔除中间变量名,只保留真正需要用户提供的输入变量。


12.7 ImagePromptTemplate — 图片提示

源码位置: libs/langchain-core/src/prompts/image.ts

用于构造包含图片的多模态提示:

import { HumanMessagePromptTemplate } from "@langchain/core/prompts";

// 在 HumanMessagePromptTemplate.fromTemplate 中使用 image_url
const messageTemplate = HumanMessagePromptTemplate.fromTemplate([
  { text: "Describe this image:" },
  { image_url: "{image_url}" },
]);

const message = await messageTemplate.format({
  image_url: "https://example.com/cat.jpg",
});
// HumanMessage({ content: [
//   { type: "text", text: "Describe this image:" },
//   { type: "image_url", image_url: { url: "https://example.com/cat.jpg" } }
// ]})

ImagePromptTemplate 继承 BasePromptTemplate,其 format() 方法渲染 template 中的变量,返回 ImageContent(包含 url 和可选的 detail 字段)。


12.8 消息模板类型体系

_StringImageMessagePromptTemplate 是内部基类,为每种消息角色提供了模板子类:

模板类生成的消息类型使用场景
HumanMessagePromptTemplateHumanMessage用户输入模板
AIMessagePromptTemplateAIMessageAI 回复模板
SystemMessagePromptTemplateSystemMessage系统指令模板
ChatMessagePromptTemplateChatMessage自定义角色模板

这些类都支持 fromTemplate() 静态方法,接受字符串或多模态内容数组。


12.9 实战练习

练习 1:带历史的对话模板

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are {role}. Respond in {language}."],
  new MessagesPlaceholder({ variableName: "chat_history", optional: true }),
  ["human", "{question}"],
]);

const messages = await prompt.formatMessages({
  role: "a math tutor",
  language: "Chinese",
  chat_history: [
    new HumanMessage("What is 1+1?"),
    new AIMessage("1+1 = 2"),
  ],
  question: "What is 2+2?",
});

练习 2:Few-Shot + ChatPromptTemplate 组合

import { FewShotChatMessagePromptTemplate, ChatPromptTemplate } from "@langchain/core/prompts";

const examplePrompt = ChatPromptTemplate.fromMessages([
  ["human", "{input}"],
  ["ai", "{output}"],
]);

const fewShot = new FewShotChatMessagePromptTemplate({
  examples: [
    { input: "2+2", output: "4" },
    { input: "3+3", output: "6" },
  ],
  examplePrompt,
  inputVariables: [],
});

const fullPrompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a calculator. Follow the examples:"],
  fewShot,
  ["human", "{question}"],
]);

const result = await fullPrompt.invoke({ question: "5+5" });

12.10 源码精读路线

优先级文件关注点
P0prompts/chat.tsChatPromptTemplate.fromMessages(), formatMessages(), MessagesPlaceholder
P0prompt_values.tsChatPromptValue, StringPromptValue, ImagePromptValue
P1prompts/few_shot.tsFewShotPromptTemplate, FewShotChatMessagePromptTemplate
P1prompts/pipeline.tsPipelinePromptTemplate, formatPipelinePrompts()
P2prompts/image.tsImagePromptTemplate
P2prompts/prompt.tsPromptTemplate(基础字符串模板)
P2prompts/base.tsBasePromptTemplate 抽象基类

本课收获总结

级别你应该掌握的
🟢 基础掌握 ChatPromptTemplate.fromMessages() 创建模板和 {variable} 变量填充
🔵 中阶理解 MessagesPlaceholder 如何与对话历史(第 10 课)配合动态注入消息
🟡 高阶掌握 PipelinePromptTemplate 的多模板组合模式和 FewShotChatMessagePromptTemplate
🟠 资深分析 Prompt 作为 Runnable 的设计:输入是变量字典,输出经 PromptValue 中间层适配不同模型
🔴 架构设计可复用的 Prompt 库:版本管理、模块化组合、partial() 预填充策略

下一课预告

第 13 课讲 ExampleSelector 与高级 Few-Shot——当有大量示例时,如何用 LengthBasedExampleSelectorSemanticSimilarityExampleSelector 智能选择最相关的几个?