🧬 长期记忆 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(短期,AD5) 范围:单个会话/thread 内 绑定:thread_id 内容:对话历史 + 中间状态 问题:会话结束即"遗忘" Store(长期,本章)★ 范围:跨会话/跨 thread 绑定:namespace(如 ("users", uid)) 内容:画像/偏好/知识(★可语义搜索) 持久:跨会话保留,越用越懂用户
图 AD6.1 · Checkpoint(会话内短期)vs Store(跨会话长期)
💡 为什么需要 Store

想象一个私人助手:用户上周说"我对花生过敏",今天问"推荐午餐"。如果只有 Checkpoint,Agent 不记得上周的话(不同会话/不同 thread)。Store 让 Agent 把"花生过敏"存进长期记忆,今天语义检索到。这就是"越用越懂你"的能力。

BaseStore 数据模型

Store 的核心是 namespace(命名空间)+ key 的两级寻址:

📄 langgraph/store/base/__init__.py:700 · BaseStore python
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 分层模型 python
# 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 的
Store 的 namespace 树 ("users",) ("users","alice") /preferences/diet ("users","bob") /preferences/diet ("assistants",) /config/... ("memories",) /facts/...
图 AD6.2 · namespace 元组形成分层树,天然隔离用户/助手/记忆类型

IndexConfig:开启语义搜索

Store 最强大的特性——原生语义搜索。配置 IndexConfig

📄 store/base/__init__.py:570 · IndexConfig python
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"] 只索引某字段
📄 store/memory/__init__.py:136 · InMemoryStore 开启语义搜索 python
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。

📄 ad6_user_memory.py · 跨会话用户画像 python
# 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

后台抽取模式(更智能)

📄 后台记忆抽取(生产常用模式) python
# 不依赖 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 语义搜索),跨进程持久化:

📄 PostgresStore 生产部署 python
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 后拼 promptinstruction.resolve 自动沿目录收集
透明度低(黑盒数据库)★高(人可直接看/编辑 .md 文件)
适合结构化数据、用户画像非结构化、协作、可审计

OpenCode 的文件式记忆

📄 session/prompt/beast.txt:113 · OpenCode 的 Memory 指令
# 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 条消息裁剪
ConversationSummaryMemoryLLM 滚动摘要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 持久
记忆注入 promptinstruction.resolve 自动收集
PostgresStore 生产SQLite/文件系统

小结

下一主题 · 可观测与生产

记忆系统讲完。下一个主题 AD7 · 可观测与追踪——Agent 是黑盒,出问题难调试。精读 Callbacks 三层体系、LangSmith 追踪、stream_mode=debug,让 Agent 可观测。