【LangChain 1.x】11、长期记忆|Store 跨会话持久化与向量搜索

摘要 :上一篇的 checkpointer 是单 thread 内的短期记忆,切换会话就会丢失记忆。本篇介绍长期记忆:使用 StoreInMemoryStore)跨 thread 持久化用户偏好 / 画像,用 namespace+key 层级组织、context 传 user_id 做隔离、IndexConfig 接入 OpenAI embedding 实现语义召回(内置 mini-RAG),并区分语义 / 情景 / 程序三类记忆。配合 DeepSeek 实测,做一个跨会话召回偏好的 Agent。

前言

上一篇,我们介绍了 Agent 的短期记忆:checkpointer 让 Agent 跨 invoke 记住对话,但只在单 thread 内有效、切换会话就会丢失记忆。

传送门:【LangChain 1.x】10、短期记忆|checkpointer 持久化与上下文治理三件套

在实际开发场景中,有很多需要跨会话的记忆,比如:用户上次说过喜欢简洁回复,这次新开会话也应该记得;用户的画像、偏好、历史订单,不应该每次都重头来。这就需要用到长期记忆。

本篇,介绍 Agent 的长期记忆:

  • 使用 StoreInMemoryStore)跨 thread 持久化数据
  • namespace + key 层级组织、按 user_id 隔离
  • 工具经 runtime.store 读写、用 context 传 user_id
  • 向量搜索IndexConfig + embed)语义召回,内置 mini-RAG
  • 实战:跨会话偏好记忆 Agent,区分三类记忆

一、长期记忆与 Store:跨会话的持久化

短期 vs 长期:checkpointer 和 Store 的分工

上一篇中的 checkpointer 是「单 thread 内」的短期记忆:让 Agent 跨 invoke 记住对话,但状态绑定 thread_id,换一个会话(thread_id)就会重新开始。

长期记忆要解决的是「跨会话」:用户上次说过喜欢简洁回复,这次新开会话也应该记得。这就需要使用 Store,与 checkpointer 两者分工如下:

checkpointer Store
作用域 单 thread(会话内) 跨 thread(会话间)
存什么 Agent 状态(messages 等) 业务数据(偏好 / 画像 / 知识)
隔离维度 thread_id namespace(自定义)

Store 基础 API

Store 可以独立于 agent 使用,先测试一下 Store 的基础使用 put/get:

python 复制代码
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()

# 写:namespace=("users",) + key + 值字典
store.put(("users",), "user_123", {"name": "小明", "language": "中文"})

# 读:按 namespace + key 精确查找,返回 Item,.value 取字典
item = store.get(("users",), "user_123")
print(item.value)   # {'name': '小明', 'language': '中文'}
print(item.key)     # user_123

# namespace 是元组、可多层:("users", user_id) 天然按用户隔离
store.put(("users", "alice"), "profile", {"city": "北京", "hobby": "篮球"})
store.put(("users", "bob"), "profile", {"city": "上海", "hobby": "足球"})
print(store.get(("users", "alice"), "profile").value)  # {'city': '北京', 'hobby': '篮球'}
print(store.get(("users", "bob"), "profile").value)    # {'city': '上海', 'hobby': '足球'}
  • put(namespace, key, dict) 写入:namespace 是元组、key 是字符串、值是字典。
  • get(namespace, key) 读取:返回 Item,.value 取值字典。
  • namespace 是元组,可以做层级:("users", user_id) 这种写法天然按用户隔离。

同一个 key(profile)放在不同 namespace 下,互不干扰:

css 复制代码
alice: {'city': '北京', 'hobby': '篮球'}
bob:   {'city': '上海', 'hobby': '足球'}

InMemoryStore 会存在内存里(适合开发/测试快速验证场景),生产环境要用 PostgresStore 等数据库落盘。

二、Store 跨 thread:短期各自独立、长期共享

光讲 store API 还看不出「跨 thread」,因为 store 是全局对象,put/get 和 thread_id 无关。要真正体现「store 跨 thread vs checkpointer 单 thread」,必须使用 agent:让同一个 agent 配置 store + checkpointer,使用两个不同 thread_id 调用,查看返回结果:

