一个没有记忆的 Agent,每次对话都是从零开始。你告诉它你叫李雷、你是 Python 工程师、你喜欢动手实践——下次对话,它完全不记得。

这不是 Bug,是架构缺失。
LLM 本身没有持久记忆,每次调用都是无状态的。要让 Agent 记住事情,需要在架构层显式地存储、管理和读取记忆。这就是记忆管理模块要解决的问题。
本篇从四个维度拆解 Agent 记忆:记忆类型的分类模型、上下文管理的三种策略、LangGraph 提供的两种记忆机制(checkpointer 和 store)、以及超长对话的自动压缩方案。
借鉴认知科学对人类记忆的分类,Agent 的记忆也可以分为四层,每一层在 LangGraph 中都有对应的实现方式:
┌──────────────────────────────────────────────────────────┐
│ 记忆层级 │
├──────────────────────┬───────────────────────────────────┤
│ 感觉记忆 (Sensory) │ 当前 Turn 的 in-flight 消息 │
│ │ 生命周期:单次 LLM 调用 │
├──────────────────────┼───────────────────────────────────┤
│ 工作记忆 (Working) │ 对话历史 MessageHistory(有限 K 轮)│
│ │ 实现:messages 列表注入 Prompt │
├──────────────────────┼───────────────────────────────────┤
│ 情景记忆 (Episodic) │ 向量化/摘要化的历史片段 │
│ │ 实现:摘要压缩 + VectorStore 检索 │
├──────────────────────┼───────────────────────────────────┤
│ 语义记忆 (Semantic) │ 长期存储的用户偏好、事实 │
│ │ 实现:LangGraph store (KV Store) │
└──────────────────────┴───────────────────────────────────┘
最短暂的记忆。一次 LLM 调用的输入和输出,用完即弃:
q = "Python 中 len([1, 2, 3]) 等于多少"
answer = llm.invoke([HumanMessage(q)])
# answer.content → "len([1, 2, 3]) 的结果等于 3。"
# 这次 invoke 结束后,这个 answer 就消失了
不需要任何机制来"管理"感觉记忆——它是 LLM 调用本身。
把之前几轮对话消息拼接进 Prompt,是最直接的记忆实现。效果立竿见影:
history = [
HumanMessage("我叫李雷,是一名 Python 工程师"),
AIMessage("你好,李雷!很高兴认识你。"),
HumanMessage("我最近在学习 LangGraph"),
AIMessage("LangGraph 很强大,特别适合构建有状态的 Agent。"),
]
test_q = "我之前告诉你我叫什么名字?"
实测对比:
有历史 → "是的,你之前告诉我你的名字是李雷,并且你是一名 Python 工程师..."
无历史 → "抱歉,我无法回忆起您之前告诉我的名字,因为作为一个 AI,我没有
持久的记忆功能来存储个人数据..."
差距非常直观。工作记忆的限制是 Token 成本随对话长度线性增长,需要结合截断或摘要来管理。
当对话历史很长时,把全部历史直接塞进 Prompt 代价太高。情景记忆的做法是先压缩,再存储:
long_history = history * 4 # 16 条消息
summary = llm.invoke([
SystemMessage("将以下对话压缩为 60 字以内的摘要,保留关键信息"),
HumanMessage(str([m.content for m in long_history])),
])
# → "李雷,Python工程师,积极学习LangGraph,赞其强大,适合构建有状态Agent。"
16 条消息压缩成 28 个字,下一轮用摘要代替原始历史,Token 消耗大幅下降。
最持久的记忆层。不随对话结束而消失,专门存储关于用户的长期事实(姓名、职业、偏好等):
# 把用户信息存入 KV Store,下一次会话直接读取
user_profile = {
"name": "李雷",
"role": "Python 工程师",
"interests": ["LangGraph", "Agent 开发"],
"level": "中级",
}
# 基于这些信息,Agent 能给出个性化的回答
# "下一步学习方向" → 推荐 LangGraph 进阶用法而不是 Python 入门
当对话历史越来越长,上下文窗口撑不住怎么办?三种策略各有取舍:
最简单粗暴——保留最近 N 条消息,其余全部丢弃:
# 保留最近 4 条消息
truncated = history[-4:]
resp = llm.invoke(truncated + [HumanMessage(test_q)])
实测用 8 个主题(16 条消息)的历史,截断到最近 4 条后问"Python 列表是什么":
截断后最早可见:'解释一下 Python 装饰器'(第 5 个主题,"列表"在第 1 个)
回答:Python 中的列表是一种内置的数据结构... (靠 LLM 自身知识回答)
丢失了我们"学过"列表这个事实,LLM 只是在用通用知识回答
适用场景:对历史连贯性要求不高的场景,或历史本身就不重要的纯问答类 Agent。
用 LLM 把长历史压缩成一段摘要,后续对话以摘要代替原始历史:
summary_resp = llm.invoke([
SystemMessage("将对话历史压缩为一段摘要(不超过 80 字),保留所有已介绍的主题名称"),
HumanMessage("n".join([f"{m.type}: {m.content}" for m in history])),
])
# → "Python列表可变有序,元组不可变省内存,字典键值对映射,集合唯一元素,
# 函数封装逻辑,类面向对象,装饰器函数包装,生成器惰性计算。"
# 16 条消息 → 66 字摘要
同样问"Python 列表是什么",摘要方案的回答能体现"这是我们讨论过的话题",而不只是通用知识。
三种策略对比:
| 策略 | Token 消耗 | 信息保留 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 截断 | 最低 | 只有近期 | 极低 | 纯问答、历史不重要 |
| 摘要 | 低 | 全局脉络 | 中 | 教学、咨询、长期对话 |
| 检索 | 最低 | 精准相关 | 高(需向量库) | 知识库、多领域 Agent |
只拉取与当前问题语义相关的历史片段——是效果最好也最复杂的方案:
# 简化演示:按关键词过滤(生产中用向量相似度)
relevant = [m for m in history if "列表" in m.content or "list" in m.content.lower()]
# 16 条 → 3 条相关历史
resp = llm.invoke(relevant + [HumanMessage(test_q)])
适用场景:知识库型 Agent、用户有大量历史记录的个人助手。
LangGraph 的 MemorySaver(checkpointer)用 thread_id 区分不同会话,在同一会话内自动积累对话历史:
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agentcheckpointer = MemorySaver()
agent = create_react_agent(model=llm, tools=[get_weather], checkpointer=checkpointer)# 同一 thread_id = 同一个会话,状态持久化
config_a = {"configurable": {"thread_id": "weather_001"}}
会话内连续三轮问天气:
[Turn 1] 用户: 北京今天天气怎么样?
Agent: 北京今天的天气是晴,温度为 25°C,东北风 3 级,空气良好。
(当前状态中消息数: 4)[Turn 2] 用户: 那上海呢? ← "那" 字没有明确指代,需要上文理解
Agent: 上海今天的天气是多云,22°C,东南风 2 级,轻度雾霾。
(当前状态中消息数: 8)[Turn 3] 用户: 这两个城市哪个更适合今天出行? ← 需要前两轮的查询结果
Agent: 考虑到上海的雾霾情况,建议您在北京出行。
(当前状态中消息数: 10)
Turn 2 和 Turn 3 的回答都依赖了前面的历史,checkpointer 自动完成了跨轮上下文管理。
不同 thread_id 之间完全独立:
[新会话 - thread_id: weather_002]
用户: 我刚才问的是哪个城市?
Agent: 您刚才问的是"哪个城市",但是没有提供具体的城市名称。
如果您需要查询某个城市的天气,请告诉我具体的城市名称。
→ 新 thread_id 没有任何历史,完全不知道刚才问过什么
同一 thread_id 隔一段时间再回来,历史依然存在:
[会话 A 继续 - 同一 thread_id]
用户: 刚才查的两个城市,再查一下深圳对比一下
Agent: 深圳今天的天气是阵雨,27°C,西南风 2 级,同时还有雷暴预警。
(进行了北京/上海/深圳三城比较)
→ 记住了前面查过北京和上海
MemorySaver 是内存实现,进程重启后数据丢失。生产中用 SqliteSaver(本地文件)或 PostgresSaver(数据库)代替。
checkpointer 解决的是单次会话内的记忆。跨会话的长期记忆需要 store:
checkpointer → 绑定 thread_id,会话生命周期内有效
store → 绑定 user_id,跨会话永久存在
from langgraph.store.memory import InMemoryStorestore = InMemoryStore()# 写入:(namespace, key, value)
store.put(("user_facts", user_id), key, {"fact": "李雷,后端工程师"})# 读取:搜索某个 namespace 下的所有条目
facts = store.search(("user_facts", user_id))
for item in facts:
print(item.value["fact"])# 精确读取
item = store.get(("user_facts", user_id), specific_key)
在会话 A 中,Agent 自动从对话中提取用户信息并存入 store:
[会话 A] 用户说了三句话 → 自动提取并存储:
• 李雷,后端工程师
• Python, Go, LangGraph, Agent 开发
• 动手实践,不喜欢纯看文档
在完全不同的会话 B(新 thread_id,但同一 user_id)中问"你认识我吗?":
[会话 B - 全新 thread_id]
用户: 你好,你认识我吗?
Agent: 你好!根据你提供的信息,我认识你。你是李雷,一位后端工程师,
擅长使用 Python、Go、LangGraph 和 Agent 进行开发。
你更喜欢动手实践,而不是仅仅阅读文档。有什么可以帮助你的吗?
→ 虽然是全新 thread_id,但 store 中的用户信息跨会话持久
不同 user_id 的数据完全隔离,互不干扰。
# 短期记忆:checkpointer — 绑定 thread_id,会话内有效
app = graph.compile(checkpointer=MemorySaver())
result = app.invoke(input, config={"configurable": {"thread_id": "abc"}})# 长期记忆:store — 绑定 user_id,跨会话有效
store = InMemoryStore()
app = graph.compile(store=store, checkpointer=MemorySaver())
# 在 node 中操作 store
store.put(("user_facts", user_id), key, {"fact": "..."})
stored = store.search(("user_facts", user_id))
生产中 InMemoryStore 替换为 PostgresStore 或 RedisStore 即可获得真正的持久化。
当消息数超过阈值时,触发自动压缩——这是让 Agent 在无限长对话中保持清醒的核心机制。
[chat 节点]
│
├─ 消息数 ≤ 阈值 → END
└─ 消息数 > 阈值 → [compress 节点] → END
def compress_node(state: SummaryState) -> dict:
messages = state["messages"]
to_compress = messages[:-2] # 保留最新 2 条,其余全部压缩
keep = messages[-2:] # 旧消息 → 新摘要
new_summary = llm.invoke([
SystemMessage("将以下内容压缩为 120 字以内的摘要"),
HumanMessage(existing_summary + old_messages_text),
]).content # RemoveMessage:告诉 add_messages reducer 删除这些消息
remove_ops = [RemoveMessage(id=m.id) for m in to_compress]
return {"messages": remove_ops, "summary": new_summary}
RemoveMessage 是 LangGraph 专用的消息删除操作符,add_messages reducer 看到它会从状态中删除对应 id 的消息。
11 轮对话,阈值设为 8 条消息:
[Turn 1-4] 消息数: 2/4/6/8 | 摘要: ○ 无 [压缩触发] 10 条 → 压缩 8 条,保留 2 条
[新摘要] Python列表常用方法包括查找、排序、添加删除等。
`dict.get()` 避免 `KeyError`,返回默认值。
`*args` 接收任意位置参数,`**kwargs` 接收任意关键字参数...[Turn 5] 消息数: 2 | 摘要: 已压缩 ← 压缩后从 2 条重新开始 [压缩再次触发] 10 条 → 压缩 8 条,保留 2 条[Turn 11] 最终汇总:
"根据我们之前的讨论,以下是您掌握的 Python 知识点汇总:
1. Python 列表推导式...
2. 集合推导式... ← 通过摘要链条传承,第 1 轮的内容还在
3. Lambda 函数..."
关键结果:11 轮对话始终只有 2-8 条 active 消息,但通过摘要链,所有历史知识都传承了下来。
class SummaryState(TypedDict):
messages: Annotated[list, add_messages] # add_messages 处理 RemoveMessage
summary: Optional[str] # 累积的历史摘要,注入 system promptdef chat_node(state: SummaryState) -> dict:
summary = state.get("summary") or ""
system_parts = ["你是助手。"]
if summary:
system_parts.append(f"nn【历史摘要】{summary}") # 摘要注入 system prompt
resp = llm.invoke([SystemMessage("".join(system_parts))] + state["messages"])
return {"messages": [resp]}
实现一套完整的 Agent 记忆系统需要考虑的点:
短期记忆(checkpointer)
thread_id长期记忆(store)
(类型, user_id) 如 ("user_facts", uid)上下文压缩
RemoveMessage 而不是替换整个 messages 列表(后者在有 checkpointer 时会有问题)记忆的读写时机
几个核心观点:
下一篇:知识库集成——RAG 作为 Agent 工具和 RAG Pipeline 的本质区别,多知识库路由,Agent 如何决定什么时候检索、检索什么、检索几次。
欢迎来我的个人主页找到更多有用的知识和有趣的产品
明日之后账号出售平台推荐:安全靠谱的交易APP有哪些
今日2025小鸡最新答案7.7 支付宝小鸡最新答案2026年7月7日
晋江app怎么复制作品信息
三国杀钟会觉醒玩法详解 三国杀钟会角色技能解析与实战技巧
Microsoft-PowerPoint-Best-Practices-Tips-and-Techniques skill 配套代码先跑哪一段:看章节入口、依赖和输出文件(skill安装版)
愉客行怎么退票