LangGraph+PostgreSQL 会话记忆持久化存储

本文基于LangGraph会话记忆管理相关内容,聚焦对话持久化存储方案,以LangChain官方推荐的PostgreSQL数据库为载体,拆解Checkpointer检查点存储与Store通用长期存储两大核心体系。通过对比两套存储机制的底层逻辑、差异特点与适用场景,结合可运行实战代码,实现会话重启续聊、跨线程记忆共享、语义检索、用户档案管理等核心能力,为LangGraph生产级智能体项目提供标准化持久化实现思路。

LangGraph将记忆存储划分为两套独立体系,分别适配不同记忆场景,是企业级项目开发的核心基础能力。其中Checkpointer检查点存储主要用于单线程短期会话状态存储,Store通用外部存储主要用于跨线程全局长期用户记忆存储。两套机制相互独立、数据隔离、能力互补,共同构成LangGraph完整的智能体记忆存储体系。

LangGraph 两大存储体系差异

在正式环境部署与代码实战前,必须清晰区分Saver 与 Store 的核心定位,避免业务场景误用。多数新手开发的核心误区,是混淆「会话运行状态记忆」与「用户业务长期记忆」,导致出现会话无法续连、用户记忆错乱、多线程数据互通异常等问题。下表全方位拆解 PostgresSaver 与 PostgresStore 的核心差异,同时对比内存临时版本,适配不同开发场景选型。

对比项 PostgresSaver PostgresStore
核心角色 Checkpointer 检查点 Store 通用外部记忆存储
核心用途 单 thread 会话短期记忆,保存 Graph 运行 state、消息历史、节点执行断点,支持中断后恢复执行、回溯历史快照 跨 thread 长期记忆,存储用户画像、用户偏好、事实记忆、业务知识库,多个会话thread_id可以共享读取同一份记忆
数据隔离维度 以 thread_id 为核心隔离,不同 thread 会话数据完全隔离 以 namespace + key 隔离,同一用户下全部 thread 可共享记忆
写入时机 自动写入:graph.invoke () 执行过程自动保存 checkpoint,无需手动调用 API 手动操作:需要业务代码主动调用 put/get/search/delete,不会自动保存
生命周期 跟随会话 thread,可配置过期清理会话快照 用户级长期记忆,独立于会话,可持久保存用户事实
典型业务场景 多轮对话上下文记忆、中断恢复对话、回滚对话历史 用户性格偏好、用户事实记忆、个人知识库、跨会话记住用户信息

环境依赖搭建与数据库部署

1、使用清华源镜像批量安装项目所需核心依赖库,包含PostgreSQL数据库驱动与LangGraph检查点持久化插件,完整安装命令如下。安装完成后可通过清单命令核对版本,确保依赖兼容可用。

CMD> pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple psycopg psycopg-binary langgraph-checkpoint-postgres
CMD>
CMD> pip list
Package                       Version
----------------------------- -----------
langgraph-checkpoint-postgres 3.1.2
psycopg                       3.3.4
psycopg-binary                3.3.4
psycopg-pool                  3.3.1

2、选用官方稳定版 PostgreSQL 18.6(Windows x86-64),该版本为官方维护的新版本,适配当前 LangGraph 持久化插件,且Windows平台仅提供64位安装包,32位系统已停止支持。

启动 postgresql-18.6-1-windows-x64.exe 安装程序后,首次加载会弹出VC++运行库安装界面,该过程耗时较长属于正常现象,并非程序卡死,耐心等待自动安装完成即可;安装流程中需自定义设置数据库管理员密码(本文统一使用测试密码1233),务必牢记该密码,用于后续数据库登录与授权操作。

安装完成后,可通过系统CMD进入数据库脚本目录,执行批量脚本快速登录数据库后台,无需手动复杂配置,登录指令及交互流程如下:

CMD> cd C:\Program Files\PostgreSQL\18\scripts
CMD> runpsql.bat
Server [localhost]:
Database [postgres]:
Port [5432]:
Username [postgres]:

