18.3 LangGraph 长期存储案例

以下是三个 LangGraph 长期存储案例,从内存版到 PostgreSQL 持久化版,逐层递进,展示如何在跨会话、跨线程的维度上记忆用户偏好等信息。


🧠 核心概念:Checkpointer ≠ Store

对比维度 Checkpointer(短期记忆) Store(长期记忆)
记忆范围 单个线程(单次会话),线程内状态持久化 跨会话、跨线程,同一用户的所有对话都可访问
存储内容 图状态(messagesstep 等动态数据) JSON 文档,按 namespace + key 组织
存储机制 checkpointer.put / checkpointer.get store.put / store.get / store.search
典型用途 同一会话内多轮对话记忆(上次聊到哪) 跨会话的用户画像(姓名、偏好、兴趣),不随会话结束而丢失
后端存储 SQLite / PostgreSQL / MySQL InMemory / PostgreSQL / 自定义 BaseStore
复制代码
User A (thread_1) ──┐
     (对话1)       Checkpointer ── 记住本会话内部:上次聊到哪、中间结果
User A (thread_2) ──┘
                         Store (跨线程) ── 记住跨会话信息:姓名、兴趣、偏好
User B (任何会话) ──────────────────┘

📦 案例一:InMemoryStore 基础版(内存长期存储)

演示跨会话记忆的核心功能:第一次对话告诉 AI 姓名,第二次新会话中 AI 依然能回忆起来。

完整代码

python 复制代码
import os
import asyncio
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.postgres import PostgresStore   # 需要安装 langgraph-store-postgres

load_dotenv()

DB_URI = "postgresql://langgraph_user:your_password@localhost:5432/langgraph_db"

llm = ChatOpenAI(
    model="qwen-plus",
    temperature=0.7,
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
    model_kwargs={"extra_body": {"enable_thinking": False}}
)

checkpointer = InMemorySaver()

def call_agent(state: MessagesState):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

async def main():
    # 正确的用法:直接 async with,不需要 await
    async with PostgresStore.from_conn_string(DB_URI) as store:
        await store.setup()   # 创建表(首次运行)
        print("✅ PostgreSQL 长期存储已连接")

        builder = StateGraph(MessagesState)
        builder.add_node("call_agent", call_agent)
        builder.add_edge(START, "call_agent")
        graph = builder.compile(checkpointer=checkpointer, store=store)

        user_id = "user_001"
        config = {"configurable": {"thread_id": f"{user_id}_thread"}}
        namespace = ("profiles", user_id)

        # 读取已有记忆
        existing = await store.aget(namespace, "user_info")
        if not existing:
            await store.aput(namespace, "user_info", {
                "name": "张明",
                "hobby": "摄影",
                "preferences": {"language": "zh-CN", "style": "detailed"}
            })
            print("💾 用户画像已写入 PostgreSQL")
        else:
            print(f"📋 读取用户画像: {existing.value}")

        # 对话示例
        print("\n用户: 我喜欢拍风景照片")
        messages = [("user", "我喜欢拍风景照片")]
        if existing:
            context = f"用户偏好:{existing.value}。请基于此回答。"
            messages.insert(0, ("system", context))

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

if __name__ == "__main__":
    asyncio.run(main())

预期输出

复制代码
📞【第一次对话(新会话)】
用户: 你好,我叫林小夕,我最喜欢喝咖啡。
================================ Ai Message =================================
你好林小夕!很高兴认识你,咖啡爱好者!☕️

💾 [长期存储写入] 用户信息已保存。

📞【第二次对话(新会话)】
用户: 你还记得我是谁吗?我喜欢什么?
================================ Ai Message =================================
当然记得!你是林小夕,你喜欢喝咖啡。☕️

📋 长期存储最终内容: {'name': '林小夕', 'preferences': ['咖啡'], 'first_seen': '2026-06-10'}

💡 案例一核心知识点

  • Store 核心方法store.put(namespace, key, value) 写入、store.get(namespace, key) 读取。
  • Namespace 设计("user_profiles", user_id) ------ 第一层表领域,第二层表用户 ID,支持分层组织。
  • Store 与 Checkpointer 同时使用:短期会话记忆 + 长期跨会话记忆互补。

🗄️ 案例二:PostgreSQL Store 持久化版(生产级方案)

