[LangChain] 19. 持久化记忆
目前我们所使用的 Memory,大多只存在于内存中,一旦进程关闭、用户刷新页面,所有历史对话都会丢失。实际项目中,我们常常希望:
- 让用户“第二次回来”还能接着上次对话继续聊
- 将多轮聊天记录保存在本地或数据库中,用于分析、审计、训练、回溯
这时候,我们就需要将 Memory 做持久化操作,从而实现“记忆不丢失”。
要进行持久化操作,无非就两种方式:
- 存储至文件
- 存储至数据库
课堂练习:手动封装一个做持久化处理的 FileChatMessageHistory 类。
import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
import fs from "fs/promises";
import { AIMessage, HumanMessage } from "langchain";
import path from "path";
export class FileChatMessageHistory extends BaseListChatMessageHistory {
constructor(filepath) {
super();
this.filepath = filepath;
this._q = Promise.resolve();
}
_enqueue(fn) {
const run = () => fn();
const p = this._q.then(run, run);
this.q = p.catch(() => {});
return p;
}
async _ensureDir() {
await fs.mkdir(path.dirname(this.filepath), { recursive: true });
}
async _atomicWrite(text) {
await this._ensureDir();
const tmp = `.tmp-${Date.now()}-${Math.random().toString(36).substring(2)}`;
await fs.writeFile(tmp, text, "utf8");
await fs.rename(tmp, this.filepath);
}
async _readRaw() {
// "[{type: 'human', content: 'xxxx'},{type: 'ai', content: 'xxxx}]"
try {
const txt = await fs.readFile(this.filepath, "utf8");
const arr = JSON.parse(txt);
return Array.isArray(arr) ? arr : [];
} catch (e) {
if (e && e.code === "ENOENT") return [];
if (e instanceof SyntaxError) {
console.warn(`历史文件损坏,已忽略:${this.filepath}`);
return [];
}
throw e;
}
}
async _saveMessages(messages) {
const raw = messages.map((m) => ({
type: m.type,
content: m.content,
}));
await this._atomicWrite(JSON.stringify(raw, null, 2));
}
// 下面的方法就是对外的接口
async getMessages() {
const raw = await this._readRaw(); // 从文件中进行读取
return raw.map((r) =>
r.type === "human"
? new HumanMessage(r.content)
: new AIMessage(r.content)
);
}
/**
* 添加一条消息
* @param {*} message 消息对象
*/
async addMessage(message) {
return this._enqueue(async () => {
const msgs = await this.getMessages();
msgs.push(message);
await this._saveMessages(msgs);
});
}
async addMessages(messages) {
return this._enqueue(async () => {
const msgs = await this.getMessages();
msgs.push(...messages);
await this._saveMessages(msgs);
});
}
async clear() {
return this._enqueue(async () => {
this._atomicWrite("[]");
});
}
}
什么时候做持久化?
不需要每一轮都写磁盘,这样太频繁了:
- 文件 I/O 频繁写入会带来性能开销;
- 若使用数据库,也可能造成 写操作压力大;
通常采用“退出写入”的机制,或者推荐如下的触发时机:
| 触发时机 | 推荐用途 |
|---|---|
| 每隔几分钟 | 自动同步、做日志 |
| 用户关闭页面 | 防止会话丢失 |
| 用户点击保存 | 主动存档、构建知识库 |
第三方存储
在 LangChain 社区也提供了现成的文件型存储类,不想自己封装的同学,可以直接使用这个第三方库。
这个库是社区提供的,所以首先需要安装 @langchain/community
然后引入 FileSystemChatMessageHistory 工具类:
import { FileSystemChatMessageHistory } from "@langchain/community/stores/message/file_system";
然后实例化该工具类:
const chatHistory = new FileSystemChatMessageHistory({
sessionId,
storageDir: chatHistoryDir, // 存储的位置
});
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
import { ChatOllama } from "@langchain/ollama";
import { FileSystemChatMessageHistory } from "@langchain/community/stores/message/file_system";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import readlineSync from "readline-sync";
// 模型
const model = new ChatOllama({
model: "llama3",
temperature: 0.7,
});
// 提示词
const prompt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(`
你是一名中文对话助手。无论用户使用何种语言,**必须始终用【简体中文】回答**。
严格遵守:
- 只用简体中文回答;除代码块外,禁止出现英文句子或段落。
- 直接给出答案与步骤,避免客套和重复提示。
- 列点请用精炼中文要点;必要时给出简短结论→理由→建议的结构。
- 若需给代码,**代码内注释与说明也用中文**(保留必要的英文关键字/标识符即可)。
- 问题含糊时,依据摘要与本轮上下文做最合理假设并继续回答,**不要反问**。
示例(用于风格约束):
- ✅ “可以。建议你这样做:… 然后… 最后…”
- ✅ “原因是:…;因此建议:…”
- ❌ “Sure, here is …”
- ❌ “Of course! …”
`),
new MessagesPlaceholder("history"), // 历史记录占位符
HumanMessagePromptTemplate.fromTemplate("{input}"),
]);
// 配置对象
const cfg = {
configurable: { sessionId: "demo-session" },
};
const chain = new RunnableWithMessageHistory({
runnable: prompt.pipe(model),
getMessageHistory: async (sessionId) => {
return new FileSystemChatMessageHistory({
sessionId,
storageDir: "./history",
inputMessagesKey: "input",
historyMessagesKey: "history",
});
},
inputMessagesKey: "input",
historyMessagesKey: "history",
});
async function chatLoop() {
console.log("开始对话,输入内容后回车;输入 /clear 清空历史,/exit 退出。");
while (true) {
// —— 用户输入 ——
const input = readlineSync.question("\n你:").trim();
if (!input) continue;
// —— 命令处理 ——
if (input === "/exit" || input === "exit" || input === "quit") {
console.log("已退出。");
break;
}
if (input === "/clear") {
const h = await chain.getMessageHistory(cfg.configurable.sessionId);
await h.clear();
console.log("已清空历史。\n");
continue;
}
try {
const stream = await chain.stream({ input }, cfg);
process.stdout.write("助理:");
for await (const chunk of stream) {
process.stdout.write(chunk.content);
}
console.log("\n");
} catch (err) {
console.error("调用模型失败:", err?.message ?? err);
continue;
}
}
}
chatLoop();
课堂演示
使用 FileSystemChatMessageHistory 进行持久化存储。
-EOF-

浙公网安备 33010602011771号