LangChain 提供了标准化的 ChatMessageHistory 体系来管理对话记忆,最基础的两种实现就是:
- 内存存储(
InMemoryChatMessageHistory) :会话级临时存储,性能最高,进程退出即清空 - 文件存储(
FileSystemChatMessageHistory) :本地 JSON 文件持久化,支持跨会话恢复历史
本文就基于 LangChain.js 带你从零实现这两种对话记忆,附带完整可运行代码和实战踩坑指南。
一、内存级对话记忆:InMemoryChatMessageHistory
内存存储是最简单的记忆方案:对话消息全部保存在 Node.js 进程内存中,读写速度极快,不需要任何外部依赖。
适用场景
- 单次会话的临时对话
- 开发调试阶段快速验证
- 短对话、轻量场景
完整实现代码
<span>import</span> <span>'dotenv/config'</span>;
<span>import</span> { <span>ChatOpenAI</span> } <span>from</span> <span>'@langchain/openai'</span>;
<span>import</span> { <span>InMemoryChatMessageHistory</span> } <span>from</span> <span>'@langchain/core/chat_history'</span>;
<span>import</span> {
<span>HumanMessage</span>,
<span>SystemMessage</span>,
<span>AIMessage</span>,
getBufferString
} <span>from</span> <span>'@langchain/core/messages'</span>;
<span>// 初始化大模型(兼容通义千问等 OpenAI 协议模型)</span>
<span>const</span> model = <span>new</span> <span>ChatOpenAI</span>({
<span>model</span>: process.<span>env</span>.<span>MODEL_NAME</span>,
<span>apiKey</span>: process.<span>env</span>.<span>OPENAI_API_KEY</span>,
<span>baseURL</span>: process.<span>env</span>.<span>OPENAI_BASE_URL</span>,
<span>temperature</span>: <span>0</span>
});
<span>async</span> <span>function</span> <span>InMemoryDemo</span>(<span></span>) {
<span>// 1. 初始化内存记忆容器</span>
<span>const</span> history = <span>new</span> <span>InMemoryChatMessageHistory</span>();
<span>const</span> systemMessage = <span>new</span> <span>SystemMessage</span>(
<span>'你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'</span>
);
<span>// 2. 第一轮对话</span>
<span>console</span>.<span>log</span>(<span>'【开启第一轮对话】'</span>);
<span>const</span> userMessage1 = <span>new</span> <span>HumanMessage</span>(<span>'你今天吃什么?'</span>);
<span>// 用户消息存入记忆</span>
<span>await</span> history.<span>addMessage</span>(userMessage1);
<span>// 组装:系统提示 + 历史对话</span>
<span>const</span> messages1 = [systemMessage, ...(<span>await</span> history.<span>getMessages</span>())];
<span>const</span> response1 = <span>await</span> model.<span>invoke</span>(messages1);
<span>// AI 回复也存入记忆</span>
<span>await</span> history.<span>addMessage</span>(response1);
<span>console</span>.<span>log</span>(<span>`助手:<span>${response1.content}</span>\n`</span>);
<span>// 3. 第二轮对话(自动携带上一轮上下文)</span>
<span>console</span>.<span>log</span>(<span>'【第二轮对话】'</span>);
<span>const</span> userMessage2 = <span>new</span> <span>HumanMessage</span>(<span>'好吃吗?'</span>);
<span>await</span> history.<span>addMessage</span>(userMessage2);
<span>const</span> messages2 = [systemMessage, ...(<span>await</span> history.<span>getMessages</span>())];
<span>const</span> response2 = <span>await</span> model.<span>invoke</span>(messages2);
<span>await</span> history.<span>addMessage</span>(response2);
<span>console</span>.<span>log</span>(<span>`助手:<span>${response2.content}</span>\n`</span>);
<span>// 4. 查看全部历史消息</span>
<span>const</span> allMessages = <span>await</span> history.<span>getMessages</span>();
<span>console</span>.<span>log</span>(<span>`共保存了 <span>${allMessages.length}</span> 条对话`</span>);
allMessages.<span>forEach</span>(<span>(<span>msg, index</span>) =></span> {
<span>const</span> prefix = msg.<span>type</span> === <span>'human'</span> ? <span>'用户'</span> : <span>'助手'</span>;
<span>console</span>.<span>log</span>(<span>`<span>${index + <span>1</span>}</span>. [<span>${prefix}</span>]: <span>${msg.content.substring(<span>0</span>, <span>50</span>)}</span>....`</span>);
});
}
<span>InMemoryDemo</span>()
.<span>catch</span>(<span>console</span>.<span>error</span>)
.<span>finally</span>(<span>() =></span> {
<span>console</span>.<span>log</span>(<span>'done'</span>);
});
核心逻辑拆解
- 初始化记忆容器:
new InMemoryChatMessageHistory()创建一个内存中的消息队列 - 消息写入:每一轮用户提问和 AI 回复,都通过
addMessage()存入记忆 - 上下文拼接:调用模型前,用
getMessages()取出全部历史,和 System 提示词拼成完整消息数组 - 无持久化:脚本执行结束,内存释放,对话历史永久丢失
特点总结
- ✅ 优点:零依赖、速度极快、API 简单
- ❌ 缺点:进程重启 / 页面刷新就丢失,无法跨会话
- 🎯 定位:短期会话记忆,作为所有记忆方案的基础载体
二、文件持久化记忆:FileSystemChatMessageHistory
内存存储的对话无法保留,程序关掉就没了。如果希望下次运行还能继续之前的对话,就需要文件持久化—— 把对话历史写入本地 JSON 文件。
依赖说明
文件存储实现不在 @langchain/core 中,需要安装社区包:
npm install @langchain/community
1. 写入对话到本地文件
<span>import</span> <span>'dotenv/config'</span>;
<span>import</span> { <span>ChatOpenAI</span> } <span>from</span> <span>'@langchain/openai'</span>;
<span>import</span> { <span>FileSystemChatMessageHistory</span> } <span>from</span> <span>'@langchain/community/stores/message/file_system'</span>;
<span>import</span> { <span>HumanMessage</span>, <span>SystemMessage</span>, <span>AIMessage</span> } <span>from</span> <span>'@langchain/core/messages'</span>;
<span>import</span> path <span>from</span> <span>'node:path'</span>;
<span>const</span> model = <span>new</span> <span>ChatOpenAI</span>({
<span>model</span>: process.<span>env</span>.<span>MODEL_NAME</span>,
<span>apiKey</span>: process.<span>env</span>.<span>OPENAI_API_KEY</span>,
<span>baseURL</span>: process.<span>env</span>.<span>OPENAI_BASE_URL</span>,
<span>temperature</span>: <span>0</span>
});
<span>async</span> <span>function</span> <span>fileHistoryWriteDemo</span>(<span></span>) {
<span>// 配置:本地文件路径 + 会话ID</span>
<span>const</span> filePath = path.<span>join</span>(process.<span>cwd</span>(), <span>'chat_history.json'</span>);
<span>const</span> sessionId = <span>'user_session_001'</span>;
<span>const</span> systemMessage = <span>new</span> <span>SystemMessage</span>(
<span>'你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'</span>
);
<span>// 初始化文件记忆容器</span>
<span>const</span> history = <span>new</span> <span>FileSystemChatMessageHistory</span>({
filePath,
sessionId
});
<span>// 第一轮对话</span>
<span>console</span>.<span>log</span>(<span>'【开启第一轮对话】'</span>);
<span>const</span> userMessage1 = <span>new</span> <span>HumanMessage</span>(<span>'红烧排骨怎么做'</span>);
<span>await</span> history.<span>addMessage</span>(userMessage1);
<span>const</span> messages1 = [systemMessage, ...(<span>await</span> history.<span>getMessages</span>())];
<span>const</span> response1 = <span>await</span> model.<span>invoke</span>(messages1);
<span>await</span> history.<span>addMessage</span>(response1);
<span>console</span>.<span>log</span>(<span>`AI 的第一次回答:<span>${response1.content}</span>\n`</span>);
<span>// 第二轮对话</span>
<span>console</span>.<span>log</span>(<span>'【第二轮对话】'</span>);
<span>const</span> userMessage2 = <span>new</span> <span>HumanMessage</span>(<span>'好吃吗?'</span>);
<span>await</span> history.<span>addMessage</span>(userMessage2);
<span>const</span> messages2 = [systemMessage, ...(<span>await</span> history.<span>getMessages</span>())];
<span>const</span> response2 = <span>await</span> model.<span>invoke</span>(messages2);
<span>await</span> history.<span>addMessage</span>(response2);
<span>console</span>.<span>log</span>(<span>`AI 的第二次回答:<span>${response2.content}</span>\n`</span>);
}
<span>fileHistoryWriteDemo</span>().<span>then</span>(<span>console</span>.<span>log</span>).<span>catch</span>(<span>console</span>.<span>error</span>);
运行后会在项目根目录生成 chat_history.json,对话数据按 sessionId 分组持久化存储。
2. 从文件恢复历史继续对话
程序重启后,只要指定相同的文件路径和 sessionId,就能自动读取之前的对话历史,继续往下聊:
<span>import</span> <span>'dotenv/config'</span>;
<span>import</span> { <span>ChatOpenAI</span> } <span>from</span> <span>'@langchain/openai'</span>;
<span>import</span> { <span>FileSystemChatMessageHistory</span> } <span>from</span> <span>'@langchain/community/stores/message/file_system'</span>;
<span>import</span> { <span>HumanMessage</span>, <span>SystemMessage</span>, <span>AIMessage</span> } <span>from</span> <span>'@langchain/core/messages'</span>;
<span>import</span> path <span>from</span> <span>'node:path'</span>;
<span>const</span> model = <span>new</span> <span>ChatOpenAI</span>({
<span>model</span>: process.<span>env</span>.<span>MODEL_NAME</span>,
<span>apiKey</span>: process.<span>env</span>.<span>OPENAI_API_KEY</span>,
<span>baseURL</span>: process.<span>env</span>.<span>OPENAI_BASE_URL</span>,
<span>temperature</span>: <span>0</span>
});
<span>async</span> <span>function</span> <span>fileHistoryRestoreDemo</span>(<span></span>) {
<span>const</span> filePath = path.<span>join</span>(process.<span>cwd</span>(), <span>'chat_history.json'</span>);
<span>const</span> sessionId = <span>'user_session_001'</span>;
<span>const</span> systemMessage = <span>new</span> <span>SystemMessage</span>(
<span>'你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'</span>
);
<span>// 使用相同的 filePath + sessionId,自动恢复历史</span>
<span>const</span> restoredHistory = <span>new</span> <span>FileSystemChatMessageHistory</span>({
filePath,
sessionId
});
<span>const</span> restoredMessages = <span>await</span> restoredHistory.<span>getMessages</span>();
<span>console</span>.<span>log</span>(<span>`从文件中恢复 <span>${restoredMessages.length}</span> 条历史信息`</span>);
restoredMessages.<span>forEach</span>(<span>(<span>msg, index</span>) =></span> {
<span>const</span> prefix = msg.<span>type</span> === <span>'human'</span> ? <span>'用户'</span> : <span>'助手'</span>;
<span>console</span>.<span>log</span>(<span>`<span>${index + <span>1</span>}</span>. [<span>${prefix}</span>]: <span>${msg.content.substring(<span>0</span>, <span>50</span>)}</span>....`</span>);
});
<span>// 继续第三轮对话</span>
<span>const</span> userMessage3 = <span>new</span> <span>HumanMessage</span>(<span>'需要什么食材'</span>);
<span>await</span> restoredHistory.<span>addMessage</span>(userMessage3);
<span>const</span> message = [systemMessage, ...(<span>await</span> restoredHistory.<span>getMessages</span>())];
<span>const</span> response3 = <span>await</span> model.<span>invoke</span>(message);
<span>console</span>.<span>log</span>(<span>`\nAI 的回答是:<span>${response3.content}</span>`</span>);
}
<span>fileHistoryRestoreDemo</span>();
核心概念
filePath:本地 JSON 文件的路径,所有会话数据都存在这里sessionId:会话唯一标识,用来隔离不同用户 / 不同对话的历史,同一个文件里可以存多个会话- 自动读写:
addMessage()会自动写入文件,getMessages()会自动从文件读取,不需要手动操作 IO
三、内存存储 vs 文件存储:怎么选?
表格
| 对比维度 | 内存存储 InMemory | 文件存储 FileSystem |
|---|---|---|
| 持久化 | ❌ 进程退出即丢失 | ✅ 本地文件永久保存 |
| 读写速度 | 极快(内存操作) | 一般(磁盘 IO) |
| 外部依赖 | 无 | 依赖本地文件系统 |
| 跨会话 | 不支持 | 支持 |
| 多会话隔离 | 需要自己实现 | 原生支持 sessionId |
| 适用场景 | 单次临时对话、开发调试 | 轻量持久化、个人工具、演示项目 |
| 缺点 | 数据易丢失 | 不适合高并发、大数据量 |
四、实战踩坑指南
1. 包导入路径易错
FileSystemChatMessageHistory 在 @langchain/community 包中,路径是:
@langchain/community/stores/message/file_system
不要写成 @langchain/core,也不要拼写错误。
2. 环境变量拼写陷阱
代码中 baseURL 对应的环境变量名必须和 .env 完全一致,注意不要出现 OPEAI_BASE_URL 这种少字母的笔误,否则会报 Missing credentials。
3. dotenv 读取路径
dotenv/config 默认从终端执行命令的目录查找 .env 文件,不是脚本文件所在目录。如果 .env 和脚本不在同一级,需要手动指定路径。
4. sessionId 必须正确隔离
不同用户、不同对话主题要用不同的 sessionId,否则多个会话的消息会混在一起,导致上下文混乱。
5. 文件存储不适合生产环境
本地文件存储没有并发控制,多进程同时写入可能损坏 JSON 文件;生产环境建议用 Redis、数据库或向量数据库做持久化。
五、总结与进阶
内存存储和文件存储是 LangChain 对话记忆体系的基础:
- 内存存储是会话内的临时载体,所有高级记忆策略最终都要在内存中组装上下文
- 文件存储是最简单的持久化方案,适合个人工具和轻量场景
但这两种方案都没有解决一个核心问题:对话会越来越长,最终超过模型的上下文窗口限制。
接下来就需要更高级的记忆管理策略:
- Token 截断:超过阈值自动删除最早的对话
- 对话摘要:旧对话压缩成摘要,保留核心信息
- 向量检索记忆:全量历史存入向量库,按需召回相关片段
我们会在后续文章中逐一拆解。
手把手落地 LangChain.js 两种基础记忆方案,代码可直接运行,踩坑清单尤其实用。适合刚接触对话记忆的 Node 开发者做快速验证与轻量持久化选型参考。