# LangChain Memory 运行流程与分类:从旧版组件到当前记忆架构

在 LLM 应用里,“记忆”听起来像一个单独组件,实际更像一套上下文读写机制。

用户发来问题之前,系统要决定从哪里加载历史;模型回答之后,系统要决定保存什么;下一轮请求到来时,系统又要把合适的历史、摘要、偏好或状态放回上下文。这个过程如果没有设计好,聊天机器人就会出现两类常见问题:该记的记不住,不该记的乱记一堆。

LangChain 早期提供过一批 Memory 组件,例如 ConversationBufferMemory、ConversationBufferWindowMemory、ConversationSummaryMemory。这些组件很适合理解记忆模式,但对新项目来说,不能再把它们当成唯一主线。当前 LangChain Agent 的短期记忆更推荐使用 LangGraph checkpointer 和 thread_id;长期记忆则使用 store、namespace 和 key 做跨会话持久化。

所以这篇会分两层讲:

  • 先讲旧版 Memory 组件的运行流程和分类,因为它能帮助理解记忆系统的底层动作。
  • 再讲当前生产项目里应该如何把这些模式迁移成 state、checkpointer、store、摘要、裁剪和长期记忆治理。

# Memory 到底做了什么

一次带记忆的调用,通常包含两个阶段。

第一个阶段发生在模型调用前:读取历史,把历史转换成 Prompt 变量。

第二个阶段发生在模型调用后:保存本轮输入和输出,让下一轮可以使用。

可以抽象成这样:

用户输入
  -> load memory variables
  -> Prompt
  -> Model
  -> OutputParser
  -> save context
  -> 下一轮继续使用
1
2
3
4
5
6
7

这也是旧版 BaseMemory 的核心思想:通过 load_memory_variables 把记忆加载到链路输入里,通过 save_context 把本轮上下文写回记忆。

Memory 运行流程

从流程图可以看到,Memory 并不是替模型“自动记住”东西,而是在模型调用前后做上下文搬运:调用前把 chat_history 等变量注入 Prompt,调用后把用户输入和模型输出保存回记忆容器。

# 旧版 Memory 的核心接口

早期 Memory 组件大多围绕几个接口工作:

  • memory_variables:告诉 chain 会额外注入哪些变量。
  • load_memory_variables:在调用前加载记忆变量。
  • save_context:在调用后保存输入输出。
  • clear:清空记忆。
  • 对应异步方法:例如 aload_memory_variables、asave_context、aclear。

这些接口的设计非常直观,但也有天然局限:它更适合线性 Chain,而不是复杂 Agent 状态机。

例如一个普通问答链路可以使用:

memory_variables = ["chat_history"]

inputs = {
    "question": "我叫什么?"
}

memory_values = memory.load_memory_variables(inputs)

chain_inputs = {
    **inputs,
    **memory_values,
}

answer = chain.invoke(chain_inputs)

