跳到内容

内存

什么是内存?

在大型语言模型和 AI 应用的语境下,内存指的是处理、保留和利用来自过去交互或数据源的信息的能力。例如:

  • 管理发送到聊天模型的消息(例如,来自长消息历史记录),以限制令牌使用量
  • 总结过去的对话,以提供来自先前交互的上下文
  • 选择少样本示例(例如,来自数据集),以指导模型响应
  • 在多个聊天会话中维护持久数据(例如,用户偏好)
  • 允许大型语言模型使用过去的信息更新其提示(例如,元提示)
  • 从长期存储系统中检索与对话或问题相关的的信息

下面,我们将详细讨论这些示例。

管理消息

编辑消息列表

聊天模型通过 消息 接受指令,这些指令可以是通用指令(例如,系统消息)或用户提供的指令(例如,人机消息)。在聊天应用中,消息通常在人类输入和模型响应之间交替出现,并随着时间的推移积累成一个列表。由于上下文窗口有限,并且令牌丰富的消息列表可能很昂贵,因此许多应用可以从主动管理消息的方法中受益。

最直接的方法是从列表中删除特定消息。可以使用 RemoveMessage 根据消息 id(每个消息的唯一标识符)来完成此操作。在下面的示例中,我们只保留列表中的最后两条消息,使用 RemoveMessage 根据它们的 id 删除旧的消息。

from langchain_core.messages import RemoveMessage

# Message list
messages = [AIMessage("Hi.", name="Bot", id="1")]
messages.append(HumanMessage("Hi.", name="Lance", id="2"))
messages.append(AIMessage("So you said you were researching ocean mammals?", name="Bot", id="3"))
messages.append(HumanMessage("Yes, I know about whales. But what others should I learn about?", name="Lance", id="4"))

# Isolate messages to delete
delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]]
print(delete_messages)
[RemoveMessage(content='', id='1'), RemoveMessage(content='', id='2')]

由于聊天模型的上下文窗口以令牌表示,因此根据我们要保留的令牌数量来修剪消息列表可能很有用。为此,我们可以使用 trim_messages 并指定要从列表中保留的令牌数量,以及 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-4o"),
    # Most chat models expect that chat history starts with either:
    # (1) a HumanMessage or
    # (2) a SystemMessage followed by a HumanMessage
    # 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,
)

与 LangGraph 的使用

在 LangGraph 中构建代理时,我们通常希望管理图状态中的消息列表。由于这是一种非常常见的用例,MessagesState 是一个内置的 LangGraph 状态模式,它包含一个 messages 键,该键是一个消息列表。MessagesState 还包含一个 add_messages 归约器,用于在应用程序运行时更新消息列表。add_messages 归约器允许我们 追加 新的消息到 messages 状态键中,如下所示。当我们使用从 my_node 返回的 {"messages": new_message} 执行状态更新时,add_messages 归约器会将 new_message 追加到现有的消息列表中。

def my_node(state: State):
    # Add a new message to the state
    new_message = HumanMessage(content="message")
    return {"messages": new_message}

add_messages 归约器内置在 MessagesState 中,也可以与我们上面讨论的 RemoveMessage 实用程序一起使用。在这种情况下,我们可以使用 delete_messages 列表执行状态更新,以从 messages 列表中删除特定消息。

def my_node(state: State):
    # Delete messages from state
    delete_messages = [RemoveMessage(id=m.id) for m in state['messages'][:-2]]
    return {"messages": delete_messages}

请参阅此操作指南 指南 和我们 LangChain Academy 课程中的模块 2,了解示例用法。

总结过去的对话

上面显示的修剪或删除消息的问题在于,我们可能会因剔除消息队列而丢失信息。因此,一些应用受益于更复杂的方法,即使用聊天模型对消息历史记录进行总结。

简单的提示和编排逻辑可以用来实现这一点。例如,在 LangGraph 中,我们可以扩展 MessagesState 以包含 summary 键。