开发阶段可以用 InMemoryStore 快速验证,生产环境必须使用 PostgreSQL Store 实现数据持久化。官方提供 PostgresStore 支持 PostgreSQL 后端存储,数据不随服务重启丢失。

准备工作

bash 复制代码
# 安装依赖
pip install "langgraph-checkpoint-postgres[store]"

完整代码

python 复制代码
import os
import asyncio
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.postgres import PostgresStore   # 👈 PostgreSQL Store

load_dotenv()

DB_URI = "postgresql://langgraph_user:your_password@localhost:5432/langgraph_db"

llm = ChatOpenAI(
    model="qwen-plus",
    temperature=0.7,
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
    model_kwargs={"extra_body": {"enable_thinking": False}}
)

checkpointer = InMemorySaver()

def call_agent(state: MessagesState):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

async def main():
    async with await PostgresStore.from_conn_string(DB_URI) as store:
        await store.setup()   # 创建表(首次运行)
        print("✅ PostgreSQL 长期存储已连接")

        builder = StateGraph(MessagesState)
        builder.add_node("call_agent", call_agent)
        builder.add_edge(START, "call_agent")
        graph = builder.compile(checkpointer=checkpointer, store=store)

        user_id = "user_001"
        config = {"configurable": {"thread_id": f"{user_id}_thread"}}
        namespace = ("profiles", user_id)

        # 读取已有记忆
        existing = await store.aget(namespace, "user_info")
        if not existing:
            await store.aput(namespace, "user_info", {
                "name": "张明",
                "hobby": "摄影",
                "preferences": {"language": "zh-CN", "style": "detailed"}
            })
            print("💾 用户画像已写入 PostgreSQL")
        else:
            print(f"📋 读取用户画像: {existing.value}")

        # 对话示例...
        print("\n用户: 我喜欢拍风景照片")
        messages = [("user", "我喜欢拍风景照片")]
        if existing:
            context = f"用户偏好:{existing.value}。请基于此回答。"
            messages.insert(0, ("system", context))

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

if __name__ == "__main__":
    asyncio.run(main())

PostgreSQL 长期存储案例:代码流程与数据交互详解

这是一个将 LangGraph 与 PostgreSQL 集成,实现 跨会话长期记忆 的异步版本代码。它利用 PostgresStore 存储用户画像(如姓名、爱好、偏好),并在对话中动态读取该记忆来增强 Agent 的回答。


一、整体架构图

Qwen模型 call_agent() compiled Graph PostgresStore main() Qwen模型 call_agent() compiled Graph PostgresStore main() #mermaid-svg-ETed0WbDtavim6g4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ETed0WbDtavim6g4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ETed0WbDtavim6g4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ETed0WbDtavim6g4 .error-icon{fill:#552222;}#mermaid-svg-ETed0WbDtavim6g4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ETed0WbDtavim6g4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ETed0WbDtavim6g4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ETed0WbDtavim6g4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ETed0WbDtavim6g4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ETed0WbDtavim6g4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ETed0WbDtavim6g4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ETed0WbDtavim6g4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ETed0WbDtavim6g4 .marker.cross{stroke:#333333;}#mermaid-svg-ETed0WbDtavim6g4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ETed0WbDtavim6g4 p{margin:0;}#mermaid-svg-ETed0WbDtavim6g4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ETed0WbDtavim6g4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ETed0WbDtavim6g4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ETed0WbDtavim6g4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ETed0WbDtavim6g4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ETed0WbDtavim6g4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ETed0WbDtavim6g4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ETed0WbDtavim6g4 .sequenceNumber{fill:white;}#mermaid-svg-ETed0WbDtavim6g4 #sequencenumber{fill:#333;}#mermaid-svg-ETed0WbDtavim6g4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ETed0WbDtavim6g4 .messageText{fill:#333;stroke:none;}#mermaid-svg-ETed0WbDtavim6g4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ETed0WbDtavim6g4 .labelText,#mermaid-svg-ETed0WbDtavim6g4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ETed0WbDtavim6g4 .loopText,#mermaid-svg-ETed0WbDtavim6g4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ETed0WbDtavim6g4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ETed0WbDtavim6g4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ETed0WbDtavim6g4 .noteText,#mermaid-svg-ETed0WbDtavim6g4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ETed0WbDtavim6g4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ETed0WbDtavim6g4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ETed0WbDtavim6g4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ETed0WbDtavim6g4 .actorPopupMenu{position:absolute;}#mermaid-svg-ETed0WbDtavim6g4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ETed0WbDtavim6g4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ETed0WbDtavim6g4 .actor-man circle,#mermaid-svg-ETed0WbDtavim6g4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ETed0WbDtavim6g4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt无画像有画像 异步连接 + 建表连接成功读取用户画像 (aget)写入默认画像 (aput)返回 existing.valuestream(messages, config)调用 call_agent(state)invoke(messages + 系统提示)返回回复{"messages": response}流式输出消息


