核心说明
会话记忆是 LangChain 实现多轮对话上下文感知的核心能力,核心分为「临时会话记忆」(内存存储,程序重启丢失)和「长期会话记忆」(持久化存储,跨会话保留)。两者均基于 RunnableWithMessageHistory 封装,但底层存储逻辑不同,以下结合购物助手场景详解实现方式、核心原理与扩展要点。
临时会话记忆(InMemory)
核心特点
基于 LangChain 内置的 InMemoryChatMessageHistory 实现,内存存储会话历史,开箱即用、读写高效,但程序重启后历史记录全部丢失,适用于临时交互、单次会话、测试场景。
完整实现代码
typescript
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 定义的会话记忆「标准化接口」,所有自定义持久化记忆必须继承该类,并实现 getMessages、addMessage、clear 三个核心方法(强制约束),确保与 RunnableWithMessageHistory 无缝兼容。
| 核心方法 | 核心职责 | 关键要求 |
|---|---|---|
getMessages() |
读取会话历史消息 | 需返回 BaseMessage[] 格式(LangChain 运行时消息) |
addMessage() |
追加新消息到历史 | 需完成「运行时消息→存储格式」转换并持久化 |
clear() |
清空会话历史 | 需重置存储介质的会话数据 |
完整实现代码
typescript
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完成消息格式转换,避免序列化错误;
总结
- 临时记忆基于
InMemoryChatMessageHistory开箱即用,适合快速实现多轮对话; - 长期记忆需继承
BaseListChatMessageHistory抽象类,核心是实现「读、存、清」三个方法,格式转换工具函数可简化开发; - 两种记忆均通过
RunnableWithMessageHistory包装对话链,配置参数(inputMessagesKey/historyMessagesKey)需与模板参数严格匹配; - 实际开发中可根据场景选择:测试/临时会话用内存记忆,生产/长期会话用文件/数据库持久化记忆。