上一篇介绍短期记忆时,我们讨论的范围始终没有离开 Thread:Thread 中的 State 通过 Checkpointer 保存,下一次调用再根据 thread_id 恢复。

但实际应用很快会遇到新的问题。

比如,用户结束当前会话,下一次重新创建一个 Thread。Agent 还应该知道他习惯使用 Python、喜欢简洁回答,或者之前已经告诉过系统自己的项目背景。

这些信息显然不适合绑定在某个 Thread 上。

LangChain 把这类需要跨 Thread、跨会话继续存在的数据归入长期记忆(Long-term Memory)。它不再主要依赖 Checkpointer,而是建立在 LangGraph 的 Store 之上。

一、长期记忆的定位

先看一个简单场景。

用户在一次会话中告诉 Agent:

代码片段Text
我主要使用 Python 开发。
回答代码问题时尽量直接一些。

几天之后,用户创建了新的会话:

代码片段Text
帮我写一个读取 CSV 的例子。

虽然已经换了 Thread,但系统仍然应知道或记住用户的偏好:

代码片段Text
主要语言 = Python
回答风格 = concise

这类信息描述的是用户长期属性,而不是当前会话的执行状态。

因此,它更适合存入长期记忆:

代码片段Text
User

 ├── Thread A

 ├── Thread B

 └── Long-term Memory

      Store

多个 Thread 可以相互独立,但只要它们属于同一个用户,就可以访问相同的长期数据。

这也是长期记忆最重要的能力:

让数据脱离单个 Thread 的生命周期,并在多个会话之间共享。

长期记忆保存的内容通常包括:

  • 用户画像
  • 用户偏好
  • 历史事实
  • 项目背景
  • Agent 学到的规则
  • 过去的重要事件

它们是否需要长期存在,取决于业务,而不是由也不应由 LangChain 自动决定。

二、Store 与 Checkpointer

LangChain 中有两个都与“保存数据”有关的组件:

代码片段Text
Checkpointer
Store

两者很容易混淆,但职责并不相同。

通过上一篇短期记忆的学习,我们已了解 Checkpointer 保存的是某个 Thread 的 Agent State。

例如:

代码片段Text
messages
current_step
tool result
interrupt
next node
...

这些数据共同描述:

代码片段Text
这段会话现在是什么状态?

Store 保存的则是独立于具体 Thread 的长期数据。

例如:

代码片段Text
用户喜欢 Python
用户偏好简洁回答
用户当前项目使用 FastAPI
用户选择了深色主题

它回答的是:

代码片段Text
有哪些信息应该在不同会话之间继续存在?

可以把两者理解为两条数据链路:

代码片段Text
Agent 执行

State

Checkpoint

Checkpointer

以及:

代码片段Text
长期信息

Namespace

Key

Value

Store

一个 Agent 可以同时使用两者:

代码片段Python
agent = create_agent(
    model=...,
    tools=...,
    checkpointer=checkpointer,
    store=store,
)

此时,Checkpointer 负责当前会话状态,Store 负责跨会话信息。它们解决的是两个层级的问题。

三、Store 的数据模型

Store 并不是简单的全局字典。

LangGraph Store 中最重要的三个概念是:

代码片段Text
Namespace
Key
Value

一条数据可以表示为:

代码片段Text
Namespace + Key → Value

例如:

代码片段Python
namespace = ("users", "u1001", "profile")
 
key = "basic"
 
value = {
    "name": "Bob",
    "language": "Python",
}

可以先做一个不完全严格、但容易理解的类比:

代码片段Text
Namespace ≈ 目录

Key ≈ 文件名

Value ≈ 文件内容

Store 中实际保存的单位称为 Item。

一个 Item 除了业务数据之外,还会包含:

代码片段Text
namespace
key
value
created_at
updated_at

因此,Store 保存的一条数据属于哪个 Namespace、对应哪个 Key、它的 Value,以及创建和更新时间。

Namespace 划分数据范围

Namespace 是字符串组成的元组:

代码片段Python
("users", "u1001")

也可以继续增加层级:

代码片段Python
("users", "u1001", "profile")
("users", "u1001", "preferences")
("users", "u1001", "memories")

这种设计比固定的一层 Key 更适合长期数据。