二、代码逐段解析

1. 导入与配置

python 复制代码
import os
import asyncio
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.postgres import PostgresStore   # PostgreSQL Store
  • 异步支持PostgresStore 使用 async/await 语法,因此主函数需要 asyncio.run()
  • MessagesState :内置状态,包含 messages: Annotated[list, add_messages],自动追加新消息。
  • InMemorySaver :此处用于短期会话记忆(同一 thread_id 内状态持久化)。长期记忆由 PostgresStore 负责。
  • PostgresStore :来自 langgraph.store.postgres,用于在 PostgreSQL 中存储 JSON 文档。

2. 数据库连接字符串

python 复制代码
DB_URI = "postgresql://langgraph_user:your_password@localhost:5432/langgraph_db"
  • 格式:postgresql://用户名:密码@主机:端口/数据库名
  • 需提前在 PostgreSQL 中创建数据库和用户(可使用 pgAdmin 或命令行)。

3. 初始化 LLM

python 复制代码
llm = ChatOpenAI(
    model="qwen-plus",
    temperature=0.7,
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
    model_kwargs={"extra_body": {"enable_thinking": False}}
)
  • 使用阿里云百炼 Qwen 模型,兼容 OpenAI API 格式。

4. 定义 Agent 节点

python 复制代码
def call_agent(state: MessagesState):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}
  • 接收当前状态(包含 messages 列表)。
  • 调用 LLM 生成回复,并返回新消息(LangGraph 会自动追加到 messages 中)。

5. 主异步函数 main()

a) 创建并设置 PostgresStore
python 复制代码
async with await PostgresStore.from_conn_string(DB_URI) as store:
    await store.setup()
    print("✅ PostgreSQL 长期存储已连接")
  • PostgresStore.from_conn_string(DB_URI) 返回一个异步上下文管理器。
  • await store.setup() 会创建必要的表(如果不存在),通常只需运行一次。
  • 退出 async with 块时自动关闭连接。
b) 构建并编译图
python 复制代码
builder = StateGraph(MessagesState)
builder.add_node("call_agent", call_agent)
builder.add_edge(START, "call_agent")
graph = builder.compile(checkpointer=checkpointer, store=store)
  • 同时传入 checkpointer(短期记忆)和 store(长期记忆)。
  • LangGraph 会自动将 store 对象注入到运行时环境中,但本例中并未在节点函数内部直接使用 store,而是在外部先读取 store 内容,再构造带上下文的系统消息。这是一种简化的做法。
c) 读写长期存储(数据交互核心)
python 复制代码
user_id = "user_001"
config = {"configurable": {"thread_id": f"{user_id}_thread"}}
namespace = ("profiles", user_id)

existing = await store.aget(namespace, "user_info")
if not existing:
    await store.aput(namespace, "user_info", {
        "name": "张明",
        "hobby": "摄影",
        "preferences": {"language": "zh-CN", "style": "detailed"}
    })
    print("💾 用户画像已写入 PostgreSQL")
else:
    print(f"📋 读取用户画像: {existing.value}")
  • Namespace("profiles", user_id) 用于组织存储的文档。第一层 "profiles" 表示领域,第二层 user_id 区分不同用户。
  • aget :异步读取。如果存在,返回 Item 对象(包含 .value 字段);否则返回 None
  • aput :异步写入/覆盖。将用户画像存储为 JSON 文档,键名为 "user_info"

📌 数据交互流程

  1. 首次运行,无画像 → 写入默认画像。
  2. 后续运行(相同 user_id)→ 直接从 PostgreSQL 读取画像,无需重新写入。
d) 构造带长期记忆的对话输入
python 复制代码
print("\n用户: 我喜欢拍风景照片")
messages = [("user", "我喜欢拍风景照片")]
if existing:
    context = f"用户偏好:{existing.value}。请基于此回答。"
    messages.insert(0, ("system", context))
  • 如果读取到了画像,就将其作为系统消息(SystemMessage)插入到对话开头,提示 LLM 利用这些信息。
  • 这种方式不依赖 Agent 内部自动调用 store,而是由外部代码手动注入记忆。
