Langchain.js | Memory | LLM 也有记忆😋😋😋

前言

书接上文 , 学习 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 **函数 :这是一个异步函数,它会执行以下操作:
    1. history.addAIChatMessage(input):将模型的回复添加到聊天历史记录 history 中。
    2. const messages = await history.getMessages():获取聊天历史记录中的所有消息。
    3. const new_lines = getBufferString(messages):将消息列表转换为字符串。
    4. const newSummary = await summaryChain.invoke({ summary, new_lines }):调用摘要生成链 summaryChain,根据当前摘要 summary 和新的聊天内容 new_lines 生成新的摘要。
    5. history.clear():清空聊天历史记录。
    6. summary = newSummary:更新摘要为新生成的摘要。

结论

通过 LangChain 提供的丰富功能,我们可以方便地实现

  • 聊天记录的存储、
  • 管理和摘要生成。

能让 LLM 在生成回答时更好地考虑上下文信息,从而提供更准确、连贯的回复。开发者可以根据具体需求灵活运用这些功能,构建出更加智能和高效的聊天应用。

相关推荐
微学AI3 小时前
GPU算力平台|在GPU算力平台部署MedicalGPT医疗大模型的应用教程
大模型·llm·gpu算力
古蓬莱掌管玉米的神5 小时前
vue3语法watch与watchEffect
前端·javascript
林涧泣5 小时前
【Uniapp-Vue3】uni-icons的安装和使用
前端·vue.js·uni-app
雾恋5 小时前
AI导航工具我开源了利用node爬取了几百条数据
前端·开源·github
拉一次撑死狗5 小时前
Vue基础(2)
前端·javascript·vue.js
祯民6 小时前
两年工作之余,我在清华大学出版社出版了一本 AI 应用书籍
前端·aigc
热情仔6 小时前
mock可视化&生成前端代码
前端
m0_748246356 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
wjs04066 小时前
用css实现一个类似于elementUI中Loading组件有缺口的加载圆环
前端·css·elementui·css实现loading圆环
爱趣五科技6 小时前
无界云剪音频教程:提升视频质感
前端·音视频