内存¶
什么是内存?¶
内存 是一种认知功能,使人们能够存储、检索和使用信息,以理解他们的现在和未来。想象一下,与一位同事一起工作,他总是忘记你告诉他的一切,需要不断重复,这是多么令人沮丧!随着 AI Agent 承担越来越复杂的任务,涉及大量的用户交互,为它们配备内存对于效率和用户满意度变得同样至关重要。有了内存,Agent 可以从反馈中学习并适应用户的偏好。本指南涵盖了基于回忆范围的两种类型的内存
短期记忆,或 线程 作用域内存,可以在任何时候从与用户的单个会话线程内部被回忆。LangGraph 将短期记忆作为您的 Agent 状态 的一部分进行管理。状态使用 检查点 持久化到数据库,以便可以随时恢复线程。短期记忆在调用图或完成步骤时更新,并且在每个步骤开始时读取状态。
长期记忆 在会话线程之间共享。它可以在任何时间和在任何线程中被回忆。记忆作用于任何自定义命名空间,而不仅仅是在单个线程 ID 内。LangGraph 提供了 存储 (参考文档),让您可以保存和回忆长期记忆。
两者对于理解和在您的应用程序中实现都很重要。
短期记忆¶
短期记忆让您的应用程序记住在单个 线程 或对话中之前的交互。一个 线程 在一个会话中组织多个交互,类似于电子邮件在一个对话中将消息分组的方式。
LangGraph 将短期记忆作为 Agent 状态的一部分进行管理,通过线程作用域的检查点进行持久化。此状态通常可以包括对话历史以及其他有状态数据,例如上传的文件、检索到的文档或生成的工件。通过将这些存储在图的状态中,机器人可以访问给定对话的完整上下文,同时保持不同线程之间的分离。
由于对话历史是表示短期记忆的最常见形式,在下一节中,我们将介绍当消息列表变得很长时管理对话历史的技术。如果您想坚持高层概念,请继续阅读 长期记忆 部分。
管理长对话历史¶
长对话对今天的 LLM 提出了挑战。完整的历史记录甚至可能无法容纳在 LLM 的上下文窗口中,从而导致不可恢复的错误。即使如果您的 LLM 在技术上支持完整的上下文长度,大多数 LLM 在长上下文中仍然表现不佳。它们会被陈旧或离题的内容“分散注意力”,同时还要承受更慢的响应时间和更高的成本。
管理短期记忆是在 精确率和召回率 与您的应用程序的其他性能要求(延迟和成本)之间取得平衡的练习。与往常一样,批判性地思考如何为您的 LLM 表示信息并查看您的数据非常重要。我们在下面介绍了一些管理消息列表的常用技术,并希望为您提供足够的背景信息,以便您为您的应用程序选择最佳的权衡方案
编辑消息列表¶
聊天模型使用 消息 接受上下文,其中包括开发者提供的指令(系统消息)和用户输入(人类消息)。在聊天应用程序中,消息在人类输入和模型响应之间交替,从而产生一个随着时间推移而变长的消息列表。由于上下文窗口是有限的,并且富含 token 的消息列表可能很昂贵,因此许多应用程序可以从使用技术手动删除或遗忘陈旧信息中获益。
最直接的方法是从列表中删除旧消息(类似于 最近最少使用缓存)。
在 LangGraph 中从列表中删除内容的典型技术是从节点返回一个更新,告诉系统删除列表的某些部分。您可以定义此更新的外观,但一种常见的方法是让您返回一个对象或字典,指定要保留哪些值。
def manage_list(existing: list, updates: Union[list, dict]):
if isinstance(updates, list):
# Normal case, add to the history
return existing + updates
elif isinstance(updates, dict) and updates["type"] == "keep":
# You get to decide what this looks like.
# For example, you could simplify and just accept a string "DELETE"
# and clear the entire list.
return existing[updates["from"]:updates["to"]]
# etc. We define how to interpret updates
class State(TypedDict):
my_list: Annotated[list, manage_list]
def my_node(state: State):
return {
# We return an update for the field "my_list" saying to
# keep only values from index -5 to the end (deleting the rest)
"my_list": {"type": "keep", "from": -5, "to": None}
}
当在键 "my_list" 下返回更新时,LangGraph 将调用 manage_list
"reducer" 函数。在该函数中,我们定义要接受的更新类型。通常,消息将添加到现有列表(对话将增长);但是,我们还添加了支持以接受字典,让您可以“保留”状态的某些部分。这使您可以编程方式删除旧消息上下文。
另一种常见的方法是让您返回一个 “remove” 对象列表,指定要删除的所有消息的 ID。如果您在 LangGraph 中使用 LangChain 消息和 add_messages
reducer(或使用相同底层功能的 MessagesState
),您可以使用 RemoveMessage
来完成此操作。
from langchain_core.messages import RemoveMessage, AIMessage
from langgraph.graph import add_messages
# ... other imports
class State(TypedDict):
# add_messages will default to upserting messages by ID to the existing list
# if a RemoveMessage is returned, it will delete the message in the list by ID
messages: Annotated[list, add_messages]
def my_node_1(state: State):
# Add an AI message to the `messages` list in the state
return {"messages": [AIMessage(content="Hi")]}
def my_node_2(state: State):
# Delete all but the last 2 messages from the `messages` list in the state
delete_messages = [RemoveMessage(id=m.id) for m in state['messages'][:-2]]
return {"messages": delete_messages}
API 参考:RemoveMessage | AIMessage | add_messages
在上面的示例中,add_messages
reducer 允许我们将新消息 附加 到 messages
状态键,如 my_node_1
中所示。当它看到 RemoveMessage
时,它将从列表中删除具有该 ID 的消息(然后 RemoveMessage 将被丢弃)。有关 LangChain 特定的消息处理的更多信息,请查看 关于使用 RemoveMessage
的操作指南。
请参阅此操作指南 指南 以及我们的 LangChain Academy 课程的模块 2,以获取示例用法。
总结过去的对话¶
如上所示,修剪或删除消息的问题是,我们可能会从消息队列的剔除中丢失信息。因此,一些应用程序受益于使用聊天模型总结消息历史的更复杂的方法。
可以使用简单的提示和编排逻辑来实现这一点。例如,在 LangGraph 中,我们可以扩展 MessagesState 以包含 summary
键。
然后,我们可以生成聊天历史的摘要,使用任何现有的摘要作为下一个摘要的上下文。在 messages
状态键中累积一定数量的消息后,可以调用此 summarize_conversation
节点。
def summarize_conversation(state: State):
# First, we get any existing summary
summary = state.get("summary", "")
# Create our summarization prompt
if summary:
# A summary already exists
summary_message = (
f"This is a summary of the conversation to date: {summary}\n\n"
"Extend the summary by taking into account the new messages above:"
)
else:
summary_message = "Create a summary of the conversation above:"
# Add prompt to our history
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = model.invoke(messages)
# Delete all but the 2 most recent messages
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": response.content, "messages": delete_messages}
请参阅此操作指南 此处 以及我们的 LangChain Academy 课程的模块 2,以获取示例用法。
知道何时删除消息¶
大多数 LLM 都有最大支持的上下文窗口(以 token 为单位)。决定何时截断消息的一个简单方法是计算消息历史记录中的 token,并在其接近该限制时截断。朴素的截断很容易自行实现,但有一些“陷阱”。一些模型 API 进一步限制了消息类型的顺序(必须以人类消息开始,不能有相同类型的连续消息等)。如果您正在使用 LangChain,您可以使用 trim_messages
实用程序,并指定要从列表中保留的 token 数量,以及用于处理边界的 strategy
(例如,保留最后的 max_tokens
)。
下面是一个例子。
from langchain_core.messages import trim_messages
trim_messages(
messages,
# Keep the last <= n_count tokens of the messages.
strategy="last",
# Remember to adjust based on your model
# or else pass a custom token_encoder
token_counter=ChatOpenAI(model="gpt-4"),
# Remember to adjust based on the desired conversation
# length
max_tokens=45,
# Most chat models expect that chat history starts with either:
# (1) a HumanMessage or
# (2) a SystemMessage followed by a HumanMessage
start_on="human",
# Most chat models expect that chat history ends with either:
# (1) a HumanMessage or
# (2) a ToolMessage
end_on=("human", "tool"),
# Usually, we want to keep the SystemMessage
# if it's present in the original history.
# The SystemMessage has special instructions for the model.
include_system=True,
)
API 参考:trim_messages
长期记忆¶
LangGraph 中的长期记忆允许系统在不同的对话或会话中保留信息。与线程作用域的短期记忆不同,长期记忆保存在自定义的“命名空间”中。
存储记忆¶
LangGraph 将长期记忆作为 JSON 文档存储在 存储 (参考文档) 中。每个记忆都组织在自定义的 namespace
(类似于文件夹)和不同的 key
(如文件名)下。命名空间通常包括用户或组织 ID 或其他标签,这使得组织信息更容易。这种结构实现了记忆的分层组织。然后通过内容过滤器支持跨命名空间搜索。请参阅下面的示例。
from langgraph.store.memory import InMemoryStore
def embed(texts: list[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] * len(texts)]
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
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",
},
)
# get the "memory" by ID
item = store.get(namespace, "a-memory")
# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
items = store.search(
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
长期记忆的思考框架¶
长期记忆是一个复杂的挑战,没有一劳永逸的解决方案。但是,以下问题提供了一个结构框架,以帮助您了解不同的技术
记忆的类型是什么?
人类使用记忆来记住 事实、经验 和 规则。AI Agent 可以以相同的方式使用记忆。例如,AI Agent 可以使用记忆来记住关于用户的特定事实以完成任务。我们在下面的 章节 中扩展了几种类型的记忆。
您何时想要更新记忆?
记忆可以作为 Agent 应用程序逻辑的一部分(例如,“在热路径上”)进行更新。在这种情况下,Agent 通常会在响应用户之前决定记住事实。或者,记忆可以作为后台任务(在后台/异步运行并生成记忆的逻辑)进行更新。我们在下面的 章节 中解释了这些方法之间的权衡。
记忆类型¶
不同的应用程序需要各种类型的记忆。虽然类比并不完美,但检查 人类记忆类型 可能很有启发性。一些研究(例如,CoALA 论文)甚至将这些人类记忆类型映射到 AI Agent 中使用的记忆类型。
记忆类型 | 存储内容 | 人类示例 | Agent 示例 |
---|---|---|---|
语义 | 事实 | 我在学校学到的东西 | 关于用户的事实 |
情景 | 经验 | 我做过的事情 | 过去的 Agent 动作 |
程序性 | 指令 | 本能或运动技能 | Agent 系统提示 |
语义记忆¶
语义记忆,在人类和 AI Agent 中,都涉及保留特定的事实和概念。在人类中,它可以包括在学校学到的信息以及对概念及其关系的理解。对于 AI Agent,语义记忆通常用于通过记住过去交互中的事实或概念来个性化应用程序。
注意:不要与“语义搜索”混淆,后者是一种使用“含义”(通常作为嵌入)查找相似内容的技术。语义记忆是心理学中的一个术语,指的是存储事实和知识,而语义搜索是一种基于含义而不是精确匹配来检索信息的方法。
配置文件¶
语义记忆可以通过不同的方式进行管理。例如,记忆可以是关于用户、组织或其他实体(包括 Agent 自身)的范围明确且特定的信息的单个、持续更新的“配置文件”。配置文件通常只是一个 JSON 文档,其中包含您选择的用于表示您的领域的各种键值对。
当记住配置文件时,您需要确保每次都更新配置文件。因此,您将需要传入之前的配置文件,并 要求模型生成新的配置文件(或一些 JSON 补丁 以应用于旧配置文件)。随着配置文件变得更大,这可能会变得容易出错,并且可能受益于将配置文件拆分为多个文档或在生成文档时进行 严格 解码,以确保记忆模式保持有效。
集合¶
或者,记忆可以是文档的集合,这些文档会随着时间的推移不断更新和扩展。每个单独的记忆可以范围更窄,更易于生成,这意味着您不太可能随着时间的推移丢失信息。对于 LLM 来说,为新信息生成新对象比将新信息与现有配置文件协调更容易。因此,文档集合往往会导致 下游更高的召回率。
但是,这转移了一些复杂性到记忆更新。模型现在必须删除或更新列表中的现有项目,这可能很棘手。此外,一些模型可能默认过度插入,而另一些模型可能默认过度更新。请参阅 Trustcall 包,了解管理此问题的一种方法,并考虑评估(例如,使用像 LangSmith 这样的工具)来帮助您调整行为。
使用文档集合也将复杂性转移到列表上的记忆搜索。Store
目前同时支持 语义搜索 和 按内容过滤。
最后,使用记忆集合可能会使向模型提供全面的上下文变得具有挑战性。虽然单个记忆可能遵循特定的模式,但这种结构可能无法捕获记忆之间的完整上下文或关系。因此,当使用这些记忆来生成响应时,模型可能缺乏重要的上下文信息,而在统一的配置文件方法中,这些信息将更容易获得。
无论采用哪种记忆管理方法,中心点是 Agent 将使用语义记忆来 支持其响应,这通常会导致更个性化和相关的交互。
情景记忆¶
情景记忆,在人类和 AI Agent 中,都涉及回忆过去的事件或动作。CoALA 论文 对此进行了很好的框架:事实可以写入语义记忆,而经验可以写入情景记忆。对于 AI Agent,情景记忆通常用于帮助 Agent 记住如何完成任务。
在实践中,情景记忆通常通过 少样本示例提示 来实现,其中 Agent 从过去的序列中学习以正确执行任务。有时“展示”比“讲述”更容易,LLM 从示例中学习效果很好。少样本学习使您可以通过使用输入-输出示例更新提示来 “编程” 您的 LLM,以说明预期的行为。虽然可以使用各种 最佳实践 来生成少样本示例,但挑战通常在于根据用户输入选择最相关的示例。
请注意,记忆 存储 只是将数据存储为少样本示例的一种方式。如果您想让开发者更多地参与,或者将少样本更紧密地与您的评估工具联系起来,您还可以使用 LangSmith Dataset 来存储您的数据。然后,动态少样本示例选择器可以开箱即用地用于实现相同的目标。LangSmith 将为您索引数据集,并根据关键词相似度(使用类似 BM25 的算法 进行基于关键词的相似度计算)检索与用户输入最相关的少样本示例。
请参阅此操作指南 视频,了解 LangSmith 中动态少样本示例选择的示例用法。另请参阅此 博客文章,展示了使用少样本提示来提高工具调用性能,以及此 博客文章,使用少样本示例来使 LLM 与人类偏好对齐。
程序性记忆¶
程序性记忆,在人类和 AI Agent 中,都涉及记住用于执行任务的规则。在人类中,程序性记忆就像关于如何执行任务的内在知识,例如通过基本的运动技能和平衡来骑自行车。另一方面,情景记忆涉及回忆特定的经验,例如第一次成功地在没有辅助轮的情况下骑自行车,或者沿着风景优美的路线进行一次难忘的自行车骑行。对于 AI Agent,程序性记忆是模型权重、Agent 代码和 Agent 提示的组合,这些共同决定了 Agent 的功能。
在实践中,Agent 修改其模型权重或重写其代码的情况相当少见。然而,Agent 修改自己的提示 更为常见。
改进 Agent 指令的一种有效方法是通过 “反思” 或元提示。这涉及使用 Agent 当前的指令(例如,系统提示)以及最近的对话或明确的用户反馈来提示 Agent。然后,Agent 根据此输入改进其自身的指令。这种方法对于预先指定指令具有挑战性的任务尤其有用,因为它允许 Agent 从其交互中学习和适应。
例如,我们构建了一个 Tweet 生成器,使用外部反馈和提示重写来为 Twitter 生成高质量的论文摘要。在这种情况下,特定的摘要提示很难先验指定,但用户很容易批评生成的 Tweet,并提供关于如何改进摘要过程的反馈。
下面的伪代码展示了如何使用 LangGraph 内存 存储 来实现这一点,使用存储来保存提示,update_instructions
节点来获取当前提示(以及从用户对话中捕获的反馈,捕获在 state["messages"]
中),更新提示,并将新提示保存回存储。然后,call_model
从存储中获取更新后的提示,并使用它来生成响应。
# Node that *uses* the instructions
def call_model(state: State, store: BaseStore):
namespace = ("agent_instructions", )
instructions = store.get(namespace, key="agent_a")[0]
# Application logic
prompt = prompt_template.format(instructions=instructions.value["instructions"])
...
# Node that updates instructions
def update_instructions(state: State, store: BaseStore):
namespace = ("instructions",)
current_instructions = store.search(namespace)[0]
# Memory logic
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})
...
写入记忆¶
虽然 人类通常在睡眠期间形成长期记忆,但 AI Agent 需要不同的方法。Agent 应该在何时以及如何创建新的记忆?Agent 写入记忆至少有两种主要方法:“在热路径上”和“在后台”。
在热路径中写入记忆¶
在运行时创建记忆既有优点也有挑战。从积极的方面来看,这种方法允许实时更新,使新记忆立即可用于后续交互。它还实现了透明性,因为当记忆被创建和存储时,用户可以收到通知。
然而,这种方法也带来了挑战。如果 Agent 需要一个新工具来决定要提交到记忆中的内容,则可能会增加复杂性。此外,推理要保存到记忆中的内容的过程可能会影响 Agent 的延迟。最后,Agent 必须在记忆创建和其他职责之间进行多任务处理,这可能会影响创建的记忆的数量和质量。
例如,ChatGPT 使用 save_memories 工具将记忆作为内容字符串进行 upsert,决定是否以及如何将此工具与每条用户消息一起使用。请参阅我们的 memory-agent 模板作为参考实现。
在后台写入记忆¶
将记忆创建为单独的后台任务具有多个优点。它消除了主应用程序中的延迟,将应用程序逻辑与记忆管理分离,并允许 Agent 更专注于任务完成。这种方法还提供了在时间记忆创建方面的灵活性,以避免冗余工作。
然而,这种方法也有其自身的挑战。确定记忆写入的频率变得至关重要,因为不频繁的更新可能会使其他线程没有新的上下文。决定何时触发记忆形成也很重要。常见的策略包括在设定的时间段后进行调度(如果发生新事件则重新调度)、使用 cron 调度或允许用户或应用程序逻辑手动触发。
请参阅我们的 memory-service 模板作为参考实现。