例如一个系统同时需要保存:

代码片段Text
用户记忆
组织知识
Agent 指令
项目上下文

就可以分别设计成:

代码片段Text
("users", "u1001", "memories")

("organizations", "org-01", "knowledge")

("agents", "support-agent", "instructions")

("projects", "project-100", "context")

因此,Namespace 更像一个逻辑作用域

它决定一组 Item 属于谁、属于哪类数据。

实际设计中,比“Key 起什么名字”更应该先考虑 Namespace。

Key 定位具体 Item

Namespace 确定范围后,Key 用于定位这个范围中的具体记录。

例如:

代码片段Text
Namespace
("users", "u1001", "preferences")

Key
language

Value
{"value": "Python"}

同一个 Namespace 下还可以存在:

代码片段Text
answer-style
theme
timezone

于是形成:

代码片段Text
("users", "u1001", "preferences")

    ├── language
    ├── answer-style
    ├── theme
    └── timezone

Namespace 与 Key 组合起来,才真正确定一条数据。

设计长期记忆时,可以先回答三个问题:

代码片段Text
这条数据属于谁?

它属于哪一类信息?

它在这一类数据中如何唯一识别?

前两个问题通常决定 Namespace,最后一个问题决定 Key。

四、Store 的基本操作

和短期记录中的 InMemorySaver 一样,LangGraph 提供 InMemoryStore,适合学习、测试和本地开发。

代码片段Python
from langgraph.store.memory import InMemoryStore
 
store = InMemoryStore()

有了 Store 后,可以进行最基本的写入、读取、搜索和删除。

写入数据

写入使用 put()

代码片段Python
namespace = ("users", "u1001", "profile")
 
store.put(
    namespace,
    "basic",
    {
        "name": "Bob",
        "language": "Python",
        "answer_style": "concise",
    },
)

put() 的基本结构是:

代码片段Python
store.put(
    namespace,
    key,
    value,
)

其中 namespace 是字符串元组,key 是字符串,value 是一个可以 JSON 序列化的字典。

如果相同的:

代码片段Text
Namespace + Key

再次执行 put(),这条 Item 会被更新。

例如:

代码片段Python
store.put(
    namespace,
    "basic",
    {
        "name": "Bob",
        "language": "Python",
        "answer_style": "detailed",
    },
)

这里不是创建第二个 basic,而是更新原有 Item。

这非常适合保存固定结构的 Profile 或 Preferences。

精确读取

如果已经知道 Namespace 和 Key,可以使用:

代码片段Python
item = store.get(namespace, "basic")
 
print(item.value)

运行结果

代码片段Text
{'name': 'Bob', 'language': 'Python', 'answer_style': 'detailed'}

从结果可以看到,第二次 put() 已经更新了原有数据。

如果指定的 Key 不存在,get() 返回 None

因此,在实际代码中通常需要判断:

代码片段Python
item = store.get(namespace, "basic")
 
if item is not None:
    print(item.value)

get() 适合:

代码片段Text
我明确知道要读哪一条数据

例如:

代码片段Text
用户 Profile
账户配置
某个项目设置
指定 Preference

搜索一组数据

如果不知道具体 Key,可以使用 search()

代码片段Python
items = store.search(namespace)
 
for item in items:
    print(item.key, item.value)

例如 Namespace 中存在:

代码片段Text
language
answer-style
theme

搜索后就可以返回这一范围中的多条 Item。

这与:

代码片段Python
store.get(namespace, key)

的使用场景不同。

get() 是精确定位。

search() 是从一个 Namespace 中寻找一组符合条件的数据。

删除数据

如果某条长期信息已经失效,可以使用:

代码片段Python
store.delete(
    namespace,
    "basic",
)

删除之后:

代码片段Python
item = store.get(namespace, "basic")
 
print(item)

运行结果

代码片段Text
None

长期记忆必须具备删除能力。

因为长期保存不等于永久保存。如果只能不断写入,无法删除旧事实和错误信息,Memory 的质量会随着使用时间不断下降。

五、Store 的搜索能力

Store 与普通 Key-Value Storage 的一个重要区别,是它可以进一步支持语义搜索。

假设一个用户已经积累了很多长期信息:

