🧬 长期记忆 Store ★
AD5 的 Checkpoint 只在单会话内有效。真实 Agent 需要跨会话记住用户——偏好、历史、画像。LangGraph 的 Store 正是为此设计:跨 thread 的持久 KV + 原生语义搜索。本章精读 BaseStore、namespace 分层、IndexConfig 语义索引,并对比 OpenCode 的"文件式记忆"范式。
本章目标
- 理解 Store 与 Checkpoint 的本质区别(长期 vs 短期)
- 精读
BaseStore的 namespace + key 数据模型 - 掌握
IndexConfig开启语义搜索的配置 - 构建跨会话的用户画像记忆系统
- 对比"API 即记忆"(Store)vs"文件即记忆"(OpenCode)两种范式
Checkpoint vs Store:短期 vs 长期
这是记忆系统最核心的区分。AD5 讲了 Checkpoint(短期),现在看 Store(长期):
想象一个私人助手:用户上周说"我对花生过敏",今天问"推荐午餐"。如果只有 Checkpoint,Agent 不记得上周的话(不同会话/不同 thread)。Store 让 Agent 把"花生过敏"存进长期记忆,今天语义检索到。这就是"越用越懂你"的能力。
BaseStore 数据模型
Store 的核心是 namespace(命名空间)+ key 的两级寻址:
class BaseStore(ABC):
"""持久化 KV 存储基类。
Stores 实现跨 thread 共享的持久记忆,
可按 user_id / assistant_id 等任意 namespace 分组。
★ 部分实现支持语义搜索(通过 index 配置开启)。
语义搜索默认禁用,需创建时提供 index 配置。
TTL(过期)也默认禁用,子类需显式 supports_ttl = True。
"""
supports_ttl: bool = False
ttl_config: TTLConfig | None = None
# 核心抽象方法:批量执行操作(get/put/search/delete 都是封装)
@abstractmethod
def batch(self, ops: Iterable[Op]) -> list[Result]: ...
@abstractmethod
async def abatch(self, ops: Iterable[Op]) -> list[Result]: ...
# ====== 便捷封装方法 ======
def get(self, namespace: tuple[str, ...], key: str) -> Item | None:
"""按 namespace + key 精确取。"""
def search(self, namespace_prefix, *, query=None, filter=None, limit=10):
"""★ 搜索:支持语义搜索(query)和元数据过滤(filter)。"""
def put(self, namespace, key, value, *, index=None) -> None:
"""存。index 可指定对哪些字段做 embedding。"""
def delete(self, namespace, key) -> None: ...
def list_namespaces(self, *, prefix=None) -> list: ...
namespace:分层命名空间
namespace 是元组,天然支持分层隔离:
# namespace 是 tuple,分层隔离不同用户/场景
store.put(
namespace=("users", "alice", "preferences"), # ← 三层命名空间
key="diet",
value={"allergies": ["peanut"], "likes": ["italian"]},
)
store.put(
namespace=("users", "bob", "preferences"), # bob 的,和 alice 隔离
key="diet",
value={"allergies": [], "likes": ["japanese"]},
)
# search 时用 namespace_prefix 匹配前缀
store.search(("users", "alice"), query="饮食偏好") # 只搜 alice 的
IndexConfig:开启语义搜索
Store 最强大的特性——原生语义搜索。配置 IndexConfig:
class IndexConfig(TypedDict):
dims: int # :577 embedding 维度(如 1536)
embed: ... # :590 embedding 函数
# - Embeddings 实例
# - 同步/异步函数
# - provider 字符串如 "openai:text-embedding-3-small"
fields: list[str] # :662 对哪些 JSON path 做 embedding
# 默认 ["$"] 整体,可指定 ["$.content"] 只索引某字段
from langgraph.store.memory import InMemoryStore
# ★ 开启语义搜索只需传 index 配置
store = InMemoryStore(
index={
"dims": 1536, # embedding 维度
"embed": "openai:text-embedding-3-small", # 用 OpenAI embedding
"fields": ["$"], # 对整个 value 做 embedding
}
)
# 存入记忆(自动 embedding)
store.put(("users", "alice", "memories"), "m1",
{"text": "我对花生过敏,喜欢意大利菜"})
# ★ 语义搜索:不靠精确匹配,靠语义相似
results = store.search(("users", "alice"), query="Alice 的饮食限制是什么?")
# 即使 query 没有"花生""过敏"字样,语义相近也能召回!
# 这就是长期记忆"越用越懂你"的基础
实战:用户画像记忆系统
pip install langgraph langchain-openai。一个能跨会话记住用户的 Agent。
# pip install langgraph langchain-openai
# export OPENAI_API_KEY="sk-..."
from langgraph.store.memory import InMemoryStore
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
model = ChatOpenAI(model="gpt-4o-mini")
# ★ 长期记忆 Store(带语义搜索)
store = InMemoryStore(index={"dims": 1536, "embed": "openai:text-embedding-3-small"})
# 工具①:记住用户信息(写入长期记忆)
@tool
def remember(fact: str) -> str:
"""记住关于用户的重要信息。当用户提到偏好/个人信息时使用。"""
# 用 user_id 作 namespace 隔离(实际从 config 取)
store.put(("user_memories",), f"fact_{hash(fact) % 10000}",
{"text": fact})
return f"已记住:{fact}"
# 工具②:回忆用户信息(语义搜索长期记忆)
@tool
def recall(query: str) -> str:
"""回忆关于用户的信息。当需要用户的历史偏好/信息时使用。"""
results = store.search(("user_memories",), query=query, limit=3)
if not results:
return "没有相关记忆"
return "\n".join(f"- {r.value['text']}" for r in results)
# ★ Agent 同时配 checkpointer(短期)和 store(长期)
agent = create_react_agent(
model, tools=[remember, recall],
checkpointer=InMemorySaver(), # 短期:对话历史
store=store, # 长期:跨会话记忆
prompt="你是私人助手。用户提到偏好时用 remember 记住;需要时用 recall 回忆。",
)
config = {"configurable": {"thread_id": "day1"}}
# ========== 会话1(周一)==========
print("=== 会话1 ===")
agent.invoke({"messages": [("user", "我对花生过敏,最喜欢意大利菜")]}, config)
agent.invoke({"messages": [("user", "记住我喜欢周末做饭")]}, config)
# ========== 会话2(周三,不同 thread!)==========
config2 = {"configurable": {"thread_id": "day3"}}
print("\n=== 会话2(跨会话,新thread)===")
result = agent.invoke({"messages": [("user", "推荐我今晚吃什么")]}, config2)
print(result["messages"][-1].content)
# ★ Agent 会用 recall 工具语义搜索到"花生过敏+意大利菜",
# 即使这是另一个会话(thread 不同)!长期记忆跨会话生效。
# ========== 会话3(周五)==========
config3 = {"configurable": {"thread_id": "day5"}}
print("\n=== 会话3 ===")
result = agent.invoke({"messages": [("user", "周末有什么计划建议")]}, config3)
# recall 到"喜欢周末做饭",给出相关建议
这个 Agent 同时用了两层记忆:Checkpoint 管本会话对话(thread_id),Store 管跨会话画像(namespace)。Agent 自己决定何时 remember(写入长期)、何时 recall(语义检索长期)。这就是"短期+长期"双记忆系统——人类记忆也是这样(工作记忆 + 长期记忆)。
记忆策略:记什么、何时记
有了 Store 基础设施,更难的问题是记忆策略——什么时候该记、记什么:
| 策略 | 原理 | 代表 |
|---|---|---|
| 显式记忆 | Agent 用 remember 工具主动记 | 本章示例 |
| 后台抽取 | 每轮结束后用单独 LLM 抽取"值得记的事实" | mem0、LangGraph 官方 memory 模板 |
| 按需检索 | Agent 用 recall 工具按当前需要语义搜索 | 本章示例 |
| 自动注入 | 每轮开始自动 search 相关记忆注入 prompt | 中间件/middleware |
后台抽取模式(更智能)
# 不依赖 Agent 主动 remember,而是每轮对话后自动抽取
def extract_and_store_memories(messages, user_id):
"""用 LLM 从对话中抽取'值得长期记住的事实'。"""
extraction = model.invoke([
SystemMessage(content=(
"从对话中抽取值得记住的用户事实(偏好/个人信息/重要决定)。"
"如果没有值得记的,返回空。JSON 数组格式。"
)),
*messages,
])
facts = parse_json(extraction.content) # [{"text": "对花生过敏"}, ...]
for fact in facts:
store.put(("user_memories", user_id), f"fact_{uuid4()}",
{"text": fact["text"]})
# 在图的末尾节点调用,自动沉淀记忆
# 优点:Agent 不用关心记忆,专注任务;缺点:多一次 LLM 调用
生产部署:PostgresStore
生产环境用 PostgresStore(含 pgvector 语义搜索),跨进程持久化:
from langgraph.store.postgres import PostgresStore
import psycopg
# 生产级:PostgreSQL + pgvector
conn = psycopg.connect("postgresql://user:pass@localhost/langgraph")
store = PostgresStore(conn=conn, index={"dims": 1536, "embed": "openai:..."})
store.setup() # 自动建表 + pgvector 索引
# 用法和 InMemoryStore 完全一样——这就是抽象的价值
agent = create_react_agent(model, tools=[...], store=store, ...)
# 进程重启、多实例部署,记忆都在 Postgres 里持久保留
范式对比:API 即记忆 vs 文件即记忆
LangGraph Store 和 OpenCode 代表两种截然不同的长期记忆范式:
| 维度 | LangGraph Store(API 即记忆) | OpenCode(文件即记忆) |
|---|---|---|
| 存储 | 程序化 KV + 向量 | Markdown 指令文件 |
| 读写 | API 调用(put/search) | Agent 用 read/write 工具操作文件 |
| 检索 | 语义搜索(embedding) | Agent grep/glob 搜索 |
| 注入 | 程序化 search 后拼 prompt | instruction.resolve 自动沿目录收集 |
| 透明度 | 低(黑盒数据库) | ★高(人可直接看/编辑 .md 文件) |
| 适合 | 结构化数据、用户画像 | 非结构化、协作、可审计 |
OpenCode 的文件式记忆
# Memory
用户记忆存在 .github/instructions/memory.instruction.md
需要时自行读写更新该文件(带 applyTo: '**' front matter)
# OpenCode 的"长期记忆"本质:
# - 是 Agent 主动维护的 markdown 文件
# - instruction.resolve (instruction.ts:46) 自动注入到 prompt
# - 用户可直接打开文件查看/编辑记忆(完全透明)
# - 没有 embedding 语义搜索,靠 Agent 自己 grep/glob
Store 适合需要语义召回的结构化数据(用户偏好、知识图谱),是程序化的、精确的。OpenCode 的文件式记忆适合开发者工具场景——记忆是 markdown,人能读能改,融入项目本身(如 AGENTS.md 就是"项目级记忆")。选择取决于应用场景:对话助手用 Store,编码工具用文件。
旧 memory 模块(了解)
LangChain 旧版有 langchain_classic/memory/ 模块(现已被 Store 取代,但概念有借鉴价值):
| 旧 memory 类 | 策略 | 对应现代方案 |
|---|---|---|
| ConversationBufferMemory | 全量保留 | Checkpoint + messages |
| ConversationBufferWindowMemory | 滑窗 k 条 | 消息裁剪 |
| ConversationSummaryMemory | LLM 滚动摘要 | compaction |
| VectorStoreRetrieverMemory | 向量检索记忆 | ★ Store 语义搜索(一脉相承) |
| ConversationEntityMemory | 抽取实体记忆 | 后台抽取 + Store |
这些"窗口/摘要/token/向量/实体"分类仍是记忆策略的经典框架。现代 LangGraph Store 是 VectorStoreRetrieverMemory 思想的进化。
与生产实践对照
| Store 概念 | OpenCode 对应 | |
|---|---|---|
| namespace 分层隔离 | → | 按 sessionID/projectID 隔离 |
| 语义搜索记忆 | → | 无(Agent 自己 grep/glob 搜文件) |
| API 即记忆(put/search) | → | 文件即记忆(read/write .md) |
| 跨会话持久 | → | .github/instructions/*.md 持久 |
| 记忆注入 prompt | → | instruction.resolve 自动收集 |
| PostgresStore 生产 | → | SQLite/文件系统 |
小结
- Store = 跨会话长期记忆(namespace + key),与 Checkpoint(短期)互补。
- namespace 元组分层隔离(用户/助手/类型);IndexConfig 开启语义搜索。
- 记忆策略:显式 remember/recall(本章)/ 后台抽取(生产常用)/ 自动注入。
- 生产用 PostgresStore(pgvector);两层记忆(Checkpoint+Store)协作。
- 两范式:Store(API 即记忆,语义检索)vs OpenCode(文件即记忆,透明可编辑)。
记忆系统讲完。下一个主题 AD7 · 可观测与追踪——Agent 是黑盒,出问题难调试。精读 Callbacks 三层体系、LangSmith 追踪、stream_mode=debug,让 Agent 可观测。