Prompt Template

Prompt 是大模型的核心,传统方式我们一般使用的是字符串拼接或者模板字符串来构造 Prompt,但是有了 LangChain 之后,我们可以构建可以复用的 Prompt 来让我们更加工程化的管理和构建 Prompt,进而制作较为复杂的 chat bot

基础 Prompt

PromptTemplate是帮助我们自定义一个包含变量的字符串模板,可以通过向该类的对象输入不同的变量值来生成模板渲染的结果。这样可以方便的定义一组的模板,然后在运行的时候根据用户的动态输入填充变量进而生成 Prompt

没有变量的 Template

先从没有变量的最基础的 template 开始吧

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

const greetingPrompt = new PromptTemplate({
    inputVariables: [],
    template: "hello, world!"
})

const formattedGreetingPrompt = await greetingPrompt.format({});

console.log(formattedGreetingPrompt)

这里运行代码块儿会出现hello, world!的输出结果,这就是一个最基础的 template,不传入任何的变量:inputVariables: []。这就如同硬编码一个字符串。调用 template 的方式就是.format,因为我们没有任何的变量,所以就是一个空对象

有变量的 Template

const multiGreetingPrompt = new PromptTemplate({
       inputVariables: ["timeOfDay", "name"],
       template: "good {timeOfDay}, {name}, {{test}}"
   });
   
   const formattedMultiGreetingPrompt = await multiGreetingPrompt.format({
       timeOfDay: "afternoon",
       name: "furina"
   })
   
   console.log(formattedMultiGreetingPrompt)

这个也蛮好懂的,用{}来包裹住变量,随后在inputVariables声明用到的变量名称,因为有了变量,所以在format()就需要传入对应的变量。

这个变量可以如同例子一样为复数个。

唯一需要注意的点是:如果你的Prompt里面需要有{}不想让它被替换成变量的话,可以使用{{}}这样的写法转义。

这么创建Template也是有点繁琐了,LangChain给了我们一个不错的简便创建方式:

这样创建Prompt的时候,会自动从字符串中推测出来需要输入的变量。

使用部分参数创建template

我们不需要一次性把所有的变量全部输入,在实际工程中,我们很大概率是会先获得一个参数,然后才获得另一个参数。类似于函数式编程的那种概念,我们给需要两个参数的Prompt Template传递一个参数之后,就会生成只需要一个参数的新的Prompt Templat。

使用动态填充参数

当我们需要一个Prompt Templat被format之后,实时地动态生成参数,这个时候就可以使用函数来对Templat的部分参数进行指定了。

函数getCurrentDate是在format被调用的时候实时运行的,也就是说我们可以在被渲染成字符串的时候获取到最新的外部信息。这里如果想要传递函数,可以使用JS的闭包来传递参数。

加入我们有一个根据时间段(早上,中午,晚上)返回不同的问候语的,并且要带上当前时间的需求:

const getCurrentDateStr = () => {
       return new Date().toLocaleDateString();
   }
   
   const generateGreeting = (timeOfDay: string) => {
       return () => {
           const date = getCurrentDateStr()
   
           switch (timeOfDay) {
               case '早上':
                   return date + ' 早上好';
               case '中午':
                   return date + ' 中午好';
               case '晚上':
                   return date + ' 晚上好';
               default:
                   return '你好'
           }
       }
   }
   
   const prompt = PromptTemplate.fromTemplate('{greeting}!')
   
   const currentTimeOfDay = '中午';
   
   const partialGreetingPrompt = await prompt.partial({
       greeting: generateGreeting(currentTimeOfDay)
   })
   
   const formattedGreetingPromptWithParam = await partialGreetingPrompt.format({});
   
   console.log(formattedGreetingPromptWithParam)
   // 12/5/2024 中午好!

也就是说,基于JavaScript的灵活性,我们可以实现官方API不支持的玩法。

chat Prompt

前面介绍到的Prompt Templat只能说是引入门的作用,因为Chat API才是目前跟LLM交互的最主要的形式,ChatPromptTemplate是最常用的工具。