e) 执行图并流式输出
python 复制代码
for event in graph.stream(
    {"messages": messages},
    config=config,
    stream_mode="values"
):
    event["messages"][-1].pretty_print()
  • stream_mode="values" 会在每个节点执行后返回完整状态。
  • 由于图中只有一个 call_agent 节点,所以会输出一次模型的回复。
  • 配置中的 thread_id 用于 InMemorySaver 区分短期会话(本例中只使用了一次会话,未跨多次调用,因此短期记忆作用不明显,但语法上保留)。

三、数据库表结构(PostgresStore 自动创建)

执行 await store.setup() 后,会在 PostgreSQL 中创建以下表:

表名 用途
store 存储文档的主表,包含字段:namespace (text\[\]), key (text), value (jsonb), created_at, updated_at
store_embeddings 可选,如果开启向量索引,存储 embedding

可通过 pgAdmin 查看数据:

sql 复制代码
SELECT * FROM store WHERE namespace = '{"profiles","user_001"}';

四、执行流程时序图(详细版)

复制代码
时间轴
│
├─ 1. 主函数启动 → 加载 .env
│
├─ 2. 异步连接 PostgreSQL
│     └─ PostgresStore.from_conn_string() 建立连接池
│     └─ store.setup() → 创建表(首次)
│
├─ 3. 读取长期记忆
│     └─ store.aget(("profiles","user_001"), "user_info")
│           └─ PostgreSQL 执行 SELECT ... WHERE namespace = '{profiles,user_001}' AND key='user_info'
│     └─ 若不存在:store.aput(...) → INSERT 用户画像
│     └─ 若存在:existing.value 包含 JSON 对象
│
├─ 4. 构建消息列表
│     └─ 如果有画像,插入 SystemMessage:"用户偏好:{...}。请基于此回答。"
│     └─ 追加 UserMessage:"我喜欢拍风景照片"
│
├─ 5. 执行图 graph.stream()
│     ├─ 图内部:call_agent 节点
│     │     └─ llm.invoke(messages)
│     │           ├─ LLM 接收系统提示(含用户画像) + 用户问题
│     │           └─ 生成回复(例如:"张明,你喜欢摄影,拍风景照片是个好主意...")
│     └─ 返回 {"messages": [AIMessage(...)]}
│
├─ 6. 流式输出最终消息(pretty_print)
│
└─ 7. 退出 async with → 关闭数据库连接

五、与短期记忆(Checkpointer)的对比

特性 本案例中的 InMemorySaver PostgresStore
存储内容 图的状态(messages 列表) 任意 JSON 文档(用户画像)
作用域 同一个 thread_id 内的多次调用 跨任意 thread_id,只要 user_id 相同
生命周期 进程结束后丢失(内存) 持久化到磁盘,服务重启不丢失
访问方式 自动恢复(LangGraph 内部处理) 需手动 store.get() / store.put()

六、可能的改进方向

  1. 在节点内部直接访问 store

    LangGraph 的运行时会将 store 注入到 RunnableConfig 中,节点函数可以通过 config["store"] 获取。这样无需在外部手动读取和拼装系统消息,Agent 可以自主调用长期记忆。

  2. 使用 store.search 进行语义检索

    如果启用了 embedding 索引,可以用自然语言查询长期记忆,而非依赖精确的 key。

  3. 结合 create_react_agent 与 Tool

    定义工具 recall_user_memoryremember_user_memory,让 Agent 自主决定何时读写长期存储。


七、运行结果示例(假设第二次运行)

复制代码
✅ PostgreSQL 长期存储已连接
📋 读取用户画像: {'name': '张明', 'hobby': '摄影', 'preferences': {'language': 'zh-CN', 'style': 'detailed'}}

用户: 我喜欢拍风景照片
================================ Human Message =================================
用户偏好:{'name': '张明', 'hobby': '摄影', 'preferences': {'language': 'zh-CN', 'style': 'detailed'}}。请基于此回答。
================================ Human Message =================================
我喜欢拍风景照片
================================== Ai Message ==================================
张明,你本来就喜欢摄影,拍风景照片正好发挥你的特长!建议试试清晨或黄昏的光线,效果更佳。📸

