# LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践

LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践

导读:你的 LangGraph Agent 每次对话都像"金鱼"------上一轮刚说完我叫张三,下一轮就忘了。问题不在模型,而在你没有用好 LangGraph 的 Memory 机制。LangGraph 提供两套记忆系统:Checkpointer 管理短期记忆(对话状态快照),Store 管理长期记忆(跨会话用户画像)。本文从 Thread/Checkpoint/Store 三个核心概念出发,完整拆解两种记忆的存储方式、读取路径和生产环境的持久化替换方案。

适合读者

  • LangGraph 应用对话"断片"、需要持久化记忆的开发者
  • 需要设计多用户隔离记忆系统的工程师
  • 正在把 InMemory 替换为 Postgres/Redis 的生产环境维护者
  • 准备 LangGraph 面试、需要系统回答"Memory怎么设计"的同学

阅读收益

  • 理解 Checkpointer 与 Store 的本质区别和适用场景
  • 掌握 thread_iduser_idnamespace 的设计原则
  • 学会 trim_messages 裁剪和摘要两种对话过长处理策略
  • 掌握生产环境替换 SQLite 为 Postgres/Redis 的方案
  • 获得可直接落地的记忆隔离检查清单

目录

  1. 为什么模型自己记不住
  2. [两套记忆系统:Checkpointer vs Store](#两套记忆系统:Checkpointer vs Store)
  3. [短期记忆:Thread 与 Checkpoint](#短期记忆:Thread 与 Checkpoint)
  4. [长期记忆:Store 与 namespace](#长期记忆:Store 与 namespace)
  5. [完整案例:跨 Thread 用户偏好](#完整案例:跨 Thread 用户偏好)
  6. [对话过长:裁剪 vs 摘要](#对话过长:裁剪 vs 摘要)
  7. 生产环境持久化替换
  8. 踩坑清单:Memory设计的8个关键问题
  9. 面试速答版
  10. 总结与延伸
  11. 文末互动

1. 为什么模型自己记不住

1.1 大模型是无状态的

复制代码
每次调用模型 API 都是独立的:

第一次调用:
  用户:"我叫张三"
  模型:"你好张三,有什么可以帮你的?"

第二次调用(完全独立,模型不知道之前说了什么):
  用户:"我叫什么?"
  模型:"我不知道你叫什么"

核心事实:大模型本身不会"记住"任何东西。每次请求都是无状态的,之前的对话不会自动带入下一次调用。

1.2 记忆必须靠外部系统维护

复制代码
LangGraph 的解法:

Checkpointer(短期记忆)
  → 保存每轮对话后的完整 State
  → 下一轮用相同 thread_id 自动恢复

Store(长期记忆)
  → 保存用户偏好、画像等跨会话信息
  → 不同 Thread 都能读取

2. 两套记忆系统:Checkpointer vs Store

2.1 极简对比

维度 Checkpointer(短期记忆) Store(长期记忆)
标识 thread_id namespace + key
保存内容 完整 State 快照 选出的关键信息
跨 Thread 不可以 可以
典型用途 对话消息、流程状态 用户偏好、用户画像
读取方式 图自动恢复 节点通过 Runtime 主动读取
生产替换 PostgresSaver / RedisSaver PostgresStore / RedisStore

2.2 一句话区分

复制代码
Checkpointer 记住"这个会话进行到哪里"
Store 记住"这个用户长期有什么信息"

2.3 两种记忆如何配合

复制代码
用户提问
    ↓
Checkpointer 加载当前 Thread 的历史消息
    ↓
Store 加载该用户的长期偏好(如"喜欢简洁回答")
    ↓
合并后组装 Prompt
    ↓
调用 LLM 生成回复
    ↓
Checkpointer 保存更新后的 State

3. 短期记忆:Thread 与 Checkpoint

3.1 Thread 是什么

python 复制代码
config = {
    "configurable": {
        "thread_id": "thread-001",  # 会话唯一标识
    }
}
复制代码
thread-001 → 用户A的第一条会话(有历史记录)
thread-002 → 用户A的第二条会话(全新,无历史)
thread-003 → 用户B的会话(与用户A隔离)

关键认知

  • Thread ≠ 用户。一个用户可以创建多个 Thread
  • 相同 thread_id 共享记忆;不同 thread_id 默认隔离
  • 即使同一个用户,换 thread_id 也换会话

3.2 Checkpoint 快照

复制代码
START → node_a → node_b → END
           ↓         ↓
        Checkpoint  Checkpoint

每个节点执行完后,LangGraph 自动保存 State 快照:
  - 当前 State 数据
  - 下一步待执行节点
  - Checkpoint ID 和父 Checkpoint
  - 创建时间和执行任务信息

3.3 Checkpointer 代码实现

python 复制代码
"""短期记忆:SQLite Checkpointer"""
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessagesState, StateGraph

# 1. 创建数据库连接
connection = sqlite3.connect(
    "checkpoints.sqlite",
    check_same_thread=False,  # 允许多线程使用同一连接
)

# 2. 创建 Checkpointer
checkpointer = SqliteSaver(connection)

# 3. 编译图时绑定
builder = StateGraph(MessagesState)
# ... 添加节点和边 ...
graph = builder.compile(checkpointer=checkpointer)

# 4. 每次调用必须提供 thread_id
config = {"configurable": {"thread_id": "thread-001"}}
result = graph.invoke(
    {"messages": [{"role": "user", "content": "我叫张三"}]},
    config=config,
)

# 5. 同一 thread_id 再次调用 → 自动加载历史
result2 = graph.invoke(
    {"messages": [{"role": "user", "content": "我叫什么?"}]},
    config=config,  # 相同 thread_id
)
# 模型回答:"你叫张三"(因为 Checkpointer 恢复了历史)

3.4 查看 Thread 状态

python 复制代码
# 获取指定 Thread 的最新状态快照
snapshot = graph.get_state(config)

print(snapshot.values)      # 当前 Checkpoint 的 State
print(snapshot.next)        # 下一步待执行节点(空元组表示结束)
print(snapshot.created_at)  # 创建时间
print(snapshot.metadata)    # 步骤、更新来源等信息

4. 长期记忆:Store 与 namespace

4.1 为什么需要 Store

复制代码
场景:用户说"记住:我喜欢简洁回答"

用 Checkpointer:
  → 保存在 thread-001 的 State 中
  → thread-002 看不到这条偏好
  → 新会话又忘了

用 Store:
  → 保存在 users/user-001/memory 下
  → thread-001、thread-002 都能读取
  → 长期有效

4.2 Store 的三元组定位

python 复制代码
# namespace + key → value
namespace = ("users", "user-001", "memories")
key = "preference"
value = {"content": "喜欢简洁回答"}

store.put(namespace, key, value)
元素 说明 示例
namespace 隔离路径,必须是元组 ("users", "user-001", "memories")
key namespace 内的唯一标识 "preference"
value 保存的数据,通常用字典 {"content": "喜欢简洁回答"}

4.3 读取 Store

python 复制代码
# 读取单条记忆
item = store.get(namespace, "preference")
print(item.value if item else None)
# 输出:{"content": "喜欢简洁回答"}

# 搜索 namespace 下的全部记忆
items = store.search(namespace, limit=10)
for item in items:
    print(item.key, item.value)

4.4 完整案例:跨 Thread 用户偏好

python 复制代码
"""跨 Thread 长期记忆完整案例"""
from typing import TypedDict
from langchain_core.messages import SystemMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import MessagesState, StateGraph
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore


class ContextState(TypedDict):
    user_id: str


def call_model(state: MessagesState, runtime: Runtime[ContextState]):
    """节点通过 Runtime 访问 Store"""
    last_message = state["messages"][-1].content
    user_id = runtime.context["user_id"]

    # 构建 namespace
    namespace = ("users", user_id, "memory")

    # 处理"记住"指令
    if last_message.startswith("记住:"):
        preference = last_message[3:]
        runtime.store.put(namespace, "preference", preference)

    # 读取长期记忆
    items = runtime.store.search(namespace, limit=5)
    memory_text = ",".join([item.value for item in items])

    # 组装 Prompt
    prompt = f"""
    你是一个客服助手。
    以下是当前用户的长期记忆:{memory_text}
    回答时可以自然参考记忆。
    """

    # 调用 LLM
    response = llm.invoke([
        SystemMessage(content=prompt),
        *state["messages"],
    ])

    return {"messages": [response]}


# 构建图
builder = StateGraph(MessagesState, context_schema=ContextState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")

# 同时绑定 Checkpointer 和 Store
graph = builder.compile(
    checkpointer=InMemorySaver(),  # 短期记忆
    store=InMemoryStore(),          # 长期记忆
)

# 用户1,thread-001:记住偏好
user_1 = ContextState(user_id="user-001")
graph.invoke(
    {"messages": [{"role": "user", "content": "记住:我喜欢简洁回答。"}]},
    config={"configurable": {"thread_id": "thread-001"}},
    context=user_1,
)

# 用户1,thread-002:仍然知道偏好
result = graph.invoke(
    {"messages": [{"role": "user", "content": "请介绍一下LangGraph?"}]},
    config={"configurable": {"thread_id": "thread-002"}},  # 不同 Thread!
    context=user_1,
)

# 用户2,thread-003:看不到用户1的记忆
user_2 = ContextState(user_id="user-002")
result = graph.invoke(
    {"messages": [{"role": "user", "content": "请介绍一下LangGraph?"}]},
    config={"configurable": {"thread_id": "thread-003"}},
    context=user_2,
)

4.5 关键对象关系图

复制代码
Context(user_id)
    ↓ 提供当前用户身份
Runtime[Context]
    ↓ 节点读取 runtime.context 和 runtime.store
namespace = ("users", user_id, "memories")
    ↓ 隔离不同用户的长期记忆
InMemoryStore
    ↓ 保存跨 Thread 数据

注意thread_iduser_id 不能混用!

  • thread_id → Checkpointer 用,标识一次会话
  • user_id → Store 用,组成 namespace 隔离用户

5. 完整案例:跨 Thread 用户偏好

复制代码
执行流程:

Step 1: user-001, thread-001
  用户:"记住:我喜欢简洁回答。"
  → Store 写入:("users", "user-001", "memory") / "preference" = "喜欢简洁回答"

Step 2: user-001, thread-002(不同 Thread!)
  用户:"请介绍一下LangGraph?"
  → Store 读取:找到 "喜欢简洁回答"
  → Prompt 包含偏好信息
  → 模型回答简洁

Step 3: user-002, thread-003(不同用户!)
  用户:"请介绍一下LangGraph?"
  → Store 读取:namespace 是 ("users", "user-002", "memory")
  → 找不到偏好
  → 模型用默认风格回答

6. 对话过长:裁剪 vs 摘要

6.1 问题:上下文窗口有限

复制代码
Checkpointer 会保存全部历史消息,但模型的上下文窗口有上限:

GPT-4: 128K tokens
Claude 3: 200K tokens
DeepSeek: 64K tokens

对话轮次多了以后,历史消息会超过窗口上限,导致:
  → 后面的消息被截断
  → 早期重要信息丢失
  → Token 费用暴涨

6.2 方案一:裁剪消息(trim_messages)

python 复制代码
from langchain_core.messages import trim_messages
from langgraph.graph import MessagesState


def call_model_with_trim(state: MessagesState):
    # 裁剪历史:只保留最近的消息
    recent_messages = trim_messages(
        state["messages"],        # 全部历史消息
        strategy="last",          # 保留最近,删除旧的
        token_counter=len,        # 用字符长度估算(生产环境用真实 tokenizer)
        max_tokens=4000,          # 保留的总长度上限
        start_on="human",       # 裁剪后第一条必须是 human
        include_system=True,      # SystemMessage 强制保留
    )

    response = model.invoke(recent_messages)
    return {"messages": [response]}

裁剪策略对比

策略 说明 适用场景
last 保留最近,删除旧的 近期信息更重要
first 保留开头,删除后面的 开头有系统指令

6.3 方案二:摘要历史

复制代码
历史消息达到阈值
    ↓
LLM 生成历史摘要(保留关键事实)
    ↓
把摘要保存到 State
    ↓
后续请求使用"摘要 + 最近消息"
python 复制代码
def summarize_history(state: MessagesState):
    """当消息过多时,生成摘要替代完整历史"""
    old_messages = state["messages"][:-10]  # 较早的消息
    recent_messages = state["messages"][-10:]  # 最近10条保留

    # 让模型生成摘要
    summary_prompt = "请总结以下对话的关键信息:"
    summary = llm.invoke([
        SystemMessage(content=summary_prompt),
        *old_messages,
    ])

    # 用摘要 + 最近消息替代完整历史
    return {
        "messages": [
            SystemMessage(content=f"历史摘要:{summary.content}"),
            *recent_messages,
        ]
    }

6.4 两种方案对比

方案 优点 缺点 适用场景
裁剪 简单、快、不调用LLM 可能丢失早期关键信息 对话较简短、近期信息更重要
摘要 保留关键事实 需要额外LLM调用、摘要可能遗漏细节 长对话、早期有重要上下文

关键建议:订单号、权限、金额等关键业务数据不要只依赖摘要,应保存在结构化 State 或 Store 中。


7. 生产环境持久化替换

7.1 当前配置的局限性

python 复制代码
# 当前:SQLite + InMemoryStore
SqliteSaver → SQLite 文件    # 进程重启后 Checkpoint 仍在
InMemoryStore → 内存         # 进程结束后数据丢失!

7.2 生产环境推荐方案

需求 开发环境 生产环境
短期记忆 Checkpointer SqliteSaver PostgresSaver / RedisSaver
长期记忆 Store InMemoryStore PostgresStore / RedisStore / MongoDBStore

7.3 替换代码示例

python 复制代码
"""生产环境:PostgreSQL 持久化"""
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore

# 短期记忆:PostgresSaver
pg_connection = "postgresql://user:pass@localhost:5432/langgraph"
checkpointer = PostgresSaver(conn=pg_connection)

# 长期记忆:PostgresStore
store = PostgresStore(conn=pg_connection)

graph = builder.compile(
    checkpointer=checkpointer,
    store=store,
)

8. 踩坑清单:Memory设计的8个关键问题

序号 问题 现象 原因 解决方案
1 thread_id 混用 不同用户看到对方的对话历史 thread_id 没隔离 每个用户独立 thread_id
2 user_id 缺失 长期记忆无法隔离用户 没传 Context 在 State/Context 中绑定 user_id
3 Store 用 InMemory 进程重启后记忆全丢 开发配置没替换 生产环境换 Postgres/Redis
4 对话过长不处理 Token 费用暴涨,早期信息丢失 没做 trim/summarize 设 max_tokens 上限,超限时裁剪或摘要
5 敏感信息存 Store 用户隐私泄露风险 Store 没做权限控制 namespace 中加入权限层级
6 记忆冲突 同一 key 被覆盖,旧记忆丢失 put 直接覆盖 用不同 key 或加时间戳版本
7 没区分短期/长期 所有信息都存在 Checkpointer 设计不清晰 对话历史→Checkpointer,偏好画像→Store
8 Checkpoint 文件膨胀 SQLite 文件越来越大 历史 Checkpoint 没清理 定期清理旧 Checkpoint

9. 面试速答版

LangGraph 有两套记忆系统:Checkpointer 管短期记忆(对话状态快照),Store 管长期记忆(跨会话用户画像)。Checkpointer 用 thread_id 标识会话,相同 thread_id 自动恢复历史;Store 用 namespace + key 定位记忆,可以跨 Thread 共享。节点通过 Runtime 读取 Context 和 Store。user_id 用于组成 namespace 隔离用户,thread_id 用于 Checkpointer 恢复会话,两者不能混用。对话过长时用 trim_messages 裁剪或 LLM 摘要。生产环境把 InMemoryStore 换成 Postgres/Redis,SQLite Checkpointer 换成 PostgresSaver。


10. 总结与延伸

10.1 核心知识点回顾

复制代码
两套记忆系统:
  Checkpointer:短期记忆,thread_id 标识,自动恢复会话
  Store:长期记忆,namespace+key 定位,跨 Thread 共享

设计原则:
  thread_id ≠ user_id
  对话历史 → Checkpointer
  用户偏好/画像 → Store
  敏感数据 → namespace 权限隔离

对话过长处理:
  裁剪(trim_messages):简单快速
  摘要:保留关键事实,需额外 LLM 调用

生产持久化:
  Checkpointer:SqliteSaver → PostgresSaver/RedisSaver
  Store:InMemoryStore → PostgresStore/RedisStore

10.2 延伸方向

  • 记忆压缩:用 Embedding 压缩历史消息,检索相关片段替代完整历史
  • 记忆分层:短期(5轮内)→ 中期(今日会话)→ 长期(用户画像)
  • 记忆遗忘:给记忆加 TTL,自动清理过期偏好
  • 记忆冲突解决:同一用户多条矛盾偏好时的优先级策略

11. 文末互动

你的 LangGraph 应用用的是 InMemoryStore 还是已经替换成数据库了?有没有遇到过进程重启后用户偏好全丢的坑?评论区聊聊你的 Memory 设计经验。

思考题:如果一个用户先说了"我喜欢简洁回答",后来又说"我喜欢详细解释",你的系统应该如何处理这两条矛盾偏好------直接覆盖、保留多条、还是让 LLM 判断优先级?欢迎在评论区讨论。


本文聚焦 LangGraph Memory 机制的工程实践。如果觉得有帮助,欢迎点赞收藏,后续会更新记忆压缩和分层管理的进阶内容。

相关推荐
ynchyong5 小时前
Python 字符组合生成器:product 与 permutations 的实战对比
python·工具·字符串生产
benchmark_cc5 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
粥里有勺糖5 小时前
视野修炼-技术周刊第131期 | Bun 与 pnpm Rust 化
前端·github·agent
Coodor5 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡
sel_95 小时前
【PEFT】参数高效微调(PEFT)技术详解:从原理到 LoRA/QLoRA 实战
人工智能·python·深度学习·算法·机器学习·参数高效微调
机器人猎头David5 小时前
机器人猎头公司(倍利福猎头公司)案例分享
人工智能·机器人·招聘·具身智能·人形机器人·猎头公司·机器人猎头公司
lilian2336 小时前
HarmonyOS 7 新特性(四)|沉浸光感:空间材质、性能分级与降级策略
前端·pytorch·华为·harmonyos·材质
月光船幽幽6 小时前
带刺反馈引擎:唤醒深层意图
人工智能·python
weixin_446260856 小时前
什么样的智能体数据才算优质?——基于ACE视角的大语言模型智能体数据生成研究
人工智能·语言模型·自然语言处理