LangChain:Memory(记忆)组件概念

Memory(记忆)作为LangChain框架的核心组件,解决了大语言模型无状态交互的痛点,通过跨线程的命名空间(namespace)管理,实现了对话历史、用户画像及行为习惯的持久化存储。

记忆概念

Memory(记忆)是一个记录先前交互信息的系统。对于AI智能体而言,记忆之所以关键,是因为它能让智能体记住之前的交互信息、从反馈中学习并适应不同用户的偏好。随着智能体处理的任务愈发复杂、需要与用户进行大量交互,这一能力在提升效率与用户满意度方面显得至关重要。

本概念指南将根据记忆的检索范围,分两类来展开介绍。

  • 短期记忆:或称线程(Thread)作用域记忆,通过在会话过程中持续记录消息历史,来追踪当前对话的进展。LangGraph会将短期记忆作为智能体状态的一部分来进行管理。状态会借助检查点机制被持久化到数据库中,以便线程能在任意时刻被恢复执行。短期记忆会在调用图或图中步骤完成时更新,而状态则会在每个步骤开始执行时被读取。
  • 长期记忆:负责存储跨会话使用的、针对特定用户或应用层级的数据,并可在多个对话线程之间共享。这些记忆可在任意时间、任意线程中被调用,且其作用域不仅限于单个线程ID,而是可以绑定到任意自定义的命名空间。LangGraph提供了存储器(Store),帮助开发者存储和调取长期记忆。

短期记忆

短期记忆能让应用在处理单个对话线程时,记住当前对话过程中的历史交互信息。类似电子邮件的组会话功能,线程能将会话中发生的多次交互有效组织起来。LangGraph会把短期记忆纳入智能体的状态中进行统一管理,并通过线程级别的检查点来持久化。

这种状态既可包含对话历史,也能承载其他有状态的数据,例如上传的文件、检索到的文档、或AI生成的内容。通过将这些信息存放到图的状态中,智能体既可以获取当前对话的完整上下文,也能确保不同线程之间的信息相互隔离。

管理短期记忆

对话历史是短期记忆中最常见的一种形式。然而,随着对话时间变长,可能会对LLM构成挑战:完整的对话历史可能会超出LLM的上下文窗口限制,从而引发不可恢复的错误。即便所用的LLM能够支持完整上下文长度,大多数模型在处理超长上下文时的表现仍不尽如人意——它们会被陈旧或偏离主题的内容分散注意力,同时响应速度变慢、运行成本升高。

聊天模型以消息列表形式接收上下文,包括开发者提供的提示指令(系统消息)和用户输入(人工消息)。在聊天应用中,人工消息与模型回复相互交替,使得消息列表随着对话逐步加长。鉴于上下文窗口存在容量限制,且消息列表越丰富,令牌成本越高,许多应用场景都需要手动剔除或遗忘陈旧信息以提升效率。

长期记忆

LangGraph中的长期记忆允许系统在不同对话或会话之间保留信息。与基于线程的短期记忆不同,长期记忆被保存在自定义的“命名空间”中。

长期记忆是一个复杂的挑战,没有万能解决方案。下面的问题框架可以帮助你选择不同的技术方案:

  • 记忆的类型是什么? 人类利用记忆来记忆事实(语义记忆)、经历(情景记忆)和规则(程序性记忆)。AI智能体也能以同样的方式利用记忆。例如,AI智能体可以利用记忆记住关于某个用户的特定事实,以顺利完成任务。
  • 你希望什么时候更新记忆? 记忆可以在智能体的应用逻辑中更新(例如,“在热路径上”)。在这种情况下,智能体通常会在回复用户之前决定要记住哪些事实。另一种选择是作为后台任务来更新记忆(在后台/异步运行,生成记忆的逻辑)。
记忆类型 存储内容 人类示例 智能体示例
语义记忆 事实 学校中所学知识 关于用户的事实
情景记忆 经历 亲身经历 智能体过往的行动
程序性记忆 指令 本能或运动技能 智能体系统提示词

语义记忆

无论是人类还是AI智能体,语义记忆都涉及对具体事实和概念的长期记忆。

对于人类而言,语义记忆可以包含在学校里获得的知识,以及对各种概念及其相互之间关系的理解。

对AI智能体而言,语义记忆常用于根据过往交互中的事实或概念对应用进行个性化定制。语义记忆可以通过不同的方式进行管理。

值得注意的是,“语义记忆”并非“语义检索”。语义检索是一种通过“含义”(通常表现为嵌入式向量)来查找相似内容的技术,而语义记忆是心理学中的术语,专指存储事实和知识的过程。两者有本质不同。

配置文件形式记忆

记忆可以看作是一份持续更新的单一“配置文件”,其中记录了关于某个用户、组织或其他实体(也包括智能体自身)定义明确且详细的信息。