可以看到,模型成功利用长期记忆中存储的姓名和爱好给出了个性化回复。即使 thread_id 改变或程序重启,只要 user_id 不变,这些记忆依然存在。


八、总结

  • PostgresStore 提供了异步 API,用于在 LangGraph 中实现跨会话、跨线程的长期记忆。
  • 数据交互通过 store.aget / store.aput 实现,数据以 JSON 格式存储在 PostgreSQL 的 store 表中。
  • 本案例演示了外部手动注入记忆 的方式,适合简单场景;复杂场景应让 Agent 通过工具或 config["store"] 自主访问长期存储。
  • Checkpointer 相辅相成:Checkpointer 负责单次会话的上下文连续性,Store 负责跨会话的用户画像沉淀。

如果你需要进一步了解如何在节点函数内部动态使用 store,或如何集成向量检索,我可以提供进阶示例。

📚 最佳实践总结

最佳实践 说明
开发用 InMemory,生产用 PostgreSQL InMemoryStore 验证逻辑,PostgresStore 部署到生产
Namespace 设计建议 (领域, 用户ID, 子类型) ------ 命名空间支持分层组织和跨命名空间搜索
Store 核心 API store.put(namespace, key, value)store.get()store.search(filter, query)
Store + Checkpointer 结合 Store 提供跨会话长期记忆(用户画像),Checkpointer 维持线程内短期状态(会话历史)
Runtime 中访问 Store 部署到 LangGraph Platform 时,runtime.store 可自动注入
语义检索支持 Store 支持配置 index 参数开启向量检索,实现基于语义而非关键词的记忆召回

如果需要进一步了解语义检索(长期记忆 + RAG)的实现,或自定义 Store 集成其他数据库,请随时告诉我。

📋 代码流程分析 + 数据库验证步骤

你提供的代码实现了一个 LangGraph + PostgreSQL 长期记忆系统,核心功能是:在 PostgreSQL 中存储用户画像(姓名、爱好、偏好),并在对话中读取该画像注入系统提示,让 Agent 提供个性化回复。


一、代码执行流程(分阶段解析)

阶段 0:环境初始化

  • 加载 .env 环境变量(含 DASHSCOPE_API_KEYDASHSCOPE_BASE_URL)。
  • 建立数据库连接字符串 postgresql://postgres:root@localhost:5432/langgraph_db

阶段 1:异步连接 PostgreSQL

python 复制代码
async with AsyncPostgresStore.from_conn_string(DB_URI) as store:
    await store.setup()
  • 使用 异步上下文管理器 创建与 PostgreSQL 的连接池。
  • store.setup() 执行 DDL 语句,自动创建 store 表(如果表不存在)。表结构如下:
    • namespace (text\[\]) ------ 分层命名空间
    • key (text) ------ 文档键名
    • value (jsonb) ------ 实际存储的 JSON 数据
    • created_at, updated_at (timestamptz)

阶段 2:构建并编译 LangGraph 图

python 复制代码
builder = StateGraph(MessagesState)
builder.add_node("call_agent", call_agent)
builder.add_edge(START, "call_agent")
graph = builder.compile(checkpointer=checkpointer, store=store)
  • 图只有一条直线:START → call_agent → END
  • checkpointer = InMemorySaver:短期记忆(仅在此程序运行期间有效,用于同一 thread_id 的多轮对话)。
  • store = AsyncPostgresStore:长期记忆,跨会话、跨程序运行持久化。

阶段 3:读写长期记忆

python 复制代码
user_id = "user_001"
namespace = ("profiles", user_id)
existing = await store.aget(namespace, "user_info")
if not existing:
    await store.aput(namespace, "user_info", {...})
else:
    print(f"读取用户画像: {existing.value}")
  • 命名空间("profiles", "user_001") ------ 类似文件夹路径,第一层是领域(profiles),第二层是用户ID。
  • 键名"user_info"
  • 若首次访问,写入默认画像;否则读取并打印。

阶段 4:构造带记忆的对话消息

python 复制代码
messages = [("user", "我喜欢拍风景照片")]
if existing:
    context = f"用户偏好:{existing.value}。请基于此回答。"
    messages.insert(0, ("system", context))
  • 如果读取到画像,将其转换为系统消息(SystemMessage),插入对话开头。
  • 用户消息紧随其后。

阶段 5:执行图并输出回复

