「LangChain」LangChain 如何实现长短期记忆?

0 前言

本文内容整理自公众号「小林面试笔记」的文章 6. LangChain 如何实现短期记忆和长期记忆?,用于记录与总结 LangChain 框架相关的核心面试知识点。

LangChain 如何实现短期记忆和长期记忆?」,可以这样作答:

记住一条主线就够:短期记忆 = State + thread_id + Checkpointer,长期记忆 = namespace/key + Store。短期记忆属于当前会话线程——Agent State 保存消息、当前步骤和中间结果,Checkpointer 按 thread_id 保存状态快照,同一 thread_id 再次调用就能恢复对话与执行状态;长期记忆不绑定线程,存进 Store——namespace 通常由租户、用户、记忆类型组成,用户新建线程后,只要可信身份相同,仍能读取自己的长期偏好。模型只负责生成任务参数,用户 ID、租户、权限等可信身份由应用运行时经 ToolRuntime 注入;工具通过 runtime.state 读当前线程状态、runtime.store 读跨线程长期数据,两个入口不能混用。长对话还要治理上下文:裁剪、删除、摘要是三种不同策略,各有风险;长期记忆不是把聊天记录全存进去,写入要讲究时机(明确被要求记住的实时写,推断出的偏好后台提炼),并且做去重、脱敏、冲突处理。生产环境要盯四件事:数据库型持久化、可信身份、记忆治理(过期/纠错/隐私)和召回评测。

面试对话里藏着这道题的两个雷区:一是拿旧版 ConversationBufferMemoryConversationSummaryMemory 作答——那是 Chain 时代的抽象,LangChain v1 新项目讲的是 Checkpointer、Store、thread_id 和 namespace;二是把「能落盘」当成「一个就够」——Checkpointer 和 Store 作用域完全不同。前面几篇我们讲过架构里的 State/Context/Store 数据分工、构建步骤里的「补齐状态与安全」、工具注册里的 ToolRuntime 可信参数注入,这篇就把「记忆」这一环补完整:该记住什么、短期怎么存、长期怎么存、怎么跨线程读、怎么治理


1 先分清:该记住什么

记忆设计的第一步不是选数据库,而是定作用域——先回答「这条信息属于哪个作用域」。

举个杭州旅行的例子:对话里聊到的出行日期、预算、下一步计划,属于当前线程的短期状态——这场对话结束就过时了;而「不吃辣、喜欢住地铁附近」这种一周后新开一个会话仍然成立的信息,才是长期记忆

区分标准就一句话:短期记忆回答「这场对话进行到哪」,长期记忆回答「这个用户是谁」。对话中的进度、草稿是前者,用户身上稳定的偏好、经验、规则是后者:

短期记忆与长期记忆的作用域对比:线程内状态由 State 与 Checkpointer 管理,跨线程的长期信息由 Store 管理

把作用域定清楚后,两条主线就诞生了:

1
2
3
短期记忆 = State + thread_id + Checkpointer

长期记忆 = namespace/key + Store

2 短期记忆如何实现

create_agent 底层运行在 LangGraph 上,Agent State 是每次执行的核心载体:默认包含 messages,也可以扩展订单号、当前步骤、工具调用次数等业务字段。但要注意——State 在内存里跑,进程退出就没了。要让短期记忆跨进程、跨实例存活,需要 Checkpointer 持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver


# Checkpointer 按 thread_id 保存线程内的 Agent State
agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[],
checkpointer=InMemorySaver(),
)

# 两次调用复用同一个 thread_id,表示它们属于同一会话线程
config = {"configurable": {"thread_id": "chat-1001"}}

# 第一轮把用户姓名写入当前线程的 messages 状态
agent.invoke(
{"messages": [{"role": "user", "content": "我叫小林"}]},
config=config,
)

# 第二轮会先恢复同一线程之前保存的状态
result = agent.invoke(
{"messages": [{"role": "user", "content": "我叫什么?"}]},
config=config,
)

两个要点:一是 InMemorySaver 只适合本地演示,生产环境要换数据库型持久化实现;二是 Checkpoint 保存的是图执行中各步的 State 快照,这正是「暂停恢复、人工审批、故障恢复」这些能力的地基——上一篇文章构建步骤里说过的「游戏存档」就应验在这里:同一 thread_id 是同一个存档位,读档就接着跑。


3 消息太多怎么办

Checkpointer 能保存状态,不等于每次都要把全部历史全量喂给模型——消息越多,Token 成本、延迟和注意力干扰都越大。处理超长历史有三类策略,各有利弊:

策略 作用 主要风险
裁剪 只选择部分消息进入本次模型上下文 持久状态仍会继续增长
删除 从 State 中永久移除旧消息 信息不可恢复
摘要 把早期历史压缩成简短语义摘要 可能遗漏细节或逐轮失真

上下文治理三策略:裁剪只影响本次输入,删除不可恢复,摘要可能失真

