上一篇介绍短期记忆时,我们讨论的范围始终没有离开 Thread:Thread 中的 State 通过 Checkpointer 保存,下一次调用再根据 thread_id 恢复。
但实际应用很快会遇到新的问题。
比如,用户结束当前会话,下一次重新创建一个 Thread。Agent 还应该知道他习惯使用 Python、喜欢简洁回答,或者之前已经告诉过系统自己的项目背景。
这些信息显然不适合绑定在某个 Thread 上。
LangChain 把这类需要跨 Thread、跨会话继续存在的数据归入长期记忆(Long-term Memory)。它不再主要依赖 Checkpointer,而是建立在 LangGraph 的 Store 之上。
一、长期记忆的定位
先看一个简单场景。
用户在一次会话中告诉 Agent:
我主要使用 Python 开发。
回答代码问题时尽量直接一些。
几天之后,用户创建了新的会话:
帮我写一个读取 CSV 的例子。
虽然已经换了 Thread,但系统仍然应知道或记住用户的偏好:
主要语言 = Python
回答风格 = concise
这类信息描述的是用户长期属性,而不是当前会话的执行状态。
因此,它更适合存入长期记忆:
User
│
├── Thread A
│
├── Thread B
│
└── Long-term Memory
↓
Store
多个 Thread 可以相互独立,但只要它们属于同一个用户,就可以访问相同的长期数据。
这也是长期记忆最重要的能力:
让数据脱离单个 Thread 的生命周期,并在多个会话之间共享。
长期记忆保存的内容通常包括:
- 用户画像
- 用户偏好
- 历史事实
- 项目背景
- Agent 学到的规则
- 过去的重要事件
它们是否需要长期存在,取决于业务,而不是由也不应由 LangChain 自动决定。
二、Store 与 Checkpointer
LangChain 中有两个都与“保存数据”有关的组件:
Checkpointer
Store
两者很容易混淆,但职责并不相同。
通过上一篇短期记忆的学习,我们已了解 Checkpointer 保存的是某个 Thread 的 Agent State。
例如:
messages
current_step
tool result
interrupt
next node
...
这些数据共同描述:
这段会话现在是什么状态?
Store 保存的则是独立于具体 Thread 的长期数据。
例如:
用户喜欢 Python
用户偏好简洁回答
用户当前项目使用 FastAPI
用户选择了深色主题
它回答的是:
有哪些信息应该在不同会话之间继续存在?
可以把两者理解为两条数据链路:
Agent 执行
↓
State
↓
Checkpoint
↓
Checkpointer
以及:
长期信息
↓
Namespace
↓
Key
↓
Value
↓
Store
一个 Agent 可以同时使用两者:
agent = create_agent(
model=...,
tools=...,
checkpointer=checkpointer,
store=store,
)
此时,Checkpointer 负责当前会话状态,Store 负责跨会话信息。它们解决的是两个层级的问题。
三、Store 的数据模型
Store 并不是简单的全局字典。
LangGraph Store 中最重要的三个概念是:
Namespace
Key
Value
一条数据可以表示为:
Namespace + Key → Value
例如:
namespace = ("users", "u1001", "profile")
key = "basic"
value = {
"name": "Bob",
"language": "Python",
}
可以先做一个不完全严格、但容易理解的类比:
Namespace ≈ 目录
Key ≈ 文件名
Value ≈ 文件内容
Store 中实际保存的单位称为 Item。
一个 Item 除了业务数据之外,还会包含:
namespace
key
value
created_at
updated_at
因此,Store 保存的一条数据属于哪个 Namespace、对应哪个 Key、它的 Value,以及创建和更新时间。
Namespace 划分数据范围
Namespace 是字符串组成的元组:
("users", "u1001")
也可以继续增加层级:
("users", "u1001", "profile")
("users", "u1001", "preferences")
("users", "u1001", "memories")
这种设计比固定的一层 Key 更适合长期数据。
例如一个系统同时需要保存:
用户记忆
组织知识
Agent 指令
项目上下文
就可以分别设计成:
("users", "u1001", "memories")
("organizations", "org-01", "knowledge")
("agents", "support-agent", "instructions")
("projects", "project-100", "context")
因此,Namespace 更像一个逻辑作用域。
它决定一组 Item 属于谁、属于哪类数据。
实际设计中,比“Key 起什么名字”更应该先考虑 Namespace。
Key 定位具体 Item
Namespace 确定范围后,Key 用于定位这个范围中的具体记录。
例如:
Namespace
("users", "u1001", "preferences")
Key
language
Value
{"value": "Python"}
同一个 Namespace 下还可以存在:
answer-style
theme
timezone
于是形成:
("users", "u1001", "preferences")
│
├── language
├── answer-style
├── theme
└── timezone
Namespace 与 Key 组合起来,才真正确定一条数据。
设计长期记忆时,可以先回答三个问题:
这条数据属于谁?
它属于哪一类信息?
它在这一类数据中如何唯一识别?
前两个问题通常决定 Namespace,最后一个问题决定 Key。
四、Store 的基本操作
和短期记录中的 InMemorySaver 一样,LangGraph 提供 InMemoryStore,适合学习、测试和本地开发。
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
有了 Store 后,可以进行最基本的写入、读取、搜索和删除。
写入数据
写入使用 put():
namespace = ("users", "u1001", "profile")
store.put(
namespace,
"basic",
{
"name": "Bob",
"language": "Python",
"answer_style": "concise",
},
)
put() 的基本结构是:
store.put(
namespace,
key,
value,
)
其中 namespace 是字符串元组,key 是字符串,value 是一个可以 JSON 序列化的字典。
如果相同的:
Namespace + Key
再次执行 put(),这条 Item 会被更新。
例如:
store.put(
namespace,
"basic",
{
"name": "Bob",
"language": "Python",
"answer_style": "detailed",
},
)
这里不是创建第二个 basic,而是更新原有 Item。
这非常适合保存固定结构的 Profile 或 Preferences。
精确读取
如果已经知道 Namespace 和 Key,可以使用:
item = store.get(namespace, "basic")
print(item.value)
运行结果
{'name': 'Bob', 'language': 'Python', 'answer_style': 'detailed'}
从结果可以看到,第二次 put() 已经更新了原有数据。
如果指定的 Key 不存在,get() 返回 None。
因此,在实际代码中通常需要判断:
item = store.get(namespace, "basic")
if item is not None:
print(item.value)
get() 适合:
我明确知道要读哪一条数据
例如:
用户 Profile
账户配置
某个项目设置
指定 Preference
搜索一组数据
如果不知道具体 Key,可以使用 search():
items = store.search(namespace)
for item in items:
print(item.key, item.value)
例如 Namespace 中存在:
language
answer-style
theme
搜索后就可以返回这一范围中的多条 Item。
这与:
store.get(namespace, key)
的使用场景不同。
get() 是精确定位。
search() 是从一个 Namespace 中寻找一组符合条件的数据。
删除数据
如果某条长期信息已经失效,可以使用:
store.delete(
namespace,
"basic",
)
删除之后:
item = store.get(namespace, "basic")
print(item)
运行结果
None
长期记忆必须具备删除能力。
因为长期保存不等于永久保存。如果只能不断写入,无法删除旧事实和错误信息,Memory 的质量会随着使用时间不断下降。
五、Store 的搜索能力
Store 与普通 Key-Value Storage 的一个重要区别,是它可以进一步支持语义搜索。
假设一个用户已经积累了很多长期信息:
用户主要使用 Python 和 FastAPI
用户喜欢深色主题
用户正在学习 LangChain
用户使用 PostgreSQL
用户希望技术回答尽量直接
当前问题是:
我的后端项目应该怎么组织?
如果每次把全部 Memory 都放进 Prompt,数据越积越多,上下文就会不断膨胀。
更合理的方式是:
当前问题
↓
搜索相关 Memory
↓
选择少量相关记录
↓
加入模型上下文
Store 可以配置 Embedding,使搜索从简单的 Namespace 查询进一步变成语义检索。
例如:
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore
embeddings = init_embeddings(
"openai:text-embedding-3-small"
)
store = InMemoryStore(
index={
"embed": embeddings,
"dims": 1536,
}
)
随后写入几条记忆:
namespace = ("users", "u1001", "memories")
store.put(
namespace,
"memory-1",
{
"text": "用户主要使用 Python 和 FastAPI 开发后端"
},
)
store.put(
namespace,
"memory-2",
{
"text": "用户喜欢深色界面"
},
)
store.put(
namespace,
"memory-3",
{
"text": "用户经常使用 PostgreSQL"
},
)
然后进行语义搜索:
results = store.search(
namespace,
query="后端开发技术栈",
limit=2,
)
for item in results:
print(item.value)
示例输出
{'text': '用户主要使用 Python 和 FastAPI 开发后端'}
{'text': '用户经常使用 PostgreSQL'}
从结果可以看到,搜索并不依赖 Key 是否包含“后端”这个词。
Embedding 会把当前 Query 与 Store 中的数据转换成向量,再根据语义相关性寻找更合适的 Item。
需要注意,InMemoryStore 默认并不会自动具备语义搜索能力。
必须配置:
index
embed
dims
等索引相关信息。
因此,Store 可以有两种常见使用方式:
已知 Key
↓
get()
不知道具体 Key
↓
search()
如果长期数据规模较小,而且结构稳定,精确读取已经足够。
如果 Memory 是不断增加的 Collection,通常更适合使用搜索。
六、跨 Thread 共享数据
长期记忆真正进入 Agent 后,需要解决另一个问题:
当前是谁在访问 Store?
一种常见方式是通过 Runtime Context 传入 user_id。
例如定义:
from dataclasses import dataclass
@dataclass
class Context:
user_id: str
随后在 Tool 中通过 ToolRuntime 获得当前 Runtime 和 Store:
from typing_extensions import TypedDict
from langchain.tools import ToolRuntime, tool
class UserInfo(TypedDict):
name: str
@tool
def save_user_info(
user_info: UserInfo,
runtime: ToolRuntime[Context],
) -> str:
"""Save user information."""
assert runtime.store is not None
runtime.store.put(
("users",),
runtime.context.user_id,
dict(user_info),
)
return "User information saved."
再提供读取 Tool:
@tool
def get_user_info(
runtime: ToolRuntime[Context],
) -> str:
"""Read user information."""
assert runtime.store is not None
item = runtime.store.get(
("users",),
runtime.context.user_id,
)
if item is None:
return "No user information found."
return str(item.value)
创建 Agent:
from langchain.agents import create_agent
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
agent = create_agent(
model="openai:gpt-5.5",
tools=[
save_user_info,
get_user_info,
],
store=store,
context_schema=Context,
)
这里真正决定长期数据归属的是:
user_id
例如:
user_id = u1001
这个用户之后可以创建很多 Thread:
u1001
│
├── thread-001
├── thread-002
└── thread-003
虽然会话不同,但三个 Thread 都可以通过:
runtime.context.user_id
定位到相同的长期数据。
因此:
thread_id
描述的是“哪一段会话”。
而:
user_id
描述的是“谁”。
长期记忆的跨会话能力,实际上来自:
多个 Thread
↓
使用相同业务身份
↓
访问同一 Namespace
↓
读取相同 Store 数据
当然,Namespace 并不一定非要按 User 划分。
还可以按:
organization_id
project_id
agent_id
tenant_id
组织。
这也是 Store 比单纯“用户记忆数据库”更通用的地方。
七、长期记忆保存什么
Store 只提供保存和查询能力,并不会自动判断什么值得记住。
长期记忆通常可以分成几类。
Semantic Memory
语义记忆保存事实和知识。
例如:
用户主要使用 Python
用户喜欢简洁回答
用户所在团队使用 PostgreSQL
当前项目基于 FastAPI
这是大多数应用最先需要实现的一类 Memory。
Episodic Memory
情景记忆保存过去发生过的事件或经历。
例如:
用户之前完成过一次数据库迁移
某种 Tool 调用方式曾经失败
过去某个案例使用方案 A 获得了较好结果
它更接近:
以前发生过什么
而不是单纯的:
这个用户是什么样
Procedural Memory
程序性记忆描述 Agent 应该如何完成某类任务。
例如根据用户反馈逐步调整:
生成代码时不要加入过多解释
技术文章中先说明机制再给代码
执行删除操作之前必须确认
这类内容影响的是 Agent 的行为方式。
实际项目不必一开始就完整实现三类 Memory。
通常可以先从用户事实和偏好开始,再根据业务需求增加其他类型。
Profile 与 Collection
即使只保存用户信息,也还存在两种常见组织方式。
第一种是 Profile:
{
"name": "Bob",
"language": "Python",
"answer_style": "concise",
"frameworks": [
"FastAPI",
"LangChain"
]
}
整个用户画像作为一条或少量几条 Item 保存。
优点是结构清晰,读取简单。
缺点是 Profile 越来越大之后,更新某一项时需要处理整份结构。
另一种是 Collection:
memory-001
用户主要使用 Python
memory-002
用户正在开发 FastAPI 项目
memory-003
用户希望回答尽量简洁
每一个事实独立存在。
这样更容易:
新增
搜索
删除
局部更新
也更适合语义检索。
但 Collection 需要额外处理:
重复
冲突
过期
召回
排序
因此,如果数据字段稳定,例如语言、时区、界面主题,Profile 更简单。
如果信息会持续增加,并且每次只需要检索其中的一部分,Collection 通常更适合。
八、什么时候写入记忆
真正困难的通常不是:
store.put(...)
而是:
什么时候应该调用 store.put()?
长期记忆主要有两种写入方式。
当前执行中写入
第一种是在 Agent 当前请求中立即判断并保存,也就是 Hot Path。
例如:
用户:
以后代码示例都使用 Python。
↓
Agent 判断
这是长期偏好
↓
写入 Store
↓
继续回答
这样做最大的好处是立即生效。
如果用户马上新建 Thread,也能够读取到刚刚保存的偏好。
但当前 Agent 同时需要处理:
回答问题
识别值得记忆的信息
确定 Memory 类型
处理已有 Memory
执行写入
请求链路会变得更复杂。
后台提取
另一种方式是先完成当前任务,再单独执行 Memory Extraction。
例如:
Conversation
↓
正常响应
↓
Memory Processor
↓
分析多轮消息
↓
提取长期事实
↓
去重与合并
↓
Store
这种方式更适合从一段较长的会话中提取:
用户画像
技术栈
项目背景
长期目标
历史事实
它不会增加主请求中的记忆处理负担。
但 Memory 不会立即出现。
如果后台提取还没有完成,新的 Thread 暂时可能读不到刚产生的信息。
实际应用中,两种方式可以组合。
例如:
以后都使用中文回答
这是一条明确、稳定,而且应该立即生效的偏好,可以同步写入。
而一段几十轮的项目讨论,则更适合结束后统一提取:
项目使用 FastAPI
数据库是 PostgreSQL
准备迁移到 Kubernetes
当前主要关注性能问题
长期记忆系统的质量,很大程度上取决于写入规则是否足够克制(清晰、明确、有用)。
不是所有出现过的信息都值得保存。
九、Memory 与 RAG
Store 支持 Embedding 和语义搜索后,看起来与 RAG 很接近。
例如,它们的技术链路确实很类似:
数据
↓
Embedding
↓
检索
↓
Context
↓
LLM
但它们管理的数据来源和生命周期不同。
RAG 通常回答:
系统知道哪些外部知识?
例如:
技术文档
产品手册
企业知识库
合同
代码库
FAQ
Memory 则更关注:
这个 Agent 需要长期记住什么?
例如:
Bob 使用 Python
Bob 喜欢简洁回答
Bob 当前在开发 FastAPI 项目
Bob 曾经做过某个决定
一个简单的判断方法是看信息描述的主体。
例如:
FastAPI 如何处理依赖注入?
属于外部知识,更适合 RAG。
而:
Bob 的项目使用 FastAPI。
描述的是用户历史信息,更适合 Memory。
真实 Agent 中两者经常同时存在:
用户问题
│
├── Memory Retrieval
│ ↓
│ 用户背景
│
└── RAG Retrieval
↓
外部知识
↓
Context
↓
LLM
例如用户询问某个数据库设计问题。
RAG 提供当前 PostgreSQL 文档和内部项目规范。
Memory 提供:
用户使用 Python
项目基于 FastAPI
数据库是 PostgreSQL
偏好简单方案
最终模型同时得到:
知识
+
用户上下文
两者并不是替代关系。
十、长期记忆的工程问题
把 Store 接入 Agent 并不困难。
真正上线后,需要长期处理的是 Memory 的生命周期。
记忆污染
用户说:
最近想试试 Java。
如果系统直接保存为:
preferred_language = Java
就可能制造错误记忆。
用户可能只是临时尝试,并没有改变长期偏好。
类似的问题还包括:
模型推断成事实
玩笑被写入 Memory
临时状态被当成长期属性
旧信息覆盖新事实
因此,重要 Memory 可以附带额外业务字段:
{
"value": "Python",
"type": "preferred_language",
"source": "explicit_user_statement",
"confidence": 1.0
}
这些字段不是 LangGraph Store 强制要求的结构,而是应用层可以自行设计的 Schema。
Store 负责:
保存
读取
搜索
删除
至于某条数据是否可信,需要应用自己控制。
冲突与更新
如果使用固定 Key:
("users", "u1001", "preferences")
↓
programming-language
那么新的确定信息可以直接更新原来的 Item。
Collection 会复杂一些。
例如同时存在:
用户主要使用 Python
用户主要使用 Java
用户主要使用 C#
如果这些数据没有时间、来源和状态信息,检索时就很难判断应该相信哪一条。
因此 Collection 通常需要进一步考虑:
新增
覆盖
合并
失效
删除
而不是简单执行:
store.put(...)
过期与删除
长期记忆也存在时效性。
例如:
姓名
通常变化较少。
而:
最近正在开发某个项目
当前学习 LangChain
最近关注某项技术
几个月之后可能已经不再成立。
Store 提供删除 Item 的能力。
部分具体 Store 实现还可以支持 TTL,但是否支持 TTL 取决于底层实现,不能假设所有 Store 都具备相同能力。
因此,生产系统最好明确:
哪些数据长期保留
哪些数据需要定期确认
哪些数据自动过期
哪些数据被新值覆盖
哪些数据允许用户主动删除
长期 Memory 如果只有 Write Path,却没有 Update 和 Delete Path,最终很容易变成一份越来越大的历史垃圾集合。
存储实现
InMemoryStore 适合:
学习
本地开发
测试
Demo
数据保存在 Python 进程内存中。
进程结束后数据就会消失。
生产环境如果真正要求长期保存,需要选择持久化 Store,例如数据库支持的 Store 实现。
因此,“Long-term Memory”描述的是:
数据脱离 Thread 长期存在
并不等于:
InMemoryStore 天然永久保存
长期记忆的逻辑作用域,与底层存储是否真正持久化,是两个不同的问题。
总结
LangChain 的长期记忆,可以理解成一套建立在 Store 之上的跨 Thread 数据管理机制。
最基本的数据组织方式是:
Namespace
↓
Key
↓
Value
↓
Store
Namespace 负责划分逻辑范围,Key 定位其中的一条 Item,Value 保存真正的业务数据。
在此基础上,Store 提供:
put
get
search
delete
等基本操作,并可以通过索引和 Embedding 支持语义搜索。
实际项目中,更需要花时间设计的并不是 API,而是 Memory 本身:
什么值得保存
怎样划分 Namespace
使用 Profile 还是 Collection
什么时候写入
什么时候检索
怎样处理冲突
什么时候更新和删除
如果保存的是稳定、明确的数据,可以使用固定 Key 的结构化 Profile。
如果信息会不断积累,而且每次只需要读取其中一部分,可以采用 Collection,并结合语义搜索。
Memory 也不需要替代 RAG。前者保存用户、Agent 和历史上下文中的长期信息,后者负责检索外部知识。两种数据共同进入模型上下文,通常比单独依赖任何一种方式更符合真实 Agent 应用的需要。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。