大模型记忆指南:从短时缓存到永久记忆,手把手带你吃透 LangChain Memory 管理

41 阅读5分钟

大模型记忆指南:从短时缓存到永久记忆,手把手带你吃透 LangChain Memory 管理

大模型很聪明,但它没有“记性”——每次对话都是“初见”。如何让 AI 拥有短期工作记忆、长期持久记忆,甚至能从海量历史中检索关键信息?本文结合 LangChain 源码实践,为你拆解 Memory 的三种核心玩法。


一、为什么大模型需要 Memory?

大模型(LLM)本质上是无状态的——每次调用 model.invoke() 都像第一次见面,它不会记得你上一句说了什么。

我们平时能“连续对话”,是因为我们把所有历史消息都塞进了 messages 数组,再一股脑传给模型。这种“简单记忆”有两个致命缺陷:

  • 上下文窗口有限(比如 200k token),塞太多会超限,或者费用飙升。
  • 每次请求都重复发送历史,随着对话变长,延迟和成本线性增长。

所以,Memory 管理的目标就是:在有限的“脑容量”内,保留最有用信息,同时让 AI 能“记住”之前聊过什么,甚至跨会话记忆。

在 LangChain 的架构里,Agent = LLM + Harness(Tool + RAG + Memory + ...),Memory 是连接用户与模型的历史纽带。


二、Memory 的三种经典思路

策略适用场景实现手段
截断(Truncation)短期对话,只保留最近 N 条或 N 个 tokenslice(-4)trimMessages
总结(Summarization)压缩长历史为简短摘要,保留核心事实每隔 20 条调用一次 LLM 生成摘要
检索(Retrieval)长期记忆,从向量库中召回相关历史将历史嵌入向量,存入 Milvus/Pinecone

本文会重点展示截断文件持久化的代码实现,并引出向量检索的未来方向。


三、短时记忆:InMemoryChatMessageHistory

先看最简单的内存级记忆——它把消息存在 RAM 里,进程重启就消失,适合单次会话。

import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';

