📖 本章学习目标
- ✅ 理解 LLM 的"失忆"问题及其对应用开发的影响
- ✅ 使用
thread_id自动管理多轮对话历史- ✅ 选择合适的状态持久化方案(MemorySaver vs PostgresSaver)
- ✅ 扩展 Agent 状态,存储自定义业务数据
- ✅ 实现基于向量检索的长期记忆系统
- ✅ 使用摘要记忆控制 Token 消耗
- ✅ 避免常见的记忆管理陷阱
一、LLM是无状态的
1、 无状态设计的优劣
在《第三章:模型抽象层与消息的输入输出》我们说明了大语言模型本质是一种无状态的数学模型,每次 API 调用都是独立的,它不记得上一次你说了什么,也不知道用户的历史偏好。如果你想实现多轮对话,那么就在每次向 AI 发起新提问时,把之前的对话历史(包括你的提问和 AI 的回答)打包,连同新问题一起发给它。
LLM的这种设计是有工程价值的,比如:
- 极高的可扩展性:无状态意味着服务商可以随意地并行处理海量请求,不需要在服务器端为每个用户维护复杂的会话状态,极大降低了系统崩溃的风险。
- 简洁与安全性:输入 Token,输出 Token,是一个纯粹的数学函数,架构极其简洁。另外,没有状态就不存在“会话间数据串扰”或“内存泄漏”的风险。对话结束后,模型端不保留数据,在隐私保护和系统架构上都更加清爽。
当然,这种设计也是带来一定的问题:
- 没有连续性: 无法记住会话导致无法维持多轮对话,因此在连续的任务上就无法更好的工作。
- 成本与延迟飙升:为了维持多轮对话,让模型“假装”有记忆,应用层必须在每次请求时,把之前所有的对话历史(上下文)重新打包发给模型。随着对话变长,发送的数据量(Token)呈线性甚至指数级增长,导致 API 费用极其昂贵,且响应速度越来越慢。
- 轨迹灾难(Trajectory Disaster) :在长对话中,早期的错误、冗余的废话会不断堆积在上下文里。模型会被这些“垃圾信息”污染,导致它不断重复之前的错误,或者在无关的细节中迷失,推理能力大幅下降。
2、LangChain.js 的记忆管理方案
为了让LLM有记忆,工程师们提出了各种记忆管理方案,这些管理方案都是应用层的,其管理的核心本质是 “计算与存储分离”,即LLM负责思考与推理,记忆管理层负责记录与检索。比如有长期记忆、短期记忆、摘要记忆、类人记忆(前沿方向)等。LangChain对主流的记忆管理都提供了完整管理方案,从大类来看主要分为短期记忆和长期记忆两个层次的记忆。
flowchart TB
subgraph Short["短期记忆(当前会话内)"]
H["对话历史<br/>Messages History"]
S["自定义状态<br/>Custom State"]
end
subgraph Long["长期记忆(跨会话)"]
DB["外部存储<br/>Database / Vector Store"]
Summary["记忆摘要<br/>Memory Summary"]
end
Agent --> Short
Agent <-->|读取/写入| Long
style Short fill:#e8f4fd,stroke:#1890ff,stroke-width:3px
style Long fill:#f6ffed,stroke:#52c41a,stroke-width:3px
记忆类型对比:
| 类型 | 作用范围 | 存储位置 | 典型用途 |
|---|---|---|---|
| 短期记忆 | 当前会话内 | 内存或数据库 | 维持多轮对话连贯性 |
| 长期记忆 | 跨会话、跨设备 | 向量数据库 | 记住用户偏好、历史任务 |
随着应用复杂度的提升,LangChain 官方现在强烈推荐在生产环境中使用 LangGraph 的 Checkpointer(检查点/状态快照)机制来替代早期的 Memory 组件。更多内容,请关注后续推出的LangGraph教程的记忆管理相关章节。
二、短期记忆:对话历史的管理
短期记忆解决的核心问题是:如何让 Agent 在多轮对话中保持上下文连贯。
1、手动管理消息历史(基础方式)
最直观的方式是手动拼接消息历史。
示例代码
import "dotenv/config";
import { createAgent } from "langchain";
const agent = createAgent({
model: "openai:gpt-4o",
tools: []
});
// 第一轮对话
const result1 = await agent.invoke({
messages: [{ role: "user", content: "我叫张三,今年 28 岁。" }],
});
// 第二轮对话:手动带上第一轮的消息
const result2 = await agent.invoke({
messages: [
// 第一轮的用户消息
{ role: "user", content: "我叫张三,今年 28 岁。" },
// 第一轮的 AI 回复
result1.messages.at(-1)!,
// 第二轮的新问题
{ role: "user", content: "我叫什么名字,多大了?" },
],
});
console.log(result2.messages.at(-1)?.content);
// 输出:你叫张三,今年 28 岁。
从上方例子可以看到,每一轮对话都需要手动将前面的对话(包括AI回复的内容)按历史顺序打包后形成新的对话发送给AI从而获得连贯的对话回复。
这种手动拼接的方式最大的问题就是维护很繁琐,也容易遗漏历史消息。从代码的角度来看可读性也极差。
2、使用 thread_id 自动管理(推荐)
LangChain的 Agent 支持通过 thread_id(会话 ID)自动持久化和恢复对话历史,无需手动维护消息列表。
第一步:配置 Checkpointer
// // src/6-short-memory.ts
import "dotenv/config";
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
// 创建内存中的状态存储
// 适合开发测试,生产环境用数据库存储
const checkpointer = new MemorySaver();
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
checkpointer, // 传入状态持久化器
});
实例化的checkpointer(检查点) 负责保存和恢复 Agent 的状态,而MemorySaver 是最简单的实现,它把状态存在内存中,在会话结束后机会销毁。这种方式适合在开发环境中使用,生产环境一般使用PostgresSaver、RedisSaver 等方式。
第二步:使用 thread_id
在 LangChain(特别是 LangGraph)中,config.thread_id 可以通俗地理解为 “对话的唯一身份证号”或“会话 ID” 。它是 LangChain 框架用来区分、隔离和找回不同对话记忆的核心钥匙。thread_id 是连接记忆系统与LLM关键纽带。
使用方法是创建一个唯一的thread_id,将其传入agent触发的配置项:
// // src/6-short-memory.ts
// 配置 thread_id,同一 thread_id 下的对话会自动保存和恢复
const config = {
configurable: {
thread_id: "user-zhangsan-session-001"
}
};
// 第一次对话
await agent.invoke(
{ messages: [{ role: "user", content: "我叫张三,喜欢 TypeScript。" }] },
config // 传入 thread_id
);
// 第二次对话(历史自动从 checkpointer 恢复)
const result = await agent.invoke(
{ messages: [{ role: "user", content: "我叫什么名字,喜欢什么编程语言?" }] },
config // 同一个 thread_id
);
console.log(result.messages.at(-1)?.content);
下面时序图可以帮助你更好的理解thread_id和checkpointer的工作原理。
sequenceDiagram
participant U as 用户
participant A as Agent
participant C as Checkpointer
U->>A: 第一次对话:"我叫张三"
A->>C: 保存状态(thread_id + messages)
A-->>U: "好的,张三"
U->>A: 第二次对话:"我叫什么名字?"
A->>C: 根据 thread_id 恢复状态
C-->>A: 返回历史消息
A-->>U: "你叫张三"
相比手动管理历史消息,使用这种自动管理的方式,优势非常明显,不仅解决了上文提到的问题,还原生支持多会话隔离。
💡 thread_id 的设计建议
thread_id是区分不同会话的唯一标识符。在实际应用中,可以用userId + sessionId的组合:// 最佳实践:用户ID + 会话ID const config = { configurable: { thread_id: `user-${userId}-session-${sessionId}` } }; // 示例: // user-12345-session-abc123 // user-12345-session-def456 (同一用户的不同会话) // user-67890-session-ghi789 (不同用户)这种组合方式既能区分不同用户,也能在同一用户的不同会话间切换,便于调试和问题追踪
3、生产环境的状态持久化
MemorySaver 把状态保存在内存里,进程重启后数据就丢了,只适合开发测试。如果要让记忆持久化通常需要借助数据库来存储,LangChain中常见的数据库类型的checkpointer主要如下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| PostgresSaver | 持久化,支持复杂查询 | 需要 PostgreSQL 数据库 | 生产环境首选 |
| RedisSaver | 高性能,支持过期策略 | 需要 Redis 服务 | 高并发场景 |
| SQLiteSaver | 轻量级,单文件 | 不支持并发写入 | 小型应用 |
使用 PostgreSQL 持久化
下面以PostgresSaver为例说明其使用方法。
第一步:安装依赖
pnpm add @langchain/langgraph-checkpoint-postgres pg
第二步:配置数据库连接
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import pg from "pg";
// 创建数据库连接池
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
// 创建 PostgresSaver 实例
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
// 初始化数据库表(只需运行一次)
await checkpointer.setup();
// 创建 Agent
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
checkpointer, // 使用 PostgreSQL 持久化
});
第三步:使用(与 MemorySaver 完全一样)
const config = {
configurable: {
thread_id: "user-12345-session-001"
}
};
// 对话历史会自动保存到 PostgreSQL
await agent.invoke(
{ messages: [{ role: "user", content: "你好!" }] },
config
);
数据存储位置:
PostgreSQL 中会创建以下表:
checkpoints:存储检查点数据checkpoint_blobs:存储大的状态数据checkpoint_writes:存储待写入的操作
⚠️ 实践建议:
- 生产环境必须使用持久化存储
- 定期备份数据库,防止数据丢失
- 监控数据库性能,必要时添加索引
三、自定义状态:扩展 Agent 的上下文
除了对话历史,很多时候我们需要在 Agent 的状态里保存业务数据。
1、默认状态结构
LangChain Agent 的默认状态只有 messages 字段:
// Agent 的默认状态结构
{
messages: BaseMessage[] // 对话历史
}
它的局限性就是只能存储消息,无法存储用户偏好、任务状态等业务数据。比如你在构建一个智能客服 Agent,需要有以下能力:
- 记录用户的会员等级(影响优惠策略)
- 保存当前的订单号(用于查询订单状态)
- 标记是否已经验证过用户身份
这些信息不适合放在对话历史中,应该作为独立的状态字段,这时候就需要自定义状态了。
2、定义自定义状态 Schema
要自定义状态,需要通过 LangGraph 的 Annotation,它可以定义包含更多字段的自定义状态。
import {
Annotation,
MessagesAnnotation
} from "@langchain/langgraph";
// 1. 定义自定义状态
const AgentState = Annotation.Root({
// 继承默认的 messages 字段(带消息合并逻辑)
...MessagesAnnotation.spec,
// 添加自定义字段:用户偏好
userPreferences: Annotation<{
language: string;
responseStyle: "concise" | "detailed";
timezone: string;
}>({
reducer: (_, next) => next, // 简单覆盖更新
default: () => ({
language: "zh-CN",
responseStyle: "concise",
timezone: "Asia/Shanghai",
}),
}),
// 添加自定义字段:任务上下文
taskContext: Annotation<string | null>({
reducer: (_, next) => next,
default: () => null,
}),
});
- 使用
MessagesAnnotation.spec可以继承默认的状态,即包含messages字段,它会自带消息合并逻辑(追加新消息) - 自定义自动可以是任意数据类型,比如例子里定义了包含语言、回复风格、时区。自定义状态需要定义好
reducer(定义如何更新,例子这里直接覆盖)和默认值default。 - 自定义状态可以定义多个,比如例子还定义了
taskContext
🔍 Annotation 和 Reducer 是什么?
LangGraph 的状态是不可变的,每次更新都产生一个新状态。
reducer定义了如何把旧值和新值合并:常见 Reducer 类型:
// 1. 直接覆盖(适合普通字段) reducer: (_, next) => next // 2. 数组追加(适合消息列表) reducer: (current, next) => [...current, ...next] // 3. 对象合并(适合配置项) reducer: (current, next) => ({ ...current, ...next }) // 4. 自定义逻辑 reducer: (current, next) => { // 你的合并逻辑 return merged; }这种设计保证了状态变更的可追踪性,是 LangGraph 能够支持时间旅行调试和断点续传的基础。更多内容请关注LangGraph教程。
3、在 Agent 中使用自定义状态
// src/6-anotation.ts
import { StateGraph } from "@langchain/langgraph";
// 2. 创建工作流并添加节点和边(使用链式调用)
const workflow = new StateGraph(AgentState)
.addNode("agent", async (state) => {
// 读取自定义状态
const preferences = state.userPreferences;
const taskContext = state.taskContext;
console.log(`[Agent节点] 用户语言:${preferences.language}`);
console.log(`[Agent节点] 回复风格:${preferences.responseStyle}`);
console.log(`[Agent节点] 当前任务上下文:${taskContext}`);
// 模拟 LLM 的回复逻辑,返回一条 AI 消息
return {
messages: [new AIMessage(`收到!已按 ${preferences.responseStyle} 风格处理任务:${taskContext || "无具体任务"}`)],
taskContext: "任务已处理完毕", // 更新自定义字段
};
})
.addNode("preferenceUpdater", async (state) => {
console.log("[PreferenceUpdater节点] 正在更新用户偏好...");
return {
userPreferences: {
...state.userPreferences,
responseStyle: "detailed", // 将回复风格改为详细模式
language: "en-US", // 将语言改为英文
},
};
})
.addEdge('__start__', "preferenceUpdater") // 从起点先更新偏好
.addEdge("preferenceUpdater", "agent") // 然后进入 agent 节点
.addEdge("agent", '__end__');
// 3. 编译工作流
const app = workflow.compile();
// 4. 运行并测试工作流
async function runWorkflow() {
console.log("--- 开始执行工作流 ---");
// 传入初始消息和配置(thread_id 用于隔离不同会话的记忆)
const result = await app.invoke({
messages: [new HumanMessage("你好,请帮我处理一下今天的任务")],
}, {
configurable: { thread_id: "user-session-001" }
});
console.log("\n--- 最终运行结果 ---");
console.log("最终任务上下文:", result.taskContext);
console.log("最终用户偏好:", result.userPreferences);
console.log("完整对话历史:");
result.messages.forEach((msg) => {
console.log(`- [${msg._getType()}]: ${msg.content}`);
});
}
runWorkflow().catch(console.error);
结果:
以上完整例子是属于LangGraph的内容,如果不理解也关系,只需要记住有这个东西即可。
四、长期记忆:跨会话的信息持久化
短期记忆虽然能帮我们处理当前上下文的任务,并保持会话连贯,但容量极其有限且生命周期极短。一旦对话窗口关闭或注意力转移,这些宝贵的信息便会迅速流失,导致 AI 无法跨越时间维度真正理解用户。为了突破这一瓶颈,让智能体从“一次性工具”进化为“长期伙伴”,我们就必须引入长期记忆。
长期记忆是信息的持久化存储库,从理论上来讲,它拥有近乎无限的容量,能够将关键的交互细节、客观事实以及习得的技能沉淀下来,保存数月甚至终身。如果说短期记忆维持了单次对话的连贯性,那么长期记忆则在某种程度上赋予了 AI 跨越时空的认知能力。比如它不仅记住了你是谁、你的喜好与习惯(语义记忆),还能回溯过往的具体经历(情景记忆)并掌握复杂的操作技能(程序记忆)。正是通过这种深度的信息沉淀,AI 才能真正实现跨会话的成长与个性化陪伴。我们熟知的龙虾、Claude Code、Hermes Agent等各类Agent都在长期记忆的实现上下了非常大的功夫。
1、短期记忆 vs 长期记忆的比较
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 作用范围 | 当前会话内 | 跨会话、跨设备 |
| 存储内容 | 完整对话历史 | 精选的重要信息 |
| 存储位置 | 内存或关系数据库 | 向量数据库 |
| 检索方式 | 按 thread_id 精确匹配 | 语义相似度检索 |
| 典型用途 | 维持对话连贯性 | 记住用户偏好、历史经验 |
Langchain为长期记忆的支持提供了对话摘要记忆、向量存储记忆、混合/分层记忆架构等方案。
2、向量存储记忆(VectorStoreMemory)
长期记忆的挑战在于当记忆量很大时,不可能把所有历史都塞进上下文(Token 限制)。一种解决方案是把重要信息以向量形式存储,在需要时通过语义相似度检索最相关的记忆。这就是向量存储记忆。
向量存储记忆是目前实现真正“长期记忆”和“语义检索”的主流方案之一。它引入了向量数据库(如 Chroma, Pinecone, Milvus 等)作为外部存储。其基本原理是将对话历史或关键信息转化为向量(Embeddings)存入向量数据库。当新对话开始时,系统会根据当前问题,在数据库中语义检索出最相关的历史片段,注入到 Prompt 中。这种方式记忆容量近乎无限,且能精准召回与当前话题相关的历史信息,非常适合知识库问答或超长周期的用户画像记录。
下面让我们以构建一个简单的知识库看看LangChain如何实现向量存储记忆。
第一步:初始化向量存储
// src/6-long-memory.ts
// 如果使用OpenAI的嵌入模型
// import { OpenAIEmbeddings } from "@langchain/openai";
// 使用通义的嵌入模型
import { AlibabaTongyiEmbeddings } from "@langchain/community/embeddings/alibaba_tongyi";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
// -------1. 初始化向量存储(作为长期记忆库)------------
// const embeddings = new OpenAIEmbeddings({ modelName: "text-embedding-3-small" });
const embeddings = new AlibabaTongyiEmbeddings({
modelName: "text-embedding-v3",
apiKey: "sk-5b6047104c7443ddb53944360a2f47fe",
});
const memoryStore = new MemoryVectorStore(embeddings);
说明:
OpenAIEmbeddings:用于将文本转换为向量MemoryVectorStore:内存中的向量存储(生产环境用 Pinecone、Chroma 等向量数据库)
Langchain 1.x版本已经将
MemoryVectorStore类迁移,需要额外安装@langchain/classic。
第二步:准备并添加要存储的文本数据
import { Document } from "@langchain/core/documents";
// 2. 准备要存储的文本数据(可以带有元数据 metadata)
const docs = [
new Document({
pageContent: "LangChain 是一个用于开发大语言模型应用的框架。",
metadata: { category: "技术", source: "官方文档" },
}),
new Document({
pageContent: "深圳是中国超一线城市,科技行业发展领先。",
metadata: { category: "地理", source: "百科" },
}),
new Document({
pageContent: "向量数据库专门用于存储和检索高维向量数据。",
metadata: { category: "技术", source: "维基百科" },
}),
new Document({
pageContent: "深圳是中国广东省的一个副省级城市,也是著名的科技中心。",
metadata: { category: "地理", source: "百科" },
}),
];
// 3. 将文档添加到内存向量存储中
// addDocuments 会自动调用嵌入模型将文本转换为向量并存入内存
const ids = await memoryStore.addDocuments(docs);
console.log("成功添加文档,分配的 ID 为:", ids);
实现向量存储的基本步骤是:
- 先使用
Document对象创建要存储的文档,文档包括内容以及元素据(如分类、时间戳等) - 将创建的文档添加到向量存储仓库中
第三步:进行向量检索
检索工具用于根据输入的内容,从已经存储的向量数据库中查询并召回相关的文档。
// 4. 执行向量相似性检索
const query = "介绍一下深圳";
console.log(`\n正在检索问题: "${query}"`);
// similaritySearch 返回最相似的 k 个文档(这里 k=2)
const results = await memoryStore.similaritySearch(query, 2);
// 5. 打印检索结果
console.log("\n检索到的最相关文档:");
results.forEach((doc, index) => {
console.log(`[${index + 1}] 内容: ${doc.pageContent}`);
console.log(` 元数据:`, doc.metadata);
});
结果如下:
其基本原理是使用similaritySearchAPI将要查询文本转换为向量,计算与存储向量的余弦相似度并返回最相似的 K 条记忆。
实际项目中,我们会将信息存储和检索定义为特定的工具,并提供给Agent使用,在适当的时候进行调用。
3、对话摘要记忆
对话摘要记忆是 LangChain 早期提供的最经典的长期记忆方案,其本质是一种平衡成本和效果、保留关键信息的记忆策略。它的基本原理是利用 LLM 将过往冗长的对话历史压缩成一段精炼的“摘要”。在每一轮对话达到一定数量上下文或达到一定轮次时,后台会自动调用 LLM 对历史消息进行总结,并丢弃原始的对话细节,只保留这段摘要。
摘要记忆的优点是极大地节省了 Token 消耗,让对话可以无限延续而不超出模型的上下文窗口。缺点则是摘要过程本身会消耗 Token 并增加延迟,且在压缩过程中可能会丢失某些具体的细节信息。
使用ConversationSummaryBufferMemory类可以实现摘要记忆。
// src/6-summary.ts
import { ChatOpenAI } from "@langchain/openai";
import { ConversationSummaryBufferMemory } from "@langchain/classic/memory";
// 创建摘要记忆
const memory = new ConversationSummaryBufferMemory({
llm: new ChatOpenAI({
model: "gpt-4o-mini" // 用小模型做摘要(省钱)
}),
maxTokenLimit: 2000, // 超过 2000 Token 就开始摘要
returnMessages: true,
});
// 加载历史(包含摘要 + 最近消息)
const { history } = await memory.loadMemoryVariables({});
console.log(history);
// 输出:[
// SystemMessage { content: "之前的对话摘要:用户介绍了自己的基本信息..." },
// HumanMessage { content: "最近的一条消息" },
// AIMessage { content: "最近的回复" }
// ]
由于做摘要记忆压缩无需复杂的推理和规划,通常我们会选择更轻量、更便宜的模型来做摘要处理。
工作原理:
flowchart LR
A["完整对话历史<br/>10000 tokens"] --> B["超过阈值?"]
B -- 是 --> C["对早期消息生成摘要<br/>500 tokens"]
B -- 否 --> D["保留全部历史"]
C --> E["摘要 + 最近 N 条消息<br/>2000 tokens"]
D --> F["发送给模型"]
E --> F
五、常见记忆模式总结
在实际的项目中,通常会结合多种方式来管理记忆,需要根据不同的使用场景,选择合适的记忆方案。
1. 决策的基本方案
| 场景 | 推荐方案 | 说明 | 复杂度 |
|---|---|---|---|
| 单轮或短对话 | 无需专门管理 | 直接传入 messages | ⭐ |
| 多轮对话(进程内) | MemorySaver + thread_id | 简单快速,开发首选 | ⭐⭐ |
| 多轮对话(持久化) | PostgresSaver / RedisSaver | 生产环境必选 | ⭐⭐⭐ |
| 跨会话用户记忆 | 向量存储 + recall_memory 工具 | 语义检索,扩展性好 | ⭐⭐⭐⭐ |
| 超长历史压缩 | ConversationSummaryBufferMemory | 平衡 Token 消耗和上下文保留 | ⭐⭐⭐ |
| 结构化业务数据 | 自定义 State + LangGraph | 需要在状态中存非消息数据 | ⭐⭐⭐⭐ |
2. 组合使用示例
在实际项目中,通常会组合使用多种方案:
// 短期记忆:PostgreSQL 持久化
const checkpointer = PostgresSaver.fromConnString(DATABASE_URL);
// 长期记忆:向量存储
const memoryStore = new PineconeStore(embeddings);
// 自定义状态:存储业务数据
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
userId: Annotation<string>(),
orderId: Annotation<string | null>(),
});
// Agent 配置
const agent = createAgent({
model: "openai:gpt-4o",
tools: [saveMemory, recallMemory],
checkpointer,
});
六、常见问题与处理方案
记忆管理看似简单,但初次接触时踩坑也是无法避免的,以下列举常见的一些问题。
问题 1:Context Window 溢出
当不加限制地累积对话历史时,最终可能会超出模型的 Context Window。
Error: This model's maximum context length is 128000 tokens.
However, your messages resulted in 150000 tokens.
解决方案: 通过限制消息的数量、使用摘要记忆、定期清理历史信息等手段可以有效避免。如果你用过Claude Code,其/clear、/compact指令做的正是这个事。
// 方案 1:限制消息数量
const MAX_MESSAGES = 20;
if (messages.length > MAX_MESSAGES) {
messages = messages.slice(-MAX_MESSAGES); // 只保留最近 20 条
}
// 方案 2:使用摘要记忆
const memory = new ConversationSummaryBufferMemory({
maxTokenLimit: 2000, // 设置合理的上限
});
// 方案 3:定期清理不重要的历史
function cleanupHistory(messages: BaseMessage[]) {
return messages.filter(msg => {
// 保留系统消息和用户的重要消息
return msg.role === "system" || isImportantMessage(msg);
});
}
最佳实践:
- 设置合理的 Token 上限(通常为 Context Window 的 70-80%)
- 优先保留最近的消息和系统消息
- 对长期运行的 Agent,定期清理历史
问题 2:thread_id 冲突导致数据泄露
如果多个用户共用同一个 thread_id,他们的对话历史会混在一起,这不仅让会话混乱,严重时可以会导致安全问题。
// 所有用户共用一个 thread_id
const config = {
configurable: { thread_id: "global-session" }
};
// 用户 A 说:"我的密码是 123456"
// 用户 B 问:"上一个用户说了什么?"
// 结果:用户 B 能看到用户 A 的密码!
使用以下方式可以减少这类问题的发生:
thread_id必须包含用户标识符- 不同用户的
thread_id绝对不能重复 - 在生产环境中进行权限校验
- 定期审计访问日志
// 每个用户独立的 thread_id
const config = {
configurable: {
thread_id: `user-${req.userId}-session-${Date.now()}`
}
};
// 或者更简洁
const config = {
configurable: { thread_id: req.userId }
};
问题3:记忆粒度不当
长期记忆的粒度设置不合理会影响检索质量,太精细则容易混入大量无关记忆,检索噪音多;太宽泛又导致关键信息被稀释,检索不准确。
// 太精细:每条消息都保存
saveMemory({ content: "你好", category: "fact" });
saveMemory({ content: "今天天气不错", category: "fact" });
saveMemory({ content: "我吃了午饭", category: "fact" });
// 太宽泛:一整天的对话保存为一条
saveMemory({
content: "今天聊了很多话题,包括工作、生活、兴趣爱好...",
category: "fact"
});
推荐做法:
// 按信息类别分开存储
// 1. 用户偏好(相对稳定)
saveMemory({
content: "用户喜欢 TypeScript 和 React",
category: "preference"
});
// 2. 重要事实(偶尔变化)
saveMemory({
content: "用户是一名前端工程师,在北京工作",
category: "fact"
});
// 3. 当前任务(频繁变化)
saveMemory({
content: "正在开发一个电商网站的项目",
category: "task"
});
// 检索时带类别过滤
recallMemory({
query: "用户的职业是什么?",
category: "fact" // 只检索事实类记忆
});
问题 4:忘记清理过期记忆
长期记忆不断累积,占用存储空间,降低检索效率。应该定期清理一些过于久远或不太重要的记忆。
// 定期清理过期记忆
async function cleanupOldMemories(daysToKeep = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
// 删除 90 天前的记忆
await memoryStore.delete({
filter: {
timestamp: { $lt: cutoffDate.toISOString() }
}
});
console.log("已清理过期记忆");
}
// 每天凌晨执行
setInterval(cleanupOldMemories, 24 * 60 * 60 * 1000);
七、本章小结
记忆系统是赋予大语言模型持续认知与个性化服务能力的核心基础设施。它突破了模型原生无状态与上下文窗口的物理限制,通过对外部信息的持久化存储、动态检索与智能更新,确保了智能体在跨越长时间维度和多轮交互中能够保持逻辑连贯与意图一致。这不仅有效规避了因重复输入冗余信息带来的高昂算力成本与响应延迟,更让 AI 能够沉淀关键事实、习得用户偏好并积累领域经验,从而从单次任务的执行工具真正进化为具备深度理解力与长期伴随价值的智能伙伴。
记忆系统在LLM和Agent的发展中是非常重要的一个体系,记忆系统设计的好坏也是Agent是否强大的核心因素之一,因此,学好记忆管理是进入Agent领域非常重要的一课,本文也仅是这个体系中浅显的一部分。
📝 核心知识点回顾
| 知识点 | 关键要点 | 适用场景 |
|---|---|---|
| 短期记忆 | MemorySaver + thread_id 自动管理 | 多轮对话 |
| 持久化存储 | PostgresSaver / RedisSaver | 生产环境 |
| 自定义状态 | LangGraph Annotation 扩展字段 | 存储业务数据 |
| 长期记忆 | 向量存储 + 语义检索 | 跨会话记忆 |
| 摘要记忆 | 压缩早期历史,控制 Token | 超长对话 |
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:实现多会话管理 创建一个支持多用户的聊天应用:
- 每个用户有独立的
thread_id - 使用
MemorySaver管理短期记忆 - 测试用户 A 和用户 B 的对话不会互相干扰
练习 2:持久化改造
将练习 1 的 MemorySaver 替换为 PostgresSaver:
- 安装 PostgreSQL
- 配置数据库连接
- 验证进程重启后对话历史仍然保留
练习 3:长期记忆系统 构建一个能记住用户偏好的助手:
- 实现
save_memory和recall_memory工具 - 测试保存用户信息(姓名、喜好、职业等)
- 测试在新会话中检索这些信息
练习 4:摘要记忆优化 模拟一个长对话场景(50+ 轮对话):
- 不使用摘要:观察 Token 消耗
- 使用
ConversationSummaryBufferMemory:对比效果 - 调整
maxTokenLimit,找到最佳的平衡点