配置文件通常只是一个JSON文档,其中包含了为表示特定领域而选取的各种键值对。在通过配置文件存储记忆时,关键是要保证每次都在对之前的配置文件进行更新。因此,你会需要将先前的配置文件传给模型,并让模型生成一份新的配置文件(或应用某种JSON修补补丁到旧配置文件中)。

随着配置文件越来越大,这个过程会变得容易出错。这时可以考虑将一份配置文件拆分成多个文档,或者在生成文档时采用严格的解码方式,以保证记忆模式始终有效。

集合形式记忆

另一种方式是,将记忆组织成一个不断被更新和扩展的文档集合。这样,每条记忆的对象范围会更具体,生成也更容易,信息因此可以保留更长时间。相比在原有配置文件中整合信息,LLM为新增信息生成新对象要更容易。因此,文档集合往往能在下游实现更高的召回率。

不过,这也会将一部分复杂性转移到记忆的更新上:模型现在需要能够删除或更新列表中的已有项,这一点实现起来可能比较棘手。另外,有些模型可能会默认过量插入,另一些模型则可能倾向于过量更新。为解决这一问题,可以参考Trustcall包,并通过评测工具(如LangSmith)进行调优。

使用文档集合还会把复杂性转移到对列表的记忆检索上。目前,Store既支持语义检索,也支持按内容进行过滤。

最后,使用记忆集合可能会让为模型提供全面的上下文变得困难。虽然每条单独的记忆可能遵循特定的模式,但这种结构未必能捕捉到记忆之间的完整上下文或关联关系。因此,当利用这些记忆来生成回复时,模型可能会缺少一些重要上下文信息,而这些信息在统一的配置文件方法中通常更容易获得。

不论采用何种记忆管理方式,核心要点在于:智能体将利用语义记忆来为其回复建立基础,这通常能带来更个性化和更契合上下文需求的交互体验。

情景记忆

对人类和AI智能体而言,情景记忆涉及对过往事件或行动的回顾。CoALA论文对这一点阐述得很好:事实可被写入语义记忆,而经历则可写入情景记忆。对于AI智能体,情景记忆常用于帮助智能体记住如何完成某项任务。

在实践中,情景记忆通常借助少样本提示来实现——智能体通过分析过去的序列来学习如何正确完成任务。有时“演示”比“告知”更有效,而LLM从示例中学习的能力很强。少样本学习让你能够通过在提示中增加输入输出示例来“编程”你的LLM,以展示你期望的行为。虽然有多种最佳实践可用于生成少样本示例,但难点通常在于:如何根据用户的输入选择最相关的示例。

需要指出的是,记忆存储只是存储少量样本示例数据的一种方式。如果希望更深度地介入,或者想让少样本示例与你的评测体系更紧密地结合,也可以使用LangSmith数据集来存储数据,并自行实现基于用户输入的检索逻辑,来选出最相关的示例。

程序性记忆

对人类和AI智能体而言,程序性记忆涉及对执行任务所用规则的记忆。对人类来说,程序性记忆就像内化的“如何做”知识——例如,通过基本的运动技能和平衡感来学会骑自行车。而情景记忆则涉及对具体经历的回顾,比如成功学会不用辅助轮骑自行车的那一刻,或一次难忘的风景优美的自行车骑行经历。

对于AI智能体而言,程序性记忆是模型权重、智能体代码和智能体提示词的综合体,这些因素共同决定了智能体的行为。

在实践中,智能体自我更新模型权重或重写其代码的情况并不常见,但修改自身提示词却较为普遍。

优化智能体指令的一种有效方法是“反思”(Reflection)或“元提示”。这涉及将智能体当前的指令(如系统提示词)连同最近的对话或明确的用户反馈一起喂给智能体,然后智能体根据这些输入来优化自身的指令。这种方法在指令难以预先明确设定的任务中特别有用,因为它允许智能体通过交互来自主学习和适应。

例如,我们构建了一个Tweet 生成器,利用外部反馈和提示重写来生成高质量的文案摘要给Twitter。在这个案例中,具体的摘要提示很难事先确定,但用户可以轻松地对生成的推文进行评价,并提供关于如何改进摘要过程的反馈。

下面的伪代码展示了如何利用LangGraph 记忆存储来实现这一思路:使用store保存提示词,update_instructions节点负责获取当前提示词(以及从state[“messages”]中捕获的用户对话反馈)、更新提示词,并将更新后的提示词存回store。随后,call_model节点从store中获取更新后的提示词,并用它来生成最终的回复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 使用指令的节点
def call_model(state: State, store: BaseStore):
# 定义存储指令的命名空间(可以理解为存储区域的分组标签)
namespace = ("agent_instructions", )
# 从存储中获取智能体的指令(假设命名空间下键为 "agent_a" 的条目)
instructions = store.get(namespace, key="agent_a")[0]
# 应用逻辑:将获取到的指令填入提示模板中,生成最终的提示词
prompt = prompt_template.format(instructions=instructions.value["instructions"])
# ... 后续调用模型生成响应