核心代码解析(history-test.mjs

const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个友好,幽默的做菜助手...");
  • SystemMessage:设置 AI 的“人设”,它会始终生效(如果每次请求都带上)。
  • HumanMessage:用户输入。
  • AIMessage:模型输出。

添加消息到记忆:

await history.addMessage(userMessage1);   // 加用户消息
const response1 = await model.invoke([systemMessage, ...(await history.getMessages())]);
await history.addMessage(response1);      // 加AI回复

关键点:

  • history.getMessages() 返回当前所有消息(数组),顺序为添加顺序。
  • 每次调用模型前,都要把 systemMessagehistory 合并成完整消息列表。
  • 模型返回的 AIMessage 对象也要存回 history,否则下次模型就“失忆”了。

遍历历史:

const allMessages = await history.getMessages();
allMessages.forEach((msg) => {
  const prefix = msg.type === 'human' ? '用户' : '助手';
  console.log(`${prefix}: ${msg.content}`);
});

这就是最基础的“数组 + 对象”记忆,但它仅存在于内存中。


四、文件持久化:FileSystemChatMessageHistory

如果你希望跨会话(比如用户第二天回来)还能记住之前的聊天,就得把历史持久化到磁盘

LangChain 提供了 FileSystemChatMessageHistory,它会将消息序列化为 JSON 文件。

import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import path from 'node:path';

写入历史(history-test2.mjs

const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001";   // 区分不同用户
const history = new FileSystemChatMessageHistory({ filePath, sessionId });
  • sessionId:允许同一个文件存储多个用户的对话(内部会用 sessionId 做 key)。
  • 首次运行会创建文件,后续会追加。

接下来就与 InMemory 用法完全一致:addMessagegetMessages

恢复历史(history-test3.mjs

const restoredHistory = new FileSystemChatMessageHistory({ filePath, sessionId });
const restoredMessages = await restoredHistory.getMessages();
console.log(`恢复了 ${restoredMessages.length} 条消息`);

甚至可以接着聊——新消息会追加到文件末尾。

文件内容示例chat_history.json):

[
  ["user_session_001", ["HumanMessage", "红烧肉怎么做"]],
  ["user_session_001", ["AIMessage", "首先准备五花肉..."]],
  ...
]

💡 适用场景:个人助手、客服机器人,用户希望保留长期偏好。


五、上下文截断:控制 token 开销

当对话超过窗口限制时,必须裁剪历史。两种常见策略:按消息条数截断、按 token 数量截断。

5.1 按消息条数截断(简单粗暴)

const maxMessages = 4;
const trimmed = allMessages.slice(-maxMessages);   // 只留最后4条

slice(-4) 保留最近 4 条消息,丢弃更早的。优点是简单,缺点是“条数”不精确——有些消息很长,有些很短,同样条数占用的 token 可能天差地别。

5.2 按 Token 数量精确截断(推荐)

LangChain 提供了 trimMessages 工具,配合 tiktoken 精确计算 token。

import { trimMessages } from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';

自定义 token 计数器:

function countTokens(messages, encoder) {
  let total = 0;
  for (const msg of messages) {
    const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
    total += encoder.encode(content).length;
  }
  return total;
}

const enc = getEncoding("cl100k_base");   // OpenAI 的编码器
const trimmedMessages = await trimMessages(allMessages, {
  maxTokens: 100,
  tokenCounter: async (msgs) => countTokens(msgs, enc),
  strategy: "last"   // 保留最后 maxTokens 个 token
});

trimMessages 原理

  • 它会从消息列表的末尾开始,逆向累积 token 数,直到达到 maxTokens 上限。
  • 如果某条消息超长,可能连它本身都放不下,则会抛出错误或截断该消息(可配置)。
  • 最终返回一个裁剪后的消息数组,不修改原始 history,你可以用新的数组调用模型。

执行结果

console.log(`保留 token 数:${countTokens(trimmedMessages, enc)}`);
// 输出 ≤ 100

🔧 为什么用 tiktoken 而不是 len(content)
不同模型的分词器不同(如 cl100k_base 对应 GPT-4/GPT-3.5-turbo),只有用对应的编码器才能精确计算实际 token 消耗,避免超限。


六、进阶之路:总结与向量检索

6.1 总结(Summarization)

当对话特别长(例如 200 条),截断会丢失早期的重要上下文。这时可以每隔一定轮数(比如每 20 条)调用一次 LLM,生成“对话摘要”,然后用摘要替换早期消息。

伪代码:

if (history.length % 20 === 0) {
  const summary = await model.invoke([SystemMessage("请总结以下对话"), ...history]);
  history.clear();   // 清空旧消息
  history.addMessage(new SystemMessage(`历史摘要:${summary.content}`));
}

这样既保留了核心信息,又控制了 token 数量。

6.2 向量检索(Retrieval)——长期记忆的终极方案

将历史消息(或用户偏好、文档)嵌入成向量,存入向量数据库(如 Milvus)。每次用户提问时,用问题向量去库中检索最相关的若干条历史,再拼接到 prompt 中。

这种方案超越了“截断”和“总结”的局限,实现了按需召回

readme.md 中提到了用 Milvus 做向量存储,并计划开发一个聊天应用 codex:每 20 条触发一次总结,生成摘要并存入 Milvus,下次对话时从 Milvus 拉取相关历史。

安装 Milvus(Docker Compose):

docker compose -f ./milvus-standalone-docker-compose.yml up -d

项目会使用 @zilliz/milvus2-sdk-node 连接 Node 应用,配合 Attu GUI 工具管理数据。


七、总结:Memory 选型指南

需求推荐方案工具/库
单次对话,轻量InMemoryChatMessageHistoryLangChain 内置
跨会话,低频访问FileSystemChatMessageHistory@langchain/community
严格控制 token 开销trimMessages + tiktokenLangChain + js-tiktoken
长对话保留核心信息周期性总结 + 截断结合自定义 + trimMessages
大规模、跨会话、智能检索向量数据库 + 嵌入检索Milvus + @zilliz/milvus2-sdk-node

实践中通常组合使用

  • 短期记忆(内存)保持流畅。
  • 定期备份到文件或数据库实现持久化。
  • 通过截断或总结控制上下文窗口。
  • 关键历史存入向量库实现长期智能召回。

最后,用一张图总结 Memory 的工作流:

graph LR
    User[用户消息] --> History[Memory 管理器]
    History -->|截断/总结| Trimmed[压缩后历史]
    History -->|向量检索| Retrieved[相关历史片段]
    Trimmed --> Prompt[组装 Prompt]
    Retrieved --> Prompt
    Prompt --> LLM[大模型]
    LLM --> Response[回复]
    Response --> History
    History -->|定期| File[(文件持久化)]
    History -->|嵌入| VectorDB[(向量数据库)]

掌握了这些,你的 AI 应用就能从“金鱼脑”进化为“大象记忆”,持续为用户提供连贯、个性化的服务。


希望这篇文章帮你理清了 Memory 的实现脉络。如果在实际项目中遇到截断策略或向量检索的细节问题,欢迎在评论区交流!