LangGraph 可让您轻松管理图表中的对话记忆。这些操作指南展示了如何为此实施不同的策略。
持久性最常见的用例之一是使用它来跟踪对话历史记录。它使继续对话变得容易。然而,随着对话越来越长,这个对话历史记录会累积起来并占用越来越多的上下文窗口。这通常是不受欢迎的,因为它会导致对 LLM 的调用更昂贵、更长时间,并且可能会出错。为了防止这种情况发生,您可能需要管理对话历史记录。注意:本指南重点介绍如何在 LangGraph 中执行此操作,您可以完全自定义此操作方式。如果您想要更现成的解决方案,可以查看 LangChain message 中提供的对应的功能from typing import Literal
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode
memory = MemorySaver()
@tool
def search(query: str):
"""Call to surf the web."""
return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
tools = [search]
tool_node = ToolNode(tools)
model = ChatAnthropic(model="claude-3-haiku-20240307")
bound_model = model.bind_tools(tools)
def should_continue(state: MessagesState):
last_message = state["messages"][-1]
if not last_message.tool_calls:
return END
return "action"
def call_model(state: MessagesState):
response = bound_model.invoke(state["messages"])
return {"messages": response}
workflow = StateGraph(MessagesState)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
["action", END],
)
workflow.add_edge("action", "agent")
app = workflow.compile(checkpointer=memory)
from langchain_core.messages import HumanMessage
config = {"configurable": {"thread_id": "2"}}
input_message = HumanMessage(content="你好我是桐人")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
event["messages"][-1].pretty_print()
input_message = HumanMessage(content="我叫什么")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
event["messages"][-1].pretty_print()
你好我是桐人
================================== Ai Message ==================================
你好!桐人是一个角色名字吗?来自某个游戏或者动画作品吧?很高兴认识你,有什么我可以帮助你的吗?
================================ Human Message =================================
我叫什么
================================== Ai Message ==================================
您刚才告诉我您的名字是“桐人”。请问您还需要了解或确认其他信息吗?
为防止对话历史记录爆炸,最直接的做法是在将消息列表传递给 LLM 之前对其进行过滤。这涉及两个部分:定义一个函数来过滤消息,然后将其添加到graph中。请参阅下面的示例,其中定义了一个非常简单的filter_messages函数,然后使用它。def filter_messages(messages: list):
# 这是一个非常简单的帮助函数,它只使用最后一条消息
return messages[-1:]
def call_model(state: MessagesState):
# 注意这里:我们在调用模型之前过滤了消息
messages = filter_messages(state["messages"])
response = bound_model.invoke(messages)
return {"messages": response}
你好我是桐人
================================== Ai Message ==================================
你好!桐人这个角色是来自哪部作品呢?如果你有关于他的问题或者想讨论的内容,都可以和我分享哦。
================================ Human Message =================================
我叫什么
================================== Ai Message ==================================
你是Qwen,一个由阿里云开发的助手。但我现在应该称呼你什么呢?请告诉我你的名字吧!
可以看到,因为我们过滤消息的时候只保留了最后一条消息,因此LLM不知道上下文,因此,不知道我叫什么。下面是langchain开箱即用的消息过滤和裁剪链接,读者可以去试一下:https://python.langchain.com/docs/how_to/filter_messages/https://python.langchain.com/docs/how_to/trim_messages/
图表的常见状态之一是消息列表。通常您只向该状态添加消息。但是,有时您可能想要删除消息(通过直接修改状态或作为图表的一部分)。为此,您可以使用修饰符RemoveMessage。关键思想是每个状态键都有一个reducer key。此key指定如何将更新组合到状态。默认MessagesState有一个messages key,该键的 Reducer 接受这些RemoveMessage修饰符。然后,该 Reducer 使用这些修饰符RemoveMessage从键中删除消息。因此请注意,仅仅因为您的图形状态有一个消息列表的键,并不意味着此RemoveMessage修饰符将起作用。您还必须有一个reducer知道如何使用它的定义。注意:许多模型都要求消息列表遵循某些规则。例如,有些模型要求消息以一条user消息开头,而有些模型则要求所有带有工具调用的消息后面都跟一条工具消息。删除消息时,您需要确保不违反这些规则。首先,我们将介绍如何手动删除消息。让我们看一下线程的当前状态:[HumanMessage(content='你好我是桐人', additional_kwargs={}, response_metadata={}, id='7054cbf0-e714-4b4c-b065-6a9eb75b7a2d'),
AIMessage(content='你好!桐人是一个来自轻小说《 Sword Art Online (刀剑神域)》的角色,对吧?你对这部作品或者角色有什么感兴趣的话题或问题想要讨论吗?', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-01-07T03:40:50.799446Z', 'done': True, 'done_reason': 'stop', 'total_duration': 2063392667, 'load_duration': 24295500, 'prompt_eval_count': 155, 'prompt_eval_duration': 247000000, 'eval_count': 40, 'eval_duration': 1788000000, 'message': {'role': 'assistant', 'content': '你好!桐人是一个来自轻小说《 Sword Art Online (刀剑神域)》的角色,对吧?你对这部作品或者角色有什么感兴趣的话题或问题想要讨论吗?', 'images': None, 'tool_calls': None}}, id='run-d68ff793-1a32-49b1-a769-5b6e59c94357-0', usage_metadata={'input_tokens': 155, 'output_tokens': 40, 'total_tokens': 195}),
HumanMessage(content='我叫什么', additional_kwargs={}, response_metadata={}, id='0a6eef9b-ddde-4a4d-90d3-414ddeda2ca9'),
AIMessage(content='您刚才提到的名字是“桐人”。请问您现在想让我称呼您为什么名字呢?如果您有其他偏好,请告诉我。', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-01-07T03:40:52.339773Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1438884083, 'load_duration': 27642042, 'prompt_eval_count': 207, 'prompt_eval_duration': 163000000, 'eval_count': 28, 'eval_duration': 1242000000, 'message': {'role': 'assistant', 'content': '您刚才提到的名字是“桐人”。请问您现在想让我称呼您为什么名字呢?如果您有其他偏好,请告诉我。', 'images': None, 'tool_calls': None}}, id='run-e066f578-da75-4ad2-805e-b6fcbf5a7e2c-0', usage_metadata={'input_tokens': 207, 'output_tokens': 28, 'total_tokens': 235})]
我们可以调用update_state并传入第一条消息的 ID。这将删除该消息。from langchain_core.messages import RemoveMessage
app.update_state(config, {"messages": RemoveMessage(id=messages[0].id)})
messages = app.get_state(config).values["messages"]
print(messages)
[AIMessage(content='你好!桐人是一个来自轻小说《刀剑神域》的角色,对吧?你有什么问题或者想讨论的内容吗?', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-01-07T03:43:39.974483Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1445857708, 'load_duration': 27660333, 'prompt_eval_count': 155, 'prompt_eval_duration': 138000000, 'eval_count': 29, 'eval_duration': 1276000000, 'message': {'role': 'assistant', 'content': '你好!桐人是一个来自轻小说《刀剑神域》的角色,对吧?你有什么问题或者想讨论的内容吗?', 'images': None, 'tool_calls': None}}, id='run-cbbaeb2b-9bcb-43a7-9e02-fa86aea86924-0', usage_metadata={'input_tokens': 155, 'output_tokens': 29, 'total_tokens': 184}),
HumanMessage(content='我叫什么', additional_kwargs={}, response_metadata={}, id='d3f38c66-0c7e-4f8e-97ce-108cc57ea47a'),
AIMessage(content='您刚才提到您的名字是“桐人”。如果您有任何其他问题或需要帮助的事情,请告诉我。', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-01-07T03:43:41.18703Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1142763958, 'load_duration': 12822958, 'prompt_eval_count': 196, 'prompt_eval_duration': 161000000, 'eval_count': 22, 'eval_duration': 964000000, 'message': {'role': 'assistant', 'content': '您刚才提到您的名字是“桐人”。如果您有任何其他问题或需要帮助的事情,请告诉我。', 'images': None, 'tool_calls': None}}, id='run-4b322f02-3689-45a8-92d8-11783e7d7869-0', usage_metadata={'input_tokens': 196, 'output_tokens': 22, 'total_tokens': 218})]
我们还可以从图表内部以编程方式删除消息。在这里,我们将修改图表以在图表运行结束时删除所有旧消息(超过 3 条消息之前的消息)。from langchain_core.messages import RemoveMessage
from langgraph.graph import END
def delete_messages(state):
messages = state["messages"]
if len(messages) > 3:
return {"messages": [RemoveMessage(id=m.id) for m in messages[:-3]]}
# 我们要修改逻辑调用 delete_messages不是立刻end
def should_continue(state: MessagesState) -> Literal["action", "delete_messages"]:
last_message = state["messages"][-1]
if not last_message.tool_calls:
return "delete_messages"
return "action"
# Define a new graph
workflow = StateGraph(MessagesState)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# 新定义的node
workflow.add_node(delete_messages)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
)
workflow.add_edge("action", "agent")
# 这里添加了一条新的边
workflow.add_edge("delete_messages", END)
app = workflow.compile(checkpointer=memory)
相当于在最后END之前调用了删除消息,当然了我们可以自定义其它删除的node和时机。这里结果就不做展示了。
持久性最常见的用例之一是使用它来跟踪对话历史记录。它使继续对话变得容易。然而,随着对话越来越长,这个对话历史记录会累积起来并占用越来越多的上下文窗口。这通常是不受欢迎的,因为它会导致对 LLM 的调用更昂贵、更长时间,并且可能会出错。解决这个问题的一种方法是创建迄今为止的对话摘要,这跟上面的过滤消息和删除消息的思路稍微有所不同。并将其与过去的 N 条消息一起使用。- 这将涉及几个步骤:检查对话是否太长(可以通过检查消息数量或消息长度来完成)
from typing import Literal
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage, RemoveMessage, HumanMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, StateGraph, START, END
memory = MemorySaver()
# 我们将添加一个`summary`属性(除了`messages`键之外,MessagesState已经有了)
class State(MessagesState):
summary: str
model = ChatAnthropic(model_name="claude-3-haiku-20240307")
# 定义调用模型的逻辑
def call_model(state: State):
# 如果存在摘要,我们将其作为系统消息添加进去
summary = state.get("summary", "")
if summary:
system_message = f"Summary of conversation earlier: {summary}"
messages = [SystemMessage(content=system_message)] + state["messages"]
else:
messages = state["messages"]
response = model.invoke(messages)
return {"messages": [response]}
# 我们现在定义确定是否结束或总结对话的逻辑
def should_continue(state: State) -> Literal["summarize_conversation", END]:
"""Return the next node to execute."""
messages = state["messages"]
# 如果消息超过六条,我们就总结对话
if len(messages) > 6:
return "summarize_conversation"
# 否则我们就结束
return END
def summarize_conversation(state: State):
# 首先,我们总结对话
summary = state.get("summary", "")
if summary:
# 如果已经存在摘要,我们使用不同的系统提示来总结它
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:"
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = model.invoke(messages)
# 我们现在需要删除不再想显示的消息
# 我将删除除最后两条消息之外的所有消息,但您可以更改此设置
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": response.content, "messages": delete_messages}
# Define a new graph
workflow = StateGraph(State)
# 定义对话节点和总结节点
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)
# 将入口点设置为对话
workflow.add_edge(START, "conversation")
# We now add a conditional edge
workflow.add_conditional_edges(
# 首先,我们定义开始节点。我们使用`conversation`。
# 这意味着这些是在调用`conversation`节点之后采取的边。
"conversation",
# 接下来,我们传入将确定下一个调用的函数。
should_continue,
)
# 我们现在从`summarize_conversation`到END添加了一个普通边。
# 这意味着在调用`summarize_conversation`之后,我们直接END
workflow.add_edge("summarize_conversation", END)
app = workflow.compile(checkpointer=memory)
这个代码比较清晰,这里不再实际运行,你可以动手试试!
另外跨thread的存储,我们在之前的持久化的部分已经讲过了,这里就不重复了,基本上就是需要添加一个store。如果想执行memory的语义搜索也可以使用相关的embeddings 和store实现,同样可以参考之前的持久化部分。