把 Agent 的记忆写进文件:内存 vs 文件,两把钥匙搞定多会话
上一篇我讲了 Agent 的 Memory 怎么在上下文预算里做截断 。这一篇换一个方向,讲 Agent 的"记事本"到底存在哪。
还是先给结论:同样的对话历史,可以存进内存,也可以落进文件。 存哪个,决定你的 Agent 是"关掉就失忆",还是"重启依然记得你昨天聊过什么"。
我手上有三个 LangChain 的小 demo,正好是一套完整的演进:
history-test:内存记忆(最基础)history-test2:文件记忆 · 写入history-test3:文件记忆 · 恢复后续聊
这一篇就顺着这三步,讲透一个概念------记忆的存储介质,决定了 Agent 的生命周期。
一个概念:Agent 的"记住"有生命周期
很多刚写 Agent 的人会有个错觉:加个 memory,模型就"一直记住了"。不是的。
Chat 模型本身无状态,所谓的记忆,其实是你每轮把历史塞回 messages。那么问题就变成:这些"塞回去的东西",你存在哪、能活多久?
text
内存 -> 快,但程序一停,记忆清零(进程级)
文件 / 存储 -> 慢一点,但能躺着,重启不丢(跨进程、跨会话)
向量库 -> 最"在线",配合检索(RAG + Memory)
这一篇只玩前两个:内存 和文件。
第一站:内存记忆(InMemoryChatMessageHistory)
最直接的实现是 InMemoryChatMessageHistory------一个放在内存里的历史对象,你往里 addMessage,再 getMessages 取回来。
js
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个幽默的做菜助手");
// 第一轮
await history.addMessage(new HumanMessage("你今天吃的什么?"));
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
await history.addMessage(response1); // 记得把回复也写回 history
// 第二轮,history 已经带上上一轮记忆
await history.addMessage(new HumanMessage("好吃吗?"));
const messages2 = [systemMessage, ...(await history.getMessages())];
const response2 = await model.invoke(messages2);
console.log(`共保存 ${(await history.getMessages()).length} 条对话`);
关键动作 :每一轮,既要把用户说的话 addMessage,也要把模型的回复 addMessage------否则历史链条会断,模型看不到自己刚才说过什么。
但它有个死穴:InMemory,对象一销毁、进程一停,全没了。 适合单次会话,适合演示,不适合要"记得住"的生产。
第二站:文件记忆,写入(FileSystemChatMessageHistory)
要跨进程持久化,LangChain 给了文件版。它的构造函数需要两把钥匙:
js
const filePath = path.join(process.cwd(), 'chat_history.json');
const sessionId = 'user_session_001'; // 多用户:一个用户一把钥匙
const history = new FileSystemChatMessageHistory({ filePath, sessionId });
filePath:记忆写在哪个文件sessionId:同一个文件里,区分"这是谁的会话"
之后 API 和内存版一模一样------addMessage / getMessages 照用,但历史会真正落盘成一个 JSON 文件。
js
const systemMessage = new SystemMessage("你是一个幽默的做菜助手");
// 第一轮
await history.addMessage(new HumanMessage("红烧肉怎么做?"));
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
await history.addMessage(response1);
// 第二轮,自动带上文件里的历史
await history.addMessage(new HumanMessage("好吃吗?"));
const messages2 = [systemMessage, ...(await history.getMessages())];
const response2 = await model.invoke(messages2);
const all = await history.getMessages();
console.log(`共保存 ${all.length} 条对话`);
跑完你会发现,chat_history.json 里真的多出一段 JSON------对话被写进了硬盘。
第三站:文件记忆,恢复后续聊(history-test3)
文件版的真正价值在"换一个进程也能接上 "。重启 Node,重新 new FileSystemChatMessageHistory(同样的 filePath + sessionId),历史就从文件里"回来"了:
js
const restoredHistory = new FileSystemChatMessageHistory({ filePath, sessionId });
const restoredMessages = await restoredHistory.getMessages();
console.log(`从文件中恢复了 ${restoredMessages.length} 条历史记录`);
restoredMessages.forEach((msg, i) => {
console.log(`${i + 1}. ${msg.type === 'human' ? '用户' : '助手'}:` +
`${msg.content.substring(0, 50)}...`);
});
// 接着续聊第三轮,模型记得前两轮
await restoredHistory.addMessage(new HumanMessage("需要哪些食材"));
const messages3 = [systemMessage, ...(await restoredHistory.getMessages())];
const response3 = await model.invoke(messages3);
await restoredHistory.addMessage(response3);
console.log(response3.content);
看到没?前面两轮的记忆"存活"到了第三轮,因为它躺在文件里,而不是藏在早已销毁的内存对象里。
一张表看懂三站
| InMemory | FileSystem | |
|---|---|---|
| 存储介质 | 内存 | 磁盘 JSON |
| 进程重启 | 清零 | 保留,可恢复 |
| 多用户区分 | 需要自己管 | sessionId |
| 典型场景 | 单次会话 / 演示 | 跨会话、需持久化 |
一个反直觉的点:记忆"放哪",比"多聪明"更早决定体验
你花大价钱把 prompt 优化得再聪明,如果记忆只存在内存里,用户重启浏览器换个进程,Everything 全忘------体验直接断崖。
所以搭 Agent 的时候,先问的不是"记忆怎么割",而是:这次对话,它该活多久? 活一场 → 内存;活几场 → 文件;要"大海捞针"地记 → 向量库(RAG + Memory)。
而这三种存储介质,管的还是同一件事:把该记得的,从"窗口塞回去"这件事,做得又稳又持久。