代码片段Text
用户主要使用 Python 和 FastAPI
用户喜欢深色主题
用户正在学习 LangChain
用户使用 PostgreSQL
用户希望技术回答尽量直接

当前问题是:

代码片段Text
我的后端项目应该怎么组织?

如果每次把全部 Memory 都放进 Prompt,数据越积越多,上下文就会不断膨胀。

更合理的方式是:

代码片段Text
当前问题

搜索相关 Memory

选择少量相关记录

加入模型上下文

Store 可以配置 Embedding,使搜索从简单的 Namespace 查询进一步变成语义检索。

例如:

代码片段Python
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,
    }
)

随后写入几条记忆:

代码片段Python
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"
    },
)

然后进行语义搜索:

代码片段Python
results = store.search(
    namespace,
    query="后端开发技术栈",
    limit=2,
)
 
for item in results:
    print(item.value)

示例输出

代码片段Text
{'text': '用户主要使用 Python 和 FastAPI 开发后端'}
{'text': '用户经常使用 PostgreSQL'}

从结果可以看到,搜索并不依赖 Key 是否包含“后端”这个词。

Embedding 会把当前 Query 与 Store 中的数据转换成向量,再根据语义相关性寻找更合适的 Item。

需要注意,InMemoryStore 默认并不会自动具备语义搜索能力。

必须配置:

代码片段Text
index
embed
dims

等索引相关信息。

因此,Store 可以有两种常见使用方式:

代码片段Text
已知 Key

get()

不知道具体 Key

search()

如果长期数据规模较小,而且结构稳定,精确读取已经足够。

如果 Memory 是不断增加的 Collection,通常更适合使用搜索。

六、跨 Thread 共享数据

长期记忆真正进入 Agent 后,需要解决另一个问题:

当前是谁在访问 Store?

一种常见方式是通过 Runtime Context 传入 user_id

例如定义:

代码片段Python
from dataclasses import dataclass
 
@dataclass
class Context:
    user_id: str

随后在 Tool 中通过 ToolRuntime 获得当前 Runtime 和 Store:

代码片段Python
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:

代码片段Python
@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:

代码片段Python
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,
)

这里真正决定长期数据归属的是:

代码片段Text
user_id

例如:

代码片段Text
user_id = u1001

这个用户之后可以创建很多 Thread:

代码片段Text
u1001

 ├── thread-001
 ├── thread-002
 └── thread-003

虽然会话不同,但三个 Thread 都可以通过:

代码片段Text
runtime.context.user_id

定位到相同的长期数据。

因此:

代码片段Text
thread_id

描述的是“哪一段会话”。

而:

代码片段Text
user_id

描述的是“谁”。

长期记忆的跨会话能力,实际上来自:

代码片段Text
多个 Thread

使用相同业务身份

访问同一 Namespace

读取相同 Store 数据

当然,Namespace 并不一定非要按 User 划分。

还可以按:

代码片段Text
organization_id
project_id
agent_id
tenant_id

组织。

这也是 Store 比单纯“用户记忆数据库”更通用的地方。

七、长期记忆保存什么

Store 只提供保存和查询能力,并不会自动判断什么值得记住。

长期记忆通常可以分成几类。

Semantic Memory

语义记忆保存事实和知识。

例如:

代码片段Text
用户主要使用 Python
用户喜欢简洁回答
用户所在团队使用 PostgreSQL
当前项目基于 FastAPI

这是大多数应用最先需要实现的一类 Memory。

Episodic Memory

情景记忆保存过去发生过的事件或经历。

例如:

代码片段Text
用户之前完成过一次数据库迁移
某种 Tool 调用方式曾经失败
过去某个案例使用方案 A 获得了较好结果

它更接近:

代码片段Text
以前发生过什么

而不是单纯的:

代码片段Text
这个用户是什么样

Procedural Memory

程序性记忆描述 Agent 应该如何完成某类任务。

例如根据用户反馈逐步调整:

代码片段Text
生成代码时不要加入过多解释
技术文章中先说明机制再给代码
执行删除操作之前必须确认

这类内容影响的是 Agent 的行为方式。

实际项目不必一开始就完整实现三类 Memory。

通常可以先从用户事实和偏好开始,再根据业务需求增加其他类型。

