LangGraph 系列 · 第 2 讲:理解记忆机制,构建有状态的智能体

96 阅读16分钟

本文带你从 0 到 1 搞懂 LangGraph 记忆机制:线程内短期记忆、跨会话长期记忆,到接入 Redis 做真正持久化,一套完整可运行示例直接上手,把“健忘聊天机器人”升级为有记忆的智能体。

1. 导读

本文是 LangGraph 系列的第 2 讲,承接第 1 讲的"图、节点、边、状态"基础,重点讲解Memory(记忆)机制:如何让智能体记住对话历史、用户偏好,以及如何通过不同的记忆存储方案(内存、SQLite、MySQL、Redis)实现短期记忆和长期记忆。

1.1 本讲你将学会

  • 理解短期记忆长期记忆的区别与应用场景
  • 使用InMemorySaver实现线程级别的对话记忆
  • 使用InMemoryStore实现跨会话的长期记忆
  • 掌握不同记忆存储方案(SQLite、MySQL、Redis)的配置与使用
  • 构建一个既能记住用户信息、又能跨会话复用的智能助手

1.2 前置要求

  • 已完成第 1 讲的学习,理解 LangGraph 的基本概念(节点、边、状态)
  • Python 3.10+
  • 推荐:pip install -U langgraph langchain-core
  • 可选:pip install sqlalchemy(用于 SQLite/MySQL)、pip install redis(用于 Redis)
  • 模型默认使用DeepSeek,需准备 API Key

2. 持久化状态与记忆机制

2.1 什么是检查点(Checkpointer)

LangGraph 内置了一个持久化层,通过检查点(Checkpointer)机制来保存和恢复图的执行状态。

当你为图配置检查点功能时,系统会在每个超级步骤(super-step)结束后,自动为当前图的状态生成一个检查点。这些检查点会被存储在一个线程(Thread)中。

每个线程可以理解为一次独立的对话或任务轨迹。由于检查点被保存在对应的线程中,你可以在图执行结束后,随时访问和恢复当时的状态。

得益于这种线程 + 检查点的机制,LangGraph 可以支持:

  • 人工介入(human-in-the-loop):在关键步骤暂停,由人类审核或补充信息后继续执行
  • 记忆(memory):在多轮对话或多次调用之间保留上下文
  • 时间回溯(time travel):回到历史某个状态,从那里重新分支执行
  • 容错(fault-tolerance):在出错或中断后,从最近的检查点继续运行,而不是从头开始

2.2 什么是记忆(Memory)

记忆是一种认识功能,允许人们存储、检索和使用信息来理解他们的现在和未来。通过记忆功能,代理可以从反馈中学习,并适应用户的偏好。

LangGraph 中的记忆主要分为两种类型:

类型作用域持久化方式适用场景示例
短期记忆(Short-term memory)单个对话线程内通过检查点(Checkpointer)机制保存到数据库,每个步骤自动更新和读取保持单次对话的上下文连贯性,用户无需重复提供信息在一场对话中,用户说"我叫Bob",后续可以直接问"我的名字是什么"
长期记忆(Long-term memory)跨多个对话线程/会话通过存储机制(如 InMemoryStore)保存,可按命名空间检索跨会话保留用户偏好和历史信息,提供个性化体验用户的偏好设置、历史记录可以在不同会话中被调用和使用

img_1.jpg

2.3 为什么需要持久化

许多 AI 应用需要记忆功能来在多次交互中共享上下文。

  • 如何做:在 LangGraph 中,这类记忆通过线程级别的持久化添加到任务 StateGraph
  • 持久化的效果:让 AI 在连续对话/交互中保持信息的连续性和一致性
  • 体验收益:一次获取的信息可在后续交互中复用,用户不用反复重复
  • 场景示例:用户在一次会话里给出偏好,下一次会话自动记住并引用,交互更个性化、更高效
  • 适用场景:复杂或多步骤任务,避免重复输入,也便于 AI 更好理解和响应需求

3. 使用 InMemorySaver 实现短期记忆

本节演示如何在 LangGraph 里开启"记忆"能力:使用MessagesState管理对话消息,用InMemorySaver作为检查点持久化,让每轮对话自动保存并在后续调用中被读取。

你将看到从安装依赖、初始化模型,到编译带持久化的图表,再到多线程(多会话)下记忆隔离的完整示例。

3.1 安装依赖

pip install -U langchain langgraph langchain-core langchain-deepseek python-dotenv

3.2 构建图与持久化配置

from langgraph.graph import StateGraph, MessagesState , START ,END
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
# 构建 Graph 
# MessagesState 是一个 State 内置对象,add_messages 是内置的一个方法,将新的消息列表追加在原列表后面
graph_builder = StateGraph(MessagesState)