用户 postgres 的口令:1233
psql (18.6)
输入 "help" 来获取帮助信息.
postgres=#

3、登录数据库后,需手动创建专属业务用户、独立数据库并完成权限授权,隔离项目业务数据,避免使用默认管理员库操作,核心SQL执行语句如下:

-- 创建用户
CREATE USER langgraph_user WITH PASSWORD '1233';

-- 创建数据库
CREATE DATABASE langgraph_db OWNER langgraph_user;

-- 授权登录
GRANT ALL PRIVILEGES ON DATABASE langgraph_db TO langgraph_user;

执行完毕后,通过 \c langgraph_db 切换至目标数据库,后续LangGraph项目所有检查点持久化数据均会存储至该数据库中。

在代码执行 checkpointer.setup() 方法时,LangGraph 会自动在目标数据库中创建4张数据表,用于存储、管理、维护会话检查点数据,无需手动建表。这部分数据表会永久留存,不会随程序重启自动删除,若未手动清理,程序再次运行时会自动读取历史表中数据,延续过往会话记忆。以下为各数据表的解析部分。

postgres-# \c langgraph_db
langgraph_db=# \dt

----------+-----------------------+--------+----------------
 架构模式 |         名称          |  类型  |     拥有者
----------+-----------------------+--------+----------------
 public   | checkpoint_blobs      | 数据表 | langgraph_user
 public   | checkpoint_migrations | 数据表 | langgraph_user
 public   | checkpoint_writes     | 数据表 | langgraph_user
 public   | checkpoints           | 数据表 | langgraph_user

langgraph_db=# \d checkpoint_blobs

---------------+-------+----------+----------+----------
     栏位      | 类型  | 校对规则 |  可空的  |   预设
---------------+-------+----------+----------+----------
 thread_id     | text  |          | not null |
 checkpoint_ns | text  |          | not null |
 channel       | text  |          | not null |
 version       | text  |          | not null |
 type          | text  |          | not null |
 blob          | bytea |          |          |

langgraph_db=# \d checkpoint_migrations

------+---------+----------+----------+------
 栏位 |  类型   | 校对规则 |  可空的  | 预设
------+---------+----------+----------+------
 v    | integer |          | not null |

langgraph_db=# \d checkpoint_writes

---------------+---------+----------+----------+----------
     栏位      |  类型   | 校对规则 |  可空的  |   预设
---------------+---------+----------+----------+----------
 thread_id     | text    |          | not null |
 checkpoint_ns | text    |          | not null |
 checkpoint_id | text    |          | not null |
 task_id       | text    |          | not null |
 idx           | integer |          | not null |
 channel       | text    |          | not null |
 type          | text    |          |          |
 blob          | bytea   |          | not null |
 task_path     | text    |          | not null |

langgraph_db=# \d checkpoints

----------------------+-------+----------+----------+-------------
         栏位         | 类型  | 校对规则 |  可空的  |    预设
----------------------+-------+----------+----------+-------------
 thread_id            | text  |          | not null |
 checkpoint_ns        | text  |          | not null |
 checkpoint_id        | text  |          | not null |
 parent_checkpoint_id | text  |          |          |
 type                 | text  |          |          |
 checkpoint           | jsonb |          | not null |
 metadata             | jsonb |          | not null |

PostgresSaver 会话短期持久化

PostgresSaver 是 LangGraph 实现单会话状态持久化的核心组件,替代默认的 InMemorySaver 内存临时存储。原生 InMemorySaver 仅将会话数据存储在内存中,程序重启、线程销毁后数据直接丢失,仅适合本地临时测试;而 PostgresSaver 将所有会话快照持久化至 PostgreSQL 数据库,实现同 thread_id 会话记忆永续、不同 thread_id 数据隔离、程序重启记忆不丢失的生产级能力。

以下代码完全可直接运行,无需修改,适配本地部署的 PostgreSQL 环境与本地大模型接口

