【langgraph 从入门到精通graphApi 篇】Memory 与长期记忆

文章目录

    • [第 8 章:Memory 与长期记忆](#第 8 章:Memory 与长期记忆)
      • [8.1 本章目标](#8.1 本章目标)
      • [8.2 核心概念](#8.2 核心概念)
        • [Checkpoint vs Store](#Checkpoint vs Store)
        • [短期记忆 vs 长期记忆架构](#短期记忆 vs 长期记忆架构)
        • [Store 命名空间设计](#Store 命名空间设计)
      • [8.3 实战](#8.3 实战)
        • [实战 1:用户偏好记忆](#实战 1:用户偏好记忆)
        • [实战 2:语义记忆搜索](#实战 2:语义记忆搜索)
      • [8.4 API 速查](#8.4 API 速查)
      • [8.5 错误与避坑指南](#8.5 错误与避坑指南)
        • [坑 1:混淆 Store 和 Checkpoint](#坑 1:混淆 Store 和 Checkpoint)
        • [坑 2:命名空间设计不当](#坑 2:命名空间设计不当)
        • [坑 3:语义搜索未配置 embedding](#坑 3:语义搜索未配置 embedding)
        • [坑 4:忘记在 compile 时传入 store](#坑 4:忘记在 compile 时传入 store)
      • [8.6 最佳实践总结](#8.6 最佳实践总结)

第 8 章:Memory 与长期记忆

8.1 本章目标

学完本章你将能够:

  1. 理解短期记忆(Checkpoint)与长期记忆(Store)的区别
  2. 掌握 InMemoryStore / PostgresStore 的配置和使用
  3. 学会在节点中通过 Runtime 访问 Store
  4. 实现语义记忆搜索功能

8.2 核心概念

Checkpoint vs Store
特性 Checkpoint(短期记忆) Store(长期记忆)
存储内容 图状态快照(State 的完整副本) 应用定义的键值数据
作用范围 单个 Thread(对话线程) 跨 Thread(跨会话)
生命周期 对话结束后可清理 持久保留
典型用途 对话连续性、HITL、容错 用户偏好、知识库、共享信息
访问方式 自动(每个 super-step 保存) 手动(节点中显式读写)

比喻:Checkpoint 就像"草稿自动保存"------每次编辑后自动保存当前状态。Store 就像"客户档案"------记录客户的长期偏好,所有对话都能访问。

短期记忆 vs 长期记忆架构

#mermaid-svg-L07FYz7tgii6awO3{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-L07FYz7tgii6awO3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-L07FYz7tgii6awO3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-L07FYz7tgii6awO3 .error-icon{fill:#552222;}#mermaid-svg-L07FYz7tgii6awO3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-L07FYz7tgii6awO3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-L07FYz7tgii6awO3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-L07FYz7tgii6awO3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-L07FYz7tgii6awO3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-L07FYz7tgii6awO3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-L07FYz7tgii6awO3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-L07FYz7tgii6awO3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-L07FYz7tgii6awO3 .marker.cross{stroke:#333333;}#mermaid-svg-L07FYz7tgii6awO3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-L07FYz7tgii6awO3 p{margin:0;}#mermaid-svg-L07FYz7tgii6awO3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-L07FYz7tgii6awO3 .cluster-label text{fill:#333;}#mermaid-svg-L07FYz7tgii6awO3 .cluster-label span{color:#333;}#mermaid-svg-L07FYz7tgii6awO3 .cluster-label span p{background-color:transparent;}#mermaid-svg-L07FYz7tgii6awO3 .label text,#mermaid-svg-L07FYz7tgii6awO3 span{fill:#333;color:#333;}#mermaid-svg-L07FYz7tgii6awO3 .node rect,#mermaid-svg-L07FYz7tgii6awO3 .node circle,#mermaid-svg-L07FYz7tgii6awO3 .node ellipse,#mermaid-svg-L07FYz7tgii6awO3 .node polygon,#mermaid-svg-L07FYz7tgii6awO3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-L07FYz7tgii6awO3 .rough-node .label text,#mermaid-svg-L07FYz7tgii6awO3 .node .label text,#mermaid-svg-L07FYz7tgii6awO3 .image-shape .label,#mermaid-svg-L07FYz7tgii6awO3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-L07FYz7tgii6awO3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-L07FYz7tgii6awO3 .rough-node .label,#mermaid-svg-L07FYz7tgii6awO3 .node .label,#mermaid-svg-L07FYz7tgii6awO3 .image-shape .label,#mermaid-svg-L07FYz7tgii6awO3 .icon-shape .label{text-align:center;}#mermaid-svg-L07FYz7tgii6awO3 .node.clickable{cursor:pointer;}#mermaid-svg-L07FYz7tgii6awO3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-L07FYz7tgii6awO3 .arrowheadPath{fill:#333333;}#mermaid-svg-L07FYz7tgii6awO3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-L07FYz7tgii6awO3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-L07FYz7tgii6awO3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L07FYz7tgii6awO3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-L07FYz7tgii6awO3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L07FYz7tgii6awO3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-L07FYz7tgii6awO3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-L07FYz7tgii6awO3 .cluster text{fill:#333;}#mermaid-svg-L07FYz7tgii6awO3 .cluster span{color:#333;}#mermaid-svg-L07FYz7tgii6awO3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-L07FYz7tgii6awO3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-L07FYz7tgii6awO3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-L07FYz7tgii6awO3 .icon-shape,#mermaid-svg-L07FYz7tgii6awO3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L07FYz7tgii6awO3 .icon-shape p,#mermaid-svg-L07FYz7tgii6awO3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-L07FYz7tgii6awO3 .icon-shape .label rect,#mermaid-svg-L07FYz7tgii6awO3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L07FYz7tgii6awO3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-L07FYz7tgii6awO3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-L07FYz7tgii6awO3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Store (长期记忆)
Checkpointer (短期记忆)
会话 2 (thread-2)
会话 1 (thread-1)
自动保存
自动保存
读写
读写
对话1
对话2
对话3
对话1
对话2
thread-1

checkpoints
thread-2

checkpoints
用户偏好

历史摘要

共享知识

Store 命名空间设计
python 复制代码
# 命名空间使用 tuple 结构,支持层级隔离
("user_123", "memories")      # 用户 123 的记忆
("user_123", "preferences")   # 用户 123 的偏好
("user_456", "memories")      # 用户 456 的记忆
("global", "knowledge")       # 全局知识库

8.3 实战

实战 1:用户偏好记忆
python 复制代码
from typing import TypedDict, Annotated
from dataclasses import dataclass
from langgraph.graph import StateGraph, START, END, add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
from langgraph.runtime import Runtime
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage


# ============================================
# 定义 Context(运行时上下文)
# ============================================
@dataclass
class Context:
    user_id: str


# ============================================
# 定义 State
# ============================================
class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]


# ============================================
# 定义节点(使用 Runtime 访问 Store)
# ============================================
async def memory_node(state: State, runtime: Runtime[Context]) -> dict:
    """
    使用 Runtime 访问 Store 实现长期记忆。
    
    关键点:
    1. 在函数签名中声明 runtime: Runtime[Context] 参数
    2. LangGraph 自动注入 Runtime 实例
    3. 通过 runtime.context 访问上下文
    4. 通过 runtime.store 访问 Store
    """
    user_id = runtime.context.user_id
    namespace = (user_id, "memories")
    
    # 从 Store 读取用户记忆
    memories = await runtime.store.asearch(namespace, limit=10)
    
    # 构建包含记忆的回复
    if memories:
        memory_text = "我记得以下信息:\n" + "\n".join(
            f"  - {m.value.get('content', '')}" for m in memories
        )
    else:
        memory_text = "我还没有关于你的记忆。"
    
    # 如果有新信息,存入 Store
    last_msg = state["messages"][-1]
    if "记住" in last_msg.content or "我叫" in last_msg.content:
        await runtime.store.aput(
            namespace,
            f"memory_{len(memories) + 1}",
            {"content": last_msg.content},
        )
    
    return {
        "messages": [AIMessage(content=f"收到!{memory_text}")]
    }


# ============================================
# 构建图
# ============================================
store = InMemoryStore()
checkpointer = MemorySaver()

builder = StateGraph(State)
builder.add_node("memory", memory_node)
builder.add_edge(START, "memory")
builder.add_edge("memory", END)

# 编译时传入 store 和 context_schema
graph = builder.compile(
    checkpointer=checkpointer,
    store=store,
    context_schema=Context,
)


# ============================================
# 异步运行
# ============================================
import asyncio

async def main():
    config = {"configurable": {"thread_id": "mem-001"}}
    context = Context(user_id="user_123")
    
    # 第一次对话
    result = await graph.ainvoke(
        {"messages": [HumanMessage(content="我叫小明,记住这个")]},
        config=config,
        context=context,
    )
    print(f"AI: {result['messages'][-1].content}")
    
    # 第二次对话(新 thread,但同一 user_id)
    config2 = {"configurable": {"thread_id": "mem-002"}}
    result = await graph.ainvoke(
        {"messages": [HumanMessage(content="你还记得我叫什么吗?")]},
        config=config2,
        context=context,
    )
    print(f"AI: {result['messages'][-1].content}")

asyncio.run(main())
实战 2:语义记忆搜索
python 复制代码
from langgraph.store.memory import InMemoryStore

# 创建带语义搜索的 Store
store = InMemoryStore(
    index={
        "embed": "openai:text-embedding-3-small",  # 嵌入模型
        "dims": 1536,  # 嵌入维度
        "fields": ["content", "$"],  # 要索引的字段
    }
)

# 存储带语义信息的记忆
async def store_with_semantics():
    await store.aput(
        ("user_123", "memories"),
        "mem_1",
        {"content": "用户喜欢喝咖啡,尤其是拿铁"},
    )
    await store.aput(
        ("user_123", "memories"),
        "mem_2",
        {"content": "用户是 Python 程序员"},
    )
    await store.aput(
        ("user_123", "memories"),
        "mem_3",
        {"content": "用户住在北京朝阳区"},
    )
    
    # 语义搜索:自然语言查询
    results = await store.asearch(
        ("user_123", "memories"),
        query="用户喜欢喝什么饮料?",  # 自然语言查询
        limit=3,
    )
    
    for item in results:
        print(f"  [{item.key}] {item.value['content']}")
    # 输出: [mem_1] 用户喜欢喝咖啡,尤其是拿铁

asyncio.run(store_with_semantics())

8.4 API 速查

API 完整签名 入参说明 返回值 说明
InMemoryStore() InMemoryStore(index=...) index: 语义搜索配置(可选) Store 对象 内存存储
PostgresStore(conn) PostgresStore(conn) conn: 数据库连接 Store 对象 Postgres 持久化
store.put(namespace, key, value) put(ns: tuple, key: str, value: dict) ns: 命名空间; key: 键; value: 值 None 存储数据
store.search(namespace, query, limit) search(ns: tuple, query: str, limit: int) ns: 命名空间; query: 查询; limit: 数量 结果列表 语义搜索(需 index)
store.aget/get/adelete 异步版本 同上 同上 节点中异步调用
Runtime[Context] Runtime[Context] Context: 上下文类型 运行时对象 节点中注入上下文
context_schema compile(context_schema=Type) context_schema: Context 类型 CompiledGraph 编译时声明 Context

8.5 错误与避坑指南

坑 1:混淆 Store 和 Checkpoint
python 复制代码
# ❌ 错误:把用户偏好存在 Checkpoint 中
# Checkpoint 是 thread 级别的,换 thread 就丢失了!
class State(TypedDict):
    user_preferences: dict  # 不应该放在 State 中

# ✅ 正确:用户偏好存在 Store 中
# Store 跨 thread 共享,通过 user_id 隔离
await store.aput(("user_123", "preferences"), "key", value)
坑 2:命名空间设计不当
python 复制代码
# ❌ 错误:所有用户共享命名空间
await store.aput(("memories"), "key", value)  # 所有用户混在一起

# ✅ 正确:按用户 ID 隔离
await store.aput(("user_123", "memories"), "key", value)
await store.aput(("user_456", "memories"), "key", value)
坑 3:语义搜索未配置 embedding
python 复制代码
# ❌ 错误:没有配置 index
store = InMemoryStore()  # 没有 index
await store.asearch(ns, query="自然语言查询")  # 无法语义搜索,只能精确匹配

# ✅ 正确:配置 index
store = InMemoryStore(
    index={
        "embed": "openai:text-embedding-3-small",
        "dims": 1536,
        "fields": ["content"],
    }
)
坑 4:忘记在 compile 时传入 store
python 复制代码
# ❌ 错误
graph = builder.compile(checkpointer=...)
# 节点中 runtime.store 不可用!

# ✅ 正确
graph = builder.compile(
    checkpointer=...,
    store=store,  # 必须传入
)

8.6 最佳实践总结

  1. 用户级数据用 Store,会话级数据用 Checkpoint:按数据生命周期选择存储
  2. 命名空间使用 (user_id, "category") 结构:层级隔离,清晰明了
  3. 生产环境使用 PostgresStore 持久化:数据不丢失,支持高并发
  4. 语义搜索时选择合适的 embedding 模型:平衡成本和效果
  5. Context Schema 用于注入运行时信息:如 user_id、tenant_id 等,不需要放在 State 中
相关推荐
火山引擎开发者社区20 小时前
一句话上线 AI Agent 应用:火山 Supabase + IGA Pages 全栈部署实践
人工智能
火山引擎开发者社区20 小时前
AI Agent 真正进入工作流:AgentKit CLI 打造云端沙箱开发环境
人工智能
蓝速科技20 小时前
蓝速 AI 双屏翻译机:实体商铺跨境沟通落地指南
人工智能
phltxy1 天前
LangChain从模型输出到RAG数据管道实战
服务器·人工智能·深度学习·语言模型·langchain
我星期八休息1 天前
网络编程—应用层HTTP协议
linux·运维·开发语言·前端·网络·网络协议·http
10x101 天前
企业 AI 真正的分水岭:不是人人都在用,而是工作方式被重新设计
人工智能
收放扳机1 天前
PCB产线的“柔性”是怎么实现的——从自动化上下料设备兼容性看产线弹性
人工智能·科技·自动化·制造·pcb工艺
一个天蝎座 白勺 程序猿1 天前
从电网改造踩坑说起:深度拆解时序大模型TimechoAI的自主可控与安全合规底气
大数据·运维·服务器·大模型·timechoai
ShineWinsu1 天前
对于Linux:UDPsocket编程基础的解析
linux·运维·网络协议·udp·ip·端口号·进程间通信