python 复制代码
store = InMemoryStore()
store.put(("users",), "alice", {"name": "Alice", "city": "北京"})

@tool
def get_user(user_id: str, runtime: ToolRuntime) -> str:
    """查用户信息(从 store 读)。"""
    item = runtime.store.get(("users",), user_id)
    return str(item.value) if item else f"未找到 {user_id}"

agent = create_agent(
    model=deepseek_llm, 
    tools=[get_user],
    store=store, 
    checkpointer=InMemorySaver(),
)

config1 = {"configurable": {"thread_id": "t1"}}
config2 = {"configurable": {"thread_id": "t2"}}

# 两个 thread 各自自我介绍
agent.invoke({"messages": [{"role": "user", "content": "我叫小明,请记住。"}]}, config=config1)
agent.invoke({"messages": [{"role": "user", "content": "我叫小红,请记住。"}]}, config=config2)

# 同样问名字 / 同样查 alice
agent.invoke({"messages": [{"role": "user", "content": "我叫什么名字?"}]}, config=config1)
agent.invoke({"messages": [{"role": "user", "content": "我叫什么名字?"}]}, config=config2)

agent.invoke({"messages": [{"role": "user", "content": "查一下 alice 的信息。"}]}, config=config1)
agent.invoke({"messages": [{"role": "user", "content": "查一下 alice 的信息。"}]}, config=config2)

两个 thread 各自做一次自我介绍(thread-1 说「小明」、thread-2 说「小红」),然后同样的问题对比:

arduino 复制代码
--- 同样问「我叫什么名字」---
  thread-1 回答: 你叫小明,我刚刚已经记住啦!
  thread-2 回答: 你叫小红呀!
--- 同样查 alice ---
  thread-1 查到: Alice / 北京
  thread-2 查到: Alice / 北京
  • 问名字:thread-1 答「小明」、thread-2 答「小红」,说明短期记忆(checkpointer)按 thread 隔离。
  • 查 alice:两个 thread 都查到了,说明长期记忆(store)跨 thread 共享。

这就是 Store 相比 checkpointer 的核心价值:短期记忆各自独立、长期记忆全局共享

三、工具读写 Store:context 动态隔离

在前面的介绍中,工具读取 store 时 user_id 是硬编码的(get_user("alice"))。

但在实际开发中 user_id 应该是运行时传入的,这就是 context 的作用:在 invoke 调用模型时,可以通过 context 传 user_id,在工具中通过 runtime.context.user_id 获取参数值。

python 复制代码
from dataclasses import dataclass
from typing_extensions import TypedDict

@dataclass
class UserContext:
    user_id: str   # 运行时传入的当前用户 ID

class UserInfo(TypedDict):
    name: str
    city: str

@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[UserContext]) -> str:
    """保存当前用户的信息到长期记忆。"""
    runtime.store.put(("users",), runtime.context.user_id, dict(user_info))
    return f"已保存用户 {runtime.context.user_id} 的信息。"

@tool
def get_user_info(runtime: ToolRuntime[UserContext]) -> str:
    """从长期记忆读取当前用户的信息。"""
    item = runtime.store.get(("users",), runtime.context.user_id)
    return str(item.value) if item else f"暂无用户 {runtime.context.user_id} 的信息。"

agent = create_agent(
    model=deepseek_llm, 
    tools=[save_user_info, get_user_info],
    store=store, 
    context_schema=UserContext,
)

# alice、bob 各自保存自己的信息
agent.invoke(
    {"messages": [{"role": "user", "content": "我叫小明,住在北京。请记住。"}]},
    context=UserContext(user_id="alice"),
)
agent.invoke(
    {"messages": [{"role": "user", "content": "我叫小红,住在上海。请记住。"}]},
    context=UserContext(user_id="bob"),
)

# 各自查自己的信息
agent.invoke(
    {"messages": [{"role": "user", "content": "查一下我的个人信息。"}]},
    context=UserContext(user_id="alice"),
)
agent.invoke(
    {"messages": [{"role": "user", "content": "查一下我的个人信息。"}]},
    context=UserContext(user_id="bob"),
)