memory.save_context(
    inputs={"question": "我叫什么?"},
    outputs={"answer": answer},
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个流程说明了 Memory 的本质:它负责在 chain 输入和外部历史之间做桥接。

# 旧版组件分类视角

旧版 LangChain Memory 组件可以按职责分成几类。

旧版 Memory 组件分类

这张图适合用来理解历史 API 的分类,但新项目不要直接照着类名选型。尤其是 ConversationBufferMemory 这类组件,在当前 LangChain 体系里更应该被理解成一种“模式”,而不是默认实现。

常见模式包括:

模式 核心思路 适合场景 主要风险
Buffer memory 保存完整对话历史 短会话、本地验证 历史无限增长
Window memory 只保留最近 N 轮 普通聊天、成本敏感 早期关键信息被裁掉
Token buffer memory 按 token 上限裁剪 上下文预算明确 需要 token 统计
Summary memory 用摘要压缩早期历史 长会话、任务连续推进 摘要可能失真
Summary buffer memory 摘要 + 最近原文 较长对话 实现和评估复杂
Entity memory 围绕实体记录信息 CRM、项目助手 实体消歧和冲突处理
Vector memory 语义检索历史片段 回忆相似事件 不适合精确偏好更新

这些分类现在仍然有价值,但价值在于帮助你设计记忆策略,而不是强迫你使用旧类。

# Buffer memory:最简单,也最危险

Buffer memory 会把完整对话历史保存下来,每次调用时全部塞回 Prompt。

优点是简单:

history = [
    ("human", "我叫 Alice。"),
    ("ai", "你好 Alice。"),
    ("human", "我喜欢 Python 示例。"),
]
1
2
3
4
5

缺点也非常明显:

  • token 成本不断增长。
  • 历史越长,模型越容易被无关内容干扰。
  • 过期信息会持续影响回答。
  • 敏感内容可能被反复带入模型调用。

生产里,Buffer memory 只适合短会话或本地验证。只要会话可能持续很多轮,就要加入窗口、摘要、裁剪或结构化状态。

# Window memory:保留最近上下文

Window memory 只保留最近 N 轮消息。

def keep_recent_turns(messages: list[dict], max_turns: int = 8) -> list[dict]:
    return messages[-max_turns * 2:]
1
2

这种方式适合大多数普通聊天,因为用户当前问题通常依赖最近几轮。

但它的风险是:关键背景可能出现在很早之前。例如用户第一轮说明“这个项目是金融系统,必须保守回答”,后面如果窗口裁掉这句话,模型就可能忘记重要约束。

所以 Window memory 更适合和摘要或结构化状态配合:

长期摘要 + 最近 N 轮消息 -> 当前上下文
1

# Token buffer memory:按预算裁剪

按轮数裁剪不够精确,因为每轮消息长度差异很大。

一条工具返回可能几千 token,一句普通回答只有几十 token。Token buffer memory 的思路是按 token 上限保留历史。

伪代码:

def trim_by_token_budget(messages, count_tokens, max_tokens: int):
    selected = []
    total = 0

    for message in reversed(messages):
        tokens = count_tokens(message.content)
        if total + tokens > max_tokens:
            break

        selected.append(message)
        total += tokens

    return list(reversed(selected))
1
2
3
4
5
6
7
8
9
10
11
12
13

生产里上下文预算应该拆开:

  • system prompt 预算。
  • 当前用户问题预算。
  • RAG 上下文预算。
  • 历史消息预算。
  • 工具结果预算。
  • 输出 token 预算。

不要让历史消息挤掉真正回答问题所需的 RAG 上下文。

# Summary memory:把历史压缩成摘要

Summary memory 会把较早对话压缩成摘要。

例如:

较早对话摘要:
用户正在开发 Flask 后端项目,使用 SQLAlchemy 和 Alembic 管理数据库。
用户偏好 Python 示例,希望回答更偏生产实践。
1
2
3

然后再加最近几轮原文:

较早对话摘要 + 最近 6 轮消息 + 当前问题
1

摘要方式适合长会话,但要注意三个问题。

第一,摘要可能丢信息。模型压缩历史时会省略一些它认为不重要、但业务上重要的细节。

第二,摘要可能带偏见。模型可能把用户临时表达误写成稳定偏好。

第三,摘要要可追踪。最好保存摘要覆盖的消息范围、生成时间、模型名和摘要版本。

生产里可以把摘要当成派生数据,而不是唯一事实来源。原始消息仍然要按保留策略归档。

# Entity memory:围绕实体组织上下文

Entity memory 不以消息轮次为中心,而是以实体为中心。

例如用户、项目、订单、系统、服务、客户都可以是实体。

{
  "entity_type": "project",
  "entity_id": "billing-service",
  "facts": {
    "language": "Python",
    "framework": "Flask",
    "database": "PostgreSQL",
    "migration_tool": "Alembic"
  }
}
1
2
3
4
5
6
7
8
9
10

适合:

  • 企业助手。
  • 项目助手。
  • CRM 助手。
  • 工单助手。
  • 多对象、多关系场景。

难点是实体识别和冲突处理。例如“支付系统”“支付服务”“billing-service”到底是不是同一个对象,需要明确的实体消歧策略。

# Vector memory:用语义检索回忆历史

Vector memory 会把历史消息、事件摘要或处理结论写入向量库,在需要时按语义召回。

适合:

  • “上次类似问题怎么处理的?”
  • “之前我们讨论过的迁移方案是什么?”
  • “有没有相似的故障案例?”

不适合:

  • 精确用户偏好。
  • 权限规则。
  • 最新状态。
  • 必须强一致的数据。

向量库擅长相似召回,不擅长精确更新。用户偏好、项目配置、权限状态应该放在结构化存储里,而不是只写向量库。

# 当前 LangChain 应该怎么做

当前 LangChain 里,记忆可以按 scope 拆成两类。

短期记忆是 thread-scoped memory,作用于一个会话线程。Agent 场景中,通常通过 checkpointer 保存 state。

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver


agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": "thread_001"}}

agent.invoke(
    {"messages": [{"role": "user", "content": "我叫 Alice。"}]},
    config,
)

agent.invoke(
    {"messages": [{"role": "user", "content": "我叫什么?"}]},
    config,
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

长期记忆是 cross-thread memory,作用于用户、租户、项目或组织。它应该通过 store 按 namespace 和 key 保存结构化数据。

namespace = ("user_memories", user_id)
key = "preferred_code_language"
value = {
    "language": "python",
    "source": "user_explicit",
}

store.put(namespace, key, value)
1
2
3
4
5
6
7
8

这和旧版 ConversationBufferMemory 不是一回事。旧版 Memory 更多关注 chain 调用前后的上下文变量;当前 Agent 记忆更关注 state、checkpoint、store、namespace 和上下文工程。

# 新旧模式怎么映射

可以这样理解迁移关系:

旧版模式 当前更推荐的设计
ConversationBufferMemory thread state 中的 messages
ConversationBufferWindowMemory 消息裁剪 / summarization middleware
ConversationTokenBufferMemory 基于 token budget 的上下文裁剪
ConversationSummaryMemory 会话摘要字段或 summary middleware
EntityMemory 结构化 store / 数据库实体表
VectorStoreRetrieverMemory 长期记忆向量检索 / 事件回忆
CombinedMemory 多来源 context engineering

如果你维护的是旧项目,可以逐步迁移,不需要一刀切重写。

迁移顺序建议:

  1. 先把记忆读写封装到自己的模块里,不让业务代码直接依赖旧类。
  2. 再区分短期会话历史和长期用户记忆。
  3. Agent 链路改用 checkpointer 管理 thread state。
  4. 长期偏好和事实迁入结构化 store。
  5. 相似历史事件再接向量检索。
  6. 最后加上摘要、裁剪、冲突检测和删除能力。

# 生产运行流程

生产里的记忆流程可以拆成读路径和写路径。

读路径:

收到请求
  -> 识别用户、租户、thread
  -> 读取短期 state
  -> 读取长期偏好和事实
  -> 检索相关历史事件
  -> 做 token 预算和排序
  -> 注入模型上下文
1
2
3
4
5
6
7

写路径:

模型完成回答
  -> 保存 thread state
  -> 抽取候选长期记忆
  -> 过滤敏感信息
  -> 做冲突检测
  -> 写入 store 或等待用户确认
1
2
3
4
5
6

读路径要快,不能拖慢用户请求。写路径可以异步,尤其是长期记忆抽取和冲突检测,不一定要阻塞本次回答。

# 记忆分类的生产边界

做分类时,不要只按组件名分,而要按业务边界分。

分类 生命周期 存储 注入方式
最近消息 当前 thread checkpointer / message table 直接作为 messages
会话摘要 当前 thread state / summary table system 或上下文字段
用户偏好 跨 thread structured store 按 key 读取
项目事实 跨 thread / project database 按 project_id 读取
历史事件 跨 thread vector store + metadata 语义召回
团队流程 长期 config / store 按任务类型读取

这样分类比“用了哪个 Memory 类”更接近生产。

# 常见坑

# 把历史消息等同于记忆

历史消息只是原始材料。真正的长期记忆应该经过抽取、确认、去重和更新。

# 每轮都把全部记忆注入 Prompt

记忆召回要按任务相关性和 token 预算控制。无关记忆越多,模型越容易分心。

# 用向量库保存所有东西

向量库不是万能记忆库。偏好、权限、配置、最新状态更适合结构化存储。

# 不处理冲突

用户偏好会变,项目事实会变,历史结论也可能被推翻。只追加不更新,会让记忆越来越脏。

# 没有删除能力

用户要求清空记忆时,系统必须能做到。没有删除能力的记忆系统不适合生产。

# 还在新项目里直接照搬旧 Memory 类

旧类适合理解模式和维护旧项目。新 Agent 应用应优先使用 checkpointer、state、store 和上下文工程。

# 生产 Checklist

上线前至少检查:

  • 是否区分短期记忆和长期记忆。
  • 是否区分 messages、summary、preferences、events、procedures。
  • 是否有 thread_id / user_id / tenant_id 隔离。
  • 是否有 token 预算和裁剪策略。
  • 是否有摘要生成和摘要版本。
  • 是否有长期记忆写入白名单。
  • 是否有敏感信息过滤。
  • 是否有冲突检测和更新策略。
  • 是否支持删除和过期。
  • 是否能追踪某次回答使用了哪些记忆。
  • 是否能统计记忆命中率、错误率和成本。

这些做好以后,记忆模块才不是一个“把历史拼进去”的小功能,而是一套可治理的上下文系统。

# 小结

LangChain 旧版 Memory 组件的核心流程,是在模型调用前加载记忆变量,在模型调用后保存上下文。Buffer、Window、Token Buffer、Summary、Entity、Vector 等模式,仍然适合理解记忆系统的设计空间。

但当前生产项目不能只停在旧类上。Agent 短期记忆更适合用 checkpointer 管理 thread state;长期记忆更适合用 store、namespace、key、向量检索和结构化数据库协同实现。

真正好的记忆系统,不是“记得越多越好”,而是能在合适的时间读取合适的上下文,并且能更新、删除、审计和解释。这样 AI 助手才会越用越顺,而不是越记越乱。

# 问题

旧版 Memory 分类的问题,是它容易让人按类名选型,而不是按业务边界设计。

ConversationBufferMemory、ConversationSummaryMemory、ConversationEntityMemory 这些名字看起来像可直接拼装的模块,但生产系统真正要解决的是:消息怎么保存、上下文怎么裁剪、摘要怎么评估、长期事实怎么治理、用户如何删除记忆、某次回答到底用了哪些记忆。

如果只套旧组件,常见问题包括:

  • 短期历史和长期事实混在一起。
  • 全量历史进入 Prompt,成本和干扰不断上升。
  • 摘要、实体、向量记忆缺少更新和冲突处理。
  • 多用户、多租户、多 thread 隔离不清晰。
  • 记忆写入不可解释,出了错很难回放。

所以 1-6 这篇的重点不是“哪个类怎么调用”,而是理解记忆读写流程和分类边界。

# 拓展

可以把 Memory 分类拓展成一套上下文系统:

  • messages:当前 thread 的原始消息。
  • summary:当前 thread 的滚动摘要。
  • facts:跨 thread 的结构化事实。
  • preferences:用户稳定偏好。
  • events:可检索的历史事件。
  • procedures:团队流程、回答风格、业务规则。

新版架构里,这些内容不一定由一个 Memory 类管理,而是由 checkpointer、Store、数据库、向量库、middleware 和业务服务共同完成。

更接近生产的流程是:

请求进入
  -> 读取 thread state
  -> 读取长期 Store
  -> 检索相关事件
  -> 做 token 预算和上下文选择
  -> 调用模型
  -> 保存 checkpoint
  -> 异步抽取长期记忆
1
2
3
4
5
6
7
8

# 实际生产是否使用

会使用这些记忆模式,但不会按旧版 Memory 类原样堆系统。

生产里仍然会有 Buffer、Window、Token Buffer、Summary、Entity、Vector 这些策略,只是实现位置变了:短期状态交给 checkpointer,长对话压缩交给 middleware,长期事实交给 Store 或业务数据库,相似历史交给向量检索。

换句话说,生产使用的是这些“模式”,不是依赖这些旧“类”。

# 现在是否抛弃

没有完全抛弃,但旧版 Memory 组件已经不是新项目主线。

当前 LangChain v1 的官方主线是:

  • 短期记忆:create_agent + checkpointer + thread_id。
  • 消息裁剪:before_model middleware、trim_messages、RemoveMessage。
  • 摘要压缩:SummarizationMiddleware。
  • 长期记忆:Store 的 namespace/key JSON 数据。

旧版 Memory 类更多适合掌握原理、维护老项目和迁移过渡。

# 最新生产如何实现

最新版生产实现应该先拆 scope。

短期记忆:

from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver


with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup()

    agent = create_agent(
        model="gpt-5.5",
        tools=[],
        checkpointer=checkpointer,
    )

    agent.invoke(
        {"messages": [{"role": "user", "content": "我正在做 Flask 项目。"}]},
        {"configurable": {"thread_id": "tenant-1:user-42:chat-1001"}},
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

长期记忆:

namespace = ("tenants", tenant_id, "users")
store.put(
    namespace,
    user_id,
    {
        "preferences": {"answer_style": "production"},
        "facts": {"main_stack": "Python Flask"},
    },
)
1
2
3
4
5
6
7
8
9

上下文治理放在 middleware 或业务编排层:裁剪 messages、生成 summary、召回 Store、过滤敏感内容、记录 trace。这样记忆系统才可扩展、可删除、可观测。

参考: