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

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

LangChain 提供了标准化的 ChatMessageHistory 体系来管理对话记忆,最基础的两种实现就是:

  • 内存存储(InMemoryChatMessageHistory) :会话级临时存储,性能最高,进程退出即清空
  • 文件存储(FileSystemChatMessageHistory) :本地 JSON 文件持久化,支持跨会话恢复历史

本文就基于 LangChain.js 带你从零实现这两种对话记忆,附带完整可运行代码和实战踩坑指南。

一、内存级对话记忆:InMemoryChatMessageHistory

内存存储是最简单的记忆方案:对话消息全部保存在 Node.js 进程内存中,读写速度极快,不需要任何外部依赖。

适用场景

  • 单次会话的临时对话
  • 开发调试阶段快速验证
  • 短对话、轻量场景

完整实现代码

javascript 复制代码
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import {
  HumanMessage,
  SystemMessage,
  AIMessage,
  getBufferString
} from '@langchain/core/messages';

// 初始化大模型(兼容通义千问等 OpenAI 协议模型)
const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
  temperature: 0
});

async function InMemoryDemo() {
  // 1. 初始化内存记忆容器
  const history = new InMemoryChatMessageHistory();
  const systemMessage = new SystemMessage(
    '你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'
  );

  // 2. 第一轮对话
  console.log('【开启第一轮对话】');
  const userMessage1 = new HumanMessage('你今天吃什么?');
  // 用户消息存入记忆
  await history.addMessage(userMessage1);
  // 组装:系统提示 + 历史对话
  const messages1 = [systemMessage, ...(await history.getMessages())];
  const response1 = await model.invoke(messages1);
  // AI 回复也存入记忆
  await history.addMessage(response1);
  console.log(`助手:${response1.content}\n`);

  // 3. 第二轮对话(自动携带上一轮上下文)
  console.log('【第二轮对话】');
  const userMessage2 = new HumanMessage('好吃吗?');
  await history.addMessage(userMessage2);
  const messages2 = [systemMessage, ...(await history.getMessages())];
  const response2 = await model.invoke(messages2);
  await history.addMessage(response2);
  console.log(`助手:${response2.content}\n`);

  // 4. 查看全部历史消息
  const allMessages = await history.getMessages();
  console.log(`共保存了 ${allMessages.length} 条对话`);
  allMessages.forEach((msg, index) => {
    const prefix = msg.type === 'human' ? '用户' : '助手';
    console.log(`${index + 1}. [${prefix}]: ${msg.content.substring(0, 50)}....`);
  });
}

InMemoryDemo()
  .catch(console.error)
  .finally(() => {
    console.log('done');
  });

核心逻辑拆解

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

特点总结

  • ✅ 优点:零依赖、速度极快、API 简单
  • ❌ 缺点:进程重启 / 页面刷新就丢失,无法跨会话
  • 🎯 定位:短期会话记忆,作为所有记忆方案的基础载体

二、文件持久化记忆:FileSystemChatMessageHistory

内存存储的对话无法保留,程序关掉就没了。如果希望下次运行还能继续之前的对话,就需要文件持久化------ 把对话历史写入本地 JSON 文件。

依赖说明

文件存储实现不在 @langchain/core 中,需要安装社区包:

bash 复制代码
npm install @langchain/community

1. 写入对话到本地文件

javascript 复制代码
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages';
import path from 'node:path';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
  temperature: 0
});

async function fileHistoryWriteDemo() {
  // 配置:本地文件路径 + 会话ID
  const filePath = path.join(process.cwd(), 'chat_history.json');
  const sessionId = 'user_session_001';
  const systemMessage = new SystemMessage(
    '你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'
  );

  // 初始化文件记忆容器
  const history = new FileSystemChatMessageHistory({
    filePath,
    sessionId
  });

  // 第一轮对话
  console.log('【开启第一轮对话】');
  const userMessage1 = new HumanMessage('红烧排骨怎么做');
  await history.addMessage(userMessage1);
  const messages1 = [systemMessage, ...(await history.getMessages())];
  const response1 = await model.invoke(messages1);
  await history.addMessage(response1);
  console.log(`AI 的第一次回答:${response1.content}\n`);

  // 第二轮对话
  console.log('【第二轮对话】');
  const userMessage2 = new HumanMessage('好吃吗?');
  await history.addMessage(userMessage2);
  const messages2 = [systemMessage, ...(await history.getMessages())];
  const response2 = await model.invoke(messages2);
  await history.addMessage(response2);
  console.log(`AI 的第二次回答:${response2.content}\n`);
}

fileHistoryWriteDemo().then(console.log).catch(console.error);

运行后会在项目根目录生成 chat_history.json,对话数据按 sessionId 分组持久化存储。

2. 从文件恢复历史继续对话

程序重启后,只要指定相同的文件路径和 sessionId,就能自动读取之前的对话历史,继续往下聊:

javascript 复制代码
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages';
import path from 'node:path';

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
  temperature: 0
});

async function fileHistoryRestoreDemo() {
  const filePath = path.join(process.cwd(), 'chat_history.json');
  const sessionId = 'user_session_001';
  const systemMessage = new SystemMessage(
    '你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧'
  );

  // 使用相同的 filePath + sessionId,自动恢复历史
  const restoredHistory = new FileSystemChatMessageHistory({
    filePath,
    sessionId
  });

  const restoredMessages = await restoredHistory.getMessages();
  console.log(`从文件中恢复 ${restoredMessages.length} 条历史信息`);
  restoredMessages.forEach((msg, index) => {
    const prefix = msg.type === 'human' ? '用户' : '助手';
    console.log(`${index + 1}. [${prefix}]: ${msg.content.substring(0, 50)}....`);
  });

  // 继续第三轮对话
  const userMessage3 = new HumanMessage('需要什么食材');
  await restoredHistory.addMessage(userMessage3);
  const message = [systemMessage, ...(await restoredHistory.getMessages())];
  const response3 = await model.invoke(message);
  console.log(`\nAI 的回答是:${response3.content}`);
}

fileHistoryRestoreDemo();

核心概念

  • filePath:本地 JSON 文件的路径,所有会话数据都存在这里
  • sessionId:会话唯一标识,用来隔离不同用户 / 不同对话的历史,同一个文件里可以存多个会话
  • 自动读写 :addMessage() 会自动写入文件,getMessages() 会自动从文件读取,不需要手动操作 IO

三、内存存储 vs 文件存储:怎么选?

表格

对比维度 内存存储 InMemory 文件存储 FileSystem
持久化 ❌ 进程退出即丢失 ✅ 本地文件永久保存
读写速度 极快(内存操作) 一般(磁盘 IO)
外部依赖 无 依赖本地文件系统
跨会话 不支持 支持
多会话隔离 需要自己实现 原生支持 sessionId
适用场景 单次临时对话、开发调试 轻量持久化、个人工具、演示项目
缺点 数据易丢失 不适合高并发、大数据量

四、实战踩坑指南

1. 包导入路径易错

FileSystemChatMessageHistory 在 @langchain/community 包中,路径是:

bash 复制代码
@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 截断:超过阈值自动删除最早的对话
  • 对话摘要:旧对话压缩成摘要,保留核心信息
  • 向量检索记忆:全量历史存入向量库,按需召回相关片段

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

相关推荐
lizhongxuan44 分钟前
Agent Sandbox 怎么选
后端
用户5446933925644 分钟前
为什么有管理员权限,Token正确,访问却是403
后端
nyaomaru44 分钟前
你的 Type Guard 可能会悄悄地与 TypeScript 类型发生偏移 🔧
后端·typescript
明月_清风44 分钟前
数据平台到底是什么?一篇文章搞懂数据平台开发
大数据·后端·数据分析
心之语歌44 分钟前
DeerFlow Docker Desktop 部署教程
后端
美好世界44 分钟前
Codex 源码导读:第二部分——一次 Turn 的完整生命周期
后端
据说幸运很容易44 分钟前
接口测试框架重构:YAML 用例驱动 + 分层设计
后端·架构
海岳云舟1 小时前
SpringCloudGateway 动态转发后端服务
后端
我的div丢了肿么办1 小时前
自定义类型和类型别名以及实例化结构体的5种方式
后端·go
小满zs1 小时前
Go语言第十三章(互斥锁,读写锁)
后端·go