3.3 初始化模型,定义节点,并编译带 InMemorySaver 的图


from langchain.chat_models import init_chat_model

# 初始化模型
llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")

# 定义一个执行节点
# 输入是 State ,输出是系统回复
def chatbot(state: MessagesState):
    # 调用大模型,并返回消息(列表)
    # 返回值会触发状态更新 add_messages
    return {"messages": [llm.invoke(state["messages"])]}

# 添加节点
graph_builder.add_node("chatbot", chatbot)
# 添加边
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)

# 为了添加持久性,我们需要在编译图表时传递检查点,使用 InMemorySaver 就可以记住以前的消息!
graph = graph_builder.compile(checkpointer=checkpointer)

补充说明:InMemorySaver()会在内存中保存检查点。每次调用graph.invoke()或graph.stream()时,如果传入相同的thread_id,系统会自动加载该线程的历史状态。

3.4 可视化工作流

from IPython.display import Image, display

# 可视化展示这个工作流
try:
    display(Image(data=graph.get_graph().draw_mermaid_png()))
except Exception as e:
    print(e)
graph TD
    A[开始] --> B[步骤 1]
    B --> C{判断点}
    C -->|是| D[步骤 2]
    C -->|否| E[步骤 3]
    D --> F[结束]
    E --> F

3.5 单线程示例

同一 thread_id 内记住用户信息

from langchain_core.messages import HumanMessage

# 使用 thread_id="1" 创建第一个对话
config = {"configurable": {"thread_id": "1"}}
input_message = HumanMessage(content="hi! I'm Bob!")

for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

输出:

================================ Human Message =================================
hi! I'm Bob!
================================== Ai Message ==================================
Hi Bob! 👋 It's great to meet you! How's your day going? 😊

3.6 同线程再次询问

记忆生效,能直接回答名字

# 继续使用相同的 thread_id="1"
input_message = HumanMessage(content="What's my name?")
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

输出:

================================ Human Message =================================
What's my name?
================================== Ai Message ==================================
Your name is **Bob**! You introduced yourself right at the beginning—nice to meet you again, Bob! 😊 How can I help you today?

关键点:因为使用了相同的thread_id="1",LangGraph 自动加载了之前的对话历史,所以 AI 能记住用户的名字。

3.7 新线程示例

更换 thread_id,记忆隔离

# 使用新的 thread_id="2",创建独立的对话线程
config_new = {"configurable": {"thread_id": "2"}}
input_message = HumanMessage(content="What's my name?")