要点:

  • context_schema=UserContext 声明上下文结构(前面第8篇介绍过)。
  • invoke 时 context=UserContext(user_id=...) 作为关键字参数传入。
  • 工具通过 runtime.context.user_id 读取当前用户,runtime.store.put/get 读写长期记忆。
  • ToolRuntime[UserContext] 泛型写法:让 IDE 知道 context 的类型(官方推荐)。

alice 和 bob 各自保存各自查询,数据按 user_id 天然隔离:

css 复制代码
--- alice 查 ---
  回答: 姓名:小明 / 所在城市:北京
--- bob 查 ---
  回答: 姓名:小红 / 所在城市:上海
  store 里 alice: {'name': '小明', 'city': '北京'}
  store 里 bob:   {'name': '小红', 'city': '上海'}

同一个 agent、同一套工具,靠 context 传不同 user_id 就能服务多个用户,数据互不串台。

四、向量搜索:内置 mini-RAG

store 的 get 方法是「精确查找」,必须知道完整的 key 才可以获取到信息。

但在实际开发中,更多场景需要做「模糊查找」,比如:用户问「运动爱好」,要召回「我喜欢打篮球」那条记忆,key 里没有「运动」但语义相关。这时可以使用 store 的向量搜索 store.search(query=...)

原理:store 接入 IndexConfig(embed 函数 + 维度),put 时会自动给每条记忆做 embedding;search 时会对 query 做 embedding,并按相似度排序召回。这是 store 内置的 mini-RAG,不用单独搭向量库。

python 复制代码
from langgraph.store.base import IndexConfig

def embed(texts):
    """文本列表 → 向量列表(用 OpenAI embedding)"""
    return openai_embeddings.embed_documents(texts)

# store 接入 IndexConfig:embed 函数 + 维度
store = InMemoryStore(index=IndexConfig(embed=embed, dims=1536))
namespace = ("users", "alice", "notes")

# 存几条笔记(put 时自动算 embedding)
for key, text in {
    "sport": "我喜欢打篮球,每周都去球场。",
    "movie": "我爱看电影,尤其是科幻片。",
    "pet":   "我养的猫叫橘子,今年三岁。",
    "tech":  "最近在学 LangChain 做 Agent。",
}.items():
    store.put(namespace, key, {"text": text})

# 语义查找(按相似度排序,score 越大越相关)
for it in store.search(namespace, query="有什么运动爱好?"):
    print(f"[{it.score:.3f}] {it.value['text']}")

三个要点:

  • IndexConfig(embed=, dims=) 来自 langgraph.store.base
  • embed 函数:文本列表→向量列表。这里使用 OpenAI 的 text-embedding-3-small(1536 维)。
  • store.search(namespace, query=...) 按相似度排序返回,每个 item 带 .score(越大越相关)。

存几条不同主题的笔记,用语义查找召回:

erlang 复制代码
query: "有什么运动爱好?"
  [0.461] 我喜欢打篮球,每周都去球场。
  [0.332] 我爱看电影,尤其是科幻片。
  ...
query: "养了什么宠物?"
  [0.423] 我养的猫叫橘子,今年三岁。
  ...

每个 query 最相关的都排在第一(score 最高)。对比 getsearch

  • get 要知道完整 key,精确但死板。
  • search 靠语义相似度,适合「只记得大概、不知道 key」的模糊查找。

五、实战:跨会话偏好记忆 Agent

把本篇中前面介绍的能力综合起来,做一个「记住用户偏好、跨会话召回」的 Agent:checkpointer(短期)+ store 带 IndexConfig(长期)+ save_preference / recall_preferences 工具 + context。

python 复制代码
@tool
def save_preference(preference: str, runtime: ToolRuntime[UserContext]) -> str:
    """把用户的一条偏好存入长期记忆。"""
    ns = ("users", runtime.context.user_id, "preferences")
    runtime.store.put(ns, preference[:20], {"text": preference})
    return f"已记住偏好:{preference}"

@tool
def recall_preferences(query: str, runtime: ToolRuntime[UserContext]) -> str:
    """按语义召回当前用户的相关偏好。"""
    ns = ("users", runtime.context.user_id, "preferences")
    items = runtime.store.search(ns, query=query)
    ...