from typing import Annotated
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, BaseMessage
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres import PostgresSaver

llm = ChatOpenAI(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy",
    temperature=0.7,
    max_tokens=512,
)

class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

def create_agent(model, tools, checkpointer):
    """构建简单对话 带checkpoint持久化记忆"""
    def chatbot_node(state: AgentState):
        response = model.invoke(state["messages"])
        return {"messages": [response]}

    builder = StateGraph(AgentState)
    builder.add_node("chatbot", chatbot_node)
    builder.add_edge(START, "chatbot")

    graph = builder.compile(checkpointer=checkpointer)
    return graph

def chat_demo(agent, thread_id: str, user_input: str):
    """单次对话 返回大模型输出"""
    config = {"configurable": {"thread_id": thread_id}}
    resp = agent.invoke(
        {"messages": [HumanMessage(content=user_input)]},
        config=config
    )
    return resp["messages"][-1].content

if __name__ == "__main__":
    DB_URI = "postgresql://langgraph_user:1233@localhost:5432/langgraph_db?sslmode=disable"

    with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
        checkpointer.setup()
        agent = create_agent(llm, [], checkpointer=checkpointer)

        print("[会话A:thread_id=user_001]")
        res1 = chat_demo(agent, thread_id="user_001", user_input="你好,我叫王瑞,今年28岁")
        print(f"AI:{res1}\n")

        res2 = chat_demo(agent, thread_id="user_001", user_input="我叫什么名字?多大?")
        print(f"AI:{res2}\n")

        print("[会话B:thread_id=user_002 全新会话,看不到user_001信息]")
        res3 = chat_demo(agent, thread_id="user_002", user_input="我叫什么名字?多大?")
        print(f"AI:{res3}\n")

        print("[模拟程序重启效果:重新获取agent,同一个thread_id依然能记住]")
        agent_new = create_agent(llm, [], checkpointer=checkpointer)
        res4 = chat_demo(agent_new, thread_id="user_001", user_input="再复述一遍我的个人信息")
        print(f"AI:{res4}\n")

代码通过 PostgresSaver.from_conn_string() 绑定本地 PostgreSQL 数据库,执行 checkpointer.setup() 自动生成前文讲解的四张检查点数据表。在 graph.compile(checkpointer=checkpointer) 编译图时注入持久化器,会话每一轮对话都会自动写入数据库。所有记忆隔离维度为 thread_id,不同 thread_id 的对话数据完全隔离、互不互通。示例中 user_001 保存了用户信息,user_002 无法读取,完美模拟多用户、多窗口独立会话场景。

CMD> python main.py

[会话A:thread_id=user_001]
AI:你好,王瑞先生。很高兴认识你!请问有什么我可以帮助你的?

AI:你好,王瑞先生。你的名字是王瑞,你今年28岁。

[会话B:thread_id=user_002 全新会话,看不到user_001信息]
AI:很抱歉,我无法获取您的名字和年龄信息,因为这涉及到个人隐私。如果您愿意,请提供更多信息,我将尽力帮助您。

[模拟程序重启效果:重新获取agent,同一个thread_id依然能记住]
AI:好的,你的名字是王瑞,你今年28岁。

PostgresStore 长期跨会话记忆

上一节中的 PostgresSaver 解决了单会话短期上下文接续、程序重启续聊的问题,但无法实现跨会话、跨线程的用户长期记忆共享。例如用户在 A 窗口设置的个人偏好、业务规则,无法在 B 窗口生效,无法满足个性化智能体、用户档案留存、全局知识库等业务需求。

而 PostgresStore 专注于用户级长期、跨线程全局记忆管理,不依赖、不绑定 thread_id,通过 namespace + key 全局维度管理记忆数据,支持用户偏好、个人档案、业务规则、知识库等长效数据的存储、查询、检索与删除,完美弥补 PostgresSaver 的能力短板。

基础记忆写入与读取

