- 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
- 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
- 培歌行学LangGraph(3):编译、执行与可视化
- 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
- 培歌行学LangGraph(5):状态管理与
graph.invoke入参深度解析 - 培歌行学LangGraph(6):Multi Schema多状态管理详解
- 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
- 培歌行学LangGraph(8):控制流详解
- 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
- 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
- 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
- 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
- 培歌行学LangGraph(13):节点执行与容错机制
- 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)
- 培歌行学LangGraph(15):持久化模式——何时保存检查点?
- 培歌行学LangGraph(16):持久化模式——查看历史检查点——你的图走过的每一步都有记录
- 培歌行学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 短期记忆 + 长期记忆 配合使用
典型的配合方式是:
- 第一次调用:从长期记忆读取数据,写入短期记忆(状态)
- 后续调用:直接从短期记忆读取,不用再查长期记忆
# 第一次调用:查长期记忆
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 节点是怎么执行的?
- LangGraph 从状态中读取节点需要的字段
- 调用节点函数
- 节点返回状态更新
- LangGraph 把更新写入状态通道
- 如果有条件边,执行路由逻辑
- 触发下一个节点
节点只需要返回变化的部分,不需要返回完整状态:
# ✅ 正确:只返回变化的部分
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 会自动注入:
state:图状态(必填)config:运行时配置(可选)runtime:运行时对象(可选)writer:流式写入器(可选)
面试题5:节点返回状态时,需要返回完整状态吗?
答:不需要。只需要返回发生变化的部分。LangGraph 会自动合并到完整状态中。
记忆管理就是给 AI 装三个脑子:
- 短期记忆记"刚才说了什么"
- 长期记忆记"你是什么样的人"
- 运行时上下文记"这次调用有什么特殊情况"
用好这三个记忆,你的 AI 才能既聪明又贴心。