培歌行学LangGraph(18):图记忆管理-让你的 AI 记住该记的事

17 阅读7分钟
  1. 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
  2. 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
  3. 培歌行学LangGraph(3):编译、执行与可视化
  4. 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
  5. 培歌行学LangGraph(5):状态管理与graph.invoke入参深度解析
  6. 培歌行学LangGraph(6):Multi Schema多状态管理详解
  7. 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
  8. 培歌行学LangGraph(8):控制流详解
  9. 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
  10. 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
  11. 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
  12. 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
  13. 培歌行学LangGraph(13):节点执行与容错机制
  14. 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)
  15. 培歌行学LangGraph(15):持久化模式——何时保存检查点?
  16. 培歌行学LangGraph(16):持久化模式——查看历史检查点——你的图走过的每一步都有记录
  17. 培歌行学LangGraph(17):检查点的四大使用场景

AI Agent 有三种记忆,就像人有三种"脑子":

记忆类型英文名说白了就是存多久
短期记忆Short-term Memory这次对话里的事会话结束就丢(除非持久化)
长期记忆Long-term Memory用户的各种偏好、资料永久保存
运行时上下文Runtime Context这次调用的"临时备注"用完就丢

7.1 短期记忆:会话内的"即时贴"

只要编译图时加了 Checkpointer,并且在调用时用同一个 thread_id,LangGraph 就会自动帮你记住这个会话里的所有状态。

# 第一轮:说"我叫小明"
graph.invoke({"messages": [HumanMessage("我叫小明")]}, config={"configurable": {"thread_id": "001"}})

# 第二轮:问"我叫什么?"
graph.invoke({"messages": [HumanMessage("我叫什么?")]}, config={"configurable": {"thread_id": "001"}})
# 模型会回答:你叫小明

这就是短期记忆——同一个 thread_id 下的状态共享。我们一直在用,不多说了。


7.2 长期记忆:用户的"档案柜"

7.2.1 长期记忆用来干嘛?

短期记忆只在同一个会话里有效。换了 thread_id,之前的信息就没了。

但有些信息我们希望跨会话、跨用户都能记住,比如:

  • 用户喜欢吃什么
  • 用户的会员等级
  • 用户的兴趣爱好

这时候就需要长期记忆

7.2.2 怎么存长期记忆?

from langgraph.store.postgres import PostgresStore

DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"

with PostgresStore.from_conn_string(DB_URL) as store:
    store.setup()  # 建表
    
    # 存 Alice 的偏好
    store.put(
        ("users", "Alice"),  # 命名空间:领域 + 用户
        "preferences",       # key:数据类型
        {                    # value:具体数据
            "course": "计算机组成原理",
            "sports": "跑步",
            "food": "紫光园奶皮子酸奶"
        }
    )
    
    # 存 Bob 的偏好
    store.put(
        ("users", "Bob"),
        "preferences",
        {"course": "数字电路", "sports": "跑步", "food": "奶皮子糖葫芦"}
    )

7.2.3 怎么读长期记忆?

在图节点里,通过 runtime.store 来读取:

def check_preferences_node(state: OverAllState, runtime: Runtime) -> OverAllState:
    username = state["username"]
    store = runtime.store
    
    # 从长期记忆中查这个用户的偏好
    item = store.get(("users", username), "preferences")
    
    if item:
        return {"preferences": item.value}
    else:
        return {}

7.2.4 短期记忆 + 长期记忆 配合使用

典型的配合方式是:

  1. 第一次调用:从长期记忆读取数据,写入短期记忆(状态)
  2. 后续调用:直接从短期记忆读取,不用再查长期记忆
# 第一次调用:查长期记忆
res = graph.invoke({"username": "Alice", "user_input": "聊聊天吧"}, config=config)
# 输出:模型知道 Alice 喜欢跑步和酸奶

# 第二次调用:直接用短期记忆(不用再查)
res = graph.invoke({"user_input": "推荐酸奶"}, config=config)
# 输出:模型直接推荐紫光园奶皮子酸奶

7.3 运行时上下文:临时的"便签纸"

7.3.1 什么时候用?

有些信息只对当次调用有用,不需要持久化,也不需要跨会话共享。比如:

  • 当前是谁在调用(用户名)
  • 这次调用的来源(Web / App / API)
  • 这次调用的 Trace ID
  • 临时的功能开关

这些信息不适合放进图状态(会被持久化),应该用运行时上下文

7.3.2 怎么用?

第一步:定义上下文类型

from dataclasses import dataclass

@dataclass
class UserContext:
    username: str
    membership_level: str  # "普通用户" / "VIP"

第二步:编译图时传入 context_schema