Store 所有记忆操作均为手动可控,开发者可精准定义记忆存储的内容、维度与时机。通过自定义命名空间,可实现多用户、多业务场景的记忆隔离,适配复杂业务架构。

from langchain_openai import ChatOpenAI
from langgraph.store.postgres import PostgresStore

llm = ChatOpenAI(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy",
    temperature=0.7,
    max_tokens=512,
)

if __name__ == "__main__":
    DB_URI = "postgresql://langgraph_user:1233@localhost:5432/langgraph_db?sslmode=disable"

    with PostgresStore.from_conn_string(DB_URI) as store:
        store.setup()

        # 定义命名空间
        namespace = ("user_001", "chat")

        # 推送记忆到命名空间
        store.put(
            namespace,
            "preference",
            {
                "style": "喜欢简短直接回答",
                "skill": "会Python编程,会使用SQL数据库"
            }
        )

        store.put(
            namespace,
            "a-memory",
            {
                "rules": [
                    "你是一个专业的Python开发人员",
                    "你只能回答与Python相关的问题",
                    "你只能回答与SQL数据库相关的问题"
                ],
                "my_key": "你好,我想知道Python的版本号",
            }
        )

        # 读取 preference
        item = store.get(namespace, "preference")
        print(f"读取preference结果: {item.value if item else None}")

        # 读取 a‑memory
        mem_item = store.get(namespace, "a-memory")
        print(f"读取a‑memory结果: {mem_item.value if mem_item else None}")
    
        # 搜索命名空间
        print("搜索命名空间:")
        items = store.search(namespace)
        for it in items:
            print(f"搜索结果 key={it.key}, value={it.value}")

运行代码后,直接创建两个命名空间,分别为preferencea-memory并依次调用store.get()读取打印,如下所示;

CMD> python main.py
读取preference结果:
{'skill': '会Python编程,会使用SQL数据库', 'style': '喜欢简短直接回答'}
读取a‑memory结果:
{'rules': ['你是一个专业的Python开发人员', '你只能回答与Python相关的问题', '你只能回答与SQL数据库相关的问题'], 'my_key': '你好,我想知道Python的版本号'}
搜索命名空间:
搜索结果
key=a-memory, value={'rules': ['你是一个专业的Python开发人员', '你只能回答与Python相关的问题', '你只能回答与SQL数据库相关的问题'], 'my_key': '你好,我想知道Python的版本号'}
搜索结果
key=preference, value={'skill': '会Python编程,会使用SQL数据库', 'style': '喜欢简短直接回答'}

语义向量检索能力适配

Store 核心高阶能力为语义向量检索,可根据用户问题语义相似度匹配对应记忆,而非传统精准匹配,是智能体个性化应答、知识库智能召回的核心能力。但是如果要使用它则必须有一个专用的向量嵌入模型来驱动语义的匹配,这种方式其实也是增加了系统的Token消耗。

两种存储模式适配不同场景:

  • PostgresStore 生产方案:原生 store.search() 向量检索必须依赖 pgvector 第三方插件,Windows 环境部署配置复杂,适合 Linux 生产服务器部署,支持海量记忆高效向量检索;
  • InMemoryStore 测试方案:内置简易内存向量检索能力,无需安装任何插件,开箱即用,适合本地开发、功能演示、语义检索逻辑调试;缺点是进程重启后所有内存数据全部丢失,仅用于测试,禁止生产使用。
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langgraph.store.memory import InMemoryStore
from langgraph.store.base import IndexConfig

# 对话模型配置项
llm = ChatOpenAI(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy",
    temperature=0.7,
    max_tokens=512,
)

# 向量嵌入模型配置项
embeddings = OpenAIEmbeddings(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy"
)

index_config = IndexConfig(
    embeddings=embeddings,
    embed_path=["content"]
)