for chunk in graph.stream({"messages": [input_message]}, config_new, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

输出:

================================ Human Message =================================
What's my name?
================================== Ai Message ==================================
I don't have access to personal information about you unless you share it with me. If you'd like, you can tell me your name, and I'll happily use it in our conversation! 😊

关键点:不同的thread_id对应不同的对话线程,状态完全隔离。thread_id="2"是一个全新的对话,没有thread_id="1"的历史记忆。

4. 使用 InMemoryStore 实现长期记忆

InMemorySaver只能在同一线程内保持记忆,如果要在跨线程、跨会话之间共享记忆,需要使用InMemoryStore或数据库存储。

4.1 InMemorySaver vs InMemoryStore

特性InMemorySaverInMemoryStore
作用域单个线程内跨线程/跨会话
持久化内存中(程序重启后丢失)内存中(程序重启后丢失)
检索方式通过 thread_id 自动加载通过命名空间(namespace)手动检索
适用场景单次对话的上下文保持用户偏好、历史记录的跨会话复用

注意:InMemoryStore和InMemorySaver都是基于内存的,程序重启后数据会丢失。如果需要真正的持久化,需要使用 SQLite、MySQL、Redis 等数据库(见第 5 节)。

4.2 安装依赖

# 安装依赖
# pip install langchain_community dashscope

4.3 初始化 InMemoryStore

from langgraph.store.memory import InMemoryStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.runnables import RunnableConfig
from langgraph.store.base import BaseStore
import uuid

# 创建带向量检索的 InMemoryStore
in_memory_store = InMemoryStore(
    index={
        "embed": DashScopeEmbeddings(model="text-embedding-v1"),
        "dims": 1536,
    }
)

4.4 实现跨会话的记忆节点

def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    """带长期记忆的模型调用节点"""
    user_id = config["configurable"]["user_id"]
    namespace = ("memories", user_id)
    
    # 从 store 中检索与当前查询相关的记忆
    memories = store.search(namespace, query=str(state["messages"][-1].content))
    info = "\n".join([d.value["data"] for d in memories])
    system_msg = f"You are a helpful assistant talking to the user. User info: {info}"

    # 如果用户要求记住某些信息,存储到长期记忆
    last_message = state["messages"][-1]
    if"remember"in last_message.content.lower():
        # 提取要记住的内容(这里简化处理,实际可以更智能)
        memory = "User name is Bob"
        store.put(namespace, str(uuid.uuid4()), {"data": memory})

    # 调用模型,传入系统提示和对话历史
    response = model.invoke(
        [{"role": "system", "content": system_msg}] + state["messages"]
    )
    return {"messages": [response]}

# 构建图,同时使用 InMemorySaver 和 InMemoryStore
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
graph = builder.compile(checkpointer=InMemorySaver(), store=in_memory_store)

4.5 跨线程记忆示例

线程 1:写入记忆(记住用户名 Bob)

config1 = {"configurable": {"thread_id": 1, "user_id": "1"}}
input_message = HumanMessage(content="hI! Remember: my name is Bob!")

for chunk in graph.stream({"messages": [input_message]}, config1, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

输出:

================================ Human Message =================================
hI! Remember: my name is Bob)!
================================== Ai Message ==================================
Hi Bob! 😊 Got it—I'll remember your name is Bob. What's up? How can I help you today?

线程 2:同一用户,不同 thread_id,检索到刚才的记忆

# 使用新的 thread_id,但保持相同的 user_id
config2 = {"configurable": {"thread_id": 2, "user_id": "1"}}
input_message = HumanMessage(content="what is my name?")

for chunk in graph.stream({"messages": [input_message]}, config2, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

输出:

================================ Human Message =================================
what is my name?
================================== Ai Message ==================================
Your name is Bob! 😊 How can I assist you today?

关键点:虽然thread_id不同(1 vs 2),但因为user_id相同(都是 "1"),系统通过namespace = ("memories", "1")检索到了之前存储的记忆。

4.5 查看存储的记忆

# 查看 user_id="1" 的所有记忆
for memory in in_memory_store.search(("memories", "1")):
    print(memory.value)

输出:

{'data': 'User name is Bob'}

5. 使用数据库实现持久化存储

InMemorySaverInMemoryStore都是基于内存的,程序重启后数据会丢失。在生产环境中,我们需要使用真正的数据库来持久化存储。

5.1 SQLite 持久化

SQLite 是最简单的选择,无需额外安装数据库服务器。

安装依赖

# pip install sqlalchemy

实现方式


from langgraph.checkpoint.sqlite import SqliteSaver
import tempfile

# 创建临时数据库文件(生产环境应使用固定路径)
db_path = tempfile.mktemp()
checkpointer = SqliteSaver.from_conn_string(db_path)

# 使用 SQLite checkpointer 编译图
graph = graph_builder.compile(checkpointer=checkpointer)

5.2 MySQL 持久化

安装依赖

# pip install sqlalchemy pymysql

实现方式

from langgraph.checkpoint.postgres import AsyncPostgresSaver
# 注意:LangGraph 目前主要支持 PostgreSQL,MySQL 需要自定义实现
# 或使用兼容 PostgreSQL 协议的 MySQL 版本

# 示例(需要根据实际情况调整)
# checkpointer = AsyncPostgresSaver.from_conn_string("postgresql://user:pass@localhost/dbname")

5.3 Redis 持久化

安装依赖

# pip install redis

实现方式

from langgraph.checkpoint.redis import RedisSaver
import redis

# 连接 Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
checkpointer = RedisSaver(redis_client)

# 使用 Redis checkpointer 编译图
graph = graph_builder.compile(checkpointer=checkpointer)

补充说明:不同存储后端的性能、可靠性、成本不同。SQLite 适合单机小规模应用,MySQL/PostgreSQL 适合需要并发访问的场景,Redis 适合需要高性能缓存的场景。

6. 实践:构建一个完整的记忆系统

这一节我们用Redis作为“记忆的持久化存储”(既保存短期记忆的 checkpoint,也可承载长期记忆的存储),把“短期记忆 + 长期存储 + 持久化”一次性串起来:

  • 短期记忆(thread 级):通过RedisSaver把图的 checkpoint 写进 Redis,实现同一thread_id的多轮对话续写,并具备持久化能力。
  • 长期存储(跨 thread / 跨会话):通过RedisStore做应用级/用户级数据存储(这里先做基础读写自检,确保生产环境可用)。

6.1 目标与架构

我们要验证 3 件事:

  1. 同一 thread_id:第二轮能“记得上一轮问过什么”(短期记忆生效)。
  2. 不同 thread_id:对话状态隔离(短期记忆隔离)。
  3. Redis 后端可用:能正常setup()并读写(为生产持久化做准备)。

6.2 环境准备(依赖 + Redis 要求)

Python 依赖:

pip install -U langchain-core langchain langgraph python-dotenv
pip install -U langgraph-checkpoint-redis
pip install -U "redis>=5.2.1"
pip install -U "redisvl>=0.5.1"

Redis 侧要求:

  • Redis 8.0+(包含 RedisJSON 和 RediSearch 模块)
  • 或 Redis < 8.0 但需安装Redis Stack/ 单独安装 RedisJSON、RediSearch

如果你只想验证“checkpointer 写 checkpoint”(短期记忆持久化),通常 Redis 本体即可;但若你要使用RedisStore(尤其是索引/检索能力),通常需要 RedisJSON、RediSearch。

使用 Docker 启动本地 Redis(开发环境推荐):

如果你本机已安装 Docker,可以直接用下面命令启动一个带 RedisJSON / RediSearch 的 Redis Stack 服务:

docker run -d --name redis-stack \
  -p 6379:6379 \
  redis/redis-stack:latest

启动后,本章示例中使用的REDIS_URI = "redis://127.0.0.1:6379"就可以直接连接到这个容器里的 Redis 服务了。

6.3 完整示例代码

可直接复制运行

下面这份脚本包含两部分:

  • (A) RedisStore 自检:验证 RedisStore 的setup/put/get/delete(长期存储基础能力)。
  • (B) RedisSaver + Agent:验证同一thread_id的多轮对话记忆、不同thread_id的隔离(短期记忆 + 持久化)。

你可以按需运行其中一部分(见 6.4)。

"""
LangGraph + Redis:短期记忆持久化(RedisSaver)+ 长期存储(RedisStore)实践示例
"""

from __future__ import annotations

from typing import Literal  # 类型注解

from langchain_core.tools import tool  # 工具装饰器
from langchain.chat_models import init_chat_model  # 初始化聊天模型
from langgraph.prebuilt import create_react_agent  # 预构建的 ReAct 代理

from dotenv import load_dotenv
from pathlib import Path

# 导入 Redis 检查点保存器
try:
    from langgraph.checkpoint.redis import RedisSaver  # Redis 检查点保存器
    from langgraph.store.redis import RedisStore  # Redis 存储
    REDIS_AVAILABLE = True
except ImportError:
    REDIS_AVAILABLE = False

# 内存检查点保存器(当未安装 Redis 相关依赖时的降级方案)
from langgraph.checkpoint.memory import InMemorySaver

# 1) 加载环境变量(用于模型 API Key 等)
env_path = Path("../") / "config" / ".env"
load_dotenv(dotenv_path=env_path)

# 2) 初始化模型(示例用 deepseek,你也可以替换为其他 provider)
model = init_chat_model("deepseek-chat", model_provider="deepseek")

# 3) 定义一个简单工具(方便 ReAct agent 演示“可记忆的多轮交互”)
@tool
def get_weather(city: Literal["nyc", "sf", "beijing", "shanghai"]):
    """获取天气信息的工具函数

    Args:
        city: 城市名称,支持 "nyc", "sf", "beijing", "shanghai"

    Returns:
        str: 天气信息
    """
    weather_data = {
        "nyc": "纽约可能是多云天气,温度适中",
        "sf": "旧金山总是阳光明媚,气候宜人",
        "beijing": "北京今天晴朗,但可能有轻微雾霾",
        "shanghai": "上海多云转晴,湿度较高",
    }
    return weather_data.get(city, "抱歉,暂时无法获取该城市的天气信息")

def test_redis_store(redis_uri: str) -> None:
    """(A) 验证 RedisStore:setup / put / get / delete"""
    ifnot REDIS_AVAILABLE:
        raise RuntimeError("未安装 Redis 相关依赖:请先安装 langgraph-checkpoint-redis 等包")

    with RedisStore.from_conn_string(redis_uri) as store:
        store.setup()

        namespace = ("test", "namespace")
        key = "test_key"
        value = {"data": "这是一个测试值", "timestamp": "2024-01-01"}

        store.put(namespace, key, value)
        got = store.get(namespace, key)
        print("store.get =>", got)

        store.delete(namespace, key)
        print("store.delete => ok")

def test_short_term_memory_with_checkpointer(redis_uri: str) -> None:
    """(B) 验证 RedisSaver:同 thread_id 记忆 + 不同 thread_id 隔离"""
    tools = [get_weather]

    if REDIS_AVAILABLE:
        with RedisSaver.from_conn_string(redis_uri) as checkpointer:
            checkpointer.setup()
            agent = create_react_agent(model, tools=tools, checkpointer=checkpointer)

            # thread_id= user123:同一线程,多轮对话可续写
            config = {"configurable": {"thread_id": "user123"}}
            res1 = agent.invoke({"messages": [("human", "旧金山的天气怎么样?")]}, config)
            print("Q1:", "旧金山的天气怎么样?")
            print("A1:", res1["messages"][-1].content)

            res2 = agent.invoke({"messages": [("human", "你还记得我刚才问的什么吗?")]}, config)
            print("Q2:", "你还记得我刚才问的什么吗?")
            print("A2:", res2["messages"][-1].content)

            # thread_id= user456:新线程,状态隔离
            config_new = {"configurable": {"thread_id": "user456"}}
            res3 = agent.invoke({"messages": [("human", "北京和上海的天气如何?")]}, config_new)
            print("Q3:", "北京和上海的天气如何?")
            print("A3:", res3["messages"][-1].content)

            # 回到 user123:应能继续接上原线程
            res4 = agent.invoke({"messages": [("human", "回到我们之前的对话,你能总结一下吗?")]}, config)
            print("Q4:", "回到我们之前的对话,你能总结一下吗?")
            print("A4:", res4["messages"][-1].content)
    else:
        # 没装 redis 依赖时,退化为内存 checkpointer(仍可演示 thread_id 记忆,但不持久化)
        agent = create_react_agent(model, tools=tools, checkpointer=InMemorySaver())
        config = {"configurable": {"thread_id": "user123"}}
        res1 = agent.invoke({"messages": [("human", "旧金山的天气怎么样?")]}, config)
        res2 = agent.invoke({"messages": [("human", "你还记得我刚才问的什么吗?")]}, config)
        print("A1:", res1["messages"][-1].content)
        print("A2:", res2["messages"][-1].content)

if __name__ == "__main__":
    # 你的 Redis 连接串(按需改成带密码的形式,例如:redis://:123456@127.0.0.1:6379)
    REDIS_URI = "redis://127.0.0.1:6379"

    # 先跑长期存储自检(RedisStore)
    # test_redis_store(REDIS_URI)

    # 再跑短期记忆 + 持久化(RedisSaver + thread_id)
    test_short_term_memory_with_checkpointer(REDIS_URI)

6.4 运行步骤(推荐)

6.4.1 先验证 RedisStore 基础读写

把代码里test_redis_store(REDIS_URI)取消注释、然后运行脚本。你会看到setup/put/get/delete的输出。

6.4.2 再验证“短期记忆 + 持久化对话线程”

保持test_short_term_memory_with_checkpointer(REDIS_URI)启用即可。它会跑 4 轮对话: 同 thread 记忆 → 新 thread 隔离 → 回到原 thread 继续接上。

6.5 常见问题与排查(结合示例)

  • ImportError:缺少 Redis 相关模块:按 6.2 安装langgraph-checkpoint-redis
  • 连接失败 / 认证失败:检查 Redis 是否启动、端口、密码是否匹配;必要时把REDIS_URI改为带密码的连接串。
  • RedisJSON / RediSearch 不可用:如果你要用RedisStore(尤其是索引/检索能力),请升级到 Redis 8+ 或安装 Redis Stack。
  • 环境变量:示例会加载../config/.env(相对脚本路径),确保你的.env路径与内容正确(主要是模型 API Key 等)。

7. 小结

本讲完成了:

  • 理解了短期记忆(线程内)和长期记忆(跨线程/跨会话)的区别
  • 掌握了InMemorySaver的使用,实现单次对话的上下文保持
  • 掌握了InMemoryStore的使用,实现跨会话的记忆复用
  • 了解了不同存储后端(SQLite、MySQL、Redis)的配置方式
  • 给出了从短期记忆、长期记忆到 Redis 持久化的可复制运行示例代码

下节预告(管理短期记忆):

  • 如何在对话变长时修剪消息(移除前 N 条或后 N 条),避免超出 LLM 上下文窗口
  • 如何从 LangGraph 状态中永久删除消息,释放无用上下文
  • 如何对历史对话做消息摘要,用精简的 summary 替代大量旧消息
  • 如何管理检查点,按需存储与检索消息历史
  • 如何编写适合自己应用的自定义策略(如消息过滤、按角色保留等)

通过这些手段,让代理在不超出 LLM 上下文窗口的前提下,仍能稳定跟踪长对话。

更多关于 LangGraph 的 HowTo,参考官方文档:LangGraph HowTos 官方文档