【LangChain.js学习】 会话记忆(临时/长期)全解析

1 阅读7分钟

核心说明

会话记忆是 LangChain 实现多轮对话上下文感知的核心能力,核心分为「临时会话记忆」(内存存储,程序重启丢失)和「长期会话记忆」(持久化存储,跨会话保留)。两者均基于 RunnableWithMessageHistory 封装,但底层存储逻辑不同,以下结合购物助手场景详解实现方式、核心原理与扩展要点。

临时会话记忆(InMemory)

核心特点

基于 LangChain 内置的 InMemoryChatMessageHistory 实现,内存存储会话历史,开箱即用、读写高效,但程序重启后历史记录全部丢失,适用于临时交互、单次会话、测试场景。

完整实现代码

import { InMemoryChatMessageHistory } from "@langchain/core/chat_history"
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"
import { RunnableWithMessageHistory } from "@langchain/core/runnables"
import { ChatOpenAI } from "@langchain/openai"// 1. 定义带记忆占位符的提示词模板
const shoppingAssistantPrompt = ChatPromptTemplate.fromMessages([
    ["system", "你是一个专业的购物助手,能根据历史对话计算商品总数"],
    // 记忆占位符:自动填充会话历史消息
    new MessagesPlaceholder("history"),
    ["human", "{question}"],
])
​
// 2. 初始化通义千问对话模型
const chatModel = new ChatOpenAI({
    model: "qwen-max",
    configuration: {
        baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
        apiKey: "[你的阿里百炼API Key]", // 替换为个人有效Key
    },
})
​
// 3. 构建基础对话链(模板 → 模型)
const chatChain = shoppingAssistantPrompt.pipe(chatModel)
​
// 4. 维护会话ID与记忆实例的映射(实现多会话隔离)
const historyBySessionId = new Map()
​
/**
 * 获取或创建内存会话记忆实例
 * @param {string} sessionId - 会话唯一标识
 * @returns {InMemoryChatMessageHistory} 记忆实例
 */
function getOrCreateMessageHistory(sessionId) {
    if (!historyBySessionId.has(sessionId)) {
        historyBySessionId.set(sessionId, new InMemoryChatMessageHistory())
    }
    return historyBySessionId.get(sessionId)!
}
​
// 5. 包装带记忆的对话链(核心:关联记忆与对话)
const chatChainWithHistory = new RunnableWithMessageHistory({
    runnable: chatChain,                // 基础对话链
    getMessageHistory: getOrCreateMessageHistory, // 记忆获取/创建方法
    inputMessagesKey: "question",       // 用户输入参数名(匹配模板{question})
    historyMessagesKey: "history",      // 记忆占位符名称(匹配MessagesPlaceholder)
})
​
// 6. 会话配置(指定唯一会话ID,隔离不同会话)
const sessionRunConfig = {
    configurable: {
        sessionId: Date.now().toString(), // 时间戳生成临时会话ID
    },
}
​
// 7. 多轮对话调用
async function runTempChat() {
    const turn1 = await chatChainWithHistory.invoke({ question: "我买了3个橘子" }, sessionRunConfig)
    const turn2 = await chatChainWithHistory.invoke({ question: "我买了2个橘子" }, sessionRunConfig)
    const turn3 = await chatChainWithHistory.invoke({ question: "我总共有多少个橘子" }, sessionRunConfig)
​
    console.log("第一轮回复:", turn1.content) // 示例:已记录你购买了3个橘子
    console.log("第二轮回复:", turn2.content) // 示例:已记录你又购买了2个橘子
    console.log("第三轮回复:", turn3.content) // 示例:你总共购买了5个橘子
}
​
runTempChat();

长期会话记忆(文件持久化)

核心特点

通过自定义实现抽象类 BaseListChatMessageHistory,将会话历史存储到本地文件(也可扩展为数据库),程序重启后历史记录仍保留,适用于生产环境、用户长期对话、客服系统等场景。