if __name__ == "__main__":
    store = InMemoryStore()

    user_id = "user_001"
    context = "chat"
    namespace = (user_id, context)

    store.put(
        namespace,
        "mem_1",
        {"content": "我平时主要用Python做分析"},
        index=index_config
    )
    store.put(
        namespace,
        "mem_2",
        {"content": "我学习PostgreSQL,经常写SQL做数据表查询和统计"},
        index=index_config
    )
    store.put(
        namespace,
        "mem_3",
        {"content": "周末喜欢爬山,户外运动,不喜欢宅在家"},
        index=index_config
    )

    # 数据检索
    search_result = store.search(
        namespace,
        query="我想了解python相关知识",
        limit=1
    )

    # 打印检索结果
    for res in search_result:
        score_text = f"{res.score:.4f}" if res.score is not None else "N/A"
        print(f"key: {res.key}")
        print(f"相似度分数: {score_text}")
        print(f"记忆内容: {res.value}\n")

运行代码,会通过调用OpenAIEmbeddings接口其后端为一个向量检索大模型来一次判定是否为我们需要寻找的内容,若找到了则返回,如下所示;

CMD> python main.py
key: mem_1
相似度分数: N/A
记忆内容: {'content': '我平时主要用Python做分析'}

记忆删除能力

为满足用户隐私授权、记忆更新、数据清零等业务需求,Store 支持单条精准删除与批量全量删除,可灵活管控用户记忆生命周期,符合隐私合规要求,适配用户注销、重置个人信息等场景。

from langchain_openai import ChatOpenAI
from langgraph.store.postgres import PostgresStore

llm = ChatOpenAI(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy",
    temperature=0.7,
    max_tokens=512,
)

if __name__ == "__main__":
    DB_URI = "postgresql://langgraph_user:1233@localhost:5432/langgraph_db?sslmode=disable"

    with PostgresStore.from_conn_string(DB_URI) as store:
        store.setup()

        # 定义命名空间
        namespace = ("user_001", "chat")

        # 推送记忆到命名空间
        store.put(
            namespace,
            "preference",
            {
                "style": "喜欢简短直接回答",
                "skill": "会Python编程,会使用SQL数据库"
            }
        )

        store.put(
            namespace,
            "a-memory",
            {
                "rules": [
                    "你是一个专业的Python开发人员",
                    "你只能回答与Python相关的问题",
                    "你只能回答与SQL数据库相关的问题"
                ],
                "my_key": "你好,我想知道Python的版本号",
            }
        )

        # 单独删除一条记忆
        store.delete(namespace, "preference")

        # 循环删除多条
        all_items = store.search(namespace)
        print(f"待删除key列表:{[x.key for x in all_items]}")
        for mem in all_items:
            store.delete(namespace, mem.key)
        print("批量删除完成,剩余记录: ", store.search(namespace))

代码运行后,则将namespace中的记忆全部清空,并输出清空后的文件列表,如下所示;

CMD> python main.py
待删除key列表:['a-memory']
批量删除完成,剩余记录:  []

工具化调用记忆

在实际项目中,记忆的读写、查询、删除需交由智能体自主触发,无需人工手动调用 API。本节将记忆操作封装为标准 Agent 工具,实现智能体自动识别用户意图、自主读写用户档案、自主清理记忆的闭环能力,完全贴合生产级智能体开发规范。

from typing_extensions import TypedDict
from dataclasses import dataclass
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langgraph.store.postgres import PostgresStore

llm = ChatOpenAI(
    model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
    base_url="http://127.0.0.1:11433/v1",
    api_key="dummy",
    temperature=0.7,
    max_tokens=512,
)

@dataclass
class Context:
    """传给智能体的上下文,携带用户编号"""
    user_id: str

class UserInfo(TypedDict):
    """用户信息数据结构"""
    name: str
    hobby: str

# ---------------------- 工具:读取用户长期记忆 ----------------------
@tool
def get_user_info(runtime: ToolRuntime[Context]) -> str:
    """查询用户档案信息,当需要获取用户姓名、爱好时调用该工具"""
    store = runtime.store
    user_id = runtime.context.user_id
    user_info = store.get(("users",), user_id)
    return str(user_info.value) if user_info else "未知用户,档案不存在"

