总结之LangGraph Agent 记忆能力

LangGraph Agent 记忆能力学习笔记

一、为什么 Agent 需要记忆

默认情况下,LLM 是无状态的——每次调用都是独立的,不记得之前说过什么。要让 Agent 具备"记忆",需要在每一轮对话之间保存和恢复对话状态

LangGraph 通过 Checkpointer 机制解决这个问题:

CheckpointerAgent用户CheckpointerAgent用户你好,我叫 Sam保存对话状态 (put)你好 Sam!我叫什么名字?恢复对话历史 (get_tuple)返回之前的对话记录你叫 Sam

二、核心概念

2.1 Checkpointer

Checkpointer 是 LangGraph 的状态持久化接口,负责:

  • put / put_writes:保存 Agent 的对话状态
  • get_tuple / list:恢复之前的对话历史
  • get_next_version:生成递增的版本号

所有 Checkpointer 都继承自 BaseCheckpointSaver

2.2 thread_id

thread_id 是会话的唯一标识。相同 thread_id 的调用共享同一份记忆,不同 thread_id 互相隔离。

config = RunnableConfig(configurable={"thread_id": "session-001"})

2.3 使用方式(通用模板)

无论使用哪种 Checkpointer,代码模板都是一样的:

from langgraph.prebuilt import create_react_agent

# 1. 创建 checkpointer(具体实现不同)
memory = XxxSaver(...)

# 2. 创建 Agent,传入 checkpointer
agent = create_react_agent(model=llm, tools=[], checkpointer=memory)

# 3. 使用相同的 thread_id 进行多轮对话
config = RunnableConfig(configurable={"thread_id": "test-001"})
agent.invoke({"messages": [("user", "你好")]}, config=config)
agent.invoke({"messages": [("user", "你还记得我吗")]}, config=config)  # Agent 有记忆

三、四种持久化方案

自定义实现

FileSaver

本地文件

官方插件

RedisSaver

Redis

PyMySQLSaver

MySQL

内置方案

MemorySaver

内存存储

方案类名存储位置持久化外部依赖适用场景
内存记忆MemorySaver内存开发调试、短期会话
Redis 记忆RedisSaverRedisRedis Stack生产环境、跨进程共享
MySQL 记忆PyMySQLSaverMySQLMySQL 8.0+生产环境、结构化存储
文件记忆FileSaver(自定义)本地 .pkl 文件单机部署、轻量持久化

四、方案一:内存记忆(MemorySaver)

特点

  • 数据存储在内存中,速度快
  • 程序重启后记忆丢失
  • 无需任何外部依赖

代码

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.runnables import RunnableConfig

# 创建内存存储器
memory = MemorySaver()

# 创建 Agent
agent = create_react_agent(
    model=llm,
    tools=[],
    checkpointer=memory,
)

# 多轮对话测试
config = RunnableConfig(configurable={"thread_id": "test-session-001"})

# 第一轮:告诉名字
agent.invoke(
    {"messages": [("user", "你好,我叫Sam,请记住我的名字")]},
    config=config,
)

# 第二轮:验证记忆(相同 thread_id,Agent 能回答出 Sam)
agent.invoke(
    {"messages": [("user", "你还记得我叫什么名字吗?")]},
    config=config,
)

# 第三轮:不同 thread_id(新会话,Agent 不知道你是谁)
new_config = RunnableConfig(configurable={"thread_id": "test-session-002"})
agent.invoke(
    {"messages": [("user", "我叫什么名字?")]},
    config=new_config,
)

验证结论

轮次thread_idAgent 是否记得名字
第一轮test-session-001
第二轮test-session-001记得(Sam)
第三轮test-session-002不记得

五、方案二:Redis 持久化记忆(RedisSaver)

特点

  • 数据存储在 Redis 中,程序重启后记忆仍在
  • 支持跨进程、跨机器共享
  • 需要 Redis Stack(包含 RediSearch 模块),普通 Redis 不行

安装依赖

pip install langgraph-checkpoint-redis

代码

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.redis import RedisSaver
from langchain_core.runnables import RunnableConfig

REDIS_URL = "redis://127.0.0.1:6379"

# with 语句自动管理连接
with RedisSaver.from_conn_string(REDIS_URL) as memory:
    # 首次使用必须调用 setup() 初始化索引
    memory.setup()

    agent = create_react_agent(
        model=llm,
        tools=[],
        checkpointer=memory,
    )

    config = RunnableConfig(configurable={"thread_id": "redis-test-001"})
    agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