更关键的是策略要跟业务挂钩,不能按固定条数一刀切:客服 Agent 要优先保护当前工单信息和给用户的承诺,代码 Agent 要保住最新的报错和修改记录——选择哪些消息留下,要综合 Token 预算、消息角色和业务重要性来判断。


4 长期记忆如何实现

这里先澄清一个容易误解的点:新的 thread_id 默认不继承旧线程的 State——这是刻意的线程隔离,不是缺陷。跨线程仍然要用的信息,需要被「提炼」出来写入 Store。

Store 的定位三要素:

1
2
3
4
5
namespace = (tenant_id, user_id, memory_type)

key = 某条记忆的稳定标识

value = JSON 数据
  • namespace:一组层级标签,通常按「租户、用户、记忆类型」组织,天然把不同租户、不同用户的数据隔开——这是防串读的第一道墙;
  • key:某条记忆的稳定标识,比如「当前偏好」记录固定用 main
  • value:记忆内容本身,JSON 格式。

读取时,已知 key 就精确读取;想按语义查找(「我记得他说过饮食相关的事」),可以给 Store 配向量索引做语义检索。但要记住:向量检索只是召回方式,不代表所有聊天记录都要变成长期记忆

那长期记忆里通常放什么?分类设计数据结构,而不是要求「全都保存」:

类型 示例
用户事实与偏好 不吃辣、偏好中文、常用 Java
历史经验 上次如何成功处理支付超时
工作规则 退款前必须核验订单归属

写入之前,还要过一遍「质检」:去重、脱敏、冲突处理、质量判断——长期记忆像写进档案室的档案,不是往流水账里记每一句话


5 如何跨线程读取

用户换一个会话(新 thread_id),怎么还能调出自己的偏好?答案在 ToolRuntime——它给工具三个入口,不能混成一个数据袋

  • runtime.state:当前线程的短期状态;
  • runtime.context:可信的用户身份与权限(由应用注入,模型不可见);
  • runtime.store:跨线程的长期数据。

模型只生成任务参数(比如偏好内容),user_id 由已认证的应用经 Context 注入——这与「工具注册」那篇的可信参数边界一脉相承。看个完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from dataclasses import dataclass

from langchain.tools import ToolRuntime, tool


@dataclass
class UserContext:
# 用户身份由可信应用注入,不暴露给模型填写
user_id: str


@tool
def remember_preference(
preference: str,
runtime: ToolRuntime[UserContext],
) -> str:
"""保存当前用户明确要求记住的偏好。"""
# namespace 将不同用户的长期记忆隔离开
namespace = (runtime.context.user_id, "preferences")
# key 为 main,本例只维护一条当前偏好记录
runtime.store.put(namespace, "main", {"text": preference})
return "偏好已保存"

这套设计的隔离逻辑是:不同 thread_id 的会话,只要可信 user_id 相同,就能访问同一份长期记忆;其他用户则由 namespace 和服务端权限隔离——谁也不能凭模型生成的身份读到别人的档案:

跨线程读取与用户隔离:可信身份相同即可共享长期记忆,namespace 与权限隔离其他用户

这就像医院的病历归档:护士只认病人手环上的身份码(可信身份),按手环码把病历放进对应档案格(namespace),绝不听任何人现场报名字——报名字既可能记错,也可能被人冒名。


6 什么时候写入长期记忆

「能记」不等于「该记」。闲聊全存只会制造噪声、冲突和隐私风险——记住每条「今天天气不错」,下次检索时真正的偏好反而被淹没。

写入时机分两种:

  • 明确要求记住的(用户说「请记住我不吃辣」):主链路实时写入,立即生效;
  • 模型推断出的偏好经验(从对话里隐隐看出的习惯):更适合会话结束后由后台任务提炼、去重、脱敏后写入——当场写容易把试探性言论当真。

无论哪种方式,写入的记录都应该保存来源、时间、置信度,并支持更新、纠错、删除——记忆是会错的,要给用户和管理员留出修正的通道。

还有一条铁律:订单金额、账户余额、库存这类实时事实,必须去查权威业务系统,长期记忆不代替真实数据库——记忆里放的是规律与偏好,不是会变动的账本。


7 生产环境要注意什么

把记忆系统搬上线,要过四道关:

  1. 可靠保存:内存实现随进程退出而丢,线上必须用数据库型 Checkpointer 和 Store,表结构与迁移纳入部署流程;
  2. 这是谁的记忆thread_idtenant_iduser_id 必须来自可信身份体系,不能信模型生成的身份,也不能让客户端随意指定他人的 namespace——记得越多,越要先回答「这是谁的记忆」,否则数据库越大,跨用户泄漏的风险越大;
  3. 记忆会过时:去重、更新、过期淘汰是常态;用户要能查看、更正、导出、删除自己的记忆;敏感信息默认不记,确需保存则加密限权——日志与 Trace 也不能成为隐私泄漏口;
  4. 不是能存能查就合格:写入了多少条不是指标,要看召回链路——信息是否值得写入、相关问题能否召回、无关问题会不会误召回、注入记忆后答案是否真的变好。