builder = StateGraph(state_schema=OverAllState, context_schema=UserContext)

第三步:调用时传入 context

res = graph.invoke(
    {"user_input": "有什么优惠活动?"},
    config=config,
    context=UserContext(username="Alice", membership_level="VIP")
)

第四步:节点中通过 runtime.context 读取

def llm_node(state: OverAllState, runtime: Runtime[UserContext]) -> OverAllState:
    ctx = runtime.context
    
    if ctx.membership_level == "VIP":
        prompt = f"您是VIP用户{ctx.username},请用尊称"
    else:
        prompt = f"用户{ctx.username},请友好回复"
    
    # ... 调用模型

7.3.3 关键点

运行时上下文仅对当次调用生效,不会被持久化。

即使你用同一个 thread_id 调用两次,第二次不传 context,那第二次就没有上下文。

# 第一次:传了上下文
graph.invoke({...}, config=config, context=UserContext(...))

# 第二次:没传上下文
graph.invoke({...}, config=config)  # runtime.context 是 None

7.3.4 什么时候用哪个?

数据类型放哪里原因
用户名、偏好设置长期记忆 Store跨会话需要记住
对话历史、中间结果短期记忆 State会话内需要共享
当前登录用户、请求来源、Trace ID运行时上下文仅当次调用有效

7.4 节点总结:LangGraph 的节点到底是个啥?

7.4.1 节点的本质

节点就是一个函数,输入状态,输出状态更新。

def my_node(state: State) -> State:
    # 处理逻辑
    return {"result": "xxx"}

7.4.2 节点函数的完整形态

除了 state,节点还可以接收三个额外的参数(LangGraph 会自动注入):

def my_node(
    state: State,                          # 图状态(必填)
    config: RunnableConfig,                # 运行时配置(可选)
    runtime: Runtime[Context],             # 运行时对象(可选)
    writer: StreamWriter                   # 流式写入器(可选)
) -> State:
    # config:可以拿到 thread_id、递归限制等
    thread_id = config["configurable"]["thread_id"]
    
    # runtime:可以访问长期记忆和上下文
    store = runtime.store
    ctx = runtime.context
    
    # 业务逻辑
    ...

7.4.3 节点是怎么触发的?

从用户视角看,节点通过来触发:

  • 普通边:固定走到下一个节点
  • 条件边:根据路由函数的返回值决定走向
  • Command.goto:节点返回时动态指定下一个节点

从源码视角看,节点订阅了特定的通道。当通道有数据写入时,节点就会被调度执行。

7.4.4 节点是怎么执行的?

  1. LangGraph 从状态中读取节点需要的字段
  2. 调用节点函数
  3. 节点返回状态更新
  4. LangGraph 把更新写入状态通道
  5. 如果有条件边,执行路由逻辑
  6. 触发下一个节点

节点只需要返回变化的部分,不需要返回完整状态:

# ✅ 正确:只返回变化的部分
def node_a(state: State) -> State:
    return {"count": state["count"] + 1}

# ❌ 没必要:返回完整状态
def node_a(state: State) -> State:
    return {
        "count": state["count"] + 1,
        "name": state["name"],     # 没变,不用返回
        "age": state["age"]        # 没变,不用返回
    }

面试题

面试题1:短期记忆和长期记忆的区别是什么?

  • 短期记忆:按 thread_id 组织,同一个会话内共享,换了 thread_id 就没了
  • 长期记忆:按命名空间组织,跨会话、跨用户共享,永久保存

面试题2:运行时上下文和状态有什么区别?

  • 状态会被持久化,同一 thread_id 的下一次调用可以恢复
  • 运行时上下文不会被持久化,仅对当次调用生效

面试题3:什么时候用长期记忆?什么时候用运行时上下文?

  • 长期记忆:存用户的偏好、资料、配置等需要跨会话记住的信息
  • 运行时上下文:传当前登录用户、请求来源、Trace ID 等仅当次调用有效的信息

面试题4:节点函数可以接收哪些参数?

:四个参数,LangGraph 会自动注入:

  1. state:图状态(必填)
  2. config:运行时配置(可选)
  3. runtime:运行时对象(可选)
  4. writer:流式写入器(可选)

面试题5:节点返回状态时,需要返回完整状态吗?

:不需要。只需要返回发生变化的部分。LangGraph 会自动合并到完整状态中。


记忆管理就是给 AI 装三个脑子:

  • 短期记忆记"刚才说了什么"
  • 长期记忆记"你是什么样的人"
  • 运行时上下文记"这次调用有什么特殊情况"

用好这三个记忆,你的 AI 才能既聪明又贴心。