普通 Redis 报 FT._LIST 错误

langgraph-checkpoint-redis 依赖 Redis Stack 中的 RediSearch 模块来创建搜索索引。
如果使用普通 Redis(如通过 apt install redis 安装的),执行 memory.setup() 时会报错:

redis.exceptions.ResponseError: unknown command 'FT._LIST'

解决方案:使用 Redis Stack(docker run redis/redis-stack)替代普通 Redis。


六、方案三:MySQL 持久化记忆(PyMySQLSaver)

特点

  • 数据存储在 MySQL 表中,支持 SQL 查询和管理
  • 程序重启后记忆仍在
  • 适合已有 MySQL 基础设施的生产环境

安装依赖

pip install langgraph-checkpoint-mysql

代码

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver  # 注意类名
from langchain_core.runnables import RunnableConfig

MYSQL_URL = "mysql://root:123456@127.0.0.1:3306/langgraph_test"

with PyMySQLSaver.from_conn_string(MYSQL_URL) as memory:
    # 首次使用必须调用 setup() 创建数据表
    memory.setup()

    agent = create_react_agent(
        model=llm,
        tools=[],
        checkpointer=memory,
    )

    config = RunnableConfig(configurable={"thread_id": "mysql-test-001"})
    agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

类名是 PyMySQLSaver,不是 MySQLSaver

langgraph-checkpoint-mysql 包提供两个类:

  • MySQLSaver:基于 mysql-connector-python(官方驱动)
  • PyMySQLSaver:基于 pymysql(纯 Python 驱动)

如果使用 PyMySQLSaver,导入路径为:

from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver

setup() 只需调用一次

setup() 会在 MySQL 中创建 checkpointscheckpoint_writescheckpoint_blobs 三张表。
首次运行后表已存在,后续可以省略(但重复调用也不会报错)。


七、方案四:文件持久化记忆(自定义 FileSaver)

特点

  • LangGraph 没有内置文件持久化方案,需要自己实现
  • 数据存储在本地 .pkl 文件中,无需外部服务
  • 适合单机部署、轻量级持久化需求

实现原理

读取流程

get_tuple / list

从 .pkl 文件加载

恢复到 InMemorySaver 内存

返回数据

写入流程

put / put_writes

委托给 InMemorySaver

pickle.dump 保存到 .pkl 文件

FileSaver

内部复用 InMemorySaver

写入后序列化到文件

启动时从文件加载

关键设计决策

决策选择原因
序列化方式pickle 二进制JSON/base64 无法序列化 bytes 等复杂对象
文件粒度每个 thread_id 一个文件隔离性好,互不影响
内存逻辑复用 InMemorySaver避免重复实现复杂的内存操作

代码:完整 FileSaver 实现

import os, pickle, random
from typing import Any, Optional, Sequence
from contextlib import AbstractContextManager

from langgraph.checkpoint.base import (
    BaseCheckpointSaver, Checkpoint, CheckpointMetadata,
    CheckpointTuple, ChannelVersions,
)
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig


class FileSaver(BaseCheckpointSaver, AbstractContextManager):
    """基于文件的 Checkpoint 持久化器"""

    def __init__(self, storage_dir: str = "./checkpoint_data"):
        super().__init__()
        self.storage_dir = storage_dir
        os.makedirs(storage_dir, exist_ok=True)
        self._memory = InMemorySaver()        # 内部复用内存逻辑
        self._loaded_threads: set[str] = set() # 避免重复加载

    def _get_checkpoint_path(self, thread_id: str) -> str:
        safe_id = thread_id.replace("/", "_").replace("\\", "_")
        return os.path.join(self.storage_dir, f"checkpoint_{safe_id}.pkl")

    def _save_to_file(self, thread_id: str) -> None:
        """pickle 二进制序列化到文件"""
        path = self._get_checkpoint_path(thread_id)
        data = {
            "thread_id": thread_id,
            "storage": dict(self._memory.storage.get(thread_id, {})),
            "writes": {k: v for k, v in self._memory.writes.items() if k[0] == thread_id},
            "blobs": {k: v for k, v in self._memory.blobs.items() if k[0] == thread_id},
        }
        with open(path, "wb") as f:
            pickle.dump(data, f)

    def _load_from_file(self, thread_id: str) -> None:
        """从文件加载到内存"""
        if thread_id in self._loaded_threads:
            return
        path = self._get_checkpoint_path(thread_id)
        if not os.path.exists(path):
            self._loaded_threads.add(thread_id)
            return
        with open(path, "rb") as f:
            data = pickle.load(f)
        if data.get("storage"):
            self._memory.storage[thread_id] = data["storage"]
        for k, v in data.get("writes", {}).items():
            self._memory.writes[k] = v
        for k, v in data.get("blobs", {}).items():
            self._memory.blobs[k] = v
        self._loaded_threads.add(thread_id)

    # ---- 核心接口 ----
    def put(self, config, checkpoint, metadata, new_versions):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        result = self._memory.put(config, checkpoint, metadata, new_versions)
        self._save_to_file(thread_id)
        return result

    def put_writes(self, config, writes, task_id, task_path=""):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        self._memory.put_writes(config, writes, task_id, task_path)
        self._save_to_file(thread_id)

    def get_tuple(self, config):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        return self._memory.get_tuple(config)

    def list(self, config, *, filter=None, before=None, limit=None):
        if config:
            thread_id = config["configurable"]["thread_id"]
            self._load_from_file(thread_id)
        return self._memory.list(config, filter=filter, before=before, limit=limit)

    def get_next_version(self, current, channel):
        current_v = 0 if current is None else int(str(current).split(".")[0])
        return f"{current_v + 1:032}.{random.random():016}"

    # ---- 异步方法(直接委托给同步方法)----
    async def aget_tuple(self, config):     return self.get_tuple(config)
    async def aput(self, config, checkpoint, metadata, new_versions):
        return self.put(config, checkpoint, metadata, new_versions)
    async def aput_writes(self, config, writes, task_id, task_path=""):
        return self.put_writes(config, writes, task_id, task_path)

    def __exit__(self, *args): pass

使用方式

memory = FileSaver(storage_dir="./checkpoint_data")
agent = create_react_agent(model=llm, tools=[], checkpointer=memory)

config = RunnableConfig(configurable={"thread_id": "file-test-001"})
agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

base64 + JSON 序列化失败

最初尝试用 base64 + JSON 序列化,但 InMemorySaver 内部的 writesblobs 字典的 key 是 tuple 类型(包含 bytes),JSON 不支持 tuple key,导致序列化时报 Incorrect padding 错误。

最终方案:使用 pickle 二进制序列化,原生支持所有 Python 对象。


八、自定义 Checkpointer 实现指南

如果你需要实现自己的 Checkpointer(如 MongoDB、SQLite 等),需要实现以下接口:

BaseCheckpointSaver

put - 保存 checkpoint

put_writes - 保存 writes

get_tuple - 获取最新 checkpoint

list - 列出 checkpoint 列表

get_next_version - 生成版本号

aget_tuple - 异步获取

aput - 异步保存

aput_writes - 异步保存 writes

方法作用必须实现
put(config, checkpoint, metadata, new_versions)保存对话状态
put_writes(config, writes, task_id, task_path)保存中间写入
get_tuple(config)获取最新状态
list(config, filter, before, limit)列出历史状态
get_next_version(current, channel)生成递增版本号
aget_tuple / aput / aput_writes异步版本

实现模板

最简单的实现方式是内部复用 InMemorySaver,只额外处理序列化/反序列化:

class MyCustomSaver(BaseCheckpointSaver, AbstractContextManager):
    def __init__(self):
        super().__init__()
        self._memory = InMemorySaver()

    def put(self, config, checkpoint, metadata, new_versions):
        result = self._memory.put(config, checkpoint, metadata, new_versions)
        self._persist(config)   # 你的持久化逻辑
        return result

    def get_tuple(self, config):
        self._restore(config)    # 你的恢复逻辑
        return self._memory.get_tuple(config)

    # ... 其他方法同理

九、方案对比总结

维度MemorySaverRedisSaverPyMySQLSaverFileSaver(自定义)
持久化
外部依赖Redis StackMySQL 8.0+
跨进程共享
性能最快中等中等(IO 开销)
实现复杂度无需实现安装即用安装即用需自己实现
数据可查询有限SQL 查询需读文件
推荐场景开发调试高并发生产已有 MySQL 基础设施单机轻量部署

十、依赖安装速查

# 内存记忆(随 langgraph 安装)
pip install langgraph

# Redis 持久化
pip install langgraph-checkpoint-redis
# 注意:需要 Redis Stack(含 RediSearch),普通 Redis 不行

# MySQL 持久化
pip install langgraph-checkpoint-mysql
# 注意:类名是 PyMySQLSaver,不是 MySQLSaver

# 文件持久化
# 无额外依赖,自定义实现
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值