记忆写入流水线:从判断是否值得写入、去重脱敏,到检索召回与使用效果的评测闭环

只有「写入、召回、使用」三步都有效,记忆系统才不是另一个不断膨胀的数据库。


8 旧 Memory 还能用吗

ConversationBufferMemoryConversationSummaryMemory 这类名字,是 Chain 时代(LLMChain 那个时代)的常见抽象,如今主要位于 langchain-classic,适合维护存量项目——但它不再是 v1 新项目的主路径。

v1 新项目的推荐组合:

1
2
3
4
5
AgentState + Checkpointer -> 管理线程内状态

Store + namespace/key -> 管理跨线程长期记忆

Middleware 或图节点 -> 管理裁剪、摘要和写入策略

对比旧版,这套组合把三件事拆得更清楚:状态作用域(线程内 vs 跨线程)、持久化机制(Checkpointer vs Store)、记忆治理(裁剪/摘要/写入策略交给 Middleware 或图节点)。对于有工具调用、暂停恢复、多用户隔离要求的 Agent,这种拆分几乎每一项都是刚需。


9 面试总结

回答这道题,最大的雷就是拿旧版 Memory 类名作答,或者把 Checkpointer 与 Store 混为一谈。先避开几个误区:

误区 正解
短期记忆 = 最近几轮对话塞进 Prompt,长期记忆 = 全部聊天记录存向量库 短期记忆是线程级 State + Checkpointer(thread_id),长期记忆是跨线程 Store(namespace/key);不是所有聊天都值得长期保存
用 ConversationBufferMemory / ConversonSummaryMemory 作答 旧版 Chain 时代抽象,在 langchain-classic;v1 讲 Checkpointer、Store、thread_id、namespace
Checkpointer 和 Store 都是持久化,选一个就够 作用域完全不同:Checkpointer 管当前线程状态,Store 管跨线程数据;混用不是换会话失忆,就是用户数据串读
上下文太长就固定裁剪最后 N 条 策略要按 Token 预算、消息角色、业务重要性定制——客服保工单与承诺,代码 Agent 保报错与修改记录
记忆系统能写能查就合格 当代系统像针对存档的档案室:写入要质检(去重、脱敏、质量判断),读取要隔离(namespace、可信身份),还要支持纠错、过期删除、隐私保护与召回评测

再按这张框架组织答案(短期与长期对照,不容易乱):

维度 短期记忆 长期记忆
作用域 当前线程(会话) 跨线程
载体 Agent State + Checkpointer Store(namespace/key/value)
关键标识 thread_id namespace=(tenant_id, user_id, memory_type) + key
内容 消息、当前步骤、中间结果 用户偏好、历史经验、工作规则
读取入口 runtime.state runtime.store(语义检索可配向量索引)
恢复能力 同 thread_id 续接,支持暂停恢复与人工审批 换线程后仍可读,namespace 隔离防串读
治理手段 裁剪、删除、摘要(按业务定制) 去重、更新、过期淘汰、隐私保护、召回评测

追问预案

  • 「怎么判断一条信息该短期记还是长期记?」——按作用域:这场对话内还要用的(进度、草稿、中间结果)是短期;换一个会话仍然成立的(口味偏好、工作规则、处理经验)才是长期。等价表述:短期回答「对话进行到哪」,长期回答「用户是谁」。
  • 「Checkpointer 和 Store 都能落盘,为什么不能二选一?」——作用域问题:Checkpointer 按 thread_id 保存当前线程的状态快照,新线程不继承;Store 按 namespace/key 保存跨线程长期数据。混用要么换会话就失忆(拿 Store 当线程存档),要么用户数据串读(拿线程状态当长期档案)。
  • 「上下文太长怎么处理?」——三策略各有代价:裁剪只减本次模型输入、持久状态继续增长;删除不可恢复;摘要可能逐轮失真。要结合 Token 预算、消息角色与业务重要性定制,不能固定条数。
  • 「长期记忆什么时候写、写什么?」——被明确要求记住的实时写入;推断出的偏好由后台任务提炼、去重、脱敏后写。写来源、时间、置信度,支持更新纠错删除;订单金额、余额这类实时事实查权威系统,不存进记忆。
  • 「怎么防止模型读到别人的记忆?」——身份可信化 + 空间隔离:user_id 由应用经 ToolRuntime context 注入(模型不可见),namespace 按租户/用户隔离,服务端再做权限校验;模型生成的身份一个都不能信。

参考

本文图片均来源于公众号「小林面试笔记」,版权归原作者所有。


「LangChain」LangChain 如何实现长短期记忆?
https://marisamagic.github.io/2026/08/26/20260826_LangChain长短期记忆/
作者
MarisaMagic
发布于
2026年8月26日
许可协议