from langgraph.graph import MessagesState
class State(MessagesState):
    summary: str

然后,我们可以使用任何现有的摘要作为下一个摘要的上下文,生成聊天历史记录的摘要。这个 summarize_conversation 节点可以在 messages 状态键中积累一定数量的消息后调用。

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 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,了解示例用法。

少样本示例

少样本学习是一种强大的技术,大型语言模型可以通过在提示中 "编程" 输入-输出示例来执行各种任务。虽然各种 最佳实践 可用于生成少样本示例,但挑战通常在于根据用户输入选择最相关的示例。

LangChain ExampleSelectors 可用于使用诸如长度、语义相似性、语义 n 元组重叠或最大边际相关性等标准来自动定制少样本示例选择。

如果少样本示例存储在 LangSmith 数据集 中,那么可以使用开箱即用的动态少样本示例选择器来实现相同的目标。LangSmith 会为您索引数据集,并允许您检索与用户输入最相关的少样本示例(使用类似 BM25 的算法 进行基于关键字的相似性)。

请参阅此操作指南 视频,了解 LangSmith 中动态少样本示例选择的示例用法。此外,请参阅此 博客文章,展示了用于提高工具调用性能的少样本提示,以及此 博客文章,它使用少样本示例将大型语言模型与人类偏好相一致。

跨聊天会话维护数据

LangGraph 的 持久性层 具有利用各种存储系统的检查点,包括内存中的键值存储或不同的数据库。这些检查点在每个执行步骤中捕获图状态,并在线程中累积,可以使用线程 ID 在稍后访问,以恢复先前的图执行。我们通过将检查点传递给 compile 方法,将持久性添加到我们的图中,如下所示。

# Compile the graph with a checkpointer
checkpointer = MemorySaver()
graph = workflow.compile(checkpointer=checkpointer)

# Invoke the graph with a thread ID
config = {"configurable": {"thread_id": "1"}}
graph.invoke(input_state, config)

# get the latest state snapshot at a later time
config = {"configurable": {"thread_id": "1"}}
graph.get_state(config)

持久性对于维持长时间运行的聊天会话至关重要。例如,用户与 AI 助理之间的聊天可能会被打断。持久性确保用户可以在任何时候继续该特定聊天会话。但是,如果用户开始与助理进行新的聊天会话会发生什么?这会生成一个新线程,并且先前会话(线程)的信息不会保留。这促使我们需要一种能够跨聊天会话(线程)维护数据的内存服务。

元提示

元提示使用大型语言模型来生成或改进其提示或指令。这种方法允许系统动态更新和改进其行为,从而可能在各种任务上实现更好的性能。这对于指令难以先验指定的任务尤其有用。

元提示可以使用过去的信息来更新提示。例如,这个 推文生成器 使用元提示来迭代地改进用于生成高质量的论文摘要以供 Twitter 使用的摘要提示。在这种情况下,我们使用 LangSmith 数据集来存储我们想要总结的几篇论文,使用简单的摘要提示生成摘要,手动审查摘要,使用 LangSmith 注释队列捕获来自人类审查的反馈,并将此反馈传递给聊天模型以重新生成摘要提示。该过程在循环中重复,直到摘要在人工审查中满足我们的标准。

从长期存储中检索相关信息

跨越许多不同内存用例的中心挑战可以简单地总结为:如何从长期存储系统中检索相关信息并将其传递给聊天模型?例如,假设我们有一个系统,它存储了大量关于用户的信息,但用户提出了一个与餐厅推荐相关的特定问题。如果我们将所有个人用户信息简单地提取出来并传递给聊天模型,那将是成本高昂的。相反,我们希望仅提取与用户当前聊天交互最相关的的信息(例如,食物偏好、位置等),并将其传递给聊天模型。

检索领域有很多工作旨在解决这一挑战。请参阅我们专注于 RAG(检索增强生成) 的教程,我们关于 检索 的概念文档,以及我们关于此主题的 开源存储库 以及 视频

评论