LangChain.js 对话记忆体系(一):内存存储与文件持久化,让 AI 拥有对话记忆

文章来源声明: 原文作者:YIAN; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689077368329388083; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

手把手落地 LangChain.js 两种基础记忆方案,代码可直接运行,踩坑清单尤其实用。适合刚接触对话记忆的 Node 开发者做快速验证与轻量持久化选型参考。

大模型本质上是**无状态**的 —— 每一次调用 `model.invoke()` 都是一次独立的请求,它不会自动记住上一轮说了什么。想要实现连贯的多轮对话,就必须手动维护对话历史,把上下文拼接到 Prompt 中再传给模型。

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>);
  });

核心逻辑拆解

  1. 初始化记忆容器:new InMemoryChatMessageHistory() 创建一个内存中的消息队列
  2. 消息写入:每一轮用户提问和 AI 回复,都通过 addMessage() 存入记忆
  3. 上下文拼接:调用模型前,用 getMessages() 取出全部历史,和 System 提示词拼成完整消息数组
  4. 无持久化:脚本执行结束,内存释放,对话历史永久丢失

特点总结

  • ✅ 优点:零依赖、速度极快、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 截断:超过阈值自动删除最早的对话
  • 对话摘要:旧对话压缩成摘要,保留核心信息
  • 向量检索记忆:全量历史存入向量库,按需召回相关片段

我们会在后续文章中逐一拆解。