在跟各种聊天模式交互的时候,构建聊天信息时,不仅仅包含了上面这样的文本内容,还需要与每条信息关联的角色信息。

比如:这条信息是由人类、AI、还是给chatbot指定的系统消息。这一类的结构化信息输入有利于大模型更好地理解对话的上下文和流程,从而生成更精准,更加自然的回复。

为了方便地构建和处理结构化的聊天信息,LangChain给出了下面四种聊天模板:

  • ChatPromptTemplate
  • SystemMessagePromptTemplate
  • AIMessagePromptTemplate
  • HumanMessagePromptTemplate
    后面三种分别对应了一段ChatMessage不同的角色,在OpenAI的定义里,每一条消息都需要一个role(角色)关联,表示消息的发送者,角色的概念对于LLM理解与构建对话流程是非常的重要的,相同的内容经由不同的角色发送的意义是不一样的。
  • system角色的消息通常用于设置对话的上下文或者指定模型采取特定的行为模式。这些消息不会显示在对话里,但是他们对大模型的行为有很重要的指导作用。可以理解成大模型的元消息,有非常高的权重,在这个角色的消息里成功构建Prompt的话可以取得很好的效果
  • user角色代表的是真实用户在对话里的发言,这些消息通常是提问,指令,或者评价,反映的是用户的行为和意图
  • assistant角色就是AI模型的回复了,这些消息是LLM根据system的指示和user的问题输入来生成的

比如我们做一个比较基础的翻译bot,来讲解一下这些Chat Templat:

import { SystemMessagePromptTemplate, HumanMessagePromptTemplate, ChatPromptTemplate} from '@langchain/core/prompts'
   
   const translateInstructionTemplate = SystemMessagePromptTemplate.fromTemplate(`你是一个专业的翻译员,你的任务是把文本从{source_lang}翻译到{target_lang}。`)
   
   const userQuestionTemplate = HumanMessagePromptTemplate.fromTemplate("请翻译这段话: {text}")
   
   const chatPrompt = ChatPromptTemplate.fromMessages([
       translateInstructionTemplate,
       userQuestionTemplate
   ])
   
   const formattedChatPrompt = await chatPrompt.formatMessages({
       source_lang: '中文',
       target_lang: '日文',
       text: '你好,世界'
   })
   
   console.log(formattedChatPrompt)

@langchain/core/prompts依赖里拿出三个方法,分别是前面介绍的四个方法里的第一,第二,第四个方法。

步骤:

  • 先构建一个system message来给llm指定核心的准则translateInstructionTemplate
  • 再构建一个用户输入的信息userQuestionTemplate
  • 随后把两个信息组合起来,形成一个对话信息chatPrompt
  • 最后我们就可以用formatMessages信息来格式化整个对话信息了,最后的结构是这样的:

    [
    SystemMessage {
      "content": "你是一个专业的翻译员,你的任务是把文本从中文翻译到日文。",
      "additional_kwargs": {},
      "response_metadata": {}
    },
    HumanMessage {
      "content": "请翻译这段话: 你好,世界",
      "additional_kwargs": {},
      "response_metadata": {}
    }
    ]

    结果是一个数组,每个每个元素都是一种Message。

    eg. 注意,后面我们要融入LCEL的写法, 要用invoke的话,就不要用formatMessages提前替换掉占位符。

同样的,chatPrompt也是有语法糖的:

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

const systemTemplate = "你是一个专业的翻译员,你的任务是把文本从{source_lang}翻译到{target_lang}。"
const humanTemplate = "请翻译这段话: {text}"

const chatPromptSimple = ChatPromptTemplate.fromMessages([
    ["system", systemTemplate],
    ['human', humanTemplate]
]);
console.log(chatPromptSimple)

这种写法也可以达成上面的第一种写法的效果,并且不调用formatMessages,这样的话模板的输出结果就是:

然后我们可以快速组装一个简单的Chain来测试一下:

import { ChatPromptTemplate } from "@langchain/core/prompts";
   import { Ollama } from "@langchain/ollama";
   import { load } from "dotenv";
   
   
   const systemTemplate = "你是一个专业的翻译员,你的任务是把文本从{source_lang}翻译到{target_lang}。"
   const humanTemplate = "请翻译这段话: {text}"
   
   const chatPromptSimple = ChatPromptTemplate.fromMessages([
       ["system", systemTemplate],
       ['human', humanTemplate]
   ]);
   
   const env = await load();
   const process = {
       env
   }
   
   const ollama = new Ollama({
       baseUrl: process.env.BASE_URL,
       model: process.env.MODEL
   })
   
   const chain = chatPromptSimple.pipe(ollama)
   
   await chain.invoke({
       source_lang: '中文',
       target_lang: '日文',
       text: '你好世界'
   })

返回的结果:

组合多个Prompt

当然了,在实际的工作中,我们可能会根据多个变量,根据多个外界环境去构造一个很复杂的Prompt,这就是PipelinePromptTemplate的应用场景。这个类可以将多个独立的Templat构造成一个完整且复杂的Prompt,提高独立Prompt的复用性,进一步增强模块化带来的优势:

有两个核心的概念

  • pipelinePrompts,一组Object,每一个Object都表示的是prompt运行之后赋值给name变量
  • finalPrompt,最终输出的Prompt

说到这里有点看不懂在说什么是吧,没关系,直接看看代码吧:

import { PromptTemplate, PipelinePromptTemplate } from '@langchain/core/prompts';
   
   // 获取当前日期
   const getCurrentDateStr = () => new Date().toLocaleDateString();
   
   // 日期模块
   const datePrompt = PromptTemplate.fromTemplate("{date}, 现在是{period}");
   const periodPrompt = await datePrompt.partial({
       date: getCurrentDateStr(), // 部分绑定日期
   });
   
   // 主人信息模块
   const infoPrompt = PromptTemplate.fromTemplate("姓名是{name},性别是{gender}");
   
   // 任务模块
   const tasksPrompt = PromptTemplate.fromTemplate(`
       我想要吃{period}的{food}。
       再重复一遍我的个人信息: {info}
   `);
   
   // 最终模板
   const fullPrompt = PromptTemplate.fromTemplate(`
       你是一个智能管家,今天是{date},
       你的主人信息是{info},
       根据上下文,完成主人的需求: {tasks}
   `);
   
   // 构建 PipelinePromptTemplate
   const composedPrompt = new PipelinePromptTemplate({
       pipelinePrompts: [
           { name: "date", prompt: periodPrompt },
           { name: "info", prompt: infoPrompt },
           { name: "tasks", prompt: tasksPrompt },
       ],
       finalPrompt: fullPrompt,
   });
   
   // 执行格式化
   const formattedPrompt = await composedPrompt.format({
       period: "早上",
       name: "张三",
       gender: "male",
       food: "fish",
   });
   
   console.log("最终结果:\n", formattedPrompt);

运行之后的结果是:

看懂了吧

PipelinePromptTemplate 是一个用于管理复杂多步骤任务的工具。它的核心作用是 按顺序执行一组子模板,将每个子模板的输出赋值给对应的变量(pipelinePrompts),然后将这些变量整合到最终模板(finalPrompt)中,生成完整的输出。总结三点:

  • pipelinePrompts 是子任务的定义,每个 prompt 的运行结果会赋值给 name。
  • finalPrompt 是最终模板,引用了子任务的变量和用户的输入。
  • 通过这种机制,可以将复杂任务拆分成多个步骤,灵活组合和复用模板。

还有一些需要强调和注意的地方:

  1. 一个变量是可以多次复用的,比方说,我的外部输入的变量periodperiodPrompttaskPrompt里都被使用了
  2. pipelinePrompts的变量可以被引用,比方说我们在taskPrompt里用了一下infoPrompt的运行结果
  3. 这个类也是可以支持动态自定义和partial的
  4. LangChain会自动分析pipeline之间的关系,尽可能进行并行化来提高运行的速度

最后,总结一下,Prompt是LLM的应用最最核心的价值,并且随时都会被修改。我这里罗列的所有Templat,可以拿来灵活的使用,管理与组装,从而发挥LLM的能力

最后修改:2024 年 12 月 13 日
收款不要了,给孩子补充点点赞数吧