大模型记忆指南:从短时缓存到永久记忆,手把手带你吃透 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 个 token | slice(-4) 或 trimMessages |
| 总结(Summarization) | 压缩长历史为简短摘要,保留核心事实 | 每隔 20 条调用一次 LLM 生成摘要 |
| 检索(Retrieval) | 长期记忆,从向量库中召回相关历史 | 将历史嵌入向量,存入 Milvus/Pinecone |
本文会重点展示截断 和文件持久化的代码实现,并引出向量检索的未来方向。
三、短时记忆:InMemoryChatMessageHistory
先看最简单的内存级记忆------它把消息存在 RAM 里,进程重启就消失,适合单次会话。
javascript
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
核心代码解析(history-test.mjs)
javascript
const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个友好,幽默的做菜助手...");
SystemMessage:设置 AI 的"人设",它会始终生效(如果每次请求都带上)。HumanMessage:用户输入。AIMessage:模型输出。
添加消息到记忆:
javascript
await history.addMessage(userMessage1); // 加用户消息
const response1 = await model.invoke([systemMessage, ...(await history.getMessages())]);
await history.addMessage(response1); // 加AI回复
关键点:
history.getMessages()返回当前所有消息(数组),顺序为添加顺序。- 每次调用模型前,都要把
systemMessage和history合并成完整消息列表。 - 模型返回的
AIMessage对象也要存回 history,否则下次模型就"失忆"了。
遍历历史:
javascript
const allMessages = await history.getMessages();
allMessages.forEach((msg) => {
const prefix = msg.type === 'human' ? '用户' : '助手';
console.log(`${prefix}: ${msg.content}`);
});
这就是最基础的"数组 + 对象"记忆,但它仅存在于内存中。
四、文件持久化:FileSystemChatMessageHistory
如果你希望跨会话(比如用户第二天回来)还能记住之前的聊天,就得把历史持久化到磁盘。
LangChain 提供了 FileSystemChatMessageHistory,它会将消息序列化为 JSON 文件。
javascript
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import path from 'node:path';
写入历史(history-test2.mjs)
javascript
const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001"; // 区分不同用户
const history = new FileSystemChatMessageHistory({ filePath, sessionId });
sessionId:允许同一个文件存储多个用户的对话(内部会用 sessionId 做 key)。- 首次运行会创建文件,后续会追加。
接下来就与 InMemory 用法完全一致:addMessage、getMessages。
恢复历史(history-test3.mjs)
javascript
const restoredHistory = new FileSystemChatMessageHistory({ filePath, sessionId });
const restoredMessages = await restoredHistory.getMessages();
console.log(`恢复了 ${restoredMessages.length} 条消息`);
甚至可以接着聊------新消息会追加到文件末尾。
文件内容示例 (chat_history.json):
json
[
["user_session_001", ["HumanMessage", "红烧肉怎么做"]],
["user_session_001", ["AIMessage", "首先准备五花肉..."]],
...
]
💡 适用场景:个人助手、客服机器人,用户希望保留长期偏好。
五、上下文截断:控制 token 开销
当对话超过窗口限制时,必须裁剪历史。两种常见策略:按消息条数截断、按 token 数量截断。
5.1 按消息条数截断(简单粗暴)
javascript
const maxMessages = 4;
const trimmed = allMessages.slice(-maxMessages); // 只留最后4条
slice(-4) 保留最近 4 条消息,丢弃更早的。优点是简单,缺点是"条数"不精确------有些消息很长,有些很短,同样条数占用的 token 可能天差地别。
5.2 按 Token 数量精确截断(推荐)
LangChain 提供了 trimMessages 工具,配合 tiktoken 精确计算 token。
javascript
import { trimMessages } from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';
自定义 token 计数器:
javascript
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,你可以用新的数组调用模型。
执行结果:
javascript
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,生成"对话摘要",然后用摘要替换早期消息。
伪代码:
javascript
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):
bash
docker compose -f ./milvus-standalone-docker-compose.yml up -d
项目会使用 @zilliz/milvus2-sdk-node 连接 Node 应用,配合 Attu GUI 工具管理数据。
七、总结:Memory 选型指南
| 需求 | 推荐方案 | 工具/库 |
|---|---|---|
| 单次对话,轻量 | InMemoryChatMessageHistory | LangChain 内置 |
| 跨会话,低频访问 | FileSystemChatMessageHistory | @langchain/community |
| 严格控制 token 开销 | trimMessages + tiktoken | LangChain + js-tiktoken |
| 长对话保留核心信息 | 周期性总结 + 截断结合 | 自定义 + trimMessages |
| 大规模、跨会话、智能检索 | 向量数据库 + 嵌入检索 | Milvus + @zilliz/milvus2-sdk-node |
实践中通常组合使用:
- 短期记忆(内存)保持流畅。
- 定期备份到文件或数据库实现持久化。
- 通过截断或总结控制上下文窗口。
- 关键历史存入向量库实现长期智能召回。
最后,用一张图总结 Memory 的工作流:
掌握了这些,你的 AI 应用就能从"金鱼脑"进化为"大象记忆",持续为用户提供连贯、个性化的服务。
希望这篇文章帮你理清了 Memory 的实现脉络。如果在实际项目中遇到截断策略或向量检索的细节问题,欢迎在评论区交流!