LangGraph 入门教程3:为你的 AI 应用添加记忆功能

本文是 LangGraph 官方入门教程的第三部分,详细介绍了如何通过持久化检查点(checkpointing)机制,为你的 AI 应用(如 Chatbot)添加记忆功能,从而实现连贯的多轮对话。

阅读时长: 5 分钟
共 2164字
作者: eimoon.com

在之前的教程中,我们的 Chatbot 已经学会了如何使用工具 (Tools)来回答用户的问题。然而,它仍然缺少一个关键能力:记忆。它无法记住之前的交互内容,这使得它难以进行连贯、有上下文的多轮对话。

LangGraph 通过其持久化检查点 (persistent checkpointing) 机制优雅地解决了这个问题。核心思想是:在编译图 (Graph) 时提供一个 checkpointer,并在调用图时指定一个 thread_id。这样,LangGraph 会在每个步骤后自动保存当前的状态 (State)。当用户使用相同的 thread_id 再次调用图时,LangGraph 会加载已保存的状态,从而让 Chatbot 能够“记起”之前的对话,无缝衔接。

稍后你会发现,检查点 (checkpointing) 的功能远比简单的聊天记忆强大。它允许你随时保存和恢复复杂的图状态,这为错误恢复、人机协作 (human-in-the-loop) 工作流、时间旅行 (time travel) 交互等高级应用场景打开了大门。不过,让我们先从基础开始,为我们的 Chatbot 添加多轮对话能力。

注意:本教程基于《为 LangGraph 应用添加工具》一文的代码。

步骤一:创建 Checkpointer

首先,我们需要创建一个检查点工具。LangGraph 提供了多种 checkpointer 实现,这里我们使用 InMemorySaver,它将所有状态保存在内存中,非常适合教学和快速原型验证。

from langgraph.checkpoint.memory import InMemorySaver

memory = InMemorySaver()

InMemorySaver 是一个内存中的检查点程序,对于本教程来说非常方便。但在生产环境中,你可能需要将其替换为 SqliteSaverPostgresSaver,并将状态持久化到数据库中。

步骤二:编译 Graph

接下来,在编译图时,将我们刚刚创建的 checkpointer 传入。这样,图在执行每个节点后,都会自动将当前 State 的快照进行保存。

graph = graph_builder.compile(checkpointer=memory)

步骤三:与 Chatbot 交互

现在,激动人心的时刻到了!我们可以开始与具备记忆功能的 Chatbot 进行对话了。

1. 指定对话线程

为本次对话选择一个唯一的标识符,我们称之为 thread_id。这个 ID 将作为保存和加载对话状态的钥匙。

config = {"configurable": {"thread_id": "1"}}

2. 发起第一次对话

调用你的 Chatbot,并传入配置。

重要提示config 参数是 stream()invoke() 方法的第二个位置参数,它不应嵌套在图的输入数据(即 {"messages": [...]})中。

user_input = "你好!我叫 Will。"

# config 是 stream() 或 invoke() 的第二个位置参数
events = graph.stream(
    {"messages": [{"role": "user", "content": user_input}]},
    config,
    stream_mode="values",
)
for event in events:
    event["messages"][-1].pretty_print()

输出结果如下:

================================ Human Message =================================

你好!我叫 Will。
================================== Ai Message ==================================

你好,Will!很高兴认识你。今天有什么可以帮你的吗?有什么具体想知道或讨论的吗?

步骤四:提出后续问题

现在,让我们问一个需要联系上下文的问题,看看 Chatbot 是否能记住我们刚才告诉它的信息。

user_input = "还记得我的名字吗?"

# 同样使用包含相同 thread_id 的 config
events = graph.stream(
    {"messages": [{"role": "user", "content": user_input}]},
    config,
    stream_mode="values",
)
for event in events:
    event["messages"][-1].pretty_print()

Chatbot 的回答:

================================ Human Message =================================

还记得我的名字吗?
================================== Ai Message ==================================