python 复制代码
for event in graph.stream({"messages": messages}, config=config, stream_mode="values"):
    event["messages"][-1].pretty_print()
  • 图内的 call_agent 节点调用 LLM,传入的消息列表:
    • SystemMessage(含画像)
    • UserMessage("我喜欢拍风景照片")
  • LLM 根据画像生成个性化回答,打印输出。

二、数据库验证步骤(使用 pgAdmin 或 psql)

✅ 前提条件

  • PostgreSQL 服务已启动。
  • 数据库 langgraph_db 已创建(如果未创建,需手动执行 CREATE DATABASE langgraph_db;)。
  • 已运行上述 Python 代码至少一次(以执行 store.setup() 建表及 store.aput 写入数据)。

步骤 1:连接到数据库

  • pgAdmin :在左侧树形菜单中找到 Servers → PostgreSQL → Databases → langgraph_db

  • psql :在命令行输入:

    bash 复制代码
    psql -U postgres -d langgraph_db

    密码:root

步骤 2:查看表是否创建成功

sql 复制代码
\dt

应该看到名为 store 的表。若使用异步版本,表名可能是 storecheckpoint_store(取决于版本),但最常见的是 store

步骤 3:查询 store 表中的长期记忆数据

sql 复制代码
SELECT * FROM store;

或者按命名空间查询:

sql 复制代码
SELECT namespace, key, value FROM store WHERE namespace = '{profiles,user_001}';

预期输出示例(JSONB 格式):

复制代码
   namespace    |    key    |                              value
----------------+-----------+------------------------------------------------------------------
 {profiles,user_001} | user_info | {"name": "张明", "hobby": "摄影", "preferences": {"language": "zh-CN", "style": "detailed"}}

步骤 4:验证数据更新(后续运行)

  • 修改代码中的画像内容(如将 hobby 改为 "绘画"),再次运行程序。
  • 再次执行上述 SQL 查询,观察 value 是否已更新。

步骤 5:检查表结构详细信息(可选)

sql 复制代码
\d store

可查看列类型、约束等。


三、可能出现的问题及排查

现象 原因 解决方法
async with ... 报错 TypeError 使用了同步 PostgresStore 而非 AsyncPostgresStore 确认导入的是 AsyncPostgresStore
store.setup() 卡住或报权限错误 数据库用户无建表权限 使用 PostgreSQL 超级用户 postgres 或授予 CREATE 权限
查询 store 表返回空 store.aput 未成功执行或 namespace/key 错误 检查代码中 namespace 是否为 ("profiles", user_id),且键名为 "user_info"
模型回答未使用画像信息 可能 existingNone 或消息构造逻辑有误 打印 existing 检查是否读取成功;确保 messages 中包含系统消息

四、总结:长期记忆的持久化流程

text 复制代码
[代码首次运行]
    └─ store.setup() → 在 PostgreSQL 中创建 store 表
    └─ store.aput() → 插入一行 JSON 文档
          └─ namespace = ['profiles', 'user_001']
          └─ key = 'user_info'
          └─ value = {"name": "张明", "hobby": "摄影", ...}

[代码再次运行(相同 user_id)]
    └─ store.aget() → 根据 namespace + key 检索
          └─ 从 store 表读取 JSON 文档
    └─ 将 JSON 内容注入系统提示
    └─ LLM 基于历史画像回答

通过以上验证步骤,你可以直观地看到 LangGraph 如何利用 PostgreSQL 实现跨会话的长期记忆存储。如果还有任何疑问,欢迎继续交流!

相关推荐
未若君雅裁2 小时前
JVM 垃圾回收算法与分代回收机制
java·jvm·算法
未若君雅裁2 小时前
JVM 垃圾回收器全景与G1深度解析
java·开发语言·jvm
hereitis贝壳3 小时前
GC.lsp:AutoCAD 中实用的轻量化公差标注插件
jvm·里氏替换原则
未若君雅裁3 小时前
JVM 调优与线上排障:参数工具内存泄漏和CPU飙高
jvm
cfm_29144 小时前
JVM类加载机制初步了解
java·jvm
周末也要写八哥5 小时前
线程的生命周期之“守护“线程
java·开发语言·jvm
未若君雅裁1 天前
JVM 运行时数据区:程序计数器、堆、虚拟机栈与栈帧
java·jvm
killerbasd1 天前
总结 6.9
jvm
IT龟苓膏1 天前
Java 并发基础:进程、线程、线程状态、synchronized、volatile 一篇讲清
java·开发语言·jvm