📖 本章学习目标
- ✅ 理解短期记忆与长期记忆的本质区别与应用场景
- ✅ 掌握基于 Checkpointer 的短期会话记忆管理
- ✅ 学会使用
BaseStore实现跨会话的长期记忆存储- ✅ 实现记忆的智能提取、摘要和注入流程
- ✅ 掌握消息历史的压缩策略,解决 Token 超限问题
- ✅ 避免常见的记忆管理陷阱和性能问题
一、两种记忆的本质区别
1、基本区别
- 短期记忆(工作记忆):当前对话内容,关闭对话窗口后消失
- 长期记忆:跨对话的用户偏好、历史习惯、重要事实
2、LangGraph 记忆架构对比
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 实现方式 | Checkpointer + messages | BaseStore / 外部数据库 |
| 作用范围 | 单个 thread_id 内 | 跨 thread_id |
| 生命周期 | 会话期间 | 持久化 |
| 容量限制 | Token 窗口限制 | 理论无限 |
| 典型数据 | 对话历史消息 | 用户档案、偏好设置 |
| 访问速度 | 快(内存/本地DB) | 慢(需查询) |
| 更新频率 | 每条消息 | 按需提取 |
两种记忆的协作关系:
二、短期记忆管理
1、自动短期记忆(Checkpointer)
第7章已介绍,Checkpointer 自动维护 thread_id 内的消息历史:
import * as dotenv from 'dotenv';
dotenv.config();
import { MemorySaver, MessagesAnnotation, StateGraph } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, AIMessage, RemoveMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
const checkpointer = new MemorySaver();
const graph = new StateGraph(MessagesAnnotation)
.addNode('chat', async (state) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
})
.addEdge('__start__', 'chat')
.addEdge('chat', '__end__')
.compile({ checkpointer });
// 同一thread_id的多次调用共享消息历史
const config = { configurable: { thread_id: 'user-123-session-1' } };
await graph.invoke(
{ messages: [new HumanMessage('我叫Alice')] },
config
);
const result = await graph.invoke(
{ messages: [new HumanMessage('我叫什么名字?')] },
config
);
// AI会回答:"你叫Alice"
代码解读:
- Checkpointer自动保存每次invoke后的messages状态
- 相同
thread_id的调用会自动加载历史消息 - 这是短期记忆的基础机制
局限性:
- 消息数量增长会导致Token超限
- 不同thread_id之间无法共享记忆
- 需要手动清理过期消息
2、消息历史压缩:解决 Token 超限
随着对话进行,消息越来越多会超出模型的上下文窗口。三种策略:
策略1:截断——只保留最近 N 条
async function chatWithTrimming(state: typeof MessagesAnnotation.State) {
// 只保留最近 10 条消息
const trimmedMessages = state.messages.slice(-10);
const response = await model.invoke(trimmedMessages);
return { messages: [response] };
}
优势:
- 实现简单
- 性能最好
- 保证最新消息完整
劣势:
5. 可能丢失早期重要信息
6. 对话连贯性受影响
策略2:过滤——只保留特定类型的消息
import { trim_messages } from '@langchain/core/messages';
async function chatWithFiltering(state: typeof MessagesAnnotation.State) {
// 使用 LangChain 内置的 trim_messages 工具
const trimmed = await trim_messages(state.messages, {
maxTokens: 2000,
strategy: 'last', // 从后往前保留
tokenCounter: model, // 用模型计算 token 数
includeSystem: true, // 保留 SystemMessage
allowPartial: false,
startOn: 'human', // 第一条保留 HumanMessage
});
const response = await model.invoke(trimmed);
return { messages: [response] };
}
优势:
- 智能控制Token数量
- 保留系统消息等重要内容
- 保证消息完整性(不会截断单条消息)
适用场景:
- 长对话但Token有限的情况
- 需要精确保留关键信息的场景
策略3:摘要——用 LLM 压缩历史
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
import { BaseMessage, SystemMessage } from '@langchain/core/messages';
const SummaryState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
summary: Annotation<string>({ default: () => '' }),
});
async function summarizeIfNeeded(state: typeof SummaryState.State) {
if (state.messages.length <= 6) return {}; // 消息少时无需摘要
const summaryPrompt = state.summary
? `已有摘要:${state.summary}\n\n请结合以下新对话更新摘要:`
: '请将以下对话总结为简洁的摘要:';
const response = await model.invoke([
new HumanMessage(summaryPrompt +
state.messages.map(m => `${m._getType()}: ${m.content}`).join('\n')
),
]);
// 删除旧消息,只保留最近2条,并保存摘要
const toDelete = state.messages.slice(0, -2)
.map(m => new RemoveMessage({ id: m.id! }));
return { summary: response.content as string, messages: toDelete };
}
工作流程:
- 检测消息数量是否超过阈值
- 调用LLM生成摘要
- 删除旧消息,保留最近几条
- 保存摘要到State
- 下次对话时将摘要作为SystemMessage传入
优势:
- 保留关键信息的同时大幅减少Token
- 对话连贯性好
- 适合超长对话场景
劣势:
- 需要额外调用LLM,增加成本
- 摘要可能丢失细节
三种策略对比:
| 策略 | 实现难度 | Token节省 | 信息保留 | 适用场景 |
|---|---|---|---|---|
| 截断 | ⭐ 简单 | ⭐⭐ 中等 | ⭐ 差 | 短对话、独立问题 |
| 过滤 | ⭐⭐ 中等 | ⭐⭐⭐ 好 | ⭐⭐⭐ 好 | 通用场景 |
| 摘要 | ⭐⭐⭐ 复杂 | ⭐⭐⭐⭐⭐ 优秀 | ⭐⭐⭐⭐ 很好 | 长对话、重要会议 |
三、长期记忆:BaseStore
1、InMemoryStore 入门
步骤1:创建Store
import { InMemoryStore } from '@langchain/langgraph';
// 创建内存存储
const store = new InMemoryStore();
InMemoryStore是内存存储,重启后消失,适合开发测试。生产环境应使用PostgreSQL、Redis等持久化存储。
步骤2:存储记忆
Store是键值存储,支持命名空间组织:
// 存储记忆
await store.put(
['users', 'alice'], // 命名空间:按层级组织数据
'preferences', // 记忆的键
{ // 记忆的值
name: 'Alice',
language: 'zh-CN',
coding_style: 'functional',
timezone: 'Asia/Shanghai',
}
);
// 读取记忆
const memory = await store.get(['users', 'alice'], 'preferences');
console.log(memory?.value); // { name: 'Alice', ... }
put(namespace, key, value)用于存储记忆:
namespace:字符串数组,类似目录结构,用于组织数据key:记忆条目的唯一标识value:任意可序列化的对象
get(namespace, key)用于读取单条记忆,返回值为{ value: ..., metadata: ... } 或undefined
命名空间设计原则:
- 按实体类型分层:[‘users’, userId]
- 按功能分类:[‘preferences’, ‘facts’, ‘sessions’]
- 避免过深嵌套:最多3层
2、在图中使用 Store
步骤1:编译时注入Store
import { LangGraphRunnableConfig } from '@langchain/langgraph';
// Store 需要在编译时注入,通过 config 在节点中访问
const graphWithStore = new StateGraph(MessagesAnnotation)
.addNode('remember', async (state, config: LangGraphRunnableConfig) => {
const userId = config.configurable?.user_id as string;
const store = config.store!;
// 读取用户的长期记忆
const userMemory = await store.get(['users', userId], 'profile');
const systemContext = userMemory
? `用户信息:${JSON.stringify(userMemory.value)}`
: '这是新用户,还没有记忆档案';
const response = await model.invoke([
new SystemMessage(systemContext),
...state.messages,
]);
return { messages: [response] };
})
.addEdge('__start__', 'remember')
.addEdge('remember', '__end__')
.compile({ checkpointer: new MemorySaver(), store });
使用compile({ store })将 Store 注入到图中,在节点内通过 config 访问 Store。从config.configurable?.user_id中获取用户 ID(user_id 是跨会话的,不同于 thread_id)。
步骤2:使用Store
const config = {
configurable: {
thread_id: 'session-001',
user_id: 'alice',
},
};
await graphWithStore.invoke(
{ messages: [new HumanMessage('你好')] },
config
);
// AI会根据用户档案个性化回复
3、自动提取并保存记忆
import { z } from 'zod';
async function extractAndSaveMemory(
state: typeof MessagesAnnotation.State,
config: LangGraphRunnableConfig
) {
const userId = config.configurable?.user_id as string;
const store = config.store!;
// 用 LLM 从对话中提取关键信息
const extractionResult = await model.withStructuredOutput(
z.object({
hasNewInfo: z.boolean(),
facts: z.array(z.string()).describe('从对话中提取的用户偏好和事实'),
})
).invoke([
new SystemMessage('从以下对话中提取用户的个人信息、偏好和重要事实'),
...state.messages.slice(-4), // 只分析最近4条
]);
if (extractionResult.hasNewInfo && extractionResult.facts.length > 0) {
// 保存提取的记忆
const existing = await store.get(['users', userId], 'facts');
const allFacts = [...(existing?.value?.facts ?? []), ...extractionResult.facts];
await store.put(['users', userId], 'facts', { facts: allFacts });
console.log(`💾 为用户 ${userId} 保存了 ${extractionResult.facts.length} 条新记忆`);
}
return {};
}
代码解读:
withStructuredOutput:强制 LLM 返回结构化 JSON- 只分析最近4条消息,避免重复提取历史消息
- 合并新旧记忆,去重可以根据业务需要进一步优化
- 这个节点通常在对话节点之后异步执行,不影响响应速度
提取时机:
- 每轮对话后自动提取
- 检测到关键词时触发(如"我喜欢"、“我讨厌”)
- 对话结束时批量提取
注意事项:
- 提取要异步执行,避免影响响应速度
- 定期清理过时或错误的记忆
- 给用户查看和删除记忆的权限
记忆提取流程图:
四、记忆检索:语义搜索
1、基于向量的记忆检索
当记忆条目很多时,需要语义搜索找到最相关的记忆:
// 搜索相关记忆(InMemoryStore 支持简单的文本搜索)
async function searchMemories(store: InMemoryStore, userId: string, query: string) {
const results = await store.search(
['users', userId], // 在指定命名空间内搜索
{ query, limit: 3 } // 返回最相关的3条
);
return results.map(item => item.value);
}
// 在节点中注入相关记忆
async function chatWithRelevantMemories(
state: typeof MessagesAnnotation.State,
config: LangGraphRunnableConfig
) {
const userId = config.configurable?.user_id as string;
const store = config.store!;
const lastMsg = state.messages[state.messages.length - 1].content as string;
// 搜索与当前消息最相关的记忆
const relevantMemories = await searchMemories(store as InMemoryStore, userId, lastMsg);
const memoryContext = relevantMemories.length > 0
? `相关记忆:\n${relevantMemories.map(m => JSON.stringify(m)).join('\n')}`
: '';
const response = await model.invoke([
new SystemMessage(`你是一个有记忆的助手。${memoryContext}`),
...state.messages,
]);
return { messages: [response] };
}
使用store.search()在命名空间内搜索与query相关的记忆,返回指定数量的结果(建议3-5条),并将搜索结果注入到SystemMessage,让LLM感知。
更高级的方案是使用向量数据库(Pinecone、Weaviate)和 嵌入模型计算相似度,使用RAG模式检索最相关记忆。
实际应用场景:
- 客服系统:搜索用户历史问题
- 学习助手:回忆之前学过的知识点
- 个人助理:记住用户的喜好和习惯
五、最佳实践和踩坑指南
💡 实践 1:记忆的分层设计
// 按重要性分层组织记忆命名空间
const namespaces = {
core: ['users', userId, 'core'], // 核心档案:姓名、语言偏好
facts: ['users', userId, 'facts'], // 事实记忆:知识点、习惯
sessions: ['users', userId, 'sessions'], // 会话摘要
};
// 示例:存储不同类型的记忆
await store.put(namespaces.core, 'profile', {
name: 'Alice',
language: 'zh-CN',
});
await store.put(namespaces.facts, 'tech_stack', {
languages: ['TypeScript', 'Python'],
frameworks: ['React', 'LangGraph'],
});
await store.put(namespaces.sessions, 'last_topic', {
topic: 'LangGraph记忆系统',
date: '2026-04-19',
});
原因:分层设计便于管理和检索,不同类型记忆有不同更新策略。
💡 实践 2:记忆更新要幂等
// ✅ 使用 upsert 语义(put 天然幂等),避免重复
await store.put(['users', userId], 'preferences', newPrefs);
// ❌ 避免无限追加相同的事实
const existing = await store.get(['users', userId], 'facts');
// 追加前先去重
const uniqueFacts = [...new Set([...(existing?.value?.facts ?? []), ...newFacts])];
await store.put(['users', userId], 'facts', { facts: uniqueFacts });
原因:防止记忆膨胀,保持数据整洁。
💡 实践 3:记忆注入位置
// ✅ 正确:将记忆放在SystemMessage中
const response = await model.invoke([
new SystemMessage(`用户偏好:${JSON.stringify(userPrefs)}`),
...state.messages,
]);
// ❌ 错误:将记忆放在HumanMessage中
const response = await model.invoke([
...state.messages,
new HumanMessage(`用户偏好:${JSON.stringify(userPrefs)}`), // 混淆身份
]);
原因:SystemMessage是系统级别的上下文,不会影响对话角色的清晰度。
⚠️ 常见问题
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 记忆注入位置错误 | LLM 忽视长期记忆 | 将记忆放在 SystemMessage 中,而非 HumanMessage |
| 注入过多记忆 | Token 超限 | 使用语义搜索只注入最相关的3-5条记忆 |
| 记忆提取在关键路径 | 响应变慢 | 将记忆提取/保存改为异步后台任务 |
user_id 和 thread_id 混淆 | 跨会话记忆失效 | user_id 在 configurable 中单独传,与 thread_id 无关 |
| 记忆永不清理 | Store无限增长 | 定期清理过期或无用记忆 |
| 提取过于频繁 | 成本高、噪音多 | 设置提取触发条件,如置信度阈值 |
📝 本章小结
核心知识点回顾
| 知识点 | 关键要点 | 应用场景 |
|---|---|---|
| 短期记忆 | Checkpointer 自动维护 messages | 单会话对话历史 |
| 消息压缩 | 截断/过滤/摘要三种策略 | 防止 Token 超限 |
| BaseStore | put/get/search 跨会话存储 | 用户档案、偏好记忆 |
| 记忆注入 | 通过 SystemMessage 注入长期记忆 | 个性化 Agent 响应 |
| 记忆提取 | LLM结构化输出提取关键信息 | 自动构建用户档案 |
| 语义搜索 | store.search 检索相关记忆 | 精准注入上下文 |
🎯 动手练习
练习 1:消息摘要压缩
- 目标:超过8条消息后自动触发摘要,并清理旧消息
- 要求:
- 实现summarizeIfNeeded节点
- 摘要要保留关键信息
- 下次对话能感知历史
- 验收标准:
- 20轮对话后 State 中的消息数量不超过4条
- 摘要准确反映之前的对话内容
- LLM能基于摘要继续对话
练习 2:用户偏好记忆
- 目标:Agent 记住用户的语言偏好和回答风格要求
- 要求:
- 第一次告诉 Agent “用简洁的语言回答”
- 第二个会话中无需重申
- 使用Store跨会话保存
- 验收标准:
- 新会话中 Agent 自动以简洁风格回答
- 用户可以查询自己的偏好设置
- 偏好可以修改和删除
练习 3:记忆搜索注入
- 目标:对话时自动搜索并注入最相关的3条长期记忆
- 要求:
- 使用 store.search 语义搜索
- 根据当前消息内容搜索
- 注入到SystemMessage
- 验收标准:
- 询问相关问题时,Agent 主动提及之前记录的信息
- 搜索结果相关度高
- 不影响响应速度
练习 4:记忆管理系统
- 目标:实现完整的记忆CRUD功能
- 要求:
- 查看:列出所有记忆
- 编辑:修改某条记忆
- 删除:移除错误记忆
- 导出:导出为JSON
- 验收标准:
- 所有CRUD操作正常工作
- 用户界面友好
- 数据一致性保证
📚 延伸阅读
下一章:第11章 —— 子图:构建模块化 Agent

2135

被折叠的 条评论
为什么被折叠?