# ---------------------- 工具:保存用户长期记忆 ----------------------
@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[Context]) -> str:
    """保存用户档案,用户告知自己姓名、爱好时调用,参数传入用户信息字典"""
    store = runtime.store
    user_id = runtime.context.user_id
    store.put(("users",), user_id, user_info)
    return "用户信息保存成功"

# ---------------------- 工具:删除用户长期记忆 ----------------------
@tool
def delete_user_memory(runtime: ToolRuntime[Context]) -> str:
    """清除该用户全部档案记忆,用户要求删除个人信息时调用"""
    store = runtime.store
    user_id = runtime.context.user_id
    store.delete(namespace=("users",), key=user_id)
    return f"已清除用户 {user_id} 的档案记忆"

if __name__ == "__main__":
    DB_URI = "postgresql://langgraph_user:1233@localhost:5432/langgraph_db?sslmode=disable"

    with PostgresStore.from_conn_string(DB_URI) as store:
        store.setup()
        test_users = [
            {"user_id": "user_123", "info": {"name": "张三", "hobby": "爬山阅读"}},
            {"user_id": "user_789", "info": {"name": "王五", "hobby": "摄影,骑行"}},
            {"user_id": "user_999", "info": {"name": "赵六", "hobby": "编程,打游戏"}}
        ]

        for item in test_users:
            store.put(
                ("users",),
                item["user_id"],
                item["info"]
            )

        agent = create_agent(
            model=llm,
            tools=[get_user_info, save_user_info, delete_user_memory],
            store=store,
            context_schema=Context
        )

        # Agent工具查询用户档案信息
        print("-----------------查询用户档案信息 -----------------")
        resp_read = agent.invoke(
            {"messages": [HumanMessage(content="帮我查一下我的个人档案")]},
            context=Context(user_id="user_123")
        )
        print(f"智能体回答:{resp_read['messages'][-1].content}")
    
        # Agent工具保存用户档案
        print("-----------------保存用户档案 -----------------")
        resp_write = agent.invoke(
            {"messages": [HumanMessage(content="我叫李四,平时爱好打篮球")]},
            context=Context(user_id="user_456")
        )
        print(f"智能体回答:{resp_write['messages'][-1].content}")

        saved = store.get(("users",), "user_456")
        print(f"数据库校验保存结果: {saved.value if saved else None}")

        # Agent工具删除用户档案
        print("-----------------删除用户档案 -----------------")
        resp_del = agent.invoke(
            {"messages": [HumanMessage(content="请删除我的个人档案信息")]},
            context=Context(user_id="user_456")
        )
        print(f"智能体回答:{resp_del['messages'][-1].content}")

        check_del = store.get(("users",), "user_456")
        print(f"删除后校验档案:{check_del}")

上述代码运行后,会自动执行查询记忆,保存新的记忆,最后再删除掉记忆,如下所示;

CMD> python main.py
-----------------查询用户档案信息 -----------------
智能体回答:您的名字是张三,您的爱好是爬山和阅读。
-----------------保存用户档案 -----------------
智能体回答:您的个人信息已经保存成功,如果您需要其他帮助,请随时告诉我。
数据库校验保存结果: {'name': '李四', 'hobby': '打篮球'}
-----------------删除用户档案 -----------------
智能体回答:您的个人档案信息已成功清除。如有其他需求,请随时告知。
删除后校验档案:None

PostgresSaver 与 PostgresStore 两套存储机制相互独立、能力互补、缺一不可,共同构成 LangGraph 企业级记忆体系,二者结合可实现「会话不断连、用户有记忆、多端多线程数据互通」的智能对话能力,所有长期记忆统一存储在 PostgreSQL 的 langgraph_store 数据表中,持久化不丢失、可溯源、可手动管理。

posted @ 2026-08-20 18:16  lyshark  阅读(20)  评论(0)    收藏  举报