当然,我记得你的名字是 Will。我总是努力记住用户与我分享的重要细节。还有其他你想聊的话题或问题吗?我在这里可以帮助你处理各种主题或任务。

看到了吗?我们没有在代码中维护任何外部的记忆列表,所有的对话历史都由 checkpointer 自动处理了!你可以通过这个 LangSmith 链路 查看完整的执行过程,了解其背后原理。

为了进一步验证,让我们尝试使用一个不同thread_id 来问同样的问题。

# 唯一的区别是我们将 thread_id 从 "1" 改为了 "2"
events = graph.stream(
    {"messages": [{"role": "user", "content": user_input}]},
    {"configurable": {"thread_id": "2"}},
    stream_mode="values",
)
for event in events:
    event["messages"][-1].pretty_print()

这次,Chatbot 的回答截然不同:

================================ Human Message =================================

还记得我的名字吗?
================================== Ai Message ==================================

抱歉,我没有任何关于你名字的先前上下文或记忆。作为一名 AI 助手,我不会保留过去对话中的信息。每次互动都是全新的开始。可以告诉我你的名字吗,以便我能在这次对话中正确地称呼你?

正如预期,由于使用了新的 thread_idcheckpointer 找不到对应的历史状态,因此 Chatbot 开始了一段全新的对话。你可以对比这次调用的 LangSmith 链路

步骤五:检查图的状态

到目前为止,我们已经在两个不同的线程上创建了几个检查点。那么,一个检查点里到底包含了什么呢?我们可以随时通过 get_state(config) 方法来检查给定 config (即特定 thread_id) 下图的当前状态。

snapshot = graph.get_state(config)
snapshot

输出如下:

StateSnapshot(values={'messages': [HumanMessage(content='你好!我叫 Will.', ...), AIMessage(content="你好,Will!很高兴认识你...", ...), HumanMessage(content='还记得我的名字吗?', ...), AIMessage(content="当然,我记得你的名字是 Will...", ...)]}, next=(), config={'configurable': {'thread_id': '1', ...}}, ...)

我们可以看到,StateSnapshot 对象包含了:

  • values: 当前的状态值,这里是包含了所有对话历史的 messages 列表。
  • config: 与此状态对应的配置,包括 thread_id
  • next: 下一个要执行的节点。由于我们的对话在本轮已经结束(走到了 END 节点),所以 next 是空的。

恭喜!借助 LangGraph 的检查点系统,你的 Chatbot 现在可以在不同会话间保持对话状态了。这为构建更自然、更具上下文感知能力的交互应用开启了无限可能。更重要的是,LangGraph 的检查点机制能够处理任意复杂的图状态,远比传统的简单聊天记忆列表更为强大和灵活。

下面是本教程中完整的代码,以供回顾:

from typing import Annotated
from typing_extensions import TypedDict

from langchain_core.messages import BaseMessage
from langchain.chat_models import init_chat_model
from langchain_tavily import TavilySearch

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver

# 1. 定义状态
class State(TypedDict):
    messages: Annotated[list, add_messages]

# 2. 初始化模型和工具
# 假设 llm 已通过 init_chat_model("openai:gpt-4o") 等方式初始化
llm = init_chat_model("openai:gpt-4o") # 替换为你选择的模型
tool = TavilySearch(max_results=2)
tools = [tool]
llm_with_tools = llm.bind_tools(tools)

# 3. 定义图的节点
def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

tool_node = ToolNode(tools=[tool])

# 4. 构建图
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.set_entry_point("chatbot")

# 5. 添加 Checkpointer 并编译
memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)

下一步

在下一篇教程中,我们将为 Chatbot 添加“人机协作” (Human-in-the-loop) 机制,以处理那些需要人工指导或验证才能继续执行的复杂情况。

关于

关注我获取更多资讯

公众号
📢 公众号
个人号
💬 个人号
使用 Hugo 构建
主题 StackJimmy 设计