Profile 与 Collection

即使只保存用户信息,也还存在两种常见组织方式。

第一种是 Profile:

代码片段JSON
{
  "name": "Bob",
  "language": "Python",
  "answer_style": "concise",
  "frameworks": [
    "FastAPI",
    "LangChain"
  ]
}

整个用户画像作为一条或少量几条 Item 保存。

优点是结构清晰,读取简单。

缺点是 Profile 越来越大之后,更新某一项时需要处理整份结构。

另一种是 Collection:

代码片段Text
memory-001
用户主要使用 Python

memory-002
用户正在开发 FastAPI 项目

memory-003
用户希望回答尽量简洁

每一个事实独立存在。

这样更容易:

代码片段Text
新增
搜索
删除
局部更新

也更适合语义检索。

但 Collection 需要额外处理:

代码片段Text
重复
冲突
过期
召回
排序

因此,如果数据字段稳定,例如语言、时区、界面主题,Profile 更简单。

如果信息会持续增加,并且每次只需要检索其中的一部分,Collection 通常更适合。

八、什么时候写入记忆

真正困难的通常不是:

代码片段Python
store.put(...)

而是:

代码片段Text
什么时候应该调用 store.put()?

长期记忆主要有两种写入方式。

当前执行中写入

第一种是在 Agent 当前请求中立即判断并保存,也就是 Hot Path。

例如:

代码片段Text
用户:
以后代码示例都使用 Python。



Agent 判断
这是长期偏好



写入 Store



继续回答

这样做最大的好处是立即生效。

如果用户马上新建 Thread,也能够读取到刚刚保存的偏好。

但当前 Agent 同时需要处理:

代码片段Text
回答问题
识别值得记忆的信息
确定 Memory 类型
处理已有 Memory
执行写入

请求链路会变得更复杂。

后台提取

另一种方式是先完成当前任务,再单独执行 Memory Extraction。

例如:

代码片段Text
Conversation

正常响应

Memory Processor

分析多轮消息

提取长期事实

去重与合并

Store

这种方式更适合从一段较长的会话中提取:

代码片段Text
用户画像
技术栈
项目背景
长期目标
历史事实

它不会增加主请求中的记忆处理负担。

但 Memory 不会立即出现。

如果后台提取还没有完成,新的 Thread 暂时可能读不到刚产生的信息。

实际应用中,两种方式可以组合。

例如:

代码片段Text
以后都使用中文回答

这是一条明确、稳定,而且应该立即生效的偏好,可以同步写入。

而一段几十轮的项目讨论,则更适合结束后统一提取:

代码片段Text
项目使用 FastAPI
数据库是 PostgreSQL
准备迁移到 Kubernetes
当前主要关注性能问题

长期记忆系统的质量,很大程度上取决于写入规则是否足够克制(清晰、明确、有用)

不是所有出现过的信息都值得保存。

九、Memory 与 RAG

Store 支持 Embedding 和语义搜索后,看起来与 RAG 很接近。

例如,它们的技术链路确实很类似:

代码片段Text
数据

Embedding

检索

Context

LLM

但它们管理的数据来源和生命周期不同。

RAG 通常回答:

代码片段Text
系统知道哪些外部知识?

例如:

代码片段Text
技术文档
产品手册
企业知识库
合同
代码库
FAQ

Memory 则更关注:

代码片段Text
这个 Agent 需要长期记住什么?

例如:

代码片段Text
Bob 使用 Python
Bob 喜欢简洁回答
Bob 当前在开发 FastAPI 项目
Bob 曾经做过某个决定

一个简单的判断方法是看信息描述的主体。

例如:

代码片段Text
FastAPI 如何处理依赖注入?

属于外部知识,更适合 RAG。

而:

代码片段Text
Bob 的项目使用 FastAPI。

描述的是用户历史信息,更适合 Memory。

真实 Agent 中两者经常同时存在:

代码片段Text
用户问题

   ├── Memory Retrieval
   │      ↓
   │   用户背景

   └── RAG Retrieval

       外部知识


       Context


         LLM

例如用户询问某个数据库设计问题。

RAG 提供当前 PostgreSQL 文档和内部项目规范。

Memory 提供:

代码片段Text
用户使用 Python
项目基于 FastAPI
数据库是 PostgreSQL
偏好简单方案

最终模型同时得到:

代码片段Text
知识
+
用户上下文

两者并不是替代关系。

十、长期记忆的工程问题

把 Store 接入 Agent 并不困难。

真正上线后,需要长期处理的是 Memory 的生命周期。

记忆污染

用户说:

代码片段Text
最近想试试 Java。

如果系统直接保存为:

代码片段Text
preferred_language = Java

就可能制造错误记忆。

用户可能只是临时尝试,并没有改变长期偏好。

类似的问题还包括:

代码片段Text
模型推断成事实
玩笑被写入 Memory
临时状态被当成长期属性
旧信息覆盖新事实

因此,重要 Memory 可以附带额外业务字段:

代码片段JSON
{
  "value": "Python",
  "type": "preferred_language",
  "source": "explicit_user_statement",
  "confidence": 1.0
}

这些字段不是 LangGraph Store 强制要求的结构,而是应用层可以自行设计的 Schema。

Store 负责:

代码片段Text
保存
读取
搜索
删除

至于某条数据是否可信,需要应用自己控制。

冲突与更新

如果使用固定 Key:

代码片段Text
("users", "u1001", "preferences")

programming-language

那么新的确定信息可以直接更新原来的 Item。

Collection 会复杂一些。

例如同时存在:

代码片段Text
用户主要使用 Python
用户主要使用 Java
用户主要使用 C#

如果这些数据没有时间、来源和状态信息,检索时就很难判断应该相信哪一条。

因此 Collection 通常需要进一步考虑:

代码片段Text
新增
覆盖
合并
失效
删除

而不是简单执行:

代码片段Python
store.put(...)

过期与删除

长期记忆也存在时效性。

例如:

代码片段Text
姓名

通常变化较少。

而:

代码片段Text
最近正在开发某个项目
当前学习 LangChain
最近关注某项技术

几个月之后可能已经不再成立。

Store 提供删除 Item 的能力。

部分具体 Store 实现还可以支持 TTL,但是否支持 TTL 取决于底层实现,不能假设所有 Store 都具备相同能力。

因此,生产系统最好明确:

代码片段Text
哪些数据长期保留
哪些数据需要定期确认
哪些数据自动过期
哪些数据被新值覆盖
哪些数据允许用户主动删除

长期 Memory 如果只有 Write Path,却没有 Update 和 Delete Path,最终很容易变成一份越来越大的历史垃圾集合。

存储实现

InMemoryStore 适合:

代码片段Text
学习
本地开发
测试
Demo

数据保存在 Python 进程内存中。

进程结束后数据就会消失。

生产环境如果真正要求长期保存,需要选择持久化 Store,例如数据库支持的 Store 实现。

因此,“Long-term Memory”描述的是:

代码片段Text
数据脱离 Thread 长期存在

并不等于:

代码片段Text
InMemoryStore 天然永久保存

长期记忆的逻辑作用域,与底层存储是否真正持久化,是两个不同的问题。

总结

LangChain 的长期记忆,可以理解成一套建立在 Store 之上的跨 Thread 数据管理机制

最基本的数据组织方式是:

代码片段Text
Namespace

Key

Value

Store

Namespace 负责划分逻辑范围,Key 定位其中的一条 Item,Value 保存真正的业务数据。

在此基础上,Store 提供:

代码片段Text
put
get
search
delete

等基本操作,并可以通过索引和 Embedding 支持语义搜索。

实际项目中,更需要花时间设计的并不是 API,而是 Memory 本身:

代码片段Text
什么值得保存
怎样划分 Namespace
使用 Profile 还是 Collection
什么时候写入
什么时候检索
怎样处理冲突
什么时候更新和删除

如果保存的是稳定、明确的数据,可以使用固定 Key 的结构化 Profile。

如果信息会不断积累,而且每次只需要读取其中一部分,可以采用 Collection,并结合语义搜索。

Memory 也不需要替代 RAG。前者保存用户、Agent 和历史上下文中的长期信息,后者负责检索外部知识。两种数据共同进入模型上下文,通常比单独依赖任何一种方式更符合真实 Agent 应用的需要。