agent = create_agent(
    model=deepseek_llm,
    tools=[save_preference, recall_preferences],
    store=InMemoryStore(index=IndexConfig(embed=embed, dims=1536)),
    checkpointer=InMemorySaver(),
    context_schema=UserContext,
    system_prompt=SYSTEM_PROMPT,
)

# 会话1:用户说偏好 → save_preference 存入 store
agent.invoke(
    {"messages": [{"role": "user", "content": "我喜欢简洁直接的回复,讨厌啰嗦。请记住。"}]},
    config={"configurable": {"thread_id": "session-1"}},
    context=UserContext(user_id="alice"),
)
# 会话2(新会话):召回偏好、按偏好回复
agent.invoke(
    {"messages": [{"role": "user", "content": "介绍一下 Python。"}]},
    config={"configurable": {"thread_id": "session-2"}},
    context=UserContext(user_id="alice"),
)

演示:会话1 用户说偏好 → save_preference 存入 store;会话2(新 thread_id)→ recall_preferences 召回偏好、按偏好回复。

ini 复制代码
--- 会话 1:用户表达偏好 ---
  [工具] save_preference 被调用:'喜欢简洁直接的回复,讨厌啰嗦。'
  store 里的偏好: ['喜欢简洁直接的回复,讨厌啰嗦。']

--- 会话 2(新会话):介绍 Python ---
  [工具] recall_preferences 被调用:query='Python 编程 语言'
  [工具] 召回 1 条:['喜欢简洁直接的回复,讨厌啰嗦。']
  回答: (按「简洁」偏好回复 Python 介绍)

会话2 是全新会话(不同 thread_id),但通过 store 召回了会话1 存的偏好,这就是长期记忆的价值。工具日志 [工具] ... 被调用 能清楚看到 save / recall 都触发了。

三类记忆

长期记忆在概念上分三类(Memory 概念指南):

类型 含义 本篇对应
语义记忆(semantic) 事实 / 知识:用户是谁、喜欢什么 store 存的偏好
情景记忆(episodic) 过往事件:之前的对话经历 checkpointer 的会话历史
程序记忆(procedural) 技能 / 规则:怎么做事 system_prompt、工具用法

一个完整的记忆系统往往三类都有:语义记忆存用户画像、情景记忆存交互历史、程序记忆存行为规则。

六、总结

本篇,把 Agent 的长期记忆梳理了一遍:

  • 长期记忆 = Store:跨 thread 持久化,和 checkpointer(单 thread)分工。
  • namespace + key:元组 namespace 层级组织,按 user_id 隔离。
  • 工具读写runtime.store 配合 context(传 user_id),同一套工具服务多用户。
  • 向量搜索IndexConfig + embed,store.search 语义召回,内置 mini-RAG。
  • 三类记忆:语义(偏好)、情景(对话)、程序(规则)。

一句话概括:短期记忆靠 checkpointer(单 thread)、长期记忆靠 Store(跨 thread);用 namespace 组织、context 隔离、向量搜索召回,再分清三类记忆的用途。

下一篇,继续介绍人机协同(Human-in-the-loop):让 Agent 在高风险动作前暂停、等人确认。

相关推荐
bloxed21 小时前
大模型应用-进阶核心技能【13课:第二阶段工程化与部署】
langchain·大模型应用
BraveWang1 天前
【LangChain 1.x】10、短期记忆|checkpointer 持久化与上下文治理三件套
langchain
zl_dfq1 天前
LangChain 之 【内存\Redis\Pinecone向量存储原理与实战】
langchain
吃饱了得干活1 天前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
Oo9201 天前
大模型是怎么随机说话的?—— Temperature、Top-k 与 LangChain 实战
langchain
一碗面4211 天前
LangSmith:LLM 应用调试与评估平台
langchain
中微极客1 天前
Agentic AI 2026学习路径:从LangChain到生产部署
人工智能·学习·langchain
陳陈陳2 天前
Workflow vs Agent:别再被“调包侠”忽悠了,一张图看懂AI工程的“骨架”与“大脑”
langchain·agent·workflow
一只小bit2 天前
LangGraph 记忆、人机交互、时间旅行和核心能力
机器学习·langchain·人机交互·langgraph