核心原理:抽象类与方法实现

BaseListChatMessageHistory 是 LangChain 定义的会话记忆「标准化接口」,所有自定义持久化记忆必须继承该类,并实现 getMessagesaddMessageclear 三个核心方法(强制约束),确保与 RunnableWithMessageHistory 无缝兼容。

核心方法核心职责关键要求
getMessages()读取会话历史消息需返回 BaseMessage[] 格式(LangChain 运行时消息)
addMessage()追加新消息到历史需完成「运行时消息→存储格式」转换并持久化
clear()清空会话历史需重置存储介质的会话数据

完整实现代码

import { BaseListChatMessageHistory } from "@langchain/core/chat_history"
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"
import { RunnableWithMessageHistory } from "@langchain/core/runnables"
import { BaseMessage, mapChatMessagesToStoredMessages, mapStoredMessagesToChatMessages, StoredMessage } from "@langchain/core/messages"
import { ChatOpenAI } from "@langchain/openai"
import fs from "fs"
import path from "path"// 1. 定义提示词模板(与临时记忆通用)
const shoppingAssistantPrompt = ChatPromptTemplate.fromMessages([
    ["system", "你是一个专业的购物助手,能根据历史对话计算商品总数"],
    new MessagesPlaceholder("history"),
    ["human", "{question}"],
])
​
// 2. 初始化模型(与临时记忆通用)
const chatModel = new ChatOpenAI({
    model: "qwen-max",
    configuration: {
        baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
        apiKey: "[你的阿里百炼API Key]",
    },
})
​
// 3. 构建基础对话链
const chatChain = shoppingAssistantPrompt.pipe(chatModel)
​
// 4. 自定义文件持久化记忆类(核心:继承抽象类+实现核心方法)
class FileChatMessageHistory extends BaseListChatMessageHistory {
    // LangChain 规范:标识组件命名空间(可选但建议添加)
    lc_namespace: string[] = ["langchain", "chat_history", "file"]
​
    /** 会话唯一标识 */
    public sessionId: string
    /** 历史文件存储路径(每个会话一个JSON文件) */
    public filePath: string
​
    /**
     * 初始化文件存储的会话记忆
     * @param {string} sessionId - 会话ID
     * @param {string} fileDir - 历史文件存储目录
     */
    constructor(sessionId: string, fileDir: string) {
        super() // 必须调用父类构造函数
        this.sessionId = sessionId
        this.filePath = path.join(fileDir, `${sessionId}.json`)
​
        // 初始化文件(不存在则创建空数组)
        if (!fs.existsSync(this.filePath)) {
            fs.mkdirSync(path.dirname(this.filePath), { recursive: true })
            fs.writeFileSync(this.filePath, "[]", "utf-8")
        }
    }
​
    /**
     * 核心方法1:读取历史消息
     * 步骤:读取文件 → 解析存储格式 → 转换为运行时格式
     */
    async getMessages(): Promise<BaseMessage[]> {
        // 读取文件原始内容
        const content: string = fs.readFileSync(this.filePath, "utf-8")
        // 解析为LangChain存储格式(StoredMessage)
        const storedMessages: StoredMessage[] = JSON.parse(content) || []
        // 转换为运行时消息格式(BaseMessage)
        return mapStoredMessagesToChatMessages(storedMessages)
    }
​
    /**
     * 核心方法2:追加新消息
     * 步骤:读取现有历史 → 追加新消息 → 转换格式 → 写入文件
     */
    async addMessage(message: BaseMessage): Promise<void> {
        // 读取现有历史(避免覆盖)
        const messages: BaseMessage[] = await this.getMessages()
        // 追加新消息
        messages.push(message)
        // 转换为存储格式(解决BaseMessage无法序列化问题)
        const storedMessages: StoredMessage[] = mapChatMessagesToStoredMessages(messages)
        // 写入文件(格式化JSON,便于阅读)
        fs.writeFileSync(this.filePath, JSON.stringify(storedMessages, null, 2), "utf-8")
    }
​
    /**
     * 核心方法3:清空历史消息
     * 步骤:重置文件内容为空数组
     */
    async clear(): Promise<void> {
        fs.writeFileSync(this.filePath, "[]", "utf-8")
    }
}
​
// 5. 维护会话ID与文件记忆实例的映射
const historyBySessionId = new Map()
​
/**
 * 获取或创建文件存储的会话记忆
 * @param {string} sessionId - 会话ID
 * @returns {FileChatMessageHistory} 记忆实例
 */
