大模型本质上是无状态的 —— 每一次调用 model.invoke() 都是一次独立的请求,它不会自动记住上一轮说了什么。想要实现连贯的多轮对话,就必须手动维护对话历史,把上下文拼接到 Prompt 中再传给模型。
LangChain 提供了标准化的 ChatMessageHistory 体系来管理对话记忆,最基础的两种实现就是:
- 内存存储(
InMemoryChatMessageHistory) :会话级临时存储,性能最高,进程退出即清空 - 文件存储(
FileSystemChatMessageHistory) :本地 JSON 文件持久化,支持跨会话恢复历史
本文就基于 LangChain.js 带你从零实现这两种对话记忆,附带完整可运行代码和实战踩坑指南。
一、内存级对话记忆:InMemoryChatMessageHistory
内存存储是最简单的记忆方案:对话消息全部保存在 Node.js 进程内存中,读写速度极快,不需要任何外部依赖。
适用场景
- 单次会话的临时对话
- 开发调试阶段快速验证
- 短对话、轻量场景
完整实现代码
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');
});
核心逻辑拆解
- 初始化记忆容器:
new InMemoryChatMessageHistory()创建一个内存中的消息队列 - 消息写入:每一轮用户提问和 AI 回复,都通过
addMessage()存入记忆 - 上下文拼接:调用模型前,用
getMessages()取出全部历史,和 System 提示词拼成完整消息数组 - 无持久化:脚本执行结束,内存释放,对话历史永久丢失
特点总结
- ✅ 优点:零依赖、速度极快、API 简单
- ❌ 缺点:进程重启 / 页面刷新就丢失,无法跨会话
- 🎯 定位:短期会话记忆,作为所有记忆方案的基础载体
二、文件持久化记忆:FileSystemChatMessageHistory
内存存储的对话无法保留,程序关掉就没了。如果希望下次运行还能继续之前的对话,就需要文件持久化—— 把对话历史写入本地 JSON 文件。
依赖说明
文件存储实现不在 @langchain/core 中,需要安装社区包:
npm install @langchain/community
1. 写入对话到本地文件
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,就能自动读取之前的对话历史,继续往下聊:
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 包中,路径是:
@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 截断:超过阈值自动删除最早的对话
- 对话摘要:旧对话压缩成摘要,保留核心信息
- 向量检索记忆:全量历史存入向量库,按需召回相关片段
我们会在后续文章中逐一拆解。