前言
书接上文 , 学习 RAG 的 Retriever(检索) , 现在学习 Memory ~
在与大语言模型(LLM)交互的过程中,聊天记录的管理和有效利用至关重要。
LangChain 作为一个强大的工具库,提供了丰富的功能来帮助开发者实现聊天记录的存储、摘要生成以及自动维护等操作。本文将深入探讨 LangChain 中聊天记录管理的相关机制,并通过具体代码示例进行详细说明。
聊天记录的存储
用户与 LLM 的所有聊天记录都会完整地存储在 chat history
中。chat history
负责将这些原始数据存储在内存中,或者对接其他数据库。
在大多数情况下,我们不会将完整的 chat history
直接嵌入到 LLM 的上下文中,而是提取聊天记录的摘要或者只返回最近几条聊天记录,这些处理逻辑在 Memory
中完成。
代码示例
javascript
import { ChatMessageHistory } from "langchain/stores/message/in_memory";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
const history = new ChatMessageHistory();
// 储存两个 Message 的信息
await history.addMessage(new HumanMessage("hi"));
await history.addMessage(new AIMessage("What can I do for you?"));
// 获取所有的历史记录
const messages = await history.getMessages();
console.log(messages);
所有 chat history
都继承自 BaseListChatMessageHistory
,其类型定义包含了获取消息、添加消息、添加用户消息、添加 AI 消息以及清空历史记录等抽象方法。
任何实现了 BaseChatMessageHistory
抽象类的都可以作为 Memory
的底层 chat history
。
由于 ChatMessageHistory
是存储在内存里的,后续我们还可以实现基于文件存储的 chat history
并复用 Memory
的能力。
用手实现
LLM 本身是无状态的,不会存储聊天历史,每次都根据上下文生成回答。因此,我们需要自己存储聊天记录,并将其作为传递给 LLM 的上下文的一部分。
代码示例
javascript
import { load } from "dotenv";
const env = await load();
const process = {
env
}
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
const chatModel = new ChatOpenAI({
modelName: "gpt-4o",
temperature: 0.7,
maxTokens: 1000,
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant. Answer all questions to the best of your ability. You are talkative and provides lots of specific details from its context. If the you does not know the answer to a question, it truthfully says you do not know."],
new MessagesPlaceholder("history_message"),
]);
const chain = prompt.pipe(chatModel);
const history = new ChatMessageHistory();
await history.addMessage(new HumanMessage("我叫 Langchain 杀手"));
const res1 = await chain.invoke({
history_message: await history.getMessages()
})
await history.addMessage(res1)
await history.addMessage(new HumanMessage("我叫什么名字?"));
const res2 = await chain.invoke({
history_message: await history.getMessages()
})
res1 的结果 :
res2 的结果 :
在这个示例中,MessagesPlaceholder
创建了一个名为 history_message
的插槽,chain
中对应的参数将会替换这部分。通过手动添加和使用聊天记录,我们可以让 LLM 在生成回答时考虑历史信息。
自动实现
RunnableWithMessageHistory
可以给任意 chain
包裹一层,从而添加聊天记录管理的能力。
代码示例
javascript
import { load } from "dotenv";
const env = await load();
const process = {
env
}
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";
const chatModel = new ChatOpenAI({
modelName: "gpt-4o",
temperature: 0.9,
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant. Answer all questions to the best of your ability."],
new MessagesPlaceholder("history_message"),
["human", "{input}"]
]);
const history = new ChatMessageHistory();
const chain = prompt.pipe(chatModel);
const chainWithHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: (_sessionId) => history,
inputMessagesKey: "input",
historyMessagesKey: "history_message",
});
const res1 = await chainWithHistory.invoke({
input: "我叫 langchain 杀手"
}, {
configurable: { sessionId: "none" }
})
const res2 = await chainWithHistory.invoke({
input: "我的名字叫什么?"
}, {
configurable: { sessionId: "none" }
})
RunnableWithMessageHistory
有几个重要参数:
runnable
:需要被包裹的chain
,可以是任意chain
。getMessageHistory
:接收一个函数,函数根据传入的_sessionId
获取对应的ChatMessageHistory
对象。inputMessagesKey
:用户传入信息的key
名称,用于自动记录用户输入。historyMessagesKey
:聊天记录在prompt
中的key
,用于自动将聊天记录注入到prompt
中。outputMessagesKey
:如果chain
有多个输出,需要指定哪个是 LLM 的回复,即需要存储的信息。
聊天记录摘要的自动生成
除了传递完整的记录到 LLM 中,我们还可以对 LLM 的历史记录进行更多操作,例如自动生成摘要。
代码示例
javascript
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
const summaryModel = new ChatOpenAI();
const summaryPrompt = ChatPromptTemplate.fromTemplate(`
Progressively summarize the lines of conversation provided, adding onto the previous summary returning a new summary
Current summary:
{summary}
New lines of conversation:
{new_lines}
New summary:
`);
const summaryChain = RunnableSequence.from([
summaryPrompt,
summaryModel,
new StringOutputParser(),
])
const newSummary = await summaryChain.invoke({
summary: "",
new_lines: "我叫浪遏"
})
await summaryChain.invoke({
summary: newSummary,
new_lines: "我是一名学生"
})
// 我叫浪遏, 是一名学生
这个 summaryChain
接受两个参数:summary
(上一次总结的信息)和 new_lines
(用户和 LLM 新的回复),通过不断调用可以逐步生成聊天记录的摘要。
demo
下面是一个完整的处理链示例,结合了聊天记录的存储、摘要生成和自动维护。
代码示例
javascript
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough } from "@langchain/core/runnables";
import { getBufferString } from "@langchain/core/messages";
// 使用 RunnableSequence.from 方法创建一个可运行的序列 chatChain
// 该序列会按顺序依次执行其中的每个可运行对象
const chatChain = RunnableSequence.from([
{
// 创建一个包含输入的对象,使用 RunnablePassthrough 来传递输入数据
// 同时,在传递过程中执行自定义函数 func
// func 函数的作用是将用户输入的消息添加到聊天历史记录 history 中
input: new RunnablePassthrough({
func: (input) => history.addUserMessage(input)
})
},
// 使用 RunnablePassthrough.assign 方法将额外的数据添加到输入对象中
// 这里将历史摘要 history_summary 添加到输入对象中,其值为变量 summary
RunnablePassthrough.assign({
history_summary: () => summary
}),
// 将聊天提示模板 chatPrompt 作为序列的一部分
// 它会根据输入数据生成合适的提示信息
chatPrompt,
// 将聊天模型 chatModel 作为序列的一部分
// 它会根据生成的提示信息生成回复
chatModel,
// 使用 StringOutputParser 将模型的输出解析为字符串
new StringOutputParser(),
// 再次使用 RunnablePassthrough 传递解析后的字符串输出
// 同时执行自定义函数 func 进行一系列操作
new RunnablePassthrough({
func: async (input) => {
// 将模型的回复添加到聊天历史记录 history 中
history.addAIChatMessage(input);
// 获取聊天历史记录中的所有消息
const messages = await history.getMessages();
// 将消息列表转换为字符串
const new_lines = getBufferString(messages);
// 调用摘要生成链 summaryChain,根据当前摘要 summary 和新的聊天内容 new_lines 生成新的摘要
const newSummary = await summaryChain.invoke({
summary,
new_lines
});
// 清空聊天历史记录
history.clear();
// 更新摘要为新生成的摘要
summary = newSummary;
}
})
]);
这个 chatChain
的主要流程包括:
- 将用户输入添加到聊天历史记录中
- 将历史摘要添加到输入对象中
- 使用聊天提示模板生成提示信息
- 将提示信息传递给聊天模型生成回复
- 将模型的输出解析为字符串
- 最后将模型的回复添加到聊天历史记录中
- 根据新的聊天内容更新摘要
- 并清空聊天历史记录
逐帧学习如下 ~ 🤡
RunnableSequence
概述
RunnableSequence
是 LangChain 中的一个工具,它允许你将多个可运行的步骤(也就是操作)按顺序组合在一起,形成一个处理链。就好比一条生产流水线,原材料(输入数据)依次经过每个工位(步骤),最终得到成品(输出结果)。
RunnableSequence.from
方法
RunnableSequence.from
方法用于创建一个 RunnableSequence
实例,它接受一个数组作为参数,数组中的每个元素代表流水线中的一个工位(步骤)。下面我们来详细看看这个数组里每个步骤都在做什么。
步骤 1:处理用户输入并添加到历史记录
javascript
{
input: new RunnablePassthrough({
func: (input) => history.addUserMessage(input)
})
}
RunnablePassthrough
:这是一个特殊的组件,它会将输入原封不动地传递给下一个步骤,但在传递之前,允许你执行一些自定义操作。func
函数 :这里的func
函数接收输入数据input
,并调用history.addUserMessage(input)
将用户输入的消息添加到聊天历史记录history
中。这个步骤的主要目的是记录用户的输入。
步骤 2:添加历史摘要到输入数据
javascript
RunnablePassthrough.assign({
history_summary: () => summary
})
RunnablePassthrough.assign
:这个方法会将输入数据和一个额外的对象合并,然后传递给下一个步骤。{ history_summary: () => summary }
:这里定义了一个额外的对象,其中history_summary
是一个属性名,其值是一个函数,该函数返回当前的历史摘要summary
。这个步骤的作用是将历史摘要添加到输入数据中,以便后续步骤使用。
步骤 3:生成聊天提示
javascript
chatPrompt
chatPrompt
是一个聊天提示模板,它会根据输入数据生成合适的提示信息。这个提示信息将作为输入传递给聊天模型,引导模型生成回复。
步骤 4:调用聊天模型生成回复
javascript
chatModel
chatModel
是一个聊天模型实例,它会根据上一步生成的提示信息生成回复。这个模型可以是 OpenAI 的 GPT 系列模型,或者其他支持聊天功能的语言模型。
步骤 5:解析模型输出为字符串
javascript
new StringOutputParser()
StringOutputParser
是一个输出解析器,它的作用是将模型的输出解析为字符串类型。因为模型的输出可能是一个复杂的数据结构,通过这个解析器可以将其转换为我们需要的字符串形式。
步骤 6:处理模型回复并更新历史摘要
javascript
new RunnablePassthrough({
func: async (input) => {
history.addAIChatMessage(input)
const messages = await history.getMessages()
const new_lines = getBufferString(messages)
const newSummary = await summaryChain.invoke({
summary,
new_lines
})
history.clear()
summary = newSummary
}
})
**RunnablePassthrough**
:同样是将输入原封不动地传递给下一个步骤,但在传递之前执行自定义操作。func
**函数 :这是一个异步函数,它会执行以下操作:history.addAIChatMessage(input)
:将模型的回复添加到聊天历史记录history
中。const messages = await history.getMessages()
:获取聊天历史记录中的所有消息。const new_lines = getBufferString(messages)
:将消息列表转换为字符串。const newSummary = await summaryChain.invoke({ summary, new_lines })
:调用摘要生成链summaryChain
,根据当前摘要summary
和新的聊天内容new_lines
生成新的摘要。history.clear()
:清空聊天历史记录。summary = newSummary
:更新摘要为新生成的摘要。
结论
通过 LangChain 提供的丰富功能,我们可以方便地实现
- 聊天记录的存储、
- 管理和摘要生成。
能让 LLM 在生成回答时更好地考虑上下文信息,从而提供更准确、连贯的回复。开发者可以根据具体需求灵活运用这些功能,构建出更加智能和高效的聊天应用。