function getOrCreateMessageHistory(sessionId: string) {
    if (!historyBySessionId.has(sessionId)) {
        historyBySessionId.set(sessionId, new FileChatMessageHistory(sessionId, `./chat_history`))
    }
    return historyBySessionId.get(sessionId)!
}
​
// 6. 包装带长期记忆的对话链(与临时记忆配置一致)
const chatChainWithHistory = new RunnableWithMessageHistory({
    runnable: chatChain,
    getMessageHistory: getOrCreateMessageHistory,
    inputMessagesKey: "question",
    historyMessagesKey: "history",
})
​
// 7. 固定会话ID(确保重启后可读取同一历史)
const sessionRunConfig = {
    configurable: {
        sessionId: "666666", // 自定义固定会话ID
    },
}
​
// 8. 多轮对话调用(程序重启后仍可读取历史)
async function runPersistentChat() {
    const turn1 = await chatChainWithHistory.invoke({ question: "我买了3个橘子" }, sessionRunConfig)
    const turn2 = await chatChainWithHistory.invoke({ question: "我买了2个橘子" }, sessionRunConfig)
    console.log("第一轮回复:", turn1.content)
    console.log("第二轮回复:", turn2.content)
​
    // 模拟程序重启后调用(仍能读取历史)
    const turn3 = await chatChainWithHistory.invoke({ question: "我总共有多少个橘子" }, sessionRunConfig)
    console.log("第三轮回复:", turn3.content) // 示例:你总共购买了5个橘子
}
​
runPersistentChat();

临时/长期记忆对比与选型

维度临时会话记忆(InMemory)长期会话记忆(文件/数据库)
存储介质内存本地文件/数据库
持久化程序重启丢失永久保留(除非手动删除)
实现复杂度极低(开箱即用)中等(需自定义类)
性能无IO开销,速度快有IO开销,速度略慢
适用场景临时交互、测试、单次会话生产环境、用户长期对话、客服系统
扩展难度无法扩展(内存限制)易扩展(支持MySQL/Redis等)

核心扩展与优化建议

存储介质扩展(文件→数据库)

自定义记忆类的核心是实现三个方法,替换存储介质只需修改方法内部逻辑:

  • 文件存储 → MySQL:getMessages 读取数据库表、addMessage 插入数据、clear 删除数据;
  • 文件存储 → Redis:利用 redis 库的 lpush/lrange/del 方法实现列表存储。

关键注意事项

  • 会话隔离:必须通过 sessionId 区分不同会话,避免多用户历史记录混淆;
  • 格式转换:务必使用 mapChatMessagesToStoredMessages/mapStoredMessagesToChatMessages 完成消息格式转换,避免序列化错误;

总结

  1. 临时记忆基于 InMemoryChatMessageHistory 开箱即用,适合快速实现多轮对话;
  2. 长期记忆需继承 BaseListChatMessageHistory 抽象类,核心是实现「读、存、清」三个方法,格式转换工具函数可简化开发;
  3. 两种记忆均通过 RunnableWithMessageHistory 包装对话链,配置参数(inputMessagesKey/historyMessagesKey)需与模板参数严格匹配;
  4. 实际开发中可根据场景选择:测试/临时会话用内存记忆,生产/长期会话用文件/数据库持久化记忆。