# 更新指令的节点
def update_instructions(state: State, store: BaseStore):
# 定义存储反馈或历史指令的命名空间
namespace = ("instructions",)
# 搜索该命名空间下的所有指令(通常取第一个作为当前指令)
instructions = store.search(namespace)[0]
# 记忆逻辑:将当前指令和对话历史(state["messages"])填入提示模板
# 让大模型根据交互反馈生成改进后的新指令
prompt = prompt_template.format(instructions=instructions.value["instructions"],
conversation=state["messages"])
output = llm.invoke(prompt) # 调用大模型,获得包含新指令的输出
new_instructions = output['new_instructions'] # 从输出中提取新指令
# 将新指令保存回存储(命名空间为 ("agent_instructions",),键为 "agent_a")
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
# ...

代码说明

  • call_model 负责根据存储中的最新指令生成响应(即“使用指令”)。
  • update_instructions 负责根据当前指令及对话中的用户反馈,让大模型自我反思并生成改进后的指令,然后保存回存储(即“更新指令”)。
  • 这种模式实现了智能体指令的动态优化,尤其适用于初始指令难以完美制定的场景。

写入记忆

智能体写入记忆主要有两种方法:“热路径”写入和“后台”写入。

在热路径中写入

在运行期间实时创建记忆既有优势也有挑战。正面来看,这种方法支持实时更新,使新记忆能够立即在后续交互中被使用,而且具有很高的透明度——用户可以在记忆被创建和存储时收到通知。

然而,这种方法也有不足:可能会增加系统的复杂性(需要引入新工具来决定要提交哪些记忆),同时推理过程会增加智能体的延迟。而且,智能体必须同时处理记忆创建和其他任务,这会影响到最终记忆的生成数量和质量。

例如,ChatGPT就使用了一个save_memories工具来将记忆保存为字符串内容,并决定是否以及在每次用户消息中如何使用这个工具。可以参考memory-agent模板来了解具体的实现方式。

在后台写入

将记忆创建作为独立的后台任务来处理有几个好处:可以消除主应用中的延迟,将应用逻辑与记忆管理分离,让智能体更专注于任务本身,同时还允许在安排记忆创建时机上保有弹性,以避免重复工作。

不过,这种方法也有自己的挑战。如何确定记忆写入的频率至关重要,更新不够频繁可能导致其他线程缺乏新的上下文;何时触发记忆的形成也需要仔细考量。常见的策略包括:设定固定时间间隔(有新事件发生时重新安排)、使用cron表达式定期触发、或允许用户或应用逻辑进行手动触发。

可以参考memory-service模板来了解具体的实现方式。

记忆存储

LangGraph将长期记忆存储为存储器中的JSON文档。每条记忆都组织在自定义命名空间(类似于文件夹)和独立键(类似于文件名)下。命名空间通常包含用户或组织ID,或其他便于组织信息的标签。这种结构支持对记忆进行分层组织。跨命名空间的搜索可以通过内容过滤器来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from langgraph.store.memory import InMemoryStore

# 定义嵌入函数:将文本列表转换为向量列表
def embed(texts: list[str]) -> list[list[float]]:
# 请替换为实际的嵌入函数或 LangChain 的嵌入对象
return [[1.0, 2.0] * len(texts)]

# InMemoryStore 将数据保存在内存字典中。生产环境中应使用数据库支持的存储。
store = InMemoryStore(index={"embed": embed, "dims": 2}) # 配置索引:嵌入函数和向量维度
user_id = "my-user" # 用户标识
application_context = "chitchat" # 应用上下文(例如聊天场景)
namespace = (user_id, application_context) # 命名空间:用于隔离不同用户/场景的数据

# 在指定命名空间下存储一条记忆(键为 "a-memory")
store.put(
namespace,
"a-memory",
{
"rules": [
"用户喜欢简短直接的语言", # 规则:用户偏好
"用户只使用英语和 Python",
],
"my-key": "my-value", # 自定义键值对
},
)

# 通过 ID 获取记忆
item = store.get(namespace, "a-memory")

# 搜索该命名空间下的记忆:可基于内容等价过滤,并按向量相似度排序
items = store.search(
namespace, filter={"my-key": "my-value"}, query="语言偏好"
)

代码说明

  • InMemoryStore 用于演示,生产环境建议改用持久化存储(如 Redis、PostgreSQL 等)。
  • 嵌入函数 embed 需替换为实际的文本向量化方法(例如 OpenAIEmbeddingsHuggingFaceEmbeddings)。
  • search 方法中的 filter 参数支持按存储的值进行精确匹配;query 会与存储内容的向量表示进行相似度检索。

关于记忆存储的更多信息,可以参考 Persistence 指南。

延伸阅读

作者

光星

发布于

2026-06-01

更新于

2026-08-15

许可协议

评论