[LangChain] 19. 持久化记忆

目前我们所使用的 Memory,大多只存在于内存中,一旦进程关闭、用户刷新页面,所有历史对话都会丢失。实际项目中,我们常常希望:

  • 让用户“第二次回来”还能接着上次对话继续聊
  • 将多轮聊天记录保存在本地或数据库中,用于分析、审计、训练、回溯

这时候,我们就需要将 Memory 做持久化操作,从而实现“记忆不丢失”。

要进行持久化操作,无非就两种方式:

  1. 存储至文件
  2. 存储至数据库

课堂练习:手动封装一个做持久化处理的 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-

posted @ 2025-11-16 23:07  Zhentiw  阅读(35)  评论(0)    收藏  举报