文章目录
-
- [第 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 本章目标
学完本章你将能够:
- 理解短期记忆(Checkpoint)与长期记忆(Store)的区别
- 掌握 InMemoryStore / PostgresStore 的配置和使用
- 学会在节点中通过 Runtime 访问 Store
- 实现语义记忆搜索功能
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 最佳实践总结
- 用户级数据用 Store,会话级数据用 Checkpoint:按数据生命周期选择存储
- 命名空间使用
(user_id, "category")结构:层级隔离,清晰明了 - 生产环境使用 PostgresStore 持久化:数据不丢失,支持高并发
- 语义搜索时选择合适的 embedding 模型:平衡成本和效果
- Context Schema 用于注入运行时信息:如 user_id、tenant_id 等,不需要放在 State 中