记忆¶
记忆 是一个记住先前交互信息的系统。对于 AI 代理而言,记忆至关重要,因为它使它们能够记住先前的交互、从反馈中学习并适应用户偏好。随着代理处理越来越复杂且包含大量用户交互的任务,这种能力对效率和用户满意度都至关重要。
本概念指南根据可回忆的范围,涵盖两种记忆类型:
-
短期记忆,或线程作用域的记忆,通过在会话中维护消息历史来跟踪正在进行的对话。LangGraph 将短期记忆作为代理状态的一部分进行管理。状态通过检查点工具持久化到数据库,以便随时恢复线程。短期记忆会在图被调用或某个步骤完成时更新,并在每一步开始时读取状态。
-
长期记忆在多次会话中存储特定用户或应用级数据,并在不同的会话线程之间_跨越_共享。它可以在任何时间、在任何线程中被召回。记忆可以被作用域到任意自定义的命名空间,而不局限于单个线程 ID。LangGraph 提供了用于保存和召回长期记忆的存储(参考文档)。
短期记忆¶
短期记忆使你的应用能够在单个线程或对话中记住先前的交互。线程将会话中的多次交互组织起来,类似于电子邮件将多条消息归为一个会话。
LangGraph 将短期记忆作为代理状态的一部分进行管理,并通过线程作用域的检查点进行持久化。该状态通常可以包含对话历史以及其他有状态数据,例如上传的文件、检索到的文档或生成的工件。通过将这些存储在图的状态中,机器人可以在保持不同线程之间隔离的同时,访问某次对话的完整上下文。
管理短期记忆¶
对话历史是最常见的短期记忆形式,而长对话对当今的 LLM 是一个挑战。完整历史可能无法装入 LLM 的上下文窗口,从而导致不可恢复的错误。即使你的 LLM 支持完整的上下文长度,大多数 LLM 在长上下文上仍表现不佳。它们会被陈旧或离题的内容“分散注意力”,同时还会导致响应速度更慢、成本更高。
聊天模型使用消息来接收上下文,其中包括开发者提供的指令(system 消息)和用户输入(human 消息)。在聊天应用中,消息在人类输入和模型响应之间交替,导致消息列表随时间不断增长。由于上下文窗口有限且富含 token 的消息列表代价高昂,许多应用可受益于使用一些技术手动移除或遗忘陈旧信息。
有关管理消息的常见技术的更多信息,请参阅添加和管理记忆指南。
长期记忆¶
LangGraph 中的长期记忆允许系统在不同对话或会话之间保留信息。与**线程作用域**的短期记忆不同,长期记忆保存在自定义“命名空间”中。
长期记忆是一个复杂的问题,没有放之四海皆准的解决方案。不过,下面的问题提供了一个框架,帮助你在不同技术之间进行取舍:
-
记忆的类型是什么? 人类用记忆来记住事实(语义记忆)、经历(情景记忆)和规则(程序性记忆)。AI 代理也可以以同样的方式使用记忆。例如,AI 代理可以使用记忆来记住关于用户的具体事实以完成任务。
-
你希望何时更新记忆? 记忆可以作为代理应用逻辑的一部分进行更新(例如“在热路径上”)。这种情况下,代理通常会在回应用户之前决定是否记住某些事实。或者,也可以将记忆更新为后台任务(在后台/异步运行并生成记忆的逻辑)。我们在下文解释这些方法之间的权衡。
记忆类型¶
不同的应用需要不同类型的记忆。虽然类比并不完美,但研究人类记忆类型可能颇有启发。有些研究(例如 CoALA 论文)甚至将这些人类记忆类型映射到了 AI 代理所用的记忆类型。
记忆类型 | 存储内容 | 人类示例 | 代理示例 |
---|---|---|---|
语义 | 事实 | 我在学校学到的东西 | 关于用户的事实 |
情景 | 经历 | 我做过的事 | 代理过去的行为 |
程序性 | 指令 | 本能或运动技能 | 代理的系统提示词 |
语义记忆¶
语义记忆在人与 AI 代理中都涉及对具体事实与概念的保留。对人类而言,它包含在学校中学到的信息以及对概念及其关系的理解。对 AI 代理而言,语义记忆通常用于通过记住以往交互中的事实或概念来实现个性化。
Note
语义记忆不同于“语义搜索”。“语义搜索”是一种利用“含义”(通常通过向量嵌入)来查找相似内容的技术。语义记忆是心理学中的术语,指存储事实和知识;而语义搜索是一种基于语义而非精确匹配来检索信息的方法。
画像(Profile)¶
语义记忆可以以不同方式管理。例如,记忆可以是关于用户、组织或其他实体(包括代理自身)的单一、持续更新的“画像”,其包含范围明确且具体的信息。画像通常就是一个 JSON 文档,内含你为你的领域挑选的各种键值对。
在记住画像时,你需要确保每次都是在**更新**该画像。因此,你需要传入之前的画像,并请求模型生成一个新的画像(或生成一个用于应用到旧画像的JSON patch)。随着画像变大,这会变得容易出错,可能需要将画像拆分为多个文档,或者在生成文档时进行**严格**解码,以确保记忆的模式保持有效。
集合(Collection)¶
或者,记忆也可以是一组会随着时间持续更新与扩展的文档。每条单独记忆可以限定得更窄、更易生成,这意味着你不太可能随着时间推移**丢失**信息。对于 LLM 来说,为新信息生成_新_对象比将新信息与现有画像进行对账更容易。因此,文档集合往往会带来更高的下游召回率。
不过,这会将部分复杂性转移到记忆更新上。模型现在必须_删除_或_更新_列表中的现有条目,这可能较棘手。此外,有些模型可能倾向于过度插入,而另一些则倾向于过度更新。参见 Trustcall 包以获取一种管理方式,并考虑使用评估(例如借助 LangSmith)来帮助你调优行为。
处理文档集合也会将复杂性转移到对列表进行记忆**搜索**上。当前的 Store
同时支持语义搜索和按内容过滤。
最后,使用记忆集合可能会使向模型提供全面上下文变得具有挑战性。尽管单条记忆可能遵循特定架构,但这种结构未必能捕获记忆之间的完整上下文或关系。因此,在用这些记忆生成响应时,模型可能缺少一些在统一画像方法中更容易获得的重要上下文信息。
无论采用哪种记忆管理方法,核心点在于代理会使用语义记忆来支撑其回答,这通常会带来更个性化、更相关的交互。
情景记忆¶
情景记忆在人与 AI 代理中都涉及回忆过去的事件或行为。CoALA 论文对此有很好的阐述:事实可以写入语义记忆,而*经历*可以写入情景记忆。对 AI 代理而言,情景记忆常用于帮助代理记住如何完成任务。
在实践中,情景记忆常通过少样本示例提示来实现,代理从过往序列中学习以正确执行任务。有时“展示”比“说明”更容易,而 LLM 擅长从示例中学习。少样本学习允许你通过在提示中加入输入-输出示例来“编程”LLM,以阐明预期行为。尽管可以使用各种最佳实践来生成少样本示例,通常的挑战在于如何根据用户输入选择最相关的示例。
请注意,记忆存储只是将数据存成少样本示例的一种方式。如果你希望有更多开发者参与,或将少样本与评估工具链更紧密地结合,也可以使用 LangSmith Dataset 来保存数据。然后可以直接使用动态少样本示例选择器实现相同目标。LangSmith 会为你索引该数据集,并基于关键字相似度(使用类似 BM25 的算法)来检索与用户输入最相关的少样本示例。
参见该操作视频教程,了解在 LangSmith 中进行动态少样本示例选择的用法。另请参阅这篇博客文章,展示了通过少样本提示提升工具调用性能,以及这篇博客文章,展示了使用少样本示例将 LLM 的偏好对齐至人类偏好。
程序性记忆¶
程序性记忆在人与 AI 代理中都涉及记住执行任务所用的规则。对于人类,程序性记忆类似内化的任务执行知识,例如通过基本的运动技能和平衡来骑自行车。相比之下,情景记忆涉及回忆具体经历,例如你第一次不带辅助轮成功骑车,或一次难忘的风景骑行。对于 AI 代理而言,程序性记忆是模型权重、代理代码和代理提示词的组合,它们共同决定了代理的功能。
在实践中,代理修改模型权重或重写代码的情况相对少见。不过,代理修改自身提示词则更常见。
一种有效细化代理指令的方法是通过“反思”或元提示。这涉及用当前指令(例如 system 提示词)加上最近的对话或明确的用户反馈来提示代理,然后代理基于这些输入来细化自身的指令。此方法对那些难以事先明确说明指令的任务尤其有用,因为它允许代理从交互中学习并自适应。
例如,我们构建了一个推文生成器,通过外部反馈与提示重写,为 Twitter 生成高质量的论文摘要。在这种情况下,具体的摘要提示很难_先验_地明确,但用户很容易批判生成的推文,并提供如何改进摘要过程的反馈。
下面的伪代码展示了如何使用 LangGraph 记忆存储来实现这一点:使用存储保存提示词;update_instructions
节点获取当前提示词(以及保存在 state["messages"]
中的与用户对话的反馈),更新提示词,然后将新提示词保存回存储;随后,call_model
从存储中获取更新后的提示词并据此生成响应。
# 使用这些指令的节点
def call_model(state: State, store: BaseStore):
namespace = ("agent_instructions", )
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",)
current_instructions = store.search(namespace)[0]
# 记忆逻辑
prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
output = llm.invoke(prompt)
new_instructions = output['new_instructions']
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
...
写入记忆¶
在热路径上¶
在运行时创建记忆既有优势也有挑战。优势在于这种方式允许实时更新,使新记忆可以立即用于后续交互;同时也带来透明性,因为可以在创建并存储记忆时通知用户。
然而,这种方法也带来挑战。如果代理需要一个用于决定是否提交记忆的新工具,会增加复杂度。此外,思考要保存什么内容的过程会影响代理时延。最后,代理必须在创建记忆与其其他职责之间进行多任务处理,可能影响记忆创建的数量与质量。
例如,ChatGPT 使用一个 save_memories 工具将记忆作为文本进行 upsert,并在处理每条用户消息时决定是否以及如何使用该工具。参见我们的 memory-agent 模板作为参考实现。
在后台¶
将创建记忆作为单独的后台任务有若干优势。它不会给主应用带来时延,将应用逻辑与记忆管理分离,并允许代理更专注地完成任务。这种方式还可以灵活安排记忆创建时机,以避免冗余工作。
不过,这种方法也有挑战。必须确定写入记忆的频率;如果更新不够频繁,其他线程可能得不到新上下文。同时也需要决定何时触发记忆形成。常见策略包括按固定时间调度(若有新事件则重新调度)、按 cron 调度,或允许用户或应用逻辑手动触发。
参见我们的 memory-service 模板作为参考实现。
记忆存储¶
LangGraph 将长期记忆作为 JSON 文档保存在一个存储中。每条记忆组织在自定义的 namespace
(类似文件夹)下,并具有唯一的 key
(类似文件名)。命名空间通常包含用户或组织 ID 或其他便于组织的信息的标签。该结构支持对记忆进行分层组织。随后可通过内容过滤跨命名空间搜索。
from langgraph.store.memory import InMemoryStore
def embed(texts: list[str]) -> list[list[float]]:
# 请用实际的嵌入函数或 LangChain 的 embeddings 对象替换
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)
store.put(
namespace,
"a-memory",
{
"rules": [
"User likes short, direct language",
"User only speaks English & python",
],
"my-key": "my-value",
},
)
# 通过 ID 获取“记忆”
item = store.get(namespace, "a-memory")
# 在此命名空间内搜索“记忆”,按内容等值过滤,并按向量相似度排序
items = store.search(
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
有关记忆存储的